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