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