6 use List::MoreUtils qw(any);
7 use Rose::DB::Object::Helpers qw(as_tree);
10 use SL::DB::MetaSetup::Part;
11 use SL::DB::Manager::Part;
13 use SL::DB::Helper::AttrHTML;
14 use SL::DB::Helper::TransNumberGenerator;
15 use SL::DB::Helper::CustomVariables (
19 use List::Util qw(sum);
21 __PACKAGE__->meta->add_relationships(
23 type => 'one to many',
24 class => 'SL::DB::Assembly',
25 manager_args => { sort_by => 'position, oid' },
26 column_map => { id => 'id' },
29 type => 'one to many',
30 class => 'SL::DB::Price',
31 column_map => { id => 'parts_id' },
34 type => 'one to many',
35 class => 'SL::DB::MakeModel',
36 manager_args => { sort_by => 'sortorder' },
37 column_map => { id => 'parts_id' },
40 type => 'one to many',
41 class => 'SL::DB::Translation',
42 column_map => { id => 'parts_id' },
45 type => 'one to many',
46 class => 'SL::DB::AssortmentItem',
47 column_map => { id => 'assortment_id' },
50 type => 'one to many',
51 class => 'SL::DB::History',
52 column_map => { id => 'trans_id' },
53 query_args => [ what_done => 'part' ],
54 manager_args => { sort_by => 'itime' },
58 __PACKAGE__->meta->initialize;
60 __PACKAGE__->attr_html('notes');
62 __PACKAGE__->before_save('_before_save_set_partnumber');
64 sub _before_save_set_partnumber {
67 $self->create_trans_number if !$self->partnumber;
75 push @errors, $::locale->text('The partnumber is missing.') if $self->id and !$self->partnumber;
76 push @errors, $::locale->text('The unit is missing.') unless $self->unit;
77 push @errors, $::locale->text('The buchungsgruppe is missing.') unless $self->buchungsgruppen_id or $self->buchungsgruppe;
79 unless ( $self->id ) {
80 push @errors, $::locale->text('The partnumber already exists.') if SL::DB::Manager::Part->get_all_count(where => [ partnumber => $self->partnumber ]);
83 if ($self->is_assortment && scalar @{$self->assortment_items} == 0) {
84 push @errors, $::locale->text('The assortment doesn\'t have any items.');
87 if ($self->is_assembly && scalar @{$self->assemblies} == 0) {
88 push @errors, $::locale->text('The assembly doesn\'t have any items.');
96 my $type = lc(shift || '');
97 die 'invalid type' unless $type =~ /^(?:part|service|assembly|assortment)$/;
99 return $self->type eq $type ? 1 : 0;
102 sub is_part { $_[0]->part_type eq 'part' }
103 sub is_assembly { $_[0]->part_type eq 'assembly' }
104 sub is_service { $_[0]->part_type eq 'service' }
105 sub is_assortment { $_[0]->part_type eq 'assortment' }
108 return $_[0]->part_type;
109 # my ($self, $type) = @_;
111 # die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
112 # $self->assembly( $type eq 'assembly' ? 1 : 0);
113 # $self->inventory_accno_id($type ne 'service' ? 1 : undef);
116 # return 'assembly' if $self->assembly;
117 # return 'part' if $self->inventory_accno_id;
122 my ($class, %params) = @_;
123 $class->new(%params, part_type => 'part');
127 my ($class, %params) = @_;
128 $class->new(%params, part_type => 'assembly');
132 my ($class, %params) = @_;
133 $class->new(%params, part_type => 'service');
137 my ($class, %params) = @_;
138 $class->new(%params, part_type => 'assortment');
141 sub last_modification {
143 return $self->mtime or $self->itime;
148 die 'not an accessor' if @_ > 1;
155 SL::DB::AssortmentItem
158 for my $class (@relations) {
159 eval "require $class";
160 return 0 if $class->_get_manager_class->get_all_count(query => [ parts_id => $self->id ]);
165 sub get_sellprice_info {
169 confess "Missing part id" unless $self->id;
171 my $object = $self->load;
173 return { sellprice => $object->sellprice,
174 price_factor_id => $object->price_factor_id };
177 sub get_ordered_qty {
179 my %result = SL::DB::Manager::Part->get_ordered_qty($self->id);
181 return $result{ $self->id };
184 sub available_units {
185 shift->unit_obj->convertible_units;
188 # autogenerated accessor is slightly off...
190 shift->buchungsgruppen(@_);
194 my ($self, %params) = @_;
196 my $date = $params{date} || DateTime->today_local;
197 my $is_sales = !!$params{is_sales};
198 my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
199 my $tk_info = $::request->cache('get_taxkey');
201 $tk_info->{$self->id} //= {};
202 $tk_info->{$self->id}->{$taxzone} //= { };
203 my $cache = $tk_info->{$self->id}->{$taxzone}->{$is_sales} //= { };
205 if (!exists $cache->{$date}) {
207 $self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone)
208 ->get_active_taxkey($date);
211 return $cache->{$date};
215 my ($self, %params) = @_;
217 my $type = (any { $_ eq $params{type} } qw(income expense inventory)) ? $params{type} : croak("Invalid 'type' parameter '$params{type}'");
218 my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
220 my $charts = $::request->cache('get_chart_id/by_part_id_and_taxzone')->{$self->id} //= {};
221 my $all_charts = $::request->cache('get_chart_id/by_id');
223 $charts->{$taxzone} ||= { };
225 if (!exists $charts->{$taxzone}->{$type}) {
226 require SL::DB::Buchungsgruppe;
227 my $bugru = SL::DB::Buchungsgruppe->load_cached($self->buchungsgruppen_id);
228 my $chart_id = ($type eq 'inventory') ? ($self->is_part ? $bugru->inventory_accno_id : undef)
229 : $bugru->call_sub("${type}_accno_id", $taxzone);
232 my $chart = $all_charts->{$chart_id} // SL::DB::Chart->load_cached($chart_id)->load;
233 $all_charts->{$chart_id} = $chart;
234 $charts->{$taxzone}->{$type} = $chart;
238 return $charts->{$taxzone}->{$type};
241 # this is designed to ignore chargenumbers, expiration dates and just give a list of how much <-> where
242 sub get_simple_stock {
243 my ($self, %params) = @_;
245 return [] unless $self->id;
248 SELECT sum(qty), warehouse_id, bin_id FROM inventory WHERE parts_id = ?
249 GROUP BY warehouse_id, bin_id
251 my $stock_info = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id);
252 [ map { bless $_, 'SL::DB::Part::SimpleStock'} @$stock_info ];
254 # helper class to have bin/warehouse accessors in stock result
255 { package SL::DB::Part::SimpleStock;
256 sub warehouse { require SL::DB::Warehouse; SL::DB::Manager::Warehouse->find_by_or_create(id => $_[0]->{warehouse_id}) }
257 sub bin { require SL::DB::Bin; SL::DB::Manager::Bin ->find_by_or_create(id => $_[0]->{bin_id}) }
260 sub displayable_name {
261 join ' ', grep $_, map $_[0]->$_, qw(partnumber description);
264 sub clone_and_reset_deep {
267 my $clone = $self->clone_and_reset; # resets id and partnumber (primary key and unique constraint)
268 $clone->makemodels( map { $_->clone_and_reset } @{$self->makemodels});
269 $clone->translations( map { $_->clone_and_reset } @{$self->translations});
271 if ( $self->is_assortment ) {
272 $clone->assortment_items( map { $_->clone } @{$self->assortment_items} );
273 foreach my $ai ( @{ $clone->assortment_items } ) {
274 $ai->assortment_id(undef);
278 if ( $self->is_assembly ) {
279 $clone->assemblies( map { $_->clone_and_reset } @{$self->assemblies});
282 if ( $self->prices ) {
283 $clone->prices( map { $_->clone } @{$self->prices}); # pricegroup_id gets reset here because it is part of a unique contraint
284 if ( $clone->prices ) {
285 foreach my $price ( @{$clone->prices} ) {
287 $price->parts_id(undef);
295 sub assembly_sellprice_sum {
298 return unless $self->is_assembly;
299 sum map { $_->linetotal_sellprice } @{$self->assemblies};
302 sub assembly_lastcost_sum {
305 return unless $self->is_assembly;
306 sum map { $_->linetotal_lastcost } @{$self->assemblies};
309 sub assortment_sellprice_sum {
312 return unless $self->is_assortment;
313 sum map { $_->linetotal_sellprice } @{$self->assortment_items};
316 sub assortment_lastcost_sum {
319 return unless $self->is_assortment;
320 sum map { $_->linetotal_lastcost } @{$self->assortment_items};
333 SL::DB::Part: Model for the 'parts' table
337 This is a standard Rose::DB::Object based model and can be used as one.
341 Although the base class is called C<Part> we usually talk about C<Articles> if
342 we mean instances of this class. This is because articles come in three
347 =item Part - a single part
349 =item Service - a part without onhand, and without inventory accounting
351 =item Assembly - a collection of both parts and services
353 =item Assortment - a collection of items (parts or assemblies)
357 These types are sadly represented by data inside the class and cannot be
358 migrated into a flag. To work around this, each C<Part> object knows what type
359 it currently is. Since the type is data driven, there ist no explicit setting
360 method for it, but you can construct them explicitly with C<new_part>,
361 C<new_service>, C<new_assembly> and C<new_assortment>. A Buchungsgruppe should be supplied in this
362 case, but it will use the default Buchungsgruppe if you don't.
364 Matching these there are assorted helper methods dealing with types,
365 e.g. L</new_part>, L</new_service>, L</new_assembly>, L</type>,
366 L</is_type> and others.
372 =item C<new_part %PARAMS>
374 =item C<new_service %PARAMS>
376 =item C<new_assembly %PARAMS>
378 Will set the appropriate data fields so that the resulting instance will be of
379 the requested type. Since accounting targets are part of the distinction,
380 providing a C<Buchungsgruppe> is recommended. If none is given the constructor
381 will load a default one and set the accounting targets from it.
385 Returns the type as a string. Can be one of C<part>, C<service>, C<assembly>.
387 =item C<is_type $TYPE>
389 Tests if the current object is a part, a service or an
390 assembly. C<$type> must be one of the words 'part', 'service' or
391 'assembly' (their plurals are ok, too).
393 Returns 1 if the requested type matches, 0 if it doesn't and
394 C<confess>es if an unknown C<$type> parameter is encountered.
402 Shorthand for C<is_type('part')> etc.
404 =item C<get_sellprice_info %params>
406 Retrieves the C<sellprice> and C<price_factor_id> for a part under
407 different conditions and returns a hash reference with those two keys.
409 If C<%params> contains a key C<project_id> then a project price list
410 will be consulted if one exists for that project. In this case the
411 parameter C<country_id> is evaluated as well: if a price list entry
412 has been created for this country then it will be used. Otherwise an
413 entry without a country set will be used.
415 If none of the above conditions is met then the information from
418 =item C<get_ordered_qty %params>
420 Retrieves the quantity that has been ordered from a vendor but that
421 has not been delivered yet. Only open purchase orders are considered.
423 =item C<get_taxkey %params>
425 Retrieves and returns a taxkey object valid for the given date
426 C<$params{date}> and tax zone C<$params{taxzone}>
427 (C<$params{taxzone_id}> is also recognized). The date defaults to the
428 current date if undefined.
430 This function looks up the income (for trueish values of
431 C<$params{is_sales}>) or expense (for falsish values of
432 C<$params{is_sales}>) account for the current part. It uses the part's
433 associated buchungsgruppe and uses the fields belonging to the tax
434 zone given by C<$params{taxzone}>.
436 The information retrieved by the function is cached.
438 =item C<get_chart %params>
440 Retrieves and returns a chart object valid for the given type
441 C<$params{type}> and tax zone C<$params{taxzone}>
442 (C<$params{taxzone_id}> is also recognized). The type must be one of
443 the three key words C<income>, C<expense> and C<inventory>.
445 This function uses the part's associated buchungsgruppe and uses the
446 fields belonging to the tax zone given by C<$params{taxzone}>.
448 The information retrieved by the function is cached.
452 Checks if this article is used in orders, invoices, delivery orders or
455 =item C<buchungsgruppe BUCHUNGSGRUPPE>
457 Used to set the accounting information from a L<SL:DB::Buchungsgruppe> object.
458 Please note, that this is a write only accessor, the original Buchungsgruppe can
459 not be retrieved from an article once set.
461 =item C<assembly_sellprice_sum>
463 Non-recursive sellprice sum of all the assembly item sellprices.
465 =item C<assortment_sellprice_sum>
467 Non-recursive sellprice sum of all the assortment item sellprices.
469 =item C<assembly_lastcost_sum>
471 Non-recursive lastcost sum of all the assembly item lastcosts.
473 =item C<assortment_lastcost_sum>
475 Non-recursive lastcost sum of all the assortment item lastcosts.
481 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
482 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>