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