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