commit 0e68056cbc17b531266c16454f3a74ae1e57dea6
Author: Geoffrey Richardson <information@kivitendo-premium.de>
Date:   Tue Nov 4 12:04:43 2014 +0100

    Rundung bei Debitorenbuchung, Kreditorenbuchung und Dialogbuchung
    
    Zwei neue Hilfsfunktionen für Form eingeführt die von ap/ar/gl genutzt
    werden:
    
    * calculate_tax wird zur Berechnung der Steuer bei
    ** update in ar, ap und gl
    ** post_transaction in AR.pm und AP.pm innerhalb von calculate_arap
    
    * calculate_arap berechnet netamount, amount und totaltax anhand einer
    vorhandenen $form in ar/ap, und formatiert und rundet die amount_$i and
    tax_$i in der $form.
    
    Das Ziel war es, daß diese drei Belege mit den gleichen Formeln zur
    Steuerberechnung bei Steuer inkl./exkl. arbeiten. Ein Vorteil im
    Vergleich zur Einkaufs- und Verkaufsrechnung ist, daß es hier keine
    Positionen/Mengen/Rabatte/etc gibt, sondern direkt Werte auf Konten
    bebucht werden, man muß nur Währungskurse berücksichtigen und beim
    Schreiben in die Datenbank auf die Vorzeichen achten. Einkaufs- und
    Verkaufsrechnungen werden hiervon nicht beeinflußt.
    
    Vor allem bei der Einstellung "MwSt. inkl" wird die Steuer nun immer auf
    zwei Stellen gerundet in die Datenbank geschrieben, und nicht (wie
    bisher in manchen Fällen) mit 5 Nachkommastellen, was zu mehreren Bugs
    geführt hat.
    
    Es gibt einen neuen Test t/form/arap.t der die Berechnungen dieser
    beiden Funktionen testet, nicht aber das eigentliche Buchen der Belege.
    
    Mit diesem Commit sollten für zukünftige Buchungen folgende Bugs behoben
    sein:
    
    1691 - Rundung bei Berichten bei Buchungen mit MwSt inkl.
    2029 - Rundungsfehler bei Dialogbuchung
    2033 - Unterschiede in Rundungen durch taxincluded
    2094 - Rundungsprobleme in Kreditorenbuchungen: Cent "kippt" bei Zahlungseinbuchung
    2435 - Rundungsfehler in Kreditorenbuchungen (Netto vs. Brutto)
    
    Dieser Commit greift in die Eingeweide von Uraltcode ein, wo die
    diversen Stellen alle mal mit copy&paste entstanden und dann langsam
    divergiert sind, hat also hohes Fehlerpotential. Es lohnt sich, den
    DATEV-Check anzuschalten.

