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' },
43 __PACKAGE__->meta->initialize;
45 __PACKAGE__->attr_html('notes');
47 __PACKAGE__->before_save('_before_save_set_partnumber');
49 sub _before_save_set_partnumber {
52 $self->create_trans_number if !$self->partnumber;
58 my $type = lc(shift || '');
59 die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
61 return $self->type eq $type ? 1 : 0;
64 sub is_part { $_[0]->part_type eq 'part' }
65 sub is_assembly { $_[0]->part_type eq 'assembly' }
66 sub is_service { $_[0]->part_type eq 'service' }
69 return $_[0]->part_type;
70 # my ($self, $type) = @_;
72 # die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
73 # $self->assembly( $type eq 'assembly' ? 1 : 0);
74 # $self->inventory_accno_id($type ne 'service' ? 1 : undef);
77 # return 'assembly' if $self->assembly;
78 # return 'part' if $self->inventory_accno_id;
83 my ($class, %params) = @_;
84 $class->new(%params, part_type => 'part');
88 my ($class, %params) = @_;
89 $class->new(%params, part_type => 'assembly');
93 my ($class, %params) = @_;
94 $class->new(%params, part_type => 'service');
99 die 'not an accessor' if @_ > 1;
107 for my $class (@relations) {
108 eval "require $class";
109 return 0 if $class->_get_manager_class->get_all_count(query => [ parts_id => $self->id ]);
114 sub get_sellprice_info {
118 confess "Missing part id" unless $self->id;
120 my $object = $self->load;
122 return { sellprice => $object->sellprice,
123 price_factor_id => $object->price_factor_id };
126 sub get_ordered_qty {
128 my %result = SL::DB::Manager::Part->get_ordered_qty($self->id);
130 return $result{ $self->id };
133 sub available_units {
134 shift->unit_obj->convertible_units;
137 # autogenerated accessor is slightly off...
139 shift->buchungsgruppen(@_);
143 my ($self, %params) = @_;
145 my $date = $params{date} || DateTime->today_local;
146 my $is_sales = !!$params{is_sales};
147 my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
148 my $tk_info = $::request->cache('get_taxkey');
150 $tk_info->{$self->id} //= {};
151 $tk_info->{$self->id}->{$taxzone} //= { };
152 my $cache = $tk_info->{$self->id}->{$taxzone}->{$is_sales} //= { };
154 if (!exists $cache->{$date}) {
156 $self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone)
157 ->get_active_taxkey($date);
160 return $cache->{$date};
164 my ($self, %params) = @_;
166 my $type = (any { $_ eq $params{type} } qw(income expense inventory)) ? $params{type} : croak("Invalid 'type' parameter '$params{type}'");
167 my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
169 my $charts = $::request->cache('get_chart_id/by_part_id_and_taxzone')->{$self->id} //= {};
170 my $all_charts = $::request->cache('get_chart_id/by_id');
172 $charts->{$taxzone} ||= { };
174 if (!exists $charts->{$taxzone}->{$type}) {
175 require SL::DB::Buchungsgruppe;
176 my $bugru = SL::DB::Buchungsgruppe->load_cached($self->buchungsgruppen_id);
177 my $chart_id = ($type eq 'inventory') ? ($self->inventory_accno_id ? $bugru->inventory_accno_id : undef)
178 : $bugru->call_sub("${type}_accno_id", $taxzone);
181 my $chart = $all_charts->{$chart_id} // SL::DB::Chart->load_cached($chart_id)->load;
182 $all_charts->{$chart_id} = $chart;
183 $charts->{$taxzone}->{$type} = $chart;
187 return $charts->{$taxzone}->{$type};
190 # this is designed to ignore chargenumbers, expiration dates and just give a list of how much <-> where
191 sub get_simple_stock {
192 my ($self, %params) = @_;
194 return [] unless $self->id;
197 SELECT sum(qty), warehouse_id, bin_id FROM inventory WHERE parts_id = ?
198 GROUP BY warehouse_id, bin_id
200 my $stock_info = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id);
201 [ map { bless $_, 'SL::DB::Part::SimpleStock'} @$stock_info ];
203 # helper class to have bin/warehouse accessors in stock result
204 { package SL::DB::Part::SimpleStock;
205 sub warehouse { require SL::DB::Warehouse; SL::DB::Manager::Warehouse->find_by_or_create(id => $_[0]->{warehouse_id}) }
206 sub bin { require SL::DB::Bin; SL::DB::Manager::Bin ->find_by_or_create(id => $_[0]->{bin_id}) }
209 sub displayable_name {
210 join ' ', grep $_, map $_[0]->$_, qw(partnumber description);
223 SL::DB::Part: Model for the 'parts' table
227 This is a standard Rose::DB::Object based model and can be used as one.
231 Although the base class is called C<Part> we usually talk about C<Articles> if
232 we mean instances of this class. This is because articles come in three
237 =item Part - a single part
239 =item Service - a part without onhand, and without inventory accounting
241 =item Assembly - a collection of both parts and services
245 These types are sadly represented by data inside the class and cannot be
246 migrated into a flag. To work around this, each C<Part> object knows what type
247 it currently is. Since the type is data driven, there ist no explicit setting
248 method for it, but you can construct them explicitly with C<new_part>,
249 C<new_service>, and C<new_assembly>. A Buchungsgruppe should be supplied in this
250 case, but it will use the default Buchungsgruppe if you don't.
252 Matching these there are assorted helper methods dealing with types,
253 e.g. L</new_part>, L</new_service>, L</new_assembly>, L</type>,
254 L</is_type> and others.
260 =item C<new_part %PARAMS>
262 =item C<new_service %PARAMS>
264 =item C<new_assembly %PARAMS>
266 Will set the appropriate data fields so that the resulting instance will be of
267 the requested type. Since accounting targets are part of the distinction,
268 providing a C<Buchungsgruppe> is recommended. If none is given the constructor
269 will load a default one and set the accounting targets from it.
273 Returns the type as a string. Can be one of C<part>, C<service>, C<assembly>.
275 =item C<is_type $TYPE>
277 Tests if the current object is a part, a service or an
278 assembly. C<$type> must be one of the words 'part', 'service' or
279 'assembly' (their plurals are ok, too).
281 Returns 1 if the requested type matches, 0 if it doesn't and
282 C<confess>es if an unknown C<$type> parameter is encountered.
290 Shorthand for C<is_type('part')> etc.
292 =item C<get_sellprice_info %params>
294 Retrieves the C<sellprice> and C<price_factor_id> for a part under
295 different conditions and returns a hash reference with those two keys.
297 If C<%params> contains a key C<project_id> then a project price list
298 will be consulted if one exists for that project. In this case the
299 parameter C<country_id> is evaluated as well: if a price list entry
300 has been created for this country then it will be used. Otherwise an
301 entry without a country set will be used.
303 If none of the above conditions is met then the information from
306 =item C<get_ordered_qty %params>
308 Retrieves the quantity that has been ordered from a vendor but that
309 has not been delivered yet. Only open purchase orders are considered.
311 =item C<get_taxkey %params>
313 Retrieves and returns a taxkey object valid for the given date
314 C<$params{date}> and tax zone C<$params{taxzone}>
315 (C<$params{taxzone_id}> is also recognized). The date defaults to the
316 current date if undefined.
318 This function looks up the income (for trueish values of
319 C<$params{is_sales}>) or expense (for falsish values of
320 C<$params{is_sales}>) account for the current part. It uses the part's
321 associated buchungsgruppe and uses the fields belonging to the tax
322 zone given by C<$params{taxzone}>.
324 The information retrieved by the function is cached.
326 =item C<get_chart %params>
328 Retrieves and returns a chart object valid for the given type
329 C<$params{type}> and tax zone C<$params{taxzone}>
330 (C<$params{taxzone_id}> is also recognized). The type must be one of
331 the three key words C<income>, C<expense> and C<inventory>.
333 This function uses the part's associated buchungsgruppe and uses the
334 fields belonging to the tax zone given by C<$params{taxzone}>.
336 The information retrieved by the function is cached.
340 Checks if this article is used in orders, invoices, delivery orders or
343 =item C<buchungsgruppe BUCHUNGSGRUPPE>
345 Used to set the accounting information from a L<SL:DB::Buchungsgruppe> object.
346 Please note, that this is a write only accessor, the original Buchungsgruppe can
347 not be retrieved from an article once set.
353 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
354 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>