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