5050ecd5ff290467e11a8891649f3805508c588e
[kivitendo-erp.git] / t / db_helper / price_tax_calculator.t
1 use Test::More;
2
3 use strict;
4
5 use lib 't';
6 use utf8;
7
8 use Carp;
9 use Data::Dumper;
10 use List::MoreUtils qw(uniq);
11 use Support::TestSetup;
12 use Test::Exception;
13 use SL::Dev::ALL qw(:ALL);
14
15 use SL::DB::Buchungsgruppe;
16 use SL::DB::Currency;
17 use SL::DB::Customer;
18 use SL::DB::DeliveryOrder;
19 use SL::DB::Employee;
20 use SL::DB::Invoice;
21 use SL::DB::Order;
22 use SL::DB::Part;
23 use SL::DB::Unit;
24 use SL::DB::TaxZone;
25
26 my ($customer, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $tax, $tax7, $taxzone);
27 my ($transdate);
28
29 sub clear_up {
30   SL::DB::Manager::Order->delete_all(all => 1);
31   SL::DB::Manager::DeliveryOrder->delete_all(all => 1);
32   SL::DB::Manager::InvoiceItem->delete_all(all => 1);
33   SL::DB::Manager::Invoice->delete_all(all => 1);
34   SL::DB::Manager::Part->delete_all(all => 1);
35   SL::DB::Manager::Customer->delete_all(all => 1);
36 };
37
38 sub reset_state {
39   my %params = @_;
40
41   $params{$_} ||= {} for qw(buchungsgruppe unit customer part tax);
42
43   clear_up();
44
45   $buchungsgruppe  = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%', %{ $params{buchungsgruppe} }) || croak "No accounting group";
46   $buchungsgruppe7 = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%')                                || croak "No accounting group for 7\%";
47   $unit            = SL::DB::Manager::Unit->find_by(name => 'kg', %{ $params{unit} })                                      || croak "No unit";
48   $employee        = SL::DB::Manager::Employee->current                                                                    || croak "No employee";
49   $tax             = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{tax} })                           || croak "No tax";
50   $tax7            = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07)                                              || croak "No tax for 7\%";
51   $taxzone         = SL::DB::Manager::TaxZone->find_by( description => 'Inland')                                           || croak "No taxzone";
52
53   $customer     = new_customer(
54     name        => 'Test Customer',
55     taxzone_id  => $taxzone->id,
56     %{ $params{customer} }
57   )->save;
58
59   @parts = ();
60   push @parts, new_part(
61     partnumber         => 'T4254',
62     description        => 'Fourty-two fifty-four',
63     lastcost           => 1.93,
64     sellprice          => 2.34,
65     buchungsgruppen_id => $buchungsgruppe->id,
66     unit               => $unit->name,
67     %{ $params{part1} }
68   )->save;
69
70   push @parts, new_part(
71     partnumber         => 'T0815',
72     description        => 'Zero EIGHT fifteeN @ 7%',
73     lastcost           => 5.473,
74     sellprice          => 9.714,
75     buchungsgruppen_id => $buchungsgruppe7->id,
76     unit               => $unit->name,
77     %{ $params{part2} }
78   )->save;
79
80   push @parts, new_part(
81     partnumber         => 'T888',
82     description        => 'Triple 8',
83     lastcost           => 0,
84     sellprice          => 0.6,
85     buchungsgruppen_id => $buchungsgruppe->id,
86     unit               => $unit->name,
87     %{ $params{part3} }
88   )->save;
89
90 }
91
92 sub new_invoice {
93   my %params  = @_;
94
95   return create_sales_invoice(
96     transdate   => $transdate,
97     taxzone_id  => $taxzone->id,
98     %params,
99   );
100 }
101
102 sub new_item {
103   my (%params) = @_;
104
105   my $part = delete($params{part}) || $parts[0];
106
107   return create_invoice_item(
108     part => $part,
109     %params,
110   );
111 }
112
113 sub test_default_invoice_one_item_19_tax_not_included() {
114   reset_state();
115
116   my $item = new_item(qty => 2.5);
117   my $invoice = new_invoice(
118     taxincluded  => 0,
119     invoiceitems => [ $item ],
120   );
121
122   my $taxkey = $item->part->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id);
123
124   # sellprice 2.34 * qty 2.5 = 5.85
125   # 19%(5.85) = 1.1115; rounded = 1.11
126   # total rounded = 6.96
127
128   # lastcost 1.93 * qty 2.5 = 4.825; rounded 4.83
129   # line marge_total = 1.02
130   # line marge_percent = 17.4358974358974
131
132   my $title = 'default invoice, one item, 19% tax not included';
133   my %data  = $invoice->calculate_prices_and_taxes;
134
135   is($item->marge_total,        1.02,             "${title}: item marge_total");
136   is($item->marge_percent,      17.4358974358974, "${title}: item marge_percent");
137   is($item->marge_price_factor, 1,                "${title}: item marge_price_factor");
138
139   is($invoice->netamount,       5.85,             "${title}: netamount");
140   is($invoice->amount,          6.96,             "${title}: amount");
141   is($invoice->marge_total,     1.02,             "${title}: marge_total");
142   is($invoice->marge_percent,   17.4358974358974, "${title}: marge_percent");
143
144   is_deeply(\%data, {
145     allocated                                    => {},
146     amounts                                      => {
147       $buchungsgruppe->income_accno_id($taxzone) => {
148         amount                                   => 5.85,
149         tax_id                                   => $tax->id,
150         taxkey                                   => 3,
151       },
152     },
153     amounts_cogs                                 => {},
154     assembly_items                               => [
155       [],
156     ],
157     exchangerate                                 => 1,
158     taxes_by_chart_id                            => {
159       $tax->chart_id                             => 1.11,
160     },
161     taxes_by_tax_id                              => {
162       $tax->id                                   => 1.1115,
163     },
164     items                                        => [
165       { linetotal                                => 5.85,
166         linetotal_cost                           => 4.83,
167         sellprice                                => 2.34,
168         tax_amount                               => 1.1115,
169         taxkey_id                                => $taxkey->id,
170       },
171     ],
172     rounding                                    =>  0,
173   }, "${title}: calculated data");
174 }
175
176 sub test_default_invoice_two_items_19_7_tax_not_included() {
177   reset_state();
178
179   my $item1   = new_item(qty => 2.5);
180   my $item2   = new_item(qty => 1.2, part => $parts[1]);
181   my $invoice = new_invoice(
182     taxincluded  => 0,
183     invoiceitems => [ $item1, $item2 ],
184   );
185
186   my $taxkey1 = $item1->part->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id);
187   my $taxkey2 = $item2->part->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id);
188
189   # item 1:
190   # sellprice 2.34 * qty 2.5 = 5.85
191   # 19%(5.85) = 1.1115; rounded = 1.11
192   # total rounded = 6.96
193
194   # lastcost 1.93 * qty 2.5 = 4.825; rounded 4.83
195   # line marge_total = 1.02
196   # line marge_percent = 17.4358974358974
197
198   # item 2:
199   # sellprice 9.714 * qty 1.2 = 11.6568 rounded 11.66
200   # 7%(11.6568) = 0.815976; rounded = 0.82
201   # 7%(11.66)   = 0.8162
202   # total rounded = 12.48
203
204   # lastcost 5.473 * qty 1.2 = 6.5676; rounded 6.57
205   # line marge_total = 5.09
206   # line marge_percent = 43.6535162950257
207
208   my $title = 'default invoice, two item, 19/7% tax not included';
209   my %data  = $invoice->calculate_prices_and_taxes;
210
211   is($item1->marge_total,        1.02,             "${title}: item1 marge_total");
212   is($item1->marge_percent,      17.4358974358974, "${title}: item1 marge_percent");
213   is($item1->marge_price_factor, 1,                "${title}: item1 marge_price_factor");
214
215   is($item2->marge_total,        5.09,             "${title}: item2 marge_total");
216   is($item2->marge_percent,      43.6535162950257, "${title}: item2 marge_percent");
217   is($item2->marge_price_factor, 1,                "${title}: item2 marge_price_factor");
218
219   is($invoice->netamount,        5.85 + 11.66,     "${title}: netamount");
220   is($invoice->amount,           6.96 + 12.48,     "${title}: amount");
221   is($invoice->marge_total,      1.02 + 5.09,      "${title}: marge_total");
222   is($invoice->marge_percent,    34.8943460879497, "${title}: marge_percent");
223
224   is_deeply(\%data, {
225     allocated                                     => {},
226     amounts                                       => {
227       $buchungsgruppe->income_accno_id($taxzone)  => {
228         amount                                    => 5.85,
229         tax_id                                    => $tax->id,
230         taxkey                                    => 3,
231       },
232       $buchungsgruppe7->income_accno_id($taxzone) => {
233         amount                                    => 11.66,
234         tax_id                                    => $tax7->id,
235         taxkey                                    => 2,
236       },
237     },
238     amounts_cogs                                  => {},
239     assembly_items                                => [
240       [], [],
241     ],
242     exchangerate                                  => 1,
243     taxes_by_chart_id                             => {
244       $tax->chart_id                              => 1.11,
245       $tax7->chart_id                             => 0.82,
246     },
247     taxes_by_tax_id                               => {
248       $tax->id                                    => 1.1115,
249       $tax7->id                                   => 0.8162,
250     },
251     items                                        => [
252       { linetotal                                => 5.85,
253         linetotal_cost                           => 4.83,
254         sellprice                                => 2.34,
255         tax_amount                               => 1.1115,
256         taxkey_id                                => $taxkey1->id,
257       },
258       { linetotal                                => 11.66,
259         linetotal_cost                           => 6.57,
260         sellprice                                => 9.714,
261         tax_amount                               => 0.8162,
262         taxkey_id                                => $taxkey2->id,
263       },
264     ],
265     rounding                                    =>  0,
266   }, "${title}: calculated data");
267 }
268
269 sub test_default_invoice_three_items_sellprice_rounding_discount() {
270   reset_state();
271
272   my $item1   = new_item(qty => 1, sellprice => 5.55, discount => .05);
273   my $item2   = new_item(qty => 1, sellprice => 5.50, discount => .05);
274   my $item3   = new_item(qty => 1, sellprice => 5.00, discount => .05);
275   my $invoice = new_invoice(
276     taxincluded  => 0,
277     invoiceitems => [ $item1, $item2, $item3 ],
278   );
279
280   my %taxkeys = map { ($_->id => $_->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($item1, $item2, $item3);
281
282   # item 1:
283   # discount = sellprice 5.55 * discount (0.05) = 0.2775; rounded 0.28
284   # linetotal = sellprice 5.55 * (1 - discount 0.05) * qty 1 = 5.2725; rounded 5.27
285   # 19%(5.27) = 1.0013; rounded = 1.00
286   # total rounded = 6.27
287
288   # lastcost 1.93 * qty 1 = 1.93; rounded 1.93
289   # line marge_total = 5.27 - 1.93 = 3.34
290   # line marge_percent = 63.3776091081594
291
292   # item 2:
293   # discount = sellprice 5.50 * discount 0.05 = 0.275; rounded 0.28
294   # linetotal = sellprice 5.50 * (1 - discount 0.05) * qty 1 = 5.225; rounded 5.23
295   # 19%(5.23) = .99370; rounded = 0.99
296   # total rounded = 6.22
297
298   # lastcost 1.93 * qty 1 = 1.93; rounded 1.93
299   # line marge_total = 5.23 - 1.93 = 3.30
300   # line marge_percent = 3.30/5.23 = 0.630975143403442
301
302   # item 3:
303   # discount = sellprice 5.00 * discount 0.05 = 0.05 = 0.25; rounded 0.25
304   # linetotal = sellprice 5.00 (1 - discount 0.05) * qty 1 = 4.75; rounded 4.75
305   # 19%(4.75) = 0.9025; rounded = 0.90
306   # total rounded = 5.65
307
308   # lastcost 1.93 * qty 1 = 1.93; rounded 1.93
309   # line marge_total = 4.75 - 1.93 = 2.82
310   # line marge_percent = 2.82/4.75 = 59.3684210526316
311
312   my $title = 'default invoice, three items, sellprice, rounding, discount';
313   my %data  = $invoice->calculate_prices_and_taxes;
314
315   is($item1->marge_total,        3.34,               "${title}: item1 marge_total");
316   is($item1->marge_percent,      63.3776091081594,   "${title}: item1 marge_percent");
317   is($item1->marge_price_factor, 1,                  "${title}: item1 marge_price_factor");
318
319   is($item2->marge_total,        3.30,               "${title}: item2 marge_total");
320   is($item2->marge_percent,      63.0975143403442,   "${title}: item2 marge_percent");
321   is($item2->marge_price_factor, 1,                  "${title}: item2 marge_price_factor");
322
323   is($item3->marge_total,        2.82,               "${title}: item3 marge_total");
324   is($item3->marge_percent,      59.3684210526316,   "${title}: item3 marge_percent");
325   is($item3->marge_price_factor, 1,                  "${title}: item3 marge_price_factor");
326
327   is($invoice->netamount,        5.27 + 5.23 + 4.75, "${title}: netamount");
328
329   # 6.27 + 6.22 + 5.65 = 18.14
330   # 1.19*(5.27 + 5.23 + 4.75) = 18.1475; rounded 18.15
331   #is($invoice->amount,           6.27 + 6.22 + 5.65, "${title}: amount");
332   is($invoice->amount,           18.15,              "${title}: amount");
333
334   is($invoice->marge_total,      3.34 + 3.30 + 2.82, "${title}: marge_total");
335   is($invoice->marge_percent,    62.0327868852459,   "${title}: marge_percent");
336
337   is_deeply(\%data, {
338     allocated                                    => {},
339     amounts                                      => {
340       $buchungsgruppe->income_accno_id($taxzone) => {
341         amount                                   => 15.25,
342         tax_id                                   => $tax->id,
343         taxkey                                   => 3,
344       },
345     },
346     amounts_cogs                                 => {},
347     assembly_items                               => [
348       [], [], [],
349     ],
350     exchangerate                                 => 1,
351     taxes_by_chart_id                            => {
352       $tax->chart_id                             => 2.9,
353     },
354     taxes_by_tax_id                              => {
355       $tax->id                                   => 2.89750,
356     },
357     items                                        => [
358       { linetotal                                => 5.27,
359         linetotal_cost                           => 1.93,
360         sellprice                                => 5.27,
361         tax_amount                               => 1.0013,
362         taxkey_id                                => $taxkeys{$item1->parts_id}->id,
363       },
364       { linetotal                                => 5.23,
365         linetotal_cost                           => 1.93,
366         sellprice                                => 5.23,
367         tax_amount                               => 0.9937,
368         taxkey_id                                => $taxkeys{$item2->parts_id}->id,
369       },
370       { linetotal                                => 4.75,
371         linetotal_cost                           => 1.93,
372         sellprice                                => 4.75,
373         tax_amount                               => 0.9025,
374         taxkey_id                                => $taxkeys{$item3->parts_id}->id,
375       }
376     ],
377     rounding                                    =>  0,
378   }, "${title}: calculated data");
379 }
380
381 sub test_default_invoice_one_item_19_tax_not_included_rounding_discount() {
382   reset_state();
383
384   my $item   = new_item(qty => 6, part => $parts[2], discount => 0.03);
385   my $invoice = new_invoice(
386     taxincluded  => 0,
387     invoiceitems => [ $item ],
388   );
389
390   my %taxkeys = map { ($_->id => $_->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($item);
391
392   # 6 parts for 0.60 with 3% discount
393   #
394   # linetotal = sellprice 0.60 * qty 6 * discount (1 - 0.03) = 3.492 rounded 3.49
395   # total = 3.49 + 0.66 = 4.15
396   #
397
398   my $title = 'default invoice, one item, sellprice, rounding, discount';
399   my %data  = $invoice->calculate_prices_and_taxes;
400
401   is($invoice->netamount,         3.49,              "${title}: netamount");
402
403   is($invoice->amount,            4.15,              "${title}: amount");
404
405   is($invoice->marge_total,       3.49,              "${title}: marge_total");
406   is($invoice->marge_percent,      100,              "${title}: marge_percent");
407
408   is_deeply(\%data, {
409     allocated                                    => {},
410     amounts                                      => {
411       $buchungsgruppe->income_accno_id($taxzone) => {
412         amount                                   => 3.49,
413         tax_id                                   => $tax->id,
414         taxkey                                   => 3,
415       },
416     },
417     amounts_cogs                                 => {},
418     assembly_items                               => [
419       [],
420     ],
421     exchangerate                                 => 1,
422     taxes_by_chart_id                            => {
423       $tax->chart_id                             => 0.66,
424     },
425     taxes_by_tax_id                              => {
426       $tax->id                                   => 0.66310,
427     },
428     items                                        => [
429       { linetotal                                => 3.49,
430         linetotal_cost                           => 0,
431         sellprice                                => 0.58,
432         tax_amount                               => 0.6631,
433         taxkey_id                                => $taxkeys{$item->parts_id}->id,
434       },
435     ],
436     rounding                                     =>  0,
437   }, "${title}: calculated data");
438 }
439
440 sub test_default_invoice_one_item_19_tax_not_included_rounding_discount_huge_qty() {
441   reset_state();
442
443   my $item   = new_item(qty => 100000, part => $parts[2], discount => 0.03, sellprice => 0.10);
444   my $invoice = new_invoice(
445     taxincluded  => 0,
446     invoiceitems => [ $item ],
447   );
448
449   my %taxkeys = map { ($_->id => $_->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($item);
450
451   my $title = 'default invoice, one item, 19% tax not included, rounding, discount, huge qty';
452   my %data  = $invoice->calculate_prices_and_taxes;
453
454   is($invoice->netamount,         9700,              "${title}: netamount");
455
456   is($invoice->amount,           11543,              "${title}: amount");
457
458   is($invoice->marge_total,       9700,              "${title}: marge_total");
459   is($invoice->marge_percent,      100,              "${title}: marge_percent");
460
461   is_deeply(\%data, {
462     allocated                                    => {},
463     amounts                                      => {
464       $buchungsgruppe->income_accno_id($taxzone) => {
465         amount                                   => 9700,
466         tax_id                                   => $tax->id,
467         taxkey                                   => 3,
468       },
469     },
470     amounts_cogs                                 => {},
471     assembly_items                               => [
472       [],
473     ],
474     exchangerate                                 => 1,
475     taxes_by_chart_id                            => {
476       $tax->chart_id                             => 1843,
477     },
478     taxes_by_tax_id                              => {
479       $tax->id                                   => 1843,
480     },
481     items                                        => [
482       { linetotal                                => 9700,
483         linetotal_cost                           => 0,
484         sellprice                                => 0.1,
485         tax_amount                               => 1843,
486         taxkey_id                                => $taxkeys{$item->parts_id}->id,
487       },
488     ],
489     rounding                                    =>  0,
490   }, "${title}: calculated data");
491 }
492
493 sub test_default_invoice_one_item_19_tax_not_included_rounding_discount_big_qty_low_sellprice() {
494   reset_state();
495
496   my $item   = new_item(qty => 10001, sellprice => 0.007, discount => 0.035);
497   my $invoice = new_invoice(
498     taxincluded  => 0,
499     invoiceitems => [ $item ],
500   );
501
502   my %taxkeys = map { ($_->id => $_->get_taxkey(date => $transdate, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($item);
503
504   # item 1:
505   # discount = sellprice 0.007 * discount (0.035) = 0.000245; rounded 0.00
506   # sellprice = sellprice 0.007 - discount 0.00 = 0.007
507   # linetotal = sellprice 0.007 * qty 10001 * (1 - 0.035) = 67.556755; rounded 67.56
508   # 19%(67.56) = 12.8364; rounded = 12.84
509   # total rounded = 80.40
510
511   # lastcost 1.93 * qty 10001 = 19301.93; rounded 19301.93
512   # line marge_total = 67.56-19301.93 = -19234.37
513   # line marge_percent = 100*-19234.37/67.56 = -28470.0562462996
514
515   my $title = 'default invoice one item 19 tax not included rounding discount big qty low sellprice';
516   my %data  = $invoice->calculate_prices_and_taxes;
517
518   is($invoice->netamount,                 67.56,    "${title}: netamount");
519
520   is($invoice->amount,                    80.40,    "${title}: amount");
521
522   is($invoice->marge_total,           -19234.37,    "${title}: marge_total");
523   is($invoice->marge_percent, -28470.0562462996,    "${title}: marge_percent");
524
525   is_deeply(\%data, {
526     allocated                                    => {},
527     amounts                                      => {
528       $buchungsgruppe->income_accno_id($taxzone) => {
529         amount                                   => 67.56,
530         tax_id                                   => $tax->id,
531         taxkey                                   => 3,
532       },
533     },
534     amounts_cogs                                 => {},
535     assembly_items                               => [
536       [],
537     ],
538     exchangerate                                 => 1,
539     taxes_by_chart_id                            => {
540       $tax->chart_id                             => 12.84,
541     },
542     taxes_by_tax_id                              => {
543       $tax->id                                   => 12.8364,
544     },
545     items                                        => [
546       { linetotal                                => 67.56,
547         linetotal_cost                           => 19301.93,
548         sellprice                                => 0.007,
549         tax_amount                               => 12.8364,
550         taxkey_id                                => $taxkeys{$item->parts_id}->id,
551       },
552     ],
553     rounding                                    =>  0,
554   }, "${title}: calculated data");
555 }
556
557
558 Support::TestSetup::login();
559
560 $transdate = DateTime->today_local;
561 $transdate->set_year(2019) if $transdate->year == 2020; # use year 2019 in 2020, because of tax rate change in Germany
562
563 test_default_invoice_one_item_19_tax_not_included();
564 test_default_invoice_two_items_19_7_tax_not_included();
565 test_default_invoice_three_items_sellprice_rounding_discount();
566 test_default_invoice_one_item_19_tax_not_included_rounding_discount();
567 test_default_invoice_one_item_19_tax_not_included_rounding_discount_huge_qty();
568 test_default_invoice_one_item_19_tax_not_included_rounding_discount_big_qty_low_sellprice();
569
570 clear_up();
571 done_testing();
572
573 # vim: ft=perl
574 # set emacs to perl mode
575 # Local Variables:
576 # mode: perl
577 # End: