Kosmetik Payment-Helper
[kivitendo-erp.git] / SL / DB / Helper / Payment.pm
index 9ac3a67..028f76f 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 
 use parent qw(Exporter);
 our @EXPORT = qw(pay_invoice);
-our @EXPORT_OK = qw(skonto_date skonto_charts amount_less_skonto within_skonto_period percent_skonto reference_account reference_amount transactions open_amount open_percent remaining_skonto_days skonto_amount check_skonto_configuration valid_skonto_amount get_payment_suggestions validate_payment_type open_sepa_transfer_amount get_payment_select_options_for_bank_transaction);
+our @EXPORT_OK = qw(skonto_date skonto_charts amount_less_skonto within_skonto_period percent_skonto reference_account reference_amount open_amount open_percent remaining_skonto_days skonto_amount check_skonto_configuration valid_skonto_amount get_payment_suggestions validate_payment_type open_sepa_transfer_amount get_payment_select_options_for_bank_transaction exchangerate forex);
 our %EXPORT_TAGS = (
   "ALL" => [@EXPORT, @EXPORT_OK],
 );
@@ -15,6 +15,8 @@ use DateTime;
 use SL::DATEV qw(:CONSTANTS);
 use SL::Locale::String qw(t8);
 use List::Util qw(sum);
+use SL::DB::Exchangerate;
+use SL::DB::Currency;
 use Carp;
 
 #
