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