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