@@ -28,17 +30,39 @@ sub pay_invoice {
 
   my $is_sales = ref($self) eq 'SL::DB::Invoice';
   my $mult = $is_sales ? 1 : -1;  # multiplier for getting the right sign depending on ar/ap
-
-  my $paid_amount = 0; # the amount that will be later added to $self->paid
+  my @new_acc_ids;
+  my $paid_amount = 0; # the amount that will be later added to $self->paid, should be in default currency
 
   # default values if not set
   $params{payment_type} = 'without_skonto' unless $params{payment_type};
   validate_payment_type($params{payment_type});
 
-  # check for required parameters
+  # check for required parameters and optional params depending on payment_type
   Common::check_params(\%params, qw(chart_id transdate));
+  if ( $params{'payment_type'} eq 'without_skonto' && abs($params{'amount'}) < 0) {
+    croak "invalid amount for payment_type 'without_skonto': $params{'amount'}\n";
+  }
+  if ($params{'payment_type'} eq 'free_skonto') {
+    # we dont like too much automagic for this payment type.
+    # we force caller input for amount and skonto amount
+    Common::check_params(\%params, qw(amount skonto_amount));
+    # secondly we dont want to handle credit notes and purchase credit notes
+    croak("Cannot use 'free skonto' for credit or debit notes") if ($params{amount} <= 0 || $params{skonto_amount} <= 0);
+    # both amount have to be rounded
+    $params{skonto_amount} = _round($params{skonto_amount});
+    $params{amount}        = _round($params{amount});
+    # lastly skonto_amount has to be smaller than the open invoice amount or payment amount ;-)
+    if ($params{skonto_amount} > abs($self->open_amount) || $params{skonto_amount} > $params{amount}) {
+      croak("Skonto amount higher than the payment or invoice amount");
+    }
+  }
 
-  my $transdate_obj = $::locale->parse_date_to_object($params{transdate});
+  my $transdate_obj;
+  if (ref($params{transdate}) eq 'DateTime') {
+    $transdate_obj = $params{transdate};
+  } else {
+    $transdate_obj = $::locale->parse_date_to_object($params{transdate});
+  };
   croak t8('Illegal date') unless ref $transdate_obj;
 
   # check for closed period
@@ -52,9 +76,30 @@ sub pay_invoice {
     croak t8('Cannot post transaction above the maximum future booking date!') if $transdate_obj > DateTime->now->add( days => $::instance_conf->get_max_future_booking_interval );
   };
 
-  # input checks:
-  if ( $params{'payment_type'} eq 'without_skonto' ) {
-    croak "invalid amount for payment_type 'without_skonto': $params{'amount'}\n" unless abs($params{'amount'}) > 0;
+  # currency is either passed or use the invoice currency if it differs from the default currency
+  # TODO remove
+  my ($exchangerate,$currency);
+  if ($params{currency} || $params{currency_id}) {
+    if ($params{currency} || $params{currency_id} ) { # currency was specified
+      $currency = SL::DB::Manager::Currency->find_by(name => $params{currency}) || SL::DB::Manager::Currency->find_by(id => $params{currency_id});
+    } else { # use invoice currency
+      $currency = SL::DB::Manager::Currency->find_by(id => $self->currency_id);
+    };
+    die "no currency" unless $currency;
+    if ($currency->id == $::instance_conf->get_currency_id) {
+      $exchangerate = 1;
+    } else {
+      my $rate = SL::DB::Manager::Exchangerate->find_by(currency_id => $currency->id,
+                                                        transdate   => $transdate_obj,
+                                                       );
+      if ($rate) {
+        $exchangerate = $is_sales ? $rate->buy : $rate->sell;
+      } else {
+        die "No exchange rate for " . $transdate_obj->to_kivitendo;
+      };
+    };
+  } else { # no currency param given or currency is the same as default_currency
+    $exchangerate = 1;
   };
 
   # options with_skonto_pt and difference_as_skonto don't require the parameter
@@ -62,7 +107,7 @@ sub pay_invoice {
   if ( $params{'payment_type'} eq 'difference_as_skonto' ) {
     croak "amount $params{amount} doesn't match open amount " . $self->open_amount . ", diff = " . ($params{amount}-$self->open_amount) if $params{amount} && abs($self->open_amount - $params{amount} ) > 0.0000001;
   } elsif ( $params{'payment_type'} eq 'with_skonto_pt' ) {
-    croak "amount $params{amount} doesn't match amount less skonto: " . $self->open_amount . "\n" if $params{amount} && abs($self->amount_less_skonto - $params{amount} ) > 0.0000001;
+    croak "amount $params{amount} doesn't match amount less skonto: " . $self->amount_less_skonto . "\n" if $params{amount} && abs($self->amount_less_skonto - $params{amount} ) > 0.0000001;
     croak "payment type with_skonto_pt can't be used if payments have already been made" if $self->paid != 0;
   };
 
@@ -75,30 +120,31 @@ sub pay_invoice {
 
   # account where money is paid to/from: bank account or cash
   my $account_bank = SL::DB::Manager::Chart->find_by(id => $params{chart_id});
-  croak "can't find bank account" unless ref $account_bank;
+  croak "can't find bank account with id " . $params{chart_id} unless ref $account_bank;
 
   my $reference_account = $self->reference_account;
   croak "can't find reference account (link = AR/AP) for invoice" unless ref $reference_account;
 
-  my $memo   = $params{'memo'}   || '';
-  my $source = $params{'source'} || '';
+  my $memo   = $params{memo}   // '';
+  my $source = $params{source} // '';
 
-  my $rounded_params_amount = _round( $params{amount} );
+  my $rounded_params_amount = _round( $params{amount} ); # / $exchangerate);
+  my $fx_gain_loss_amount = 0; # for fx_gain and fx_loss
 
   my $db = $self->db;
-  $db->do_transaction(sub {
+  $db->with_transaction(sub {
     my $new_acc_trans;
 
     # all three payment type create 1 AR/AP booking (the paid part)
-    # difference_as_skonto creates n skonto bookings (1 for each tax type)
-    # with_skonto_pt creates 1 bank booking and n skonto bookings (1 for each tax type)
+    # difference_as_skonto creates n skonto bookings (1 for each buchungsgruppe type)
+    # with_skonto_pt creates 1 bank booking and n skonto bookings (1 for each buchungsgruppe type)
     # without_skonto creates 1 bank booking
 
     # as long as there is no automatic tax, payments are always booked with
     # taxkey 0
 
     unless ( $params{payment_type} eq 'difference_as_skonto' ) {
-      # cases with_skonto_pt and without_skonto
+      # cases with_skonto_pt, free_skonto and without_skonto
 
       # for case with_skonto_pt we need to know the corrected amount at this
       # stage if we are going to use $params{amount}
@@ -107,30 +153,77 @@ sub pay_invoice {
       $pay_amount = $self->amount_less_skonto if $params{payment_type} eq 'with_skonto_pt';
 
       # bank account and AR/AP
-      $paid_amount += $pay_amount;
+      $paid_amount += $pay_amount * $exchangerate;
+
+      my $amount = (-1 * $pay_amount) * $mult;
+
 
       # total amount against bank, do we already know this by now?
       $new_acc_trans = SL::DB::AccTransaction->new(trans_id   => $self->id,
                                                    chart_id   => $account_bank->id,
                                                    chart_link => $account_bank->link,
-                                                   amount     => (-1 * $pay_amount) * $mult,
+                                                   amount     => $amount,
                                                    transdate  => $transdate_obj,
                                                    source     => $source,
                                                    memo       => $memo,
+                                                   project_id => $params{project_id} ? $params{project_id} : undef,
                                                    taxkey     => 0,
                                                    tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
       $new_acc_trans->save;
-    };
 
-    if ( $params{payment_type} eq 'difference_as_skonto' or $params{payment_type} eq 'with_skonto_pt' ) {
+      push @new_acc_ids, $new_acc_trans->acc_trans_id;
+      # deal with fxtransaction
+      if ( $self->currency_id != $::instance_conf->get_currency_id ) {
+        my $fxamount = _round($amount - ($amount * $exchangerate));
+        $new_acc_trans = SL::DB::AccTransaction->new(trans_id       => $self->id,
+                                                     chart_id       => $account_bank->id,
+                                                     chart_link     => $account_bank->link,
+                                                     amount         => $fxamount * -1,
+                                                     transdate      => $transdate_obj,
+                                                     source         => $source,
+                                                     memo           => $memo,
+                                                     taxkey         => 0,
+                                                     fx_transaction => 1,
+                                                     tax_id         => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+        $new_acc_trans->save;
+        push @new_acc_ids, $new_acc_trans->acc_trans_id;
+        # if invoice exchangerate differs from exchangerate of payment
+        # deal with fxloss and fxamount
+        if ($self->exchangerate and $self->exchangerate != 1 and $self->exchangerate != $exchangerate) {
+          my $fxgain_chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxgain_accno_id) || die "Can't determine fxgain chart";
+          my $fxloss_chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxloss_accno_id) || die "Can't determine fxloss chart";
+          my $gain_loss_amount = _round($amount * ($exchangerate - $self->exchangerate ) * -1,2);
+          my $gain_loss_chart = $gain_loss_amount > 0 ? $fxgain_chart : $fxloss_chart;
+          $fx_gain_loss_amount = $gain_loss_amount;
+
+          $new_acc_trans = SL::DB::AccTransaction->new(trans_id       => $self->id,
+                                                       chart_id       => $gain_loss_chart->id,
+                                                       chart_link     => $gain_loss_chart->link,
+                                                       amount         => $gain_loss_amount,
+                                                       transdate      => $transdate_obj,
+                                                       source         => $source,
+                                                       memo           => $memo,
+                                                       taxkey         => 0,
+                                                       fx_transaction => 0,
+                                                       tax_id         => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+          $new_acc_trans->save;
+          push @new_acc_ids, $new_acc_trans->acc_trans_id;
+
+        }
+      }
+    }
+    # better everything except without_skonto
+    if ($params{payment_type} eq 'difference_as_skonto' or $params{payment_type} eq 'with_skonto_pt'
+        or $params{payment_type} eq 'free_skonto' ) {
 
       my $total_skonto_amount;
       if ( $params{payment_type} eq 'with_skonto_pt' ) {
         $total_skonto_amount = $self->skonto_amount;
       } elsif ( $params{payment_type} eq 'difference_as_skonto' ) {
         $total_skonto_amount = $self->open_amount;
-      };
-
+      } elsif ( $params{payment_type} eq 'free_skonto') {
+        $total_skonto_amount = $params{skonto_amount};
+      }
       my @skonto_bookings = $self->skonto_charts($total_skonto_amount);
 
       # error checking:
@@ -148,21 +241,25 @@ sub pay_invoice {
         my $amount = -1 * $skonto_booking->{skonto_amount};
         $new_acc_trans = SL::DB::AccTransaction->new(trans_id   => $self->id,
                                                      chart_id   => $skonto_booking->{'chart_id'},
-                                                     chart_link => SL::DB::Manager::Chart->find_by(id => $skonto_booking->{'chart_id'})->{'link'},
+                                                     chart_link => SL::DB::Manager::Chart->find_by(id => $skonto_booking->{'chart_id'})->link,
                                                      amount     => $amount * $mult,
                                                      transdate  => $transdate_obj,
                                                      source     => $params{source},
                                                      taxkey     => 0,
                                                      tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+
+        # the acc_trans entries are saved individually, not added to $self and then saved all at once
         $new_acc_trans->save;
+        push @new_acc_ids, $new_acc_trans->acc_trans_id;
 
         $reference_amount -= abs($amount);
-        $paid_amount      += -1 * $amount;
+        $paid_amount      += -1 * $amount * $exchangerate;
         $skonto_amount_check -= $skonto_booking->{'skonto_amount'};
       };
-      die "difference_as_skonto calculated incorrectly, sum of calculated payments doesn't add up to open amount $total_open_amount, reference_amount = $reference_amount\n" unless _round($reference_amount) == 0;
-
-    };
+      if ( $params{payment_type} eq 'difference_as_skonto' ) {
+          die "difference_as_skonto calculated incorrectly, sum of calculated payments doesn't add up to open amount $total_open_amount, reference_amount = $reference_amount\n" unless _round($reference_amount) == 0;
+      }
+    }
 
     my $arap_amount = 0;
 
@@ -175,79 +272,137 @@ sub pay_invoice {
       # with_skonto_pt for completely unpaid invoices we just use the value
       # from the invoice
       $arap_amount = $total_open_amount;
-    };
+    } elsif ( $params{payment_type} eq 'free_skonto' ) {
+      # we forced positive values and forced rounding at the beginning
+      # therefore the above comment can be safely applied for this payment type
+      $arap_amount = $params{amount} + $params{skonto_amount};
+    }
 
     # regardless of payment_type there is always only exactly one arap booking
     # TODO: compare $arap_amount to running total
     my $arap_booking= SL::DB::AccTransaction->new(trans_id   => $self->id,
                                                   chart_id   => $reference_account->id,
                                                   chart_link => $reference_account->link,
-                                                  amount     => $arap_amount * $mult,
+                                                  amount     => _round($arap_amount * $mult * $exchangerate - $fx_gain_loss_amount),
                                                   transdate  => $transdate_obj,
                                                   source     => '', #$params{source},
                                                   taxkey     => 0,
                                                   tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
     $arap_booking->save;
+    push @new_acc_ids, $arap_booking->acc_trans_id;
+
+    # hook for invoice_for_advance_payment DATEV always pairs, acc_trans_id has to be higher than arap_booking ;-)
+    if ($self->invoice_type eq 'invoice_for_advance_payment') {
+      my $clearing_chart = SL::DB::Chart->new(id => $::instance_conf->get_advance_payment_clearing_chart_id)->load;
+      die "No Clearing Chart for Advance Payment" unless ref $clearing_chart eq 'SL::DB::Chart';
+
+      # what does ptc say
+      my %inv_calc = $self->calculate_prices_and_taxes();
+      my @trans_ids = keys %{ $inv_calc{amounts} };
+      die "Invalid state for advance payment more than one trans_id" if (scalar @trans_ids > 1);
+      my $entry = delete $inv_calc{amounts}{$trans_ids[0]};
+      my $tax;
+      if ($entry->{tax_id}) {
+        $tax = SL::DB::Manager::Tax->find_by(id => $entry->{tax_id}); # || die "Can't find tax with id " . $entry->{tax_id};
+      }
+      if ($tax and $tax->rate != 0) {
+        my ($netamount, $taxamount);
+        my $roundplaces = 2;
+        # we dont have a clue about skonto, that's why we use $arap_amount as taxincluded
+        ($netamount, $taxamount) = Form->calculate_tax($arap_amount, $tax->rate, 1, $roundplaces);
+        # for debugging database set
+        my $fullmatch = $netamount == $entry->{amount} ? '::netamount total true' : '';
+        my $transfer_chart = $tax->taxkey == 2 ? SL::DB::Chart->new(id => $::instance_conf->get_advance_payment_taxable_7_id)->load
+                          :  $tax->taxkey == 3 ? SL::DB::Chart->new(id => $::instance_conf->get_advance_payment_taxable_19_id)->load
+                          :  undef;
+        die "No Transfer Chart for Advance Payment" unless ref $transfer_chart eq 'SL::DB::Chart';
+
+        my $arap_full_booking= SL::DB::AccTransaction->new(trans_id   => $self->id,
+                                                           chart_id   => $clearing_chart->id,
+                                                           chart_link => $clearing_chart->link,
+                                                           amount     => $arap_amount * -1, # full amount
+                                                           transdate  => $transdate_obj,
+                                                           source     => 'Automatic Tax Booking for Payment in Advance' . $fullmatch,
+                                                           taxkey     => 0,
+                                                           tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+        $arap_full_booking->save;
+        push @new_acc_ids, $arap_full_booking->acc_trans_id;
+
+        my $arap_tax_booking= SL::DB::AccTransaction->new(trans_id   => $self->id,
+                                                          chart_id   => $transfer_chart->id,
+                                                          chart_link => $transfer_chart->link,
+                                                          amount     => _round($netamount), # full amount
+                                                          transdate  => $transdate_obj,
+                                                          source     => 'Automatic Tax Booking for Payment in Advance' . $fullmatch,
+                                                          taxkey     => $tax->taxkey,
+                                                          tax_id     => $tax->id);
+        $arap_tax_booking->save;
+        push @new_acc_ids, $arap_tax_booking->acc_trans_id;
+
+        my $tax_booking= SL::DB::AccTransaction->new(trans_id   => $self->id,
+                                                     chart_id   => $tax->chart_id,
+                                                     chart_link => $tax->chart->link,
+                                                     amount     => _round($taxamount),
+                                                     transdate  => $transdate_obj,
+                                                     source     => 'Automatic Tax Booking for Payment in Advance' . $fullmatch,
+                                                     taxkey     => 0,
+                                                     tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
 
-    $self->paid($self->paid+$paid_amount) if $paid_amount;
+        $tax_booking->save;
+        push @new_acc_ids, $tax_booking->acc_trans_id;
+      }
+    }
+    $fx_gain_loss_amount *= -1 if $self->is_sales;
+    $self->paid($self->paid + _round($paid_amount) + $fx_gain_loss_amount) if $paid_amount;
     $self->datepaid($transdate_obj);
     $self->save;
 
-  my $datev_check = 0;
-  if ( $is_sales )  {
-    if ( (  $self->invoice && $::instance_conf->get_datev_check_on_sales_invoice  ) ||
-         ( !$self->invoice && $::instance_conf->get_datev_check_on_ar_transaction )) {
-      $datev_check = 1;
-    };
-  } else {
-    if ( (  $self->invoice && $::instance_conf->get_datev_check_on_purchase_invoice ) ||
-         ( !$self->invoice && $::instance_conf->get_datev_check_on_ap_transaction   )) {
-      $datev_check = 1;
-    };
-  };
+    # make sure transactions will be reloaded the next time $self->transactions
+    # is called, as pay_invoice saves the acc_trans objects individually rather
+    # than adding them to the transaction relation array.
+    $self->forget_related('transactions');
+
+    my $datev_check = 0;
+    if ( $is_sales )  {
+      if ( (  $self->invoice && $::instance_conf->get_datev_check_on_sales_invoice  ) ||
+           ( !$self->invoice && $::instance_conf->get_datev_check_on_ar_transaction )) {
+        $datev_check = 1;
+      }
+    } else {
+      if ( (  $self->invoice && $::instance_conf->get_datev_check_on_purchase_invoice ) ||
+           ( !$self->invoice && $::instance_conf->get_datev_check_on_ap_transaction   )) {
+        $datev_check = 1;
+      }
+    }
 
-  if ( $datev_check ) {
+    if ( $datev_check ) {
 
-    my $datev = SL::DATEV->new(
-      exporttype => DATEV_ET_BUCHUNGEN,
-      format     => DATEV_FORMAT_KNE,
-      dbh        => $db->dbh,
-      trans_id   => $self->{id},
-    );
+      my $datev = SL::DATEV->new(
+        dbh        => $db->dbh,
+        trans_id   => $self->{id},
+      );
 
-    $datev->clean_temporary_directories;
-    $datev->export;
+      $datev->generate_datev_data;
 
-    if ($datev->errors) {
-      # this exception should be caught by do_transaction, which handles the rollback
-      die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
+      if ($datev->errors) {
+        # this exception should be caught by with_transaction, which handles the rollback
+        die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
+      }
     }
-  };
 
-  }) || die t8('error while paying invoice #1 : ', $self->invnumber) . $db->error . "\n";
+    1;
 
-  return 1;
-};
+  }) || die t8('error while paying invoice #1 : ', $self->invnumber) . $db->error . "\n";
+  return wantarray ? @new_acc_ids : 1;
+}
 
 sub skonto_date {
 
   my $self = shift;
 
-  my $is_sales = ref($self) eq 'SL::DB::Invoice';
-
-  my $skonto_date;
-
-  if ( $is_sales ) {
-    return undef unless ref $self->payment_terms;
-    return undef unless $self->payment_terms->terms_skonto > 0;
-    $skonto_date = DateTime->from_object(object => $self->transdate)->add(days => $self->payment_terms->terms_skonto);
-  } else {
-    return undef unless ref $self->vendor->payment_terms;
-    return undef unless $self->vendor->payment_terms->terms_skonto > 0;
-    $skonto_date = DateTime->from_object(object => $self->transdate)->add(days => $self->vendor->payment_terms->terms_skonto);
-  };
-
-  return $skonto_date;
+  return undef unless ref $self->payment_terms;
+  return undef unless $self->payment_terms->terms_skonto > 0;
+  return DateTime->from_object(object => $self->transdate)->add(days => $self->payment_terms->terms_skonto);
 };
 
 sub reference_account {
@@ -300,7 +455,7 @@ sub open_amount {
   # if the difference is 0.01 Cent this may end up as 0.009999999999998
   # numerically, so round this value when checking for cent threshold >= 0.01
 
-  return $self->amount - $self->paid;
+  return ($self->amount // 0) - ($self->paid // 0);
 };
 
 sub open_percent {
@@ -337,31 +492,23 @@ sub remaining_skonto_days {
 sub percent_skonto {
   my $self = shift;
 
-  my $is_sales = ref($self) eq 'SL::DB::Invoice';
-
   my $percent_skonto = 0;
 
-  if ( $is_sales ) {
-    return undef unless ref $self->payment_terms;
-    return undef unless $self->payment_terms->percent_skonto > 0;
-    $percent_skonto = $self->payment_terms->percent_skonto;
-  } else {
-    return undef unless ref $self->vendor->payment_terms;
-    return undef unless $self->vendor->payment_terms->terms_skonto > 0;
-    $percent_skonto = $self->vendor->payment_terms->percent_skonto;
-  };
+  return undef unless ref $self->payment_terms;
+  return undef unless $self->payment_terms->percent_skonto > 0;
+  $percent_skonto = $self->payment_terms->percent_skonto;
 
   return $percent_skonto;
 };
 
 sub amount_less_skonto {
   # amount that has to be paid if skonto applies, always return positive rounded values
+  # no, rare case, but credit_notes and negative ap have negative amounts
+  # and therefore this comment may be misguiding
   # the result is rounded so we can directly compare it with the user input
   my $self = shift;
 
-  my $is_sales = ref($self) eq 'SL::DB::Invoice';
-
-  my $percent_skonto = $self->percent_skonto;
+  my $percent_skonto = $self->percent_skonto || 0;
 
   return _round($self->amount - ( $self->amount * $percent_skonto) );
 
@@ -374,13 +521,19 @@ sub check_skonto_configuration {
 
   my $skonto_configured = 1; # default is assume skonto works
 
-  my $transactions = $self->transactions;
-  foreach my $transaction (@{ $transactions }) {
+  my $transactions = $self->transactions;
+  foreach my $transaction (@{ $self->transactions }) {
     # find all transactions with an AR_amount or AP_amount link
-    my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->{taxkey}]);
+    my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->taxkey, id => $transaction->tax_id ]);
+
+    # acc_trans entries for the taxes (chart_link == A[RP]_tax) often
+    # have combinations of taxkey & tax_id that don't exist in
+    # tax. Those must be skipped.
+    next if !$tax && ($transaction->chart_link !~ m{A[RP]_amount});
+
     croak "no tax for taxkey " . $transaction->{taxkey} unless ref $tax;
 
-    $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->{chart_link}) };
+    $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->chart_link) };
     if ( $is_sales && $transaction->{chartlinks}->{AR_amount} ) {
       $skonto_configured = 0 unless $tax->skonto_sales_chart_id;
     } elsif ( !$is_sales && $transaction->{chartlinks}->{AP_amount}) {
@@ -429,8 +582,7 @@ sub skonto_charts {
   # TODO: check whether there are negative values in invoice / acc_trans ... credited items
 
   # don't check whether skonto applies, because user may want to override this
-  # return undef unless $self->percent_skonto;  # for is_sales
-  # return undef unless $self->vendor->payment_terms->percent_skonto;  # for purchase
+  # return undef unless $self->percent_skonto;
 
   my $is_sales = ref($self) eq 'SL::DB::Invoice';
 
@@ -448,8 +600,8 @@ sub skonto_charts {
 
   my $reference_ARAP_amount = 0;
 
-  my $transactions = $self->transactions;
-  foreach my $transaction (@{ $transactions }) {
+  my $transactions = $self->transactions;
+  foreach my $transaction (@{ $self->transactions }) {
     # find all transactions with an AR_amount or AP_amount link
     $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->{chart_link}) };
     # second condition is that we can determine an automatic Skonto account for each AR_amount entry
@@ -458,7 +610,8 @@ sub skonto_charts {
         # $reference_ARAP_amount += $transaction->{amount} * $mult;
 
         # quick hack that works around problem of non-unique tax keys in SKR04
-        my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->{taxkey}]);
+        # ? use tax_id in acc_trans
+        my $tax = SL::DB::Manager::Tax->get_first( where => [id => $transaction->{tax_id}]);
         croak "no tax for taxkey " . $transaction->{taxkey} unless ref $tax;
 
         if ( $is_sales ) {
@@ -493,8 +646,8 @@ sub skonto_charts {
         };
 
         push @skonto_charts, $rec;
-      };
-  };
+      }
+  }
 
   # if the rounded sum of all rounding_errors reaches 0.01 this sum is
   # subtracted from the largest skonto_amount
@@ -544,29 +697,44 @@ sub valid_skonto_amount {
 sub get_payment_select_options_for_bank_transaction {
   my ($self, $bt_id, %params) = @_;
 
-  my $bt = SL::DB::Manager::BankTransaction->find_by( id => $bt_id );
-  die unless $bt;
-
-  my $open_amount = $self->open_amount;
 
+  # CAVEAT template code expects with_skonto_pt at position 1 for visual help
+  # due to skonto_charts, we cannot offer skonto for credit notes and neg ap
+  my $skontoable = $self->amount > 0 ? 1 : 0;
   my @options;
-  if ( $open_amount &&                   # invoice amount not 0
-       $self->skonto_date &&             # check whether skonto applies
-       abs(abs($self->amount_less_skonto) - abs($bt->amount)) < 0.01 &&
-       $self->check_skonto_configuration) {
-         if ( $self->within_skonto_period($bt->transdate) ) {
-           push(@options, { payment_type => 'without_skonto', display => t8('without skonto') });
-           push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt'), selected => 1 });
-         } else {
-           push(@options, { payment_type => 'without_skonto', display => t8('without skonto') }, selected => 1 );
-           push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt')});
-         };
-  };
-
+  if(!$self->skonto_date) {
+    push(@options, { payment_type => 'without_skonto', display => t8('without skonto'), selected => 1 });
+    # wrong call to presenter or not implemented? disabled option is ignored
+    # push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt'), disabled => 1 });
+    push(@options, { payment_type => 'free_skonto', display => t8('free skonto') }) if $skontoable;
+    return @options;
+  }
+  # valid skonto date, check if skonto is preferred
+  my $bt = SL::DB::BankTransaction->new(id => $bt_id)->load;
+  if ($self->skonto_date && $self->within_skonto_period($bt->transdate)) {
+    push(@options, { payment_type => 'without_skonto', display => t8('without skonto') });
+    push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt'), selected => 1 }) if $skontoable;
+  } else {
+    push(@options, { payment_type => 'without_skonto', display => t8('without skonto') , selected => 1 });
+    push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt')}) if $skontoable;
+  }
+  push(@options, { payment_type => 'free_skonto', display => t8('free skonto') }) if $skontoable;
   return @options;
+}
 
-};
+sub exchangerate {
+  my ($self) = @_;
 
+  return 1 if $self->currency_id == $::instance_conf->get_currency_id;
+
+  die "transdate isn't a DateTime object:" . ref($self->transdate) unless ref($self->transdate) eq 'DateTime';
+  my $rate = SL::DB::Manager::Exchangerate->find_by(currency_id => $self->currency_id,
+                                                    transdate   => $self->transdate,
+                                                   );
+  return undef unless $rate;
+
+  return $self->is_sales ? $rate->buy : $rate->sell; # also undef if not defined
+};
 
 sub get_payment_suggestions {
 
@@ -606,24 +774,27 @@ sub get_payment_suggestions {
   return 1;
 };
 
-sub transactions {
-  my ($self) = @_;
-
-  return unless $self->id;
-
-  require SL::DB::AccTransaction;
-  SL::DB::Manager::AccTransaction->get_all(query => [ trans_id => $self->id ]);
-}
+# locales for payment type
+#
+# $main::locale->text('without_skonto')
+# $main::locale->text('with_skonto_pt')
+# $main::locale->text('difference_as_skonto')
+#
 
 sub validate_payment_type {
   my $payment_type = shift;
 
-  my %allowed_payment_types = map { $_ => 1 } qw(without_skonto with_skonto_pt difference_as_skonto);
+  my %allowed_payment_types = map { $_ => 1 } qw(without_skonto with_skonto_pt difference_as_skonto free_skonto);
   croak "illegal payment type: $payment_type, must be one of: " . join(' ', keys %allowed_payment_types) unless $allowed_payment_types{ $payment_type };
 
   return 1;
 }
 
+sub forex {
+  my ($self) = @_;
+  $self->currency_id == $::instance_conf->get_currency_id ? return 0 : return 1;
+};
+
 sub _round {
   my $value = shift;
   my $num_dec = 2;
@@ -660,6 +831,20 @@ Create a payment booking for an existing invoice object (type ar/ap/is/ir) via
 a configured bank account.
 
 This function deals with all the acc_trans entries and also updates paid and datepaid.
+The params C<transdate> and C<chart_id> are mandantory.
+If the default payment ('without_skonto') is used the param amount is also
+mandantory.
+If the payment type ('free_skonto') is used the number params skonto_amount and amount
+are as well mandantory and need to be positive. Furthermore the skonto amount has
+to be lower than the payment or open invoice amount.
+
+Transdate can either be a date object or a date string.
+Chart_id is the id of the payment booking chart.
+Amount is either a positive or negative number, but never 0.
+
+CAVEAT! The helper tries to get the sign right and all calls from BankTransaction are
+positive (abs($value)) values.
+
 
 Example:
 
@@ -668,20 +853,31 @@ Example:
   $ap->pay_invoice(chart_id      => $bank->chart_id,
                    amount        => $ap->open_amount,
                    transdate     => DateTime->now->to_kivitendo,
-                   memo          => 'foobar;
-                   source        => 'barfoo;
+                   memo          => 'foobar',
+                   source        => 'barfoo',
                    payment_type  => 'without_skonto',  # default if not specified
+                   project_id    => 25,
                   );
 
 or with skonto:
   $ap->pay_invoice(chart_id      => $bank->chart_id,
                    amount        => $ap->amount,       # doesn't need to be specified
                    transdate     => DateTime->now->to_kivitendo,
-                   memo          => 'foobar;
-                   source        => 'barfoo;
+                   memo          => 'foobar',
+                   source        => 'barfoo',
                    payment_type  => 'with_skonto',
                   );
 
+or in a certain currency:
+  $ap->pay_invoice(chart_id      => $bank->chart_id,
+                   amount        => 500,
+                   currency      => 'USD',
+                   transdate     => DateTime->now->to_kivitendo,
+                   memo          => 'foobar',
+                   source        => 'barfoo',
+                   payment_type  => 'with_skonto_pt',
+                  );
+
 Allowed payment types are:
   without_skonto with_skonto_pt difference_as_skonto
 
@@ -733,6 +929,14 @@ are negative values in acc_trans. E.g. one invoice with a positive value for
 
 Skonto doesn't/shouldn't apply if the invoice contains credited items.
 
+If no amount is given the whole open amout is paid.
+
+If neither currency or currency_id are given as params, the currency of the
+invoice is assumed to be the payment currency.
+
+If successful the return value will be 1 in scalar context or in list context
+the two ids (acc_trans_id) of the newly created bookings.
+
 =item C<reference_account>
 
 Returns a chart object which is the chart of the invoice with link AR or AP.
@@ -745,12 +949,11 @@ Example (1200 is the AR account for SKR04):
 =item C<percent_skonto>
 
 Returns the configured skonto percentage of the payment terms of an invoice,
-e.g. 0.02 for 2%. Payment terms come from invoice settings for ar, from vendor
-settings for ap.
+e.g. 0.02 for 2%. Payment terms come from invoice settingssettings for ap.
 
 =item C<amount_less_skonto>
 
-If the invoice has a payment term (via ar for sales, via vendor for purchase),
+If the invoice has a payment term,
 calculate the amount to be paid in the case of skonto.  This doesn't check,
 whether skonto applies (i.e. skonto doesn't wasn't exceeded), it just subtracts
 the configured percentage (e.g. 2%) from the total amount.
@@ -942,36 +1145,36 @@ defaults. E.g. when creating a SEPA bank transfer for vendor invoices a company
 might always want to pay quickly making use of skonto, while another company
 might always want to pay as late as possible.
 
-=item C<transactions>
-
-Returns all acc_trans Objects of an ar/ap object.
-
-Example in console to print account numbers and booked amounts of an invoice:
-  my $invoice = invoice(invnumber => '144');
-  foreach my $acc_trans ( @{ $invoice->transactions } ) {
-    print $acc_trans->chart->accno . " : " . $acc_trans->amount_as_number . "\n"
-  };
-  # 1200 : 226,00000
-  # 1800 : -226,00000
-  # 4300 : 100,00000
-  # 3801 : 7,00000
-  # 3806 : 19,00000
-  # 4400 : 100,00000
-  # 1200 : -226,00000
-
 =item C<get_payment_select_options_for_bank_transaction $banktransaction_id %params>
 
 Make suggestion for a skonto payment type by returning an HTML blob of the options
 of a HTML drop-down select with the most likely option preselected.
 
-This is a helper function for BankTransaction/ajax_payment_suggestion.
+This is a helper function for BankTransaction/ajax_payment_suggestion and
+template/webpages/bank_transactions/invoices.html
 
 We are working with an existing payment, so difference_as_skonto never makes sense.
 
+If skonto is not possible (skonto_date does not exists) simply return
+the single 'no skonto' option as a visual hint.
+
 If skonto is possible (skonto_date exists), add two possibilities:
 without_skonto and with_skonto_pt if payment date is within skonto_date,
 preselect with_skonto_pt, otherwise preselect without skonto.
 
+=item C<exchangerate>
+
+Returns 1 immediately if the record uses the default currency.
+
+Returns the exchangerate in database format for the invoice according to that
+invoice's transdate, returning 'buy' for sales, 'sell' for purchases.
+
+If no exchangerate can be found for that day undef is returned.
+
+=item C<forex>
+
+Returns 1 if record uses a different currency, 0 if the default currency is used.
+
 =back
 
 =head1 TODO AND CAVEATS
@@ -983,9 +1186,11 @@ preselect with_skonto_pt, otherwise preselect without skonto.
 when looking at open amount, maybe consider that there may already be queued
 amounts in SEPA Export
 
-=item *
+=item * C<skonto_charts>
+
+Cannot handle negative skonto amounts, will always calculate the skonto amount
+for credit notes or negative ap transactions with a positive sign.
 
-Can only handle default currency.
 
 =back