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