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