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 (
20 __PACKAGE__->meta->add_relationships(
22 type => 'one to many',
23 class => 'SL::DB::Assembly',
24 manager_args => { sort_by => 'position, oid' },
25 column_map => { id => 'id' },
28 type => 'one to many',
29 class => 'SL::DB::Price',
30 column_map => { id => 'parts_id' },
33 type => 'one to many',
34 class => 'SL::DB::MakeModel',
35 manager_args => { sort_by => 'sortorder' },
36 column_map => { id => 'parts_id' },
39 type => 'one to many',
40 class => 'SL::DB::Translation',
41 column_map => { id => 'parts_id' },
44 type => 'one to many',
45 class => 'SL::DB::AssortmentItem',
46 column_map => { id => 'assortment_id' },
50 __PACKAGE__->meta->initialize;
52 __PACKAGE__->attr_html('notes');
54 __PACKAGE__->before_save('_before_save_set_partnumber');
56 sub _before_save_set_partnumber {
59 $self->create_trans_number if !$self->partnumber;
67 push @errors, $::locale->text('The partnumber is missing.') unless $self->partnumber;
68 push @errors, $::locale->text('The unit is missing.') unless $self->unit;
69 push @errors, $::locale->text('The buchungsgruppe is missing.') unless $self->buchungsgruppen_id or $self->buchungsgruppe;
71 unless ( $self->id ) {
72 push @errors, $::locale->text('The partnumber already exists.') if SL::DB::Manager::Part->get_all_count(where => [ partnumber => $self->partnumber ]);
75 if ($self->is_assortment && scalar @{$self->assortment_items} == 0) {
76 push @errors, $::locale->text('The assortment doesn\'t have any items.');
79 if ($self->is_assembly && scalar @{$self->assemblies} == 0) {
80 push @errors, $::locale->text('The assembly doesn\'t have any items.');
88 my $type = lc(shift || '');
89 die 'invalid type' unless $type =~ /^(?:part|service|assembly|assortment)$/;
91 return $self->type eq $type ? 1 : 0;
94 sub is_part { $_[0]->part_type eq 'part' }
95 sub is_assembly { $_[0]->part_type eq 'assembly' }
96 sub is_service { $_[0]->part_type eq 'service' }
97 sub is_assortment { $_[0]->part_type eq 'assortment' }
100 return $_[0]->part_type;
101 # my ($self, $type) = @_;
103 # die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
104 # $self->assembly( $type eq 'assembly' ? 1 : 0);
105 # $self->inventory_accno_id($type ne 'service' ? 1 : undef);
108 # return 'assembly' if $self->assembly;
109 # return 'part' if $self->inventory_accno_id;
114 my ($class, %params) = @_;
115 $class->new(%params, part_type => 'part');
119 my ($class, %params) = @_;
120 $class->new(%params, part_type => 'assembly');
124 my ($class, %params) = @_;
125 $class->new(%params, part_type => 'service');
129 my ($class, %params) = @_;
130 $class->new(%params, part_type => 'assortment');
135 die 'not an accessor' if @_ > 1;
142 SL::DB::AssortmentItem
145 for my $class (@relations) {
146 eval "require $class";
147 return 0 if $class->_get_manager_class->get_all_count(query => [ parts_id => $self->id ]);
152 sub get_sellprice_info {
156 confess "Missing part id" unless $self->id;
158 my $object = $self->load;
160 return { sellprice => $object->sellprice,
161 price_factor_id => $object->price_factor_id };
164 sub get_ordered_qty {
166 my %result = SL::DB::Manager::Part->get_ordered_qty($self->id);
168 return $result{ $self->id };
171 sub available_units {
172 shift->unit_obj->convertible_units;
175 # autogenerated accessor is slightly off...
177 shift->buchungsgruppen(@_);
181 my ($self, %params) = @_;
183 my $date = $params{date} || DateTime->today_local;
184 my $is_sales = !!$params{is_sales};
185 my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
186 my $tk_info = $::request->cache('get_taxkey');
188 $tk_info->{$self->id} //= {};
189 $tk_info->{$self->id}->{$taxzone} //= { };
190 my $cache = $tk_info->{$self->id}->{$taxzone}->{$is_sales} //= { };
192 if (!exists $cache->{$date}) {
194 $self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone)
195 ->get_active_taxkey($date);
198 return $cache->{$date};
202 my ($self, %params) = @_;
204 my $type = (any { $_ eq $params{type} } qw(income expense inventory)) ? $params{type} : croak("Invalid 'type' parameter '$params{type}'");
205 my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
207 my $charts = $::request->cache('get_chart_id/by_part_id_and_taxzone')->{$self->id} //= {};
208 my $all_charts = $::request->cache('get_chart_id/by_id');
210 $charts->{$taxzone} ||= { };
212 if (!exists $charts->{$taxzone}->{$type}) {
213 require SL::DB::Buchungsgruppe;
214 my $bugru = SL::DB::Buchungsgruppe->load_cached($self->buchungsgruppen_id);
215 my $chart_id = ($type eq 'inventory') ? ($self->inventory_accno_id ? $bugru->inventory_accno_id : undef)
216 : $bugru->call_sub("${type}_accno_id", $taxzone);
219 my $chart = $all_charts->{$chart_id} // SL::DB::Chart->load_cached($chart_id)->load;
220 $all_charts->{$chart_id} = $chart;
221 $charts->{$taxzone}->{$type} = $chart;
225 return $charts->{$taxzone}->{$type};
228 # this is designed to ignore chargenumbers, expiration dates and just give a list of how much <-> where
229 sub get_simple_stock {
230 my ($self, %params) = @_;
232 return [] unless $self->id;
235 SELECT sum(qty), warehouse_id, bin_id FROM inventory WHERE parts_id = ?
236 GROUP BY warehouse_id, bin_id
238 my $stock_info = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id);
239 [ map { bless $_, 'SL::DB::Part::SimpleStock'} @$stock_info ];
241 # helper class to have bin/warehouse accessors in stock result
242 { package SL::DB::Part::SimpleStock;
243 sub warehouse { require SL::DB::Warehouse; SL::DB::Manager::Warehouse->find_by_or_create(id => $_[0]->{warehouse_id}) }
244 sub bin { require SL::DB::Bin; SL::DB::Manager::Bin ->find_by_or_create(id => $_[0]->{bin_id}) }
247 sub displayable_name {
248 join ' ', grep $_, map $_[0]->$_, qw(partnumber description);
251 sub clone_and_reset_deep {
254 my $clone = $self->clone_and_reset; # resets id and partnumber (primary key and unique constraint)
255 $clone->makemodels( map { $_->clone_and_reset } @{$self->makemodels});
256 $clone->translations( map { $_->clone_and_reset } @{$self->translations});
258 if ( $self->is_assortment ) {
259 $clone->assortment_items( map { $_->clone } @{$self->assortment_items} );
260 foreach my $ai ( @{ $clone->assortment_items } ) {
261 $ai->assortment_id(undef);
265 if ( $self->is_assembly ) {
266 $clone->assemblies( map { $_->clone_and_reset } @{$self->assemblies});
269 if ( $self->prices ) {
270 $clone->prices( map { $_->clone } @{$self->prices}); # pricegroup_id gets reset here because it is part of a unique contraint
271 if ( $clone->prices ) {
272 foreach my $price ( @{$clone->prices} ) {
274 $price->parts_id(undef);
292 SL::DB::Part: Model for the 'parts' table
296 This is a standard Rose::DB::Object based model and can be used as one.
300 Although the base class is called C<Part> we usually talk about C<Articles> if
301 we mean instances of this class. This is because articles come in three
306 =item Part - a single part
308 =item Service - a part without onhand, and without inventory accounting
310 =item Assembly - a collection of both parts and services
312 =item Assortment - a collection of parts
316 These types are sadly represented by data inside the class and cannot be
317 migrated into a flag. To work around this, each C<Part> object knows what type
318 it currently is. Since the type is data driven, there ist no explicit setting
319 method for it, but you can construct them explicitly with C<new_part>,
320 C<new_service>, C<new_assembly> and C<new_assortment>. A Buchungsgruppe should be supplied in this
321 case, but it will use the default Buchungsgruppe if you don't.
323 Matching these there are assorted helper methods dealing with types,
324 e.g. L</new_part>, L</new_service>, L</new_assembly>, L</type>,
325 L</is_type> and others.
331 =item C<new_part %PARAMS>
333 =item C<new_service %PARAMS>
335 =item C<new_assembly %PARAMS>
337 Will set the appropriate data fields so that the resulting instance will be of
338 the requested type. Since accounting targets are part of the distinction,
339 providing a C<Buchungsgruppe> is recommended. If none is given the constructor
340 will load a default one and set the accounting targets from it.
344 Returns the type as a string. Can be one of C<part>, C<service>, C<assembly>.
346 =item C<is_type $TYPE>
348 Tests if the current object is a part, a service or an
349 assembly. C<$type> must be one of the words 'part', 'service' or
350 'assembly' (their plurals are ok, too).
352 Returns 1 if the requested type matches, 0 if it doesn't and
353 C<confess>es if an unknown C<$type> parameter is encountered.
361 Shorthand for C<is_type('part')> etc.
363 =item C<get_sellprice_info %params>
365 Retrieves the C<sellprice> and C<price_factor_id> for a part under
366 different conditions and returns a hash reference with those two keys.
368 If C<%params> contains a key C<project_id> then a project price list
369 will be consulted if one exists for that project. In this case the
370 parameter C<country_id> is evaluated as well: if a price list entry
371 has been created for this country then it will be used. Otherwise an
372 entry without a country set will be used.
374 If none of the above conditions is met then the information from
377 =item C<get_ordered_qty %params>
379 Retrieves the quantity that has been ordered from a vendor but that
380 has not been delivered yet. Only open purchase orders are considered.
382 =item C<get_taxkey %params>
384 Retrieves and returns a taxkey object valid for the given date
385 C<$params{date}> and tax zone C<$params{taxzone}>
386 (C<$params{taxzone_id}> is also recognized). The date defaults to the
387 current date if undefined.
389 This function looks up the income (for trueish values of
390 C<$params{is_sales}>) or expense (for falsish values of
391 C<$params{is_sales}>) account for the current part. It uses the part's
392 associated buchungsgruppe and uses the fields belonging to the tax
393 zone given by C<$params{taxzone}>.
395 The information retrieved by the function is cached.
397 =item C<get_chart %params>
399 Retrieves and returns a chart object valid for the given type
400 C<$params{type}> and tax zone C<$params{taxzone}>
401 (C<$params{taxzone_id}> is also recognized). The type must be one of
402 the three key words C<income>, C<expense> and C<inventory>.
404 This function uses the part's associated buchungsgruppe and uses the
405 fields belonging to the tax zone given by C<$params{taxzone}>.
407 The information retrieved by the function is cached.
411 Checks if this article is used in orders, invoices, delivery orders or
414 =item C<buchungsgruppe BUCHUNGSGRUPPE>
416 Used to set the accounting information from a L<SL:DB::Buchungsgruppe> object.
417 Please note, that this is a write only accessor, the original Buchungsgruppe can
418 not be retrieved from an article once set.
424 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
425 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>