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