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