diff --git a/SL/AP.pm b/SL/AP.pm
index f254734ef..dddb8b3ea 100644
--- a/SL/AP.pm
+++ b/SL/AP.pm
@@ -55,6 +55,7 @@ sub post_transaction {
   my $exchangerate = 0;
 
   $form->{defaultcurrency} = $form->get_default_currency($myconfig);
+  $form->{taxincluded} = 0 unless $form->{taxincluded};
 
   ($null, $form->{department_id}) = split(/--/, $form->{department});
 
@@ -69,62 +70,17 @@ sub post_transaction {
     $form->{AP_amounts}{"amount_$i"} =
       (split(/--/, $form->{"AP_amount_$i"}))[0];
   }
+
   ($form->{AP_amounts}{payables}) = split(/--/, $form->{APselected});
   ($form->{AP_payables})          = split(/--/, $form->{APselected});
 
-  # reverse and parse amounts
-  for my $i (1 .. $form->{rowcount}) {
-    $form->{"amount_$i"} =
-      $form->round_amount(
-                         $form->parse_amount($myconfig, $form->{"amount_$i"}) *
-                           $form->{exchangerate} * -1,
-                         2);
-    $amount += ($form->{"amount_$i"} * -1);
-
-    # parse tax_$i for later
-    $form->{"tax_$i"} = $form->parse_amount($myconfig, $form->{"tax_$i"}) * -1;
-  }
-
-  # this is for ap
-  $form->{amount} = $amount;
-
-  # taxincluded doesn't make sense if there is no amount
-  $form->{taxincluded} = 0 if ($form->{amount} == 0);
-
-  for my $i (1 .. $form->{rowcount}) {
-    ($form->{"tax_id_$i"}, undef) = split /--/, $form->{"taxchart_$i"};
-
-    my $query =
-      qq|SELECT c.accno, t.taxkey, t.rate | .
-      qq|FROM tax t LEFT JOIN chart c on (c.id=t.chart_id) | .
-      qq|WHERE t.id = ? | .
-      qq|ORDER BY c.accno|;
-    my $sth = $dbh->prepare($query);
-    $sth->execute($form->{"tax_id_$i"}) || $form->dberror($query . " (" . $form->{"tax_id_$i"} . ")");
-    ($form->{AP_amounts}{"tax_$i"}, $form->{"taxkey_$i"}, $form->{"taxrate_$i"}) = $sth->fetchrow_array();
-
-    $sth->finish;
-
-    my ($tax, $diff);
-    if ($form->{taxincluded} *= 1) {
-      $tax = $form->{"amount_$i"} - ($form->{"amount_$i"} / ($form->{"taxrate_$i"} + 1));
-      $amount = $form->{"amount_$i"} - $tax;
-      $form->{"amount_$i"} = $form->round_amount($amount, 2);
-      $diff += $amount - $form->{"amount_$i"};
-      $form->{"tax_$i"} = $form->round_amount($tax, 2);
-      $form->{netamount} += $form->{"amount_$i"};
-    } else {
-      $form->{"tax_$i"} = $form->{"amount_$i"} * $form->{"taxrate_$i"};
-      $form->{netamount} += $form->{"amount_$i"};
-    }
-    $form->{total_tax} += $form->{"tax_$i"} * -1;
-  }
+  # calculate the totals while calculating and reformatting the $amount_$i and $tax_$i
+  ($form->{netamount},$form->{total_tax},$form->{invtotal}) = $form->calculate_arap('buy',$form->{taxincluded}, $form->{exchangerate});
 
   # adjust paidaccounts if there is no date in the last row
   $form->{paidaccounts}-- unless ($form->{"datepaid_$form->{paidaccounts}"});
 
   $form->{invpaid} = 0;
-  $form->{netamount} *= -1;
 
   # add payments
   for my $i (1 .. $form->{paidaccounts}) {
@@ -140,8 +96,8 @@ sub post_transaction {
   $form->{invpaid} =
     $form->round_amount($form->{invpaid} * $form->{exchangerate}, 2);
 
-  # store invoice total, this goes into ap table
-  $form->{invtotal} = $form->{netamount} + $form->{total_tax};
+  # # store invoice total, this goes into ap table
+  # $form->{invtotal} = $form->{netamount} + $form->{total_tax};
 
   # amount for total AP
   $form->{payables} = $form->{invtotal};
diff --git a/SL/AR.pm b/SL/AR.pm
index 95aba2680..af78117bb 100644
--- a/SL/AR.pm
+++ b/SL/AR.pm
@@ -69,40 +69,11 @@ sub post_transaction {
   $form->{AR_amounts}{receivables} = $form->{ARselected};
   $form->{AR}{receivables}         = $form->{ARselected};
 
-  # parsing
-  for $i (1 .. $form->{rowcount}) {
-    $form->{"amount_$i"} = $form->round_amount($form->parse_amount($myconfig, $form->{"amount_$i"}) * $form->{exchangerate}, 2);
-    $form->{amount}     += $form->{"amount_$i"};
-    $form->{"tax_$i"}    = $form->parse_amount($myconfig, $form->{"tax_$i"});
-  }
-
-  # this is for ar
-  $form->{tax}       = 0;
-  $form->{netamount} = 0;
-  $form->{total_tax} = 0;
-
-  # taxincluded doesn't make sense if there is no amount
-  $form->{taxincluded} = 0 unless $form->{amount};
-
-  for $i (1 .. $form->{rowcount}) {
-    ($form->{"tax_id_$i"}) = split /--/, $form->{"taxchart_$i"};
+  $form->{tax}       = 0; # is this still needed?
 
-    $query = qq|SELECT c.accno, t.taxkey, t.rate FROM tax t LEFT JOIN chart c ON (c.id = t.chart_id) WHERE t.id = ? ORDER BY c.accno|;
-    ($form->{AR_amounts}{"tax_$i"}, $form->{"taxkey_$i"}, $form->{"taxrate_$i"}) = selectrow_query($form, $dbh, $query, $form->{"tax_id_$i"});
-
-    if ($form->{taxincluded} *= 1) {
-      $tax = $form->{"korrektur_$i"}
-        ? $form->{"tax_$i"}
-        : $form->{"amount_$i"} - ($form->{"amount_$i"} / ($form->{"taxrate_$i"} + 1)); # should be same as taxrate * amount / (taxrate + 1)
-      $form->{"amount_$i"} = $form->round_amount($form->{"amount_$i"} - $tax, 2);
-      $form->{"tax_$i"}    = $form->round_amount($tax, 2);
-    } else {
-      $form->{"tax_$i"}    = $form->{"amount_$i"} * $form->{"taxrate_$i"} unless $form->{"korrektur_$i"};
-      $form->{"tax_$i"}    = $form->round_amount($form->{"tax_$i"} * $form->{exchangerate}, 2);
-    }
-    $form->{netamount}  += $form->{"amount_$i"};
-    $form->{total_tax}  += $form->{"tax_$i"};
-  }
+  # main calculation of rowcount loop inside Form method, amount_$i and tax_$i get formatted
+  $form->{taxincluded} = 0 unless $form->{taxincluded};
+  ($form->{netamount},$form->{total_tax},$form->{amount}) = $form->calculate_arap('sell', $form->{taxincluded}, $form->{exchangerate});
 
   # adjust paidaccounts if there is no date in the last row
   # this does not apply to stornos, where the paid field is set manually
@@ -117,7 +88,6 @@ sub post_transaction {
       $form->{datepaid}  = $form->{"datepaid_$i"};
     }
 
-    $form->{amount} = $form->{netamount} + $form->{total_tax};
   }
   $form->{paid}   = $form->round_amount($form->{paid} * ($form->{exchangerate} || 1), 2);
 
diff --git a/SL/Form.pm b/SL/Form.pm
index 9d032bf69..a61a6c8bb 100644
--- a/SL/Form.pm
+++ b/SL/Form.pm
@@ -79,6 +79,7 @@ use Template;
 use URI;
 use List::Util qw(first max min sum);
 use List::MoreUtils qw(all any apply);
+use SL::DB::Tax;
 
 use strict;
 
@@ -3446,6 +3447,82 @@ sub prepare_for_printing {
   return $self;
 }
 
+sub calculate_arap {
+  my ($self,$buysell,$taxincluded,$exchangerate,$roundplaces) = @_;
+
+  # this function is used to calculate netamount, total_tax and amount for AP and
+  # AR transactions (Kreditoren-/Debitorenbuchungen) by going over all lines
+  # (1..$rowcount)
+  # Thus it needs a fully prepared $form to work on.
+  # calculate_arap assumes $form->{amount_$i} entries still need to be parsed
+
+  # The calculated total values are all rounded (default is to 2 places) and
+  # returned as parameters rather than directly modifying form.  The aim is to
+  # make the calculation of AP and AR behave identically.  There is a test-case
+  # for this function in t/form/arap.t
+
+  # While calculating the totals $form->{amount_$i} and $form->{tax_$i} are
+  # modified and formatted and receive the correct sign for writing straight to
+  # acc_trans, depending on whether they are ar or ap.
+
+  # check parameters
+  die "taxincluded needed in Form->calculate_arap" unless defined $taxincluded;
+  die "exchangerate needed in Form->calculate_arap" unless defined $exchangerate;
+  die 'illegal buysell parameter, has to be \"buy\" or \"sell\" in Form->calculate_arap\n' unless $buysell =~ /^(buy|sell)$/;
+  $roundplaces = 2 unless $roundplaces;
+
+  my $sign = 1;  # adjust final results for writing amount to acc_trans
+  $sign = -1 if $buysell eq 'buy';
+
+  my ($netamount,$total_tax,$amount);
+
+  my $tax;
+
+  # parse and round amounts, setting correct sign for writing to acc_trans
+  for my $i (1 .. $self->{rowcount}) {
+    $self->{"amount_$i"} = $self->round_amount($self->parse_amount(\%::myconfig, $self->{"amount_$i"}) * $exchangerate * $sign, $roundplaces);
+
+    $amount += $self->{"amount_$i"} * $sign;
+  }
+
+  for my $i (1 .. $self->{rowcount}) {
+    next unless $self->{"amount_$i"};
+    ($self->{"tax_id_$i"}) = split /--/, $self->{"taxchart_$i"};
+    my $tax_id = $self->{"tax_id_$i"};
+
+    my $selected_tax = SL::DB::Manager::Tax->find_by(id => "$tax_id");
+
+    if ( $selected_tax ) {
+
+      if ( $buysell eq 'sell' ) {
+        $self->{AR_amounts}{"tax_$i"} = $selected_tax->chart->accno unless $selected_tax->taxkey == 0;
+      } else {
+        $self->{AP_amounts}{"tax_$i"} = $selected_tax->chart->accno unless $selected_tax->taxkey == 0;
+      };
+
+      $self->{"taxkey_$i"} = $selected_tax->taxkey;
+      $self->{"taxrate_$i"} = $selected_tax->rate;
+    };
+
+    ($self->{"amount_$i"}, $self->{"tax_$i"}) = $self->calculate_tax($self->{"amount_$i"},$self->{"taxrate_$i"},$taxincluded,$roundplaces);
+
+    $netamount  += $self->{"amount_$i"};
+    $total_tax  += $self->{"tax_$i"};
+
+  }
+  $amount = $netamount + $total_tax;
+
+  # due to $sign amount_$i und tax_$i already have the right sign for acc_trans
+  # but reverse sign of totals for writing amounts to ar
+  if ( $buysell eq 'buy' ) {
+    $netamount *= -1;
+    $amount    *= -1;
+    $total_tax *= -1;
+  };
+
+  return($netamount,$total_tax,$amount);
+}
+
 sub format_dates {
   my ($self, $dateformat, $longformat, @indices) = @_;
 
@@ -3558,6 +3635,39 @@ sub layout {
   return $layout;
 }
 
+sub calculate_tax {
+  # this function calculates the net amount and tax for the lines in ar, ap and
+  # gl and is used for update as well as post. When used with update the return
+  # value of amount isn't needed
+
+  # calculate_tax should always work with positive values, or rather as the user inputs them
+  # calculate_tax uses db/perl numberformat, i.e. parsed numbers
+  # convert to negative numbers (when necessary) only when writing to acc_trans
+  # the amount from $form for ap/ar/gl is currently always rounded to 2 decimals before it reaches here
+  # for post_transaction amount already contains exchangerate and correct sign and is rounded
+  # calculate_tax doesn't (need to) know anything about exchangerate
+
+  my ($self,$amount,$taxrate,$taxincluded,$roundplaces) = @_;
+
+  $roundplaces = 2 unless defined $roundplaces;
+
+  my $tax;
+
+  if ($taxincluded *= 1) {
+    # calculate tax (unrounded), subtract from amount, round amount and round tax
+    $tax       = $amount - ($amount / ($taxrate + 1)); # equivalent to: taxrate * amount / (taxrate + 1)
+    $amount    = $self->round_amount($amount - $tax, $roundplaces);
+    $tax       = $self->round_amount($tax, $roundplaces);
+  } else {
+    $tax       = $amount * $taxrate;
+    $tax       = $self->round_amount($tax, $roundplaces);
+  }
+
+  $tax = 0 unless $tax;
+
+  return ($amount,$tax);
+};
+
 1;
 
 __END__
diff --git a/bin/mozilla/ap.pl b/bin/mozilla/ap.pl
index 7b16657e5..f290c340e 100644
--- a/bin/mozilla/ap.pl
+++ b/bin/mozilla/ap.pl
@@ -483,23 +483,15 @@ sub update {
   my $count = 0;
   my (@a, $j, $totaltax);
   for my $i (1 .. $form->{rowcount}) {
-    $form->{"amount_$i"} =
-      $form->parse_amount(\%myconfig, $form->{"amount_$i"});
-    $form->{"tax_$i"} = $form->parse_amount(\%myconfig, $form->{"tax_$i"});
+    $form->{"amount_$i"} = $form->parse_amount(\%myconfig, $form->{"amount_$i"});
     if ($form->{"amount_$i"}) {
       push @a, {};
       $j = $#a;
       my ($taxkey, $rate) = split(/--/, $form->{"taxchart_$i"});
-      if ($taxkey > 1) {
-        if ($form->{taxincluded}) {
-          $form->{"tax_$i"} = $form->{"amount_$i"} / ($rate + 1) * $rate;
-        } else {
-          $form->{"tax_$i"} = $form->{"amount_$i"} * $rate;
-        }
-      } else {
-        $form->{"tax_$i"} = 0;
-      }
-      $form->{"tax_$i"} = $form->round_amount($form->{"tax_$i"}, 2);
+
+      # calculate tax exactly the same way as AP in post_transaction via form->calculate_tax
+      my $tmpnetamount;
+      ($tmpnetamount,$form->{"tax_$i"}) = $form->calculate_tax($form->{"amount_$i"},$rate,$form->{taxincluded},2);
 
       $totaltax += $form->{"tax_$i"};
       map { $a[$j]->{$_} = $form->{"${_}_$i"} } @flds;
diff --git a/bin/mozilla/ar.pl b/bin/mozilla/ar.pl
index fcd9e83c6..64efbc7e6 100644
--- a/bin/mozilla/ar.pl
+++ b/bin/mozilla/ar.pl
@@ -522,21 +522,13 @@ sub update {
 
   for my $i (1 .. $form->{rowcount}) {
     $form->{"amount_$i"} = $form->parse_amount(\%myconfig, $form->{"amount_$i"});
-    $form->{"tax_$i"} = $form->parse_amount(\%myconfig, $form->{"tax_$i"});
     if ($form->{"amount_$i"}) {
       push @a, {};
       my $j = $#a;
       my ($taxkey, $rate) = split(/--/, $form->{"taxchart_$i"});
-      if ($taxkey > 1) {
-        if ($form->{taxincluded}) {
-          $form->{"tax_$i"} = $form->{"amount_$i"} / ($rate + 1) * $rate;
-        } else {
-          $form->{"tax_$i"} = $form->{"amount_$i"} * $rate;
-        }
-      } else {
-        $form->{"tax_$i"} = 0;
-      }
-      $form->{"tax_$i"} = $form->round_amount($form->{"tax_$i"}, 2);
+
+      my $tmpnetamount;
+      ($tmpnetamount,$form->{"tax_$i"}) = $form->calculate_tax($form->{"amount_$i"},$rate,$form->{taxincluded},2);
 
       $totaltax += $form->{"tax_$i"};
       map { $a[$j]->{$_} = $form->{"${_}_$i"} } @flds;
diff --git a/bin/mozilla/gl.pl b/bin/mozilla/gl.pl
index a4ee01d3e..b1c640a2f 100644
--- a/bin/mozilla/gl.pl
+++ b/bin/mozilla/gl.pl
@@ -594,14 +594,9 @@ sub update {
         } else {
           $credittax = 1;
         }
-        if ($form->{taxincluded}) {
-          $form->{"tax_$i"} = $amount / ($rate + 1) * $rate;
-        } else {
-          $form->{"tax_$i"} = $amount * $rate;
-        }
-      } else {
-        $form->{"tax_$i"} = 0;
-      }
+      };
+      my ($tmpnetamount,$tmpdiff);
+      ($tmpnetamount,$form->{"tax_$i"},$tmpdiff) = $form->calculate_tax($amount,$rate,$form->{taxincluded} *= 1,2);
 
       for (@flds) { $a[$j]->{$_} = $form->{"${_}_$i"} }
       $count++;
@@ -1067,16 +1062,15 @@ sub post_transaction {
       } else {
         $credittax = 1;
       }
-      if ($form->{taxincluded}) {
-        $form->{"tax_$i"} = $amount / ($rate + 1) * $rate;
-        if ($debitcredit) {
-          $form->{"debit_$i"} = $form->{"debit_$i"} - $form->{"tax_$i"};
-        } else {
-          $form->{"credit_$i"} = $form->{"credit_$i"} - $form->{"tax_$i"};
-        }
+
+      my ($tmpnetamount,$tmpdiff);
+      ($tmpnetamount,$form->{"tax_$i"},$tmpdiff) = $form->calculate_tax($amount,$rate,$form->{taxincluded} *= 1,2);
+      if ($debitcredit) {
+        $form->{"debit_$i"} = $tmpnetamount;
       } else {
-        $form->{"tax_$i"} = $amount * $rate;
+        $form->{"credit_$i"} = $tmpnetamount;
       }
+
     } else {
       $form->{"tax_$i"} = 0;
     }
diff --git a/t/form/arap.t b/t/form/arap.t
new file mode 100644
index 000000000..a46e66138
--- /dev/null
+++ b/t/form/arap.t
@@ -0,0 +1,309 @@
+use strict;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+use Carp;
+use Test::Exception;
+
+# this test tests the functions calculate_arap and calculate_tax in SL/Form.pm
+# calculate_arap is used for post_invoice in AR and AP
+# calculate_tax is used in calculate_arap as well as update in ar/ap/gl and post_transaction in gl 
+
+my ($ar_tax_19, $ar_tax_7,$ar_tax_0);
+my $config = {};
+$config->{numberformat} = '1.000,00';
+
+sub reset_state {
+  my %params = @_;
+
+  $params{$_} ||= {} for qw(ar_tax_19 ar_tax_7 ar_tax_0 );
+
+  # delete rowcount lines in form, would be better to reset form completely
+  for my $hv ( 1 .. 10 ) {
+      foreach my $type ( qw(amount tax tax_id tax_chart) ) {
+          delete $::form{"$type\_$hv"};
+      };
+  };
+
+  $ar_tax_19 = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{ar_tax_19} })  || croak "No 19% tax";
+  $ar_tax_7  = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07, %{ $params{ar_tax_7} })   || croak "No 7% tax";
+  $ar_tax_0  = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00, %{ $params{ar_tax_0} })   || croak "No 0% tax";
+
+};
+
+sub arap_test {
+  my ($testcase) = @_;
+
+  reset_state;
+
+  # values from testcase
+  $::form->{taxincluded}     = $testcase->{taxincluded};
+  $::form->{currency}        = $testcase->{currency};
+  $::form->{rowcount}        = scalar @{$testcase->{lines}};
+
+  # parse exchangerate, because it was added in the same numberformat as the
+  # other amounts in the testcases
+  $testcase->{exchangerate}    = $::form->parse_amount(\%::myconfig, $testcase->{exchangerate});
+
+  foreach my $a ( 1 .. scalar @{$testcase->{lines}} ) {
+    my ($taxrate, $form_amount, $netamount, $taxamount, $totalamount) = @{ @{ $testcase->{lines} }[$a-1] };
+    my $tax;
+    if ( $taxrate == 19 ) {
+        $tax = $ar_tax_19;
+    } elsif ( $taxrate == 7 ) {
+        $tax = $ar_tax_7;
+    } elsif ( $taxrate == 0 ) {
+        $tax = $ar_tax_0;
+    } else {
+        croak "illegal taxrate $taxrate";
+    };
+
+    $::form->{"amount_$a"}   = $form_amount;
+    $::form->{"tax_$a"}      = $taxamount;  # tax according to UI, will recalculate anyway?
+    $::form->{"taxchart_$a"} = $tax->id . '--' . $tax->rate;
+
+  };
+
+  # calculate totals using lines in $::form
+  ($::form->{netamount},$::form->{total_tax},$::form->{amount}) = $::form->calculate_arap($testcase->{'buysell'}, $::form->{taxincluded}, $testcase->{'exchangerate'});
+
+  # create tests comparing calculated and expected values
+  is($::form->format_amount(\%::myconfig , $::form->{total_tax} , 2) , $testcase->{'total_taxamount'} , "total tax   = $testcase->{'total_taxamount'}");
+  is($::form->format_amount(\%::myconfig , $::form->{netamount} , 2) , $testcase->{'total_netamount'} , "netamount   = $testcase->{'total_netamount'}");
+  is($::form->format_amount(\%::myconfig , $::form->{amount}    , 2) , $testcase->{'total_amount'}    , "totalamount = $testcase->{'total_amount'}");
+  is($::form->{taxincluded}, $testcase->{'taxincluded'}, "taxincluded = $testcase->{'taxincluded'}");
+
+};
+
+sub calculate_tax_test {
+  my ($amount, $rate, $taxincluded, $net, $tax, $total, $dec) = @_;
+  # amount, rate and taxincluded are the values that we want to calculate with
+  # net, tax and total are the values that we expect, dec is the number of decimals we round to
+
+  my ($calculated_net,$calculated_tax) = $::form->calculate_tax($amount,$rate,$taxincluded,$dec);
+
+  is($tax, $calculated_tax, "calculated tax for taxincluded = $taxincluded for net $amount and rate $rate is = $calculated_tax");
+  is($calculated_net, $net, "calculated net for taxincluded = $taxincluded for net $amount and rate $rate is = $net");
+};
+
+Support::TestSetup::login();
+
+# define the various lines that can be used for the testcases
+# always use positive values for buy/sell, like in the interface
+#                   tax  input   net      tax   total  type
+my @testline1  = qw(19   56,53   47,50   9,03   56,53  sell);
+my @testline2  = qw(19   11,90   10,00   1,90   11,90  sell);
+my @testline3  = qw( 7   14,39   13,45   0,94   11,90  sell);
+my @testline4  = qw(19  133,08  133,08  25,29  158,37  sell);
+my @testline5  = qw( 0  100,00   83,00   0,00   83,00  sell);  # exchangerate of 0,83
+my @testline6  = qw(19   56,53   47,50   9,03   56,53   buy);
+my @testline7  = qw(19  309,86  309,86  58,87  368,73   buy);
+my @testline8  = qw( 7  130,00  121,50   8,50  130,00   buy);
+my @testline9  = qw( 7  121,49  121,49   8,50  129,99   buy);
+my @testline10 = qw( 7  121,50  121,50   8,51  130,01   buy);
+my @testline11 = qw(19   -2,77   -2,77  -0,53   -3,30   buy);
+my @testline12 = qw( 7   12,88   12,88   0,90   13,78   buy);
+my @testline13 = qw(19   41,93   41,93   7,97   49,90   buy);
+my @testline14 = qw(19   84,65   84,65  16,08  107,73   buy);
+my @testline15 = qw(19    8,39    8,39   1,59    9,98   buy);
+my @testline16 = qw(19  100,73   84,65  16,08  107,73   buy);
+my @testline17 = qw(19    9,99    8,39   1,60    9,99   buy);
+
+# create testcases, made up of one or more lines, with expected values
+
+my $testcase1 = {
+    lines           => [ \@testline1 ], # lines to be used in testcase
+    total_amount    => '56,53',  # expected result
+    total_netamount => '47,50',  # expected result
+    total_taxamount => '9,03',   # expected result
+    # invoice parameters:
+    taxincluded     => 1,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'sell',
+};
+
+my $testcase2 = {
+    lines           => [ \@testline1, \@testline2, \@testline3 ],
+    total_amount    => '82,82',
+    total_netamount => '70,95',
+    total_taxamount => '11,87',
+    taxincluded     => 1,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'sell',
+};
+
+my $testcase3 = {
+    lines           => [ \@testline4 ],
+    total_amount    => '158,37',
+    total_netamount => '133,08',
+    total_taxamount => '25,29',
+    taxincluded     => 0,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'sell',
+};
+
+my $testcase4 = {
+    lines           => [ \@testline5 ],
+    total_amount    => '83,00',
+    total_netamount => '83,00',
+    total_taxamount => '0,00',
+    taxincluded     => 0,
+    exchangerate    => '0,83',
+    currency        => 'USD',
+    buysell         => 'sell',
+};
+
+my $testcase6 = {
+    lines           => [ \@testline6 ],
+    total_amount    => '56,53',
+    total_netamount => '47,50',
+    total_taxamount => '9,03',
+    taxincluded     => 1,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'buy',
+};
+
+my $testcase7 = {
+    lines           => [ \@testline7 ],
+    total_netamount => '309,86',
+    total_taxamount => '58,87',
+    total_amount    => '368,73',
+    taxincluded     => 0,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'buy',
+};
+
+my $testcase8 = {
+    lines           => [ \@testline8 ],
+    total_netamount => '121,50',
+    total_taxamount => '8,50',
+    total_amount    => '130,00',
+    taxincluded     => 1,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'buy',
+};
+
+my $testcase9 = {
+    lines           => [ \@testline9 ],
+    total_netamount => '121,49',
+    total_taxamount => '8,50',
+    total_amount    => '129,99',
+    taxincluded     => 0,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'buy',
+};
+
+my $testcase10 = {
+    lines           => [ \@testline10 ],
+    total_netamount => '121,50',
+    total_taxamount => '8,51',
+    total_amount    => '130,01',
+    taxincluded     => 0,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'buy',
+};
+
+my $testcase11 = {
+    # mixed invoices, -2,77€ net with 19% as credit note, 12,88€ net with 7%
+    lines           => [ \@testline11 , \@testline12 ],
+    total_netamount => '10,11',
+    total_taxamount => '0,37',
+    total_amount    => '10,48',
+    taxincluded     => 0,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'buy',
+};
+
+my $testcase12 = {
+    # ap transaction, example from bug 2435
+    lines           => [ \@testline13 ],
+    total_netamount => '41,93',
+    total_taxamount => '7,97',
+    total_amount    => '49,90',
+    taxincluded     => 0,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'buy',
+};
+
+my $testcase13 = {
+    # ap transaction, example from bug 2094, tax not included
+    lines           => [ \@testline14 , \@testline15 ],
+    total_netamount => '93,04',
+    total_taxamount => '17,67',
+    total_amount    => '110,71',
+    taxincluded     => 0,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'buy',
+};
+
+my $testcase14 = {
+    # ap transaction, example from bug 2094, tax included
+    lines           => [ \@testline16 , \@testline17 ],
+    total_netamount => '93,04',
+    total_taxamount => '17,68',
+    total_amount    => '110,72',
+    taxincluded     => 1,
+    exchangerate    => 1,
+    currency        => 'EUR',
+    buysell         => 'buy',
+};
+
+# run tests
+arap_test($testcase1);
+arap_test($testcase2);
+arap_test($testcase3);
+arap_test($testcase4);
+arap_test($testcase6);
+arap_test($testcase7);
+arap_test($testcase8);
+arap_test($testcase9);
+arap_test($testcase10);
+arap_test($testcase11);
+arap_test($testcase12);
+arap_test($testcase13);
+arap_test($testcase14);
+
+# tests for calculate_tax:
+
+# tests for 1 Cent, calculated tax should be 0
+calculate_tax_test(0.01,0.07,1,0.01,0.00,0.01,2);
+calculate_tax_test(0.01,0.19,1,0.01,0.00,0.01,2);
+
+# tax for rate 7% taxincluded flips at 0.08
+calculate_tax_test(0.07,0.07,1,0.07,0.00,0.07,2);
+calculate_tax_test(0.08,0.07,1,0.07,0.01,0.08,2);
+
+# tax for rate 7% taxexcluded flips at 0.08
+calculate_tax_test(0.07,0.07,0,0.07,0.00,0.07,2);
+calculate_tax_test(0.08,0.07,0,0.08,0.01,0.09,2);
+
+# tax for rate 19% taxexcluded flips at 0.03
+calculate_tax_test(0.02,0.19,0,0.02,0.00,0.02,2);
+calculate_tax_test(0.03,0.19,0,0.03,0.01,0.04,2);
+
+# tax for rate 19% taxincluded flips at 0.04
+calculate_tax_test(0.03,0.19,1,0.03,0.00,0.03,2);
+calculate_tax_test(0.04,0.19,1,0.03,0.01,0.04,2);
+
+calculate_tax_test(8.39,0.19,0,8.39,1.59,9.98,2);
+calculate_tax_test(9.99,0.19,1,8.39,1.60,9.99,2);
+
+calculate_tax_test(11.21,0.07,0,11.21,0.78,11.99,2);
+calculate_tax_test(11.22,0.07,0,11.22,0.79,12.01,2);
+calculate_tax_test(12.00,0.07,1,11.21,0.79,12.00,2);
+
+done_testing(82);
+
+1;
