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