6 use List::MoreUtils qw(any);
9 use SL::DB::MetaSetup::Part;
10 use SL::DB::Manager::Part;
12 use SL::DB::Helper::TransNumberGenerator;
13 use SL::DB::Helper::CustomVariables (
18 __PACKAGE__->meta->add_relationships(
20 type => 'one to many',
21 class => 'SL::DB::Assembly',
22 column_map => { id => 'id' },
26 class => 'SL::DB::PartsGroup',
27 column_map => { partsgroup_id => 'id' },
31 class => 'SL::DB::PriceFactor',
32 column_map => { price_factor_id => 'id' },
35 type => 'one to many',
36 class => 'SL::DB::Price',
37 column_map => { id => 'parts_id' },
40 type => 'one to many',
41 class => 'SL::DB::MakeModel',
42 column_map => { id => 'parts_id' },
45 type => 'one to many',
46 class => 'SL::DB::Translation',
47 column_map => { id => 'parts_id' },
51 __PACKAGE__->meta->initialize;
53 __PACKAGE__->before_save('_before_save_set_partnumber');
55 sub _before_save_set_partnumber {
58 $self->create_trans_number if $self->partnumber eq '';
64 my $type = lc(shift || '');
65 die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
67 return $self->type eq $type ? 1 : 0;
70 sub is_part { $_[0]->is_type('part') }
71 sub is_assembly { $_[0]->is_type('assembly') }
72 sub is_service { $_[0]->is_type('service') }
75 my ($self, $type) = @_;
77 die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
78 $self->assembly( $type eq 'assembly' ? 1 : 0);
79 $self->inventory_accno_id($type ne 'service' ? 1 : undef);
82 return 'assembly' if $self->assembly;
83 return 'part' if $self->inventory_accno_id;
88 my ($class, %params) = @_;
89 $class->new(%params, type => 'part');
93 my ($class, %params) = @_;
94 $class->new(%params, type => 'assembly');
98 my ($class, %params) = @_;
99 $class->new(%params, type => 'service');
104 die 'not an accessor' if @_ > 1;
113 for my $class (@relations) {
114 eval "require $class";
115 return 0 if $class->_get_manager_class->get_all_count(query => [ parts_id => $self->id ]);
120 sub get_sellprice_info {
124 confess "Missing part id" unless $self->id;
126 my $object = $self->load;
128 return { sellprice => $object->sellprice,
129 price_factor_id => $object->price_factor_id };
132 sub get_ordered_qty {
134 my %result = SL::DB::Manager::Part->get_ordered_qty($self->id);
136 return $result{ $self->id };
139 sub available_units {
140 shift->unit_obj->convertible_units;
143 # autogenerated accessor is slightly off...
145 shift->buchungsgruppen(@_);
149 my ($self, %params) = @_;
151 my $date = $params{date} || DateTime->today_local;
152 my $is_sales = !!$params{is_sales};
153 my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
155 $self->{__partpriv_taxkey_information} ||= { };
156 my $tk_info = $self->{__partpriv_taxkey_information};
158 $tk_info->{$taxzone} ||= { };
159 $tk_info->{$taxzone}->{$is_sales} ||= { };
161 if (!exists $tk_info->{$taxzone}->{$is_sales}->{$date}) {
162 $tk_info->{$taxzone}->{$is_sales}->{$date} =
163 $self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone)
165 ->get_active_taxkey($date);
168 return $tk_info->{$taxzone}->{$is_sales}->{$date};
172 my ($self, %params) = @_;
174 my $type = (any { $_ eq $params{type} } qw(income expense inventory)) ? $params{type} : croak("Invalid 'type' parameter '$params{type}'");
175 my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
177 $self->{__partpriv_get_chart_id} ||= { };
178 my $charts = $self->{__partpriv_get_chart_id};
180 $charts->{$taxzone} ||= { };
182 if (!exists $charts->{$taxzone}->{$type}) {
183 my $bugru = $self->buchungsgruppe;
184 my $chart_id = ($type eq 'inventory') ? ($self->inventory_accno_id ? $bugru->inventory_accno_id : undef)
185 : $bugru->call_sub("${type}_accno_id_${taxzone}");
187 $charts->{$taxzone}->{$type} = $chart_id ? SL::DB::Chart->new(id => $chart_id)->load : undef;
190 return $charts->{$taxzone}->{$type};
203 SL::DB::Part: Model for the 'parts' table
207 This is a standard Rose::DB::Object based model and can be used as one.
211 Although the base class is called C<Part> we usually talk about C<Articles> if
212 we mean instances of this class. This is because articles come in three
217 =item Part - a single part
219 =item Service - a part without onhand, and without inventory accounting
221 =item Assembly - a collection of both parts and services
225 These types are sadly represented by data inside the class and cannot be
226 migrated into a flag. To work around this, each C<Part> object knows what type
227 it currently is. Since the type ist data driven, there ist no explicit setting
228 method for it, but you can construct them explicitly with C<new_part>,
229 C<new_service>, and C<new_assembly>. A Buchungsgruppe should be supplied in this
230 case, but it will use the default Buchungsgruppe if you don't.
232 Matching these there are assorted helper methods dealing with types,
233 e.g. L</new_part>, L</new_service>, L</new_assembly>, L</type>,
234 L</is_type> and others.
240 =item C<new_part %PARAMS>
242 =item C<new_service %PARAMS>
244 =item C<new_assembly %PARAMS>
246 Will set the appropriate data fields so that the resulting instance will be of
247 tthe requested type. Since part of the distinction are accounting targets,
248 providing a C<Buchungsgruppe> is recommended. If none is given the constructor
249 will load a default one and set the accounting targets from it.
253 Returns the type as a string. Can be one of C<part>, C<service>, C<assembly>.
255 =item C<is_type $TYPE>
257 Tests if the current object is a part, a service or an
258 assembly. C<$type> must be one of the words 'part', 'service' or
259 'assembly' (their plurals are ok, too).
261 Returns 1 if the requested type matches, 0 if it doesn't and
262 C<confess>es if an unknown C<$type> parameter is encountered.
270 Shorthand for C<is_type('part')> etc.
272 =item C<get_sellprice_info %params>
274 Retrieves the C<sellprice> and C<price_factor_id> for a part under
275 different conditions and returns a hash reference with those two keys.
277 If C<%params> contains a key C<project_id> then a project price list
278 will be consulted if one exists for that project. In this case the
279 parameter C<country_id> is evaluated as well: if a price list entry
280 has been created for this country then it will be used. Otherwise an
281 entry without a country set will be used.
283 If none of the above conditions is met then the information from
286 =item C<get_ordered_qty %params>
288 Retrieves the quantity that has been ordered from a vendor but that
289 has not been delivered yet. Only open purchase orders are considered.
291 =item C<get_taxkey %params>
293 Retrieves and returns a taxkey object valid for the given date
294 C<$params{date}> and tax zone C<$params{taxzone}>
295 (C<$params{taxzone_id}> is also recognized). The date defaults to the
296 current date if undefined.
298 This function looks up the income (for trueish values of
299 C<$params{is_sales}>) or expense (for falsish values of
300 C<$params{is_sales}>) account for the current part. It uses the part's
301 associated buchungsgruppe and uses the fields belonging to the tax
302 zone given by C<$params{taxzone}> (range 0..3).
304 The information retrieved by the function is cached.
306 =item C<get_chart %params>
308 Retrieves and returns a chart object valid for the given type
309 C<$params{type}> and tax zone C<$params{taxzone}>
310 (C<$params{taxzone_id}> is also recognized). The type must be one of
311 the three key words C<income>, C<expense> and C<inventory>.
313 This function uses the part's associated buchungsgruppe and uses the
314 fields belonging to the tax zone given by C<$params{taxzone}> (range
317 The information retrieved by the function is cached.
321 Checks if this articke is used in orders, invoices, delivery orders or
324 =item C<buchungsgruppe BUCHUNGSGRUPPE>
326 Used to set the accounting informations from a L<SL:DB::Buchungsgruppe> object.
327 Please note, that this is a write only accessor, the original Buchungsgruppe can
328 not be retrieved from an article once set.
334 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
335 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>