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