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 column_map => { id => 'id' },
27 type => 'one to many',
28 class => 'SL::DB::Price',
29 column_map => { id => 'parts_id' },
32 type => 'one to many',
33 class => 'SL::DB::MakeModel',
34 column_map => { id => 'parts_id' },
37 type => 'one to many',
38 class => 'SL::DB::Translation',
39 column_map => { id => 'parts_id' },
42 type => 'one to many',
43 class => 'SL::DB::AssortmentItem',
44 column_map => { id => 'assortment_id' },
48 __PACKAGE__->meta->initialize;
50 __PACKAGE__->attr_html('notes');
52 __PACKAGE__->before_save('_before_save_set_partnumber');
54 sub _before_save_set_partnumber {
57 $self->create_trans_number if !$self->partnumber;
63 my $type = lc(shift || '');
64 die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
66 return $self->type eq $type ? 1 : 0;
69 sub is_part { $_[0]->part_type eq 'part' }
70 sub is_assembly { $_[0]->part_type eq 'assembly' }
71 sub is_service { $_[0]->part_type eq 'service' }
74 return $_[0]->part_type;
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, part_type => 'part');
93 my ($class, %params) = @_;
94 $class->new(%params, part_type => 'assembly');
98 my ($class, %params) = @_;
99 $class->new(%params, part_type => 'service');
104 die 'not an accessor' if @_ > 1;
112 for my $class (@relations) {
113 eval "require $class";
114 return 0 if $class->_get_manager_class->get_all_count(query => [ parts_id => $self->id ]);
119 sub get_sellprice_info {
123 confess "Missing part id" unless $self->id;
125 my $object = $self->load;
127 return { sellprice => $object->sellprice,
128 price_factor_id => $object->price_factor_id };
131 sub get_ordered_qty {
133 my %result = SL::DB::Manager::Part->get_ordered_qty($self->id);
135 return $result{ $self->id };
138 sub available_units {
139 shift->unit_obj->convertible_units;
142 # autogenerated accessor is slightly off...
144 shift->buchungsgruppen(@_);
148 my ($self, %params) = @_;
150 my $date = $params{date} || DateTime->today_local;
151 my $is_sales = !!$params{is_sales};
152 my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
153 my $tk_info = $::request->cache('get_taxkey');
155 $tk_info->{$self->id} //= {};
156 $tk_info->{$self->id}->{$taxzone} //= { };
157 my $cache = $tk_info->{$self->id}->{$taxzone}->{$is_sales} //= { };
159 if (!exists $cache->{$date}) {
161 $self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone)
162 ->get_active_taxkey($date);
165 return $cache->{$date};
169 my ($self, %params) = @_;
171 my $type = (any { $_ eq $params{type} } qw(income expense inventory)) ? $params{type} : croak("Invalid 'type' parameter '$params{type}'");
172 my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
174 my $charts = $::request->cache('get_chart_id/by_part_id_and_taxzone')->{$self->id} //= {};
175 my $all_charts = $::request->cache('get_chart_id/by_id');
177 $charts->{$taxzone} ||= { };
179 if (!exists $charts->{$taxzone}->{$type}) {
180 require SL::DB::Buchungsgruppe;
181 my $bugru = SL::DB::Buchungsgruppe->load_cached($self->buchungsgruppen_id);
182 my $chart_id = ($type eq 'inventory') ? ($self->inventory_accno_id ? $bugru->inventory_accno_id : undef)
183 : $bugru->call_sub("${type}_accno_id", $taxzone);
186 my $chart = $all_charts->{$chart_id} // SL::DB::Chart->load_cached($chart_id)->load;
187 $all_charts->{$chart_id} = $chart;
188 $charts->{$taxzone}->{$type} = $chart;
192 return $charts->{$taxzone}->{$type};
195 # this is designed to ignore chargenumbers, expiration dates and just give a list of how much <-> where
196 sub get_simple_stock {
197 my ($self, %params) = @_;
199 return [] unless $self->id;
202 SELECT sum(qty), warehouse_id, bin_id FROM inventory WHERE parts_id = ?
203 GROUP BY warehouse_id, bin_id
205 my $stock_info = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id);
206 [ map { bless $_, 'SL::DB::Part::SimpleStock'} @$stock_info ];
208 # helper class to have bin/warehouse accessors in stock result
209 { package SL::DB::Part::SimpleStock;
210 sub warehouse { require SL::DB::Warehouse; SL::DB::Manager::Warehouse->find_by_or_create(id => $_[0]->{warehouse_id}) }
211 sub bin { require SL::DB::Bin; SL::DB::Manager::Bin ->find_by_or_create(id => $_[0]->{bin_id}) }
214 sub displayable_name {
215 join ' ', grep $_, map $_[0]->$_, qw(partnumber description);
228 SL::DB::Part: Model for the 'parts' table
232 This is a standard Rose::DB::Object based model and can be used as one.
236 Although the base class is called C<Part> we usually talk about C<Articles> if
237 we mean instances of this class. This is because articles come in three
242 =item Part - a single part
244 =item Service - a part without onhand, and without inventory accounting
246 =item Assembly - a collection of both parts and services
250 These types are sadly represented by data inside the class and cannot be
251 migrated into a flag. To work around this, each C<Part> object knows what type
252 it currently is. Since the type is data driven, there ist no explicit setting
253 method for it, but you can construct them explicitly with C<new_part>,
254 C<new_service>, and C<new_assembly>. A Buchungsgruppe should be supplied in this
255 case, but it will use the default Buchungsgruppe if you don't.
257 Matching these there are assorted helper methods dealing with types,
258 e.g. L</new_part>, L</new_service>, L</new_assembly>, L</type>,
259 L</is_type> and others.
265 =item C<new_part %PARAMS>
267 =item C<new_service %PARAMS>
269 =item C<new_assembly %PARAMS>
271 Will set the appropriate data fields so that the resulting instance will be of
272 the requested type. Since accounting targets are part of the distinction,
273 providing a C<Buchungsgruppe> is recommended. If none is given the constructor
274 will load a default one and set the accounting targets from it.
278 Returns the type as a string. Can be one of C<part>, C<service>, C<assembly>.
280 =item C<is_type $TYPE>
282 Tests if the current object is a part, a service or an
283 assembly. C<$type> must be one of the words 'part', 'service' or
284 'assembly' (their plurals are ok, too).
286 Returns 1 if the requested type matches, 0 if it doesn't and
287 C<confess>es if an unknown C<$type> parameter is encountered.
295 Shorthand for C<is_type('part')> etc.
297 =item C<get_sellprice_info %params>
299 Retrieves the C<sellprice> and C<price_factor_id> for a part under
300 different conditions and returns a hash reference with those two keys.
302 If C<%params> contains a key C<project_id> then a project price list
303 will be consulted if one exists for that project. In this case the
304 parameter C<country_id> is evaluated as well: if a price list entry
305 has been created for this country then it will be used. Otherwise an
306 entry without a country set will be used.
308 If none of the above conditions is met then the information from
311 =item C<get_ordered_qty %params>
313 Retrieves the quantity that has been ordered from a vendor but that
314 has not been delivered yet. Only open purchase orders are considered.
316 =item C<get_taxkey %params>
318 Retrieves and returns a taxkey object valid for the given date
319 C<$params{date}> and tax zone C<$params{taxzone}>
320 (C<$params{taxzone_id}> is also recognized). The date defaults to the
321 current date if undefined.
323 This function looks up the income (for trueish values of
324 C<$params{is_sales}>) or expense (for falsish values of
325 C<$params{is_sales}>) account for the current part. It uses the part's
326 associated buchungsgruppe and uses the fields belonging to the tax
327 zone given by C<$params{taxzone}>.
329 The information retrieved by the function is cached.
331 =item C<get_chart %params>
333 Retrieves and returns a chart object valid for the given type
334 C<$params{type}> and tax zone C<$params{taxzone}>
335 (C<$params{taxzone_id}> is also recognized). The type must be one of
336 the three key words C<income>, C<expense> and C<inventory>.
338 This function uses the part's associated buchungsgruppe and uses the
339 fields belonging to the tax zone given by C<$params{taxzone}>.
341 The information retrieved by the function is cached.
345 Checks if this article is used in orders, invoices, delivery orders or
348 =item C<buchungsgruppe BUCHUNGSGRUPPE>
350 Used to set the accounting information from a L<SL:DB::Buchungsgruppe> object.
351 Please note, that this is a write only accessor, the original Buchungsgruppe can
352 not be retrieved from an article once set.
358 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
359 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>