952bce278ba93057f3ef3e4523b7aaab379253f6
[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
8 use SL::DBUtils;
9 use SL::DB::MetaSetup::Part;
10 use SL::DB::Manager::Part;
11 use SL::DB::Chart;
12 use SL::DB::Helper::TransNumberGenerator;
13 use SL::DB::Helper::CustomVariables (
14   module      => 'IC',
15   cvars_alias => 1,
16 );
17
18 __PACKAGE__->meta->add_relationships(
19   assemblies                     => {
20     type         => 'one to many',
21     class        => 'SL::DB::Assembly',
22     column_map   => { id => 'id' },
23   },
24   partsgroup                     => {
25     type         => 'one to one',
26     class        => 'SL::DB::PartsGroup',
27     column_map   => { partsgroup_id => 'id' },
28   },
29   price_factor   => {
30     type         => 'one to one',
31     class        => 'SL::DB::PriceFactor',
32     column_map   => { price_factor_id => 'id' },
33   },
34   prices         => {
35     type         => 'one to many',
36     class        => 'SL::DB::Price',
37     column_map   => { id => 'parts_id' },
38   },
39   makemodels     => {
40     type         => 'one to many',
41     class        => 'SL::DB::MakeModel',
42     column_map   => { id => 'parts_id' },
43   },
44   translations   => {
45     type         => 'one to many',
46     class        => 'SL::DB::Translation',
47     column_map   => { id => 'parts_id' },
48   },
49 );
50
51 __PACKAGE__->meta->initialize;
52
53 __PACKAGE__->before_save('_before_save_set_partnumber');
54
55 sub _before_save_set_partnumber {
56   my ($self) = @_;
57
58   $self->create_trans_number if $self->partnumber eq '';
59   return 1;
60 }
61
62 sub is_type {
63   my $self = shift;
64   my $type  = lc(shift || '');
65   die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
66
67   return $self->type eq $type ? 1 : 0;
68 }
69
70 sub is_part     { $_[0]->is_type('part') }
71 sub is_assembly { $_[0]->is_type('assembly') }
72 sub is_service  { $_[0]->is_type('service') }
73
74 sub 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, type => 'part');
90 }
91
92 sub new_assembly {
93   my ($class, %params) = @_;
94   $class->new(%params, type => 'assembly');
95 }
96
97 sub new_service {
98   my ($class, %params) = @_;
99   $class->new(%params, 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     SL::DB::RMAItem
111   );
112
113   for my $class (@relations) {
114     eval "require $class";
115     return 0 if $class->_get_manager_class->get_all_count(query => [ parts_id => $self->id ]);
116   }
117   return 1;
118 }
119
120 sub get_sellprice_info {
121   my $self   = shift;
122   my %params = @_;
123
124   confess "Missing part id" unless $self->id;
125
126   my $object = $self->load;
127
128   return { sellprice       => $object->sellprice,
129            price_factor_id => $object->price_factor_id };
130 }
131
132 sub get_ordered_qty {
133   my $self   = shift;
134   my %result = SL::DB::Manager::Part->get_ordered_qty($self->id);
135
136   return $result{ $self->id };
137 }
138
139 sub available_units {
140   shift->unit_obj->convertible_units;
141 }
142
143 # autogenerated accessor is slightly off...
144 sub buchungsgruppe {
145   shift->buchungsgruppen(@_);
146 }
147
148 sub get_taxkey {
149   my ($self, %params) = @_;
150
151   my $date     = $params{date} || DateTime->today_local;
152   my $is_sales = !!$params{is_sales};
153   my $taxzone  = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
154
155   $self->{__partpriv_taxkey_information} ||= { };
156   my $tk_info = $self->{__partpriv_taxkey_information};
157
158   $tk_info->{$taxzone}              ||= { };
159   $tk_info->{$taxzone}->{$is_sales} ||= { };
160
161   if (!exists $tk_info->{$taxzone}->{$is_sales}->{$date}) {
162     $tk_info->{$taxzone}->{$is_sales}->{$date} =
163       $self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone)
164       ->load
165       ->get_active_taxkey($date);
166   }
167
168   return $tk_info->{$taxzone}->{$is_sales}->{$date};
169 }
170
171 sub get_chart {
172   my ($self, %params) = @_;
173
174   my $type    = (any { $_ eq $params{type} } qw(income expense inventory)) ? $params{type} : croak("Invalid 'type' parameter '$params{type}'");
175   my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
176
177   $self->{__partpriv_get_chart_id} ||= { };
178   my $charts = $self->{__partpriv_get_chart_id};
179
180   $charts->{$taxzone} ||= { };
181
182   if (!exists $charts->{$taxzone}->{$type}) {
183     my $bugru    = $self->buchungsgruppe;
184     my $chart_id = ($type eq 'inventory') ? ($self->inventory_accno_id ? $bugru->inventory_accno_id : undef)
185                  :                          $bugru->call_sub("${type}_accno_id_${taxzone}");
186
187     $charts->{$taxzone}->{$type} = $chart_id ? SL::DB::Chart->new(id => $chart_id)->load : undef;
188   }
189
190   return $charts->{$taxzone}->{$type};
191 }
192
193 1;
194
195 __END__
196
197 =pod
198
199 =encoding utf-8
200
201 =head1 NAME
202
203 SL::DB::Part: Model for the 'parts' table
204
205 =head1 SYNOPSIS
206
207 This is a standard Rose::DB::Object based model and can be used as one.
208
209 =head1 TYPES
210
211 Although the base class is called C<Part> we usually talk about C<Articles> if
212 we mean instances of this class. This is because articles come in three
213 flavours called:
214
215 =over 4
216
217 =item Part     - a single part
218
219 =item Service  - a part without onhand, and without inventory accounting
220
221 =item Assembly - a collection of both parts and services
222
223 =back
224
225 These types are sadly represented by data inside the class and cannot be
226 migrated into a flag. To work around this, each C<Part> object knows what type
227 it currently is. Since the type ist data driven, there ist no explicit setting
228 method for it, but you can construct them explicitly with C<new_part>,
229 C<new_service>, and C<new_assembly>. A Buchungsgruppe should be supplied in this
230 case, but it will use the default Buchungsgruppe if you don't.
231
232 Matching these there are assorted helper methods dealing with types,
233 e.g.  L</new_part>, L</new_service>, L</new_assembly>, L</type>,
234 L</is_type> and others.
235
236 =head1 FUNCTIONS
237
238 =over 4
239
240 =item C<new_part %PARAMS>
241
242 =item C<new_service %PARAMS>
243
244 =item C<new_assembly %PARAMS>
245
246 Will set the appropriate data fields so that the resulting instance will be of
247 tthe requested type. Since part of the distinction are accounting targets,
248 providing a C<Buchungsgruppe> is recommended. If none is given the constructor
249 will load a default one and set the accounting targets from it.
250
251 =item C<type>
252
253 Returns the type as a string. Can be one of C<part>, C<service>, C<assembly>.
254
255 =item C<is_type $TYPE>
256
257 Tests if the current object is a part, a service or an
258 assembly. C<$type> must be one of the words 'part', 'service' or
259 'assembly' (their plurals are ok, too).
260
261 Returns 1 if the requested type matches, 0 if it doesn't and
262 C<confess>es if an unknown C<$type> parameter is encountered.
263
264 =item C<is_part>
265
266 =item C<is_service>
267
268 =item C<is_assembly>
269
270 Shorthand for C<is_type('part')> etc.
271
272 =item C<get_sellprice_info %params>
273
274 Retrieves the C<sellprice> and C<price_factor_id> for a part under
275 different conditions and returns a hash reference with those two keys.
276
277 If C<%params> contains a key C<project_id> then a project price list
278 will be consulted if one exists for that project. In this case the
279 parameter C<country_id> is evaluated as well: if a price list entry
280 has been created for this country then it will be used. Otherwise an
281 entry without a country set will be used.
282
283 If none of the above conditions is met then the information from
284 C<$self> is used.
285
286 =item C<get_ordered_qty %params>
287
288 Retrieves the quantity that has been ordered from a vendor but that
289 has not been delivered yet. Only open purchase orders are considered.
290
291 =item C<get_taxkey %params>
292
293 Retrieves and returns a taxkey object valid for the given date
294 C<$params{date}> and tax zone C<$params{taxzone}>
295 (C<$params{taxzone_id}> is also recognized). The date defaults to the
296 current date if undefined.
297
298 This function looks up the income (for trueish values of
299 C<$params{is_sales}>) or expense (for falsish values of
300 C<$params{is_sales}>) account for the current part. It uses the part's
301 associated buchungsgruppe and uses the fields belonging to the tax
302 zone given by C<$params{taxzone}> (range 0..3).
303
304 The information retrieved by the function is cached.
305
306 =item C<get_chart %params>
307
308 Retrieves and returns a chart object valid for the given type
309 C<$params{type}> and tax zone C<$params{taxzone}>
310 (C<$params{taxzone_id}> is also recognized). The type must be one of
311 the three key words C<income>, C<expense> and C<inventory>.
312
313 This function uses the part's associated buchungsgruppe and uses the
314 fields belonging to the tax zone given by C<$params{taxzone}> (range
315 0..3).
316
317 The information retrieved by the function is cached.
318
319 =item C<orphaned>
320
321 Checks if this articke is used in orders, invoices, delivery orders or
322 assemblies.
323
324 =item C<buchungsgruppe BUCHUNGSGRUPPE>
325
326 Used to set the accounting informations from a L<SL:DB::Buchungsgruppe> object.
327 Please note, that this is a write only accessor, the original Buchungsgruppe can
328 not be retrieved from an article once set.
329
330 =back
331
332 =head1 AUTHORS
333
334 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
335 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
336
337 =cut