8b149ad4c0e0b646d89554bc8e35ffd2148f3ee5
[kivitendo-erp.git] / SL / DB / Part.pm
1 package SL::DB::Part;
2
3 use strict;
4
5 use Carp;
6 use List::MoreUtils qw(any);
7 use Rose::DB::Object::Helpers qw(as_tree);
8
9 use SL::DBUtils;
10 use SL::DB::MetaSetup::Part;
11 use SL::DB::Manager::Part;
12 use SL::DB::Chart;
13 use SL::DB::Helper::AttrHTML;
14 use SL::DB::Helper::TransNumberGenerator;
15 use SL::DB::Helper::CustomVariables (
16   module      => 'IC',
17   cvars_alias => 1,
18 );
19
20 __PACKAGE__->meta->add_relationships(
21   assemblies                     => {
22     type         => 'one to many',
23     class        => 'SL::DB::Assembly',
24     column_map   => { id => 'id' },
25   },
26   prices         => {
27     type         => 'one to many',
28     class        => 'SL::DB::Price',
29     column_map   => { id => 'parts_id' },
30   },
31   makemodels     => {
32     type         => 'one to many',
33     class        => 'SL::DB::MakeModel',
34     column_map   => { id => 'parts_id' },
35   },
36   translations   => {
37     type         => 'one to many',
38     class        => 'SL::DB::Translation',
39     column_map   => { id => 'parts_id' },
40   },
41   assortment_items => {
42     type         => 'one to many',
43     class        => 'SL::DB::AssortmentItem',
44     column_map   => { id => 'assortment_id' },
45   },
46 );
47
48 __PACKAGE__->meta->initialize;
49
50 __PACKAGE__->attr_html('notes');
51
52 __PACKAGE__->before_save('_before_save_set_partnumber');
53
54 sub _before_save_set_partnumber {
55   my ($self) = @_;
56
57   $self->create_trans_number if !$self->partnumber;
58   return 1;
59 }
60
61 sub is_type {
62   my $self = shift;
63   my $type  = lc(shift || '');
64   die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
65
66   return $self->type eq $type ? 1 : 0;
67 }
68
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' }
72
73 sub type {
74   return $_[0]->part_type;
75   # my ($self, $type) = @_;
76   # if (@_ > 1) {
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);
80   # }
81
82   # return 'assembly' if $self->assembly;
83   # return 'part'     if $self->inventory_accno_id;
84   # return 'service';
85 }
86
87 sub new_part {
88   my ($class, %params) = @_;
89   $class->new(%params, part_type => 'part');
90 }
91
92 sub new_assembly {
93   my ($class, %params) = @_;
94   $class->new(%params, part_type => 'assembly');
95 }
96
97 sub new_service {
98   my ($class, %params) = @_;
99   $class->new(%params, part_type => 'service');
100 }
101
102 sub orphaned {
103   my ($self) = @_;
104   die 'not an accessor' if @_ > 1;
105
106   my @relations = qw(
107     SL::DB::InvoiceItem
108     SL::DB::OrderItem
109     SL::DB::Inventory
110   );
111
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 ]);
115   }
116   return 1;
117 }
118
119 sub get_sellprice_info {
120   my $self   = shift;
121   my %params = @_;
122
123   confess "Missing part id" unless $self->id;
124
125   my $object = $self->load;
126
127   return { sellprice       => $object->sellprice,
128            price_factor_id => $object->price_factor_id };
129 }
130
131 sub get_ordered_qty {
132   my $self   = shift;
133   my %result = SL::DB::Manager::Part->get_ordered_qty($self->id);
134
135   return $result{ $self->id };
136 }
137
138 sub available_units {
139   shift->unit_obj->convertible_units;
140 }
141
142 # autogenerated accessor is slightly off...
143 sub buchungsgruppe {
144   shift->buchungsgruppen(@_);
145 }
146
147 sub get_taxkey {
148   my ($self, %params) = @_;
149
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');
154
155   $tk_info->{$self->id}                                      //= {};
156   $tk_info->{$self->id}->{$taxzone}                          //= { };
157   my $cache = $tk_info->{$self->id}->{$taxzone}->{$is_sales} //= { };
158
159   if (!exists $cache->{$date}) {
160     $cache->{$date} =
161       $self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone)
162       ->get_active_taxkey($date);
163   }
164
165   return $cache->{$date};
166 }
167
168 sub get_chart {
169   my ($self, %params) = @_;
170
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;
173
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');
176
177   $charts->{$taxzone} ||= { };
178
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);
184
185     if ($chart_id) {
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;
189     }
190   }
191
192   return $charts->{$taxzone}->{$type};
193 }
194
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) = @_;
198
199   return [] unless $self->id;
200
201   my $query = <<'';
202     SELECT sum(qty), warehouse_id, bin_id FROM inventory WHERE parts_id = ?
203     GROUP BY warehouse_id, bin_id
204
205   my $stock_info = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id);
206   [ map { bless $_, 'SL::DB::Part::SimpleStock'} @$stock_info ];
207 }
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}) }
212 }
213
214 sub displayable_name {
215   join ' ', grep $_, map $_[0]->$_, qw(partnumber description);
216 }
217
218 1;
219
220 __END__
221
222 =pod
223
224 =encoding utf-8
225
226 =head1 NAME
227
228 SL::DB::Part: Model for the 'parts' table
229
230 =head1 SYNOPSIS
231
232 This is a standard Rose::DB::Object based model and can be used as one.
233
234 =head1 TYPES
235
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
238 flavours called:
239
240 =over 4
241
242 =item Part     - a single part
243
244 =item Service  - a part without onhand, and without inventory accounting
245
246 =item Assembly - a collection of both parts and services
247
248 =back
249
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.
256
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.
260
261 =head1 FUNCTIONS
262
263 =over 4
264
265 =item C<new_part %PARAMS>
266
267 =item C<new_service %PARAMS>
268
269 =item C<new_assembly %PARAMS>
270
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.
275
276 =item C<type>
277
278 Returns the type as a string. Can be one of C<part>, C<service>, C<assembly>.
279
280 =item C<is_type $TYPE>
281
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).
285
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.
288
289 =item C<is_part>
290
291 =item C<is_service>
292
293 =item C<is_assembly>
294
295 Shorthand for C<is_type('part')> etc.
296
297 =item C<get_sellprice_info %params>
298
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.
301
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.
307
308 If none of the above conditions is met then the information from
309 C<$self> is used.
310
311 =item C<get_ordered_qty %params>
312
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.
315
316 =item C<get_taxkey %params>
317
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.
322
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}>.
328
329 The information retrieved by the function is cached.
330
331 =item C<get_chart %params>
332
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>.
337
338 This function uses the part's associated buchungsgruppe and uses the
339 fields belonging to the tax zone given by C<$params{taxzone}>.
340
341 The information retrieved by the function is cached.
342
343 =item C<orphaned>
344
345 Checks if this article is used in orders, invoices, delivery orders or
346 assemblies.
347
348 =item C<buchungsgruppe BUCHUNGSGRUPPE>
349
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.
353
354 =back
355
356 =head1 AUTHORS
357
358 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
359 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
360
361 =cut