89173102a0c3a9df4c424de171e55c65368ee8a9
[kivitendo-erp.git] / SL / DB / Helper / PriceTaxCalculator.pm
1 package SL::DB::Helper::PriceTaxCalculator;
2
3 use strict;
4
5 use parent qw(Exporter);
6 our @EXPORT = qw(calculate_prices_and_taxes _calculate_item);
7
8 use Carp;
9 use List::Util qw(sum min max);
10
11 sub calculate_prices_and_taxes {
12   my ($self, %params) = @_;
13
14   require SL::DB::Chart;
15   require SL::DB::Currency;
16   require SL::DB::Default;
17   require SL::DB::InvoiceItem;
18   require SL::DB::Part;
19   require SL::DB::PriceFactor;
20   require SL::DB::Unit;
21
22   SL::DB::Part->load_cached(map { $_->parts_id } @{ $self->items }) if @{ $self->items || [] };
23
24   my %units_by_name       = map { ( $_->name => $_ ) } @{ SL::DB::Manager::Unit->get_all        };
25   my %price_factors_by_id = map { ( $_->id   => $_ ) } @{ SL::DB::Manager::PriceFactor->get_all };
26
27   my %data = ( lastcost_total      => 0,
28                invoicediff         => 0,
29                last_incex_chart_id => undef,
30                units_by_name       => \%units_by_name,
31                price_factors_by_id => \%price_factors_by_id,
32                taxes               => { },
33                amounts             => { },
34                amounts_cogs        => { },
35                allocated           => { },
36                assembly_items      => [ ],
37                exchangerate        => undef,
38                is_sales            => $self->can('customer') && $self->customer,
39                is_invoice          => (ref($self) =~ /Invoice/) || $params{invoice},
40                items               => [ ],
41              );
42
43   # set exchangerate in $data>{exchangerate}
44   if ( ref($self) eq 'SL::DB::Order' ) {
45     # orders store amount in the order currency
46     $data{exchangerate} = 1;
47   } else {
48     # invoices store amount in the default currency
49     _get_exchangerate($self, \%data, %params);
50     # $data{exchangerate} = $self->exchangerate; # untested alternative for setting exchangerate
51   };
52
53   $self->netamount(  0);
54   $self->marge_total(0);
55
56   SL::DB::Manager::Chart->cache_taxkeys(date => $self->transdate);
57
58   my $idx = 0;
59   foreach my $item ($self->items) {
60     $idx++;
61     _calculate_item($self, $item, $idx, \%data, %params);
62   }
63
64   _calculate_amounts($self, \%data, %params);
65
66   return $self unless wantarray;
67
68   return map { ($_ => $data{$_}) } qw(taxes amounts amounts_cogs allocated exchangerate assembly_items items rounding);
69 }
70
71 sub _get_exchangerate {
72   my ($self, $data, %params) = @_;
73
74   my $currency = $self->currency_id ? SL::DB::Currency->load_cached($self->currency_id)->name || '' : '';
75   if ($currency ne SL::DB::Default->get_default_currency) {
76     $data->{exchangerate}   = $::form->check_exchangerate(\%::myconfig, $currency, $self->transdate, $data->{is_sales} ? 'buy' : 'sell');
77     $data->{exchangerate} ||= $params{exchangerate};
78   }
79   $data->{exchangerate} ||= 1;
80 }
81
82 sub _calculate_item {
83   my ($self, $item, $idx, $data, %params) = @_;
84
85   my $part       = SL::DB::Part->load_cached($item->parts_id);
86   return unless $item->part;
87
88   my $part_unit  = $data->{units_by_name}->{ $part->unit };
89   my $item_unit  = $data->{units_by_name}->{ $item->unit };
90
91   croak("Undefined unit " . $part->unit) if !$part_unit;
92   croak("Undefined unit " . $item->unit) if !$item_unit;
93
94   $item->base_qty($item_unit->convert_to($item->qty, $part_unit));
95   $item->fxsellprice($item->sellprice) if $data->{is_invoice};
96
97   my $num_dec   = max 2, _num_decimal_places($item->sellprice);
98
99
100   # don't include rounded discount into sellprice for calculation
101   # any time the sellprice is multiplied with qty discount has to be considered as part of the multiplication
102   my $sellprice = $item->sellprice;
103
104   $item->price_factor(      ! $item->price_factor_obj   ? 1 : ($item->price_factor_obj->factor   || 1));
105   $item->marge_price_factor(! $part->price_factor ? 1 : ($part->price_factor->factor || 1));
106   my $linetotal = _round($sellprice * (1 - $item->discount) * $item->qty / $item->price_factor, 2) * $data->{exchangerate};
107   $linetotal    = _round($linetotal,                                                            2);
108
109   $data->{invoicediff} += $sellprice * (1 - $item->discount) * $item->qty * $data->{exchangerate} / $item->price_factor - $linetotal if $self->taxincluded;
110
111   my $linetotal_cost = 0;
112
113   if (!$linetotal) {
114     $item->marge_total(  0);
115     $item->marge_percent(0);
116
117   } else {
118     my $lastcost       = !(($item->lastcost // 0) * 1) ? ($part->lastcost || 0) : $item->lastcost;
119     $linetotal_cost    = _round($lastcost * $item->qty / $item->marge_price_factor, 2);
120
121     $item->marge_total(  $linetotal - $linetotal_cost);
122     $item->marge_percent($item->marge_total * 100 / $linetotal);
123
124     $self->marge_total(  $self->marge_total + $item->marge_total);
125     $data->{lastcost_total} += $linetotal_cost;
126   }
127
128   my $taxkey     = $part->get_taxkey(date => $self->transdate, is_sales => $data->{is_sales}, taxzone => $self->taxzone_id);
129   my $tax_rate   = $taxkey->tax->rate;
130   my $tax_amount = undef;
131
132   if ($self->taxincluded) {
133     $tax_amount = $linetotal * $tax_rate / ($tax_rate + 1);
134     $sellprice  = $sellprice             / ($tax_rate + 1);
135
136   } else {
137     $tax_amount = $linetotal * $tax_rate;
138   }
139
140   if ($taxkey->tax->chart_id) {
141     $data->{taxes}->{ $taxkey->tax->chart_id } ||= 0;
142     $data->{taxes}->{ $taxkey->tax->chart_id }  += $tax_amount;
143   } elsif ($tax_amount) {
144     die "tax_amount != 0 but no chart_id for taxkey " . $taxkey->id . " tax " . $taxkey->tax->id;
145   }
146
147   $self->netamount($self->netamount + $sellprice * (1 - $item->discount) * $item->qty / $item->price_factor);
148
149   my $chart = $part->get_chart(type => $data->{is_sales} ? 'income' : 'expense', taxzone => $self->taxzone_id);
150   $data->{amounts}->{ $chart->id }           ||= { taxkey => $taxkey->taxkey_id, tax_id => $taxkey->tax_id, amount => 0 };
151   $data->{amounts}->{ $chart->id }->{amount}  += $linetotal;
152   $data->{amounts}->{ $chart->id }->{amount}  -= $tax_amount if $self->taxincluded;
153
154   push @{ $data->{assembly_items} }, [];
155   if ($part->is_assembly) {
156     _calculate_assembly_item($self, $data, $part, $item->base_qty, $item_unit->convert_to(1, $part_unit));
157   } elsif ($part->is_part) {
158     if ($data->{is_invoice}) {
159       $item->allocated(_calculate_part_item($self, $data, $part, $item->base_qty, $item_unit->convert_to(1, $part_unit)));
160     }
161   }
162
163   $data->{last_incex_chart_id} = $chart->id if $data->{is_sales};
164
165   my $item_sellprice = _round($sellprice * (1 - $item->discount), $num_dec);
166
167   push @{ $data->{items} }, {
168     linetotal      => $linetotal,
169     linetotal_cost => $linetotal_cost,
170     sellprice      => $item_sellprice,
171     tax_amount     => $tax_amount,
172     taxkey_id      => $taxkey->id,
173   };
174
175   _dbg("CALCULATE! ${idx} i.qty " . $item->qty . " i.sellprice " . $item->sellprice . " sellprice $sellprice num_dec $num_dec taxamount $tax_amount " .
176        "i.linetotal $linetotal netamount " . $self->netamount . " marge_total " . $item->marge_total . " marge_percent " . $item->marge_percent);
177 }
178
179 sub _calculate_amounts {
180   my ($self, $data, %params) = @_;
181
182   my $tax_diff = 0;
183   foreach my $chart_id (keys %{ $data->{taxes} }) {
184     my $rounded                  = _round($data->{taxes}->{$chart_id} * $data->{exchangerate}, 2);
185     $tax_diff                   += $data->{taxes}->{$chart_id} * $data->{exchangerate} - $rounded if $self->taxincluded;
186     $data->{taxes}->{$chart_id}  = $rounded;
187   }
188
189   my $amount    = _round(($self->netamount + $tax_diff) * $data->{exchangerate}, 2);
190   my $diff      = $amount - ($self->netamount + $tax_diff) * $data->{exchangerate};
191   my $netamount = $amount;
192
193   if ($self->taxincluded) {
194     $data->{invoicediff}                                         += $diff;
195     $data->{amounts}->{ $data->{last_incex_chart_id} }->{amount} += $data->{invoicediff} if $data->{last_incex_chart_id};
196   }
197
198   _dbg("Sna " . $self->netamount . " idiff " . $data->{invoicediff} . " tdiff ${tax_diff}");
199
200   my $tax              = sum values %{ $data->{taxes} };
201   $amount              = $netamount + $tax;
202   my $grossamount      = _round($amount, 2, 1);
203   $data->{rounding}    = _round($grossamount - $amount, 2);
204   $data->{arap_amount} = $grossamount;
205
206   $self->netamount(    $netamount);
207   $self->amount(       $grossamount);
208   $self->marge_percent($self->netamount ? ($self->netamount - $data->{lastcost_total}) * 100 / $self->netamount : 0);
209 }
210
211 sub _calculate_assembly_item {
212   my ($self, $data, $part, $total_qty, $base_factor) = @_;
213
214   return 0 if $::instance_conf->get_inventory_system eq 'periodic' || !$data->{is_invoice};
215
216   foreach my $assembly_entry (@{ $part->assemblies }) {
217     push @{ $data->{assembly_items}->[-1] }, { part      => $assembly_entry->part,
218                                                qty       => $total_qty * $assembly_entry->qty,
219                                                allocated => 0 };
220
221     if ($assembly_entry->part->is_assembly) {
222       _calculate_assembly_item($self, $data, $assembly_entry->part, $total_qty * $assembly_entry->qty);
223     } elsif ($assembly_entry->part->is_part) {
224       my $allocated = _calculate_part_item($self, $data, $assembly_entry->part, $total_qty * $assembly_entry->qty);
225       $data->{assembly_items}->[-1]->[-1]->{allocated} = $allocated;
226     }
227   }
228 }
229
230 sub _calculate_part_item {
231   my ($self, $data, $part, $total_qty, $base_factor) = @_;
232
233   _dbg("cpsi tq " . $total_qty);
234
235   return 0 if $::instance_conf->get_inventory_system eq 'periodic' || !$data->{is_invoice} || !$total_qty;
236
237   my ($entry);
238   $base_factor           ||= 1;
239   my $remaining_qty        = $total_qty;
240   my $expense_income_chart = $part->get_chart(type => $data->{is_sales} ? 'expense' : 'income', taxzone => $self->taxzone_id);
241   my $inventory_chart      = $part->get_chart(type => 'inventory',                              taxzone => $self->taxzone_id);
242
243   my $iterator             = SL::DB::Manager::InvoiceItem->get_all_iterator(query => [ and => [ parts_id => $part->id,
244                                                                                                 \'(base_qty + allocated) < 0' ] ]);
245
246   while (($remaining_qty > 0) && ($entry = $iterator->next)) {
247     my $qty = min($remaining_qty, $entry->base_qty * -1 - $entry->allocated - $data->{allocated}->{ $entry->id });
248     _dbg("qty $qty");
249
250     next unless $qty;
251
252     my $linetotal = _round(($entry->sellprice * (1 - $entry->discount) * $qty) / $base_factor, 2);
253
254     $data->{amounts_cogs}->{ $expense_income_chart->id } -= $linetotal;
255     $data->{amounts_cogs}->{ $inventory_chart->id      } += $linetotal;
256
257     $data->{allocated}->{ $entry->id } ||= 0;
258     $data->{allocated}->{ $entry->id }  += $qty;
259     $remaining_qty                      -= $qty;
260   }
261
262   $iterator->finish;
263
264   return $remaining_qty - $total_qty;
265 }
266
267 sub _round {
268   return $::form->round_amount(@_);
269 }
270
271 sub _num_decimal_places {
272   return length( (split(/\./, '' . ($_[0] * 1), 2))[1] || '' );
273 }
274
275 sub _dbg {
276   # $::lxdebug->message(0, join(' ', @_));
277 }
278
279 1;
280 __END__
281
282 =pod
283
284 =encoding utf8
285
286 =head1 NAME
287
288 SL::DB::Helper::PriceTaxCalculator - Mixin for calculating the prices,
289 amounts and taxes of orders, quotations, invoices
290
291 =head1 FUNCTIONS
292
293 =over 4
294
295 =item C<calculate_prices_and_taxes %params>
296
297 Calculates the prices, amounts and taxes for an order, a quotation or
298 an invoice.
299
300 The function assumes that the mixing package has a certain layout and
301 provides certain functions:
302
303 =over 2
304
305 =item C<transdate>
306
307 The record's date.
308
309 =item C<customer> or C<vendor>
310
311 Determines if the record is a sales or purchase record.
312
313 =item C<items>
314
315 Accessor returning all line items for this record. The line items
316 themselves must again have a certain layout. Instances of
317 L<SL::DB::OrderItem> and L<SL::DB::InvoiceItem> are supported.
318
319 =back
320
321 The following values are calculated and set for C<$self>: C<amount>,
322 C<netamount>, C<marge_percent>, C<marge_total>.
323
324 The following values are calculated and set for each line item:
325 C<base_qty>, C<price_factor>, C<marge_price_factor>, C<marge_total>,
326 C<marge_percent>.
327
328 The objects are not saved.
329
330 Returns C<$self> in scalar context.
331
332 In array context a hash with the following keys is returned:
333
334 =over 2
335
336 =item C<taxes>
337
338 A hash reference with the calculated taxes. The keys are chart IDs,
339 the values the calculated taxes.
340
341 =item C<amounts>
342
343 A hash reference with the calculated amounts. The keys are chart IDs,
344 the values are hash references containing the two keys C<amount> and
345 C<taxkey>.
346
347 =item C<amounts_cogs>
348
349 A hash reference with the calculated amounts for costs of goods
350 sold. The keys are chart IDs, the values the calculated amounts.
351
352 =item C<assembly_items>
353
354 An array reference with as many entries as there are items in the
355 record. Each entry is again an array reference of hash references with
356 the keys C<part> (an instance of L<SL::DB::Part>), C<qty> and
357 C<allocated>. Is only valid for invoices and can be used to populate
358 the C<invoice> table with entries for assemblies.
359
360 =item C<allocated>
361
362 A hash reference. The keys are IDs of entries in the C<invoice>
363 table. The values are the new values for the entry's C<allocated>
364 column. Only valid for invoices.
365
366 =item C<exchangerate>
367
368 The exchangerate used for the calculation.
369
370 =item C<items>
371
372 An array reference. For each line item this array contains a hash ref
373 entry with additional values that have been calculated for that item
374 but that aren't stored in the item object itself. These include
375 C<linetotal>, C<linetotal_cost>, C<sellprice>, C<tax_amount> and
376 C<taxkey_id>.
377
378 The items are stored in the same order the items are stored in the
379 object that L</calculate_prices_and_taxes> has been called on.
380
381 For example:
382
383   my $invoice     = SL::DB::Invoice->new(id => 12345)->load;
384   my %data        = $invoice->calculate_prices_and_taxes;
385
386   print "line total of second item: " . $data{items}->[1]->{linetotal};
387
388 =back
389
390 =back
391
392 =head1 BUGS
393
394 Nothing here yet.
395
396 =head1 AUTHOR
397
398 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
399
400 =cut