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