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