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