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