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