|  | package SL::DB::Helper::Payment;
 | 
  
    |  | 
 | 
  
    |  | use strict;
 | 
  
    |  | 
 | 
  
    |  | use parent qw(Exporter);
 | 
  
    |  | our @EXPORT = qw(pay_invoice);
 | 
  
    |  | our @EXPORT_OK = qw(skonto_date amount_less_skonto within_skonto_period percent_skonto reference_account open_amount skonto_amount check_ valid_skonto_amount validate_payment_type get_payment_select_options_for_bank_transaction exchangerate forex _skonto_charts_and_tax_correction);
 | 
  
    |  | our %EXPORT_TAGS = (
 | 
  
    |  |   "ALL" => [@EXPORT, @EXPORT_OK],
 | 
  
    |  | );
 | 
  
    |  | 
 | 
  
    |  | require SL::DB::Chart;
 | 
  
    |  | 
 | 
  
    |  | use Carp;
 | 
  
    |  | use Data::Dumper;
 | 
  
    |  | use DateTime;
 | 
  
    |  | use List::Util qw(sum);
 | 
  
    |  | use Params::Validate qw(:all);
 | 
  
    |  | 
 | 
  
    |  | use SL::DATEV qw(:CONSTANTS);
 | 
  
    |  | use SL::DB::Exchangerate;
 | 
  
    |  | use SL::DB::Currency;
 | 
  
    |  | use SL::HTML::Util;
 | 
  
    |  | use SL::Locale::String qw(t8);
 | 
  
    |  | 
 | 
  
    |  | #
 | 
  
    |  | # Public functions not exported by default
 | 
  
    |  | #
 | 
  
    |  | 
 | 
  
    |  | sub pay_invoice {
 | 
  
    |  |   my ($self, %params) = @_;
 | 
  
    |  | 
 | 
  
    |  |   require SL::DB::Tax;
 | 
  
    |  | 
 | 
  
    |  |   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 @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 and optional params depending on payment_type
 | 
  
    |  |   Common::check_params(\%params, qw(chart_id transdate amount));
 | 
  
    |  |   Common::check_params(\%params, qw(bt_id)) unless $params{payment_type} eq 'without_skonto';
 | 
  
    |  | 
 | 
  
    |  |   # three valid cases, test logical params in depth, before proceeding ...
 | 
  
    |  |   if ( $params{'payment_type'} eq 'without_skonto' && abs($params{'amount'}) < 0) {
 | 
  
    |  |     croak "invalid amount for payment_type 'without_skonto': $params{'amount'}\n";
 | 
  
    |  |   } elsif ($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(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 or equal than the open invoice amount
 | 
  
    |  |     if ($params{skonto_amount} > _round($self->open_amount)) {
 | 
  
    |  |       croak("Skonto amount:" . $params{skonto_amount} . " higher than the payment or open invoice amount:" . $self->open_amount);
 | 
  
    |  |     }
 | 
  
    |  |   } elsif ( $params{'payment_type'} eq 'with_skonto_pt' ) {
 | 
  
    |  |     # options with_skonto_pt doesn't require the parameter
 | 
  
    |  |     # amount, but if amount is passed, make sure it matches the expected value
 | 
  
    |  |     # note: the parameter isn't used at all - amount_less_skonto will always be used
 | 
  
    |  |     # partial skonto payments are therefore impossible to book
 | 
  
    |  |     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;
 | 
  
    |  |   }
 | 
  
    |  | 
 | 
  
    |  | 
 | 
  
    |  |   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
 | 
  
    |  |   my $closedto = $::locale->parse_date_to_object($::instance_conf->get_closedto);
 | 
  
    |  |   if ( ref $closedto && $transdate_obj < $closedto ) {
 | 
  
    |  |     croak t8('Cannot post payment for a closed period!');
 | 
  
    |  |   };
 | 
  
    |  | 
 | 
  
    |  |   # check for maximum number of future days
 | 
  
    |  |   if ( $::instance_conf->get_max_future_booking_interval > 0 ) {
 | 
  
    |  |     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 );
 | 
  
    |  |   };
 | 
  
    |  | 
 | 
  
    |  |   # currency is either passed or use the invoice currency if it differs from the default currency
 | 
  
    |  |   # TODO remove
 | 
  
    |  |   my ($exchangerate, $currency, $return_bank_amount);
 | 
  
    |  |   $return_bank_amount = 0;
 | 
  
    |  |   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;
 | 
  
    |  |   };
 | 
  
    |  | 
 | 
  
    |  |   # absolute skonto amount for invoice, use as reference sum to see if the
 | 
  
    |  |   # calculated skontos add up
 | 
  
    |  |   # only needed for payment_term "with_skonto_pt"
 | 
  
    |  | 
 | 
  
    |  |   my $skonto_amount_check = $self->skonto_amount; # variable should be zero after calculating all skonto
 | 
  
    |  |   my $total_open_amount   = $self->open_amount;
 | 
  
    |  | 
 | 
  
    |  |   # 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 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 $rounded_params_amount = _round( $params{amount} ); # / $exchangerate);
 | 
  
    |  |   my $fx_gain_loss_amount = 0; # for fx_gain and fx_loss
 | 
  
    |  | 
 | 
  
    |  |   my $db = $self->db;
 | 
  
    |  |   $db->with_transaction(sub {
 | 
  
    |  |     my $new_acc_trans;
 | 
  
    |  | 
 | 
  
    |  |     # all three payment type create 1 AR/AP booking (the paid part)
 | 
  
    |  |     # with_skonto_pt creates 1 bank booking and n skonto bookings (1 for each tax type)
 | 
  
    |  |     # and one tax correction as a gl booking
 | 
  
    |  |     # without_skonto creates 1 bank booking
 | 
  
    |  | 
 | 
  
    |  |     unless ( $rounded_params_amount == 0 ) {
 | 
  
    |  |       # cases with_skonto_pt, free_skonto and without_skonto
 | 
  
    |  | 
 | 
  
    |  |       # for case with_skonto_pt we need to know the corrected amount at this
 | 
  
    |  |       # stage because we don't use $params{amount} ?!
 | 
  
    |  | 
 | 
  
    |  |       my $pay_amount = $rounded_params_amount;
 | 
  
    |  |       $pay_amount = $self->amount_less_skonto if $params{payment_type} eq 'with_skonto_pt';
 | 
  
    |  | 
 | 
  
    |  |       # bank account and AR/AP
 | 
  
    |  |       $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     => $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;
 | 
  
    |  |       $return_bank_amount += $amount;
 | 
  
    |  | 
 | 
  
    |  |       push @new_acc_ids, $new_acc_trans->acc_trans_id;
 | 
  
    |  |       # deal with fxtransaction
 | 
  
    |  |       if ( $self->currency_id != $::instance_conf->get_currency_id && $exchangerate != 1) {
 | 
  
    |  |         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";
 | 
  
    |  |           # would nearly work if $amount is in foreign currency. Old code in AP.pm
 | 
  
    |  |           # and old code says USD * ( rate invoice - rate payment )
 | 
  
    |  |           my $gain_loss_amount = _round($amount * ($exchangerate - $self->exchangerate ) * -1,2);
 | 
  
    |  |           #                               EUR / rate payment   * ( rate invoice - rate bank transaction)
 | 
  
    |  |           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;
 | 
  
    |  | 
 | 
  
    |  |         }
 | 
  
    |  |       }
 | 
  
    |  |     }
 | 
  
    |  |     # skonto cases
 | 
  
    |  |     if ($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 'free_skonto') {
 | 
  
    |  |         $total_skonto_amount = $params{skonto_amount};
 | 
  
    |  |       }
 | 
  
    |  |       my @skonto_bookings = $self->_skonto_charts_and_tax_correction(amount => $total_skonto_amount, bt_id => $params{bt_id},
 | 
  
    |  |                                                                      transdate_obj => $transdate_obj, memo => $params{memo},
 | 
  
    |  |                                                                      source => $params{source});
 | 
  
    |  |       my $reference_amount = $total_skonto_amount;
 | 
  
    |  | 
 | 
  
    |  |       # create an acc_trans entry for each result of $self->skonto_charts
 | 
  
    |  |       foreach my $skonto_booking ( @skonto_bookings ) {
 | 
  
    |  |         next unless $skonto_booking->{'chart_id'};
 | 
  
    |  |         next unless $skonto_booking->{'skonto_amount'} != 0;
 | 
  
    |  |         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,
 | 
  
    |  |                                                      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 * $exchangerate;
 | 
  
    |  |         $skonto_amount_check -= $skonto_booking->{'skonto_amount'};
 | 
  
    |  |       }
 | 
  
    |  |     }
 | 
  
    |  |     my $arap_amount = 0;
 | 
  
    |  | 
 | 
  
    |  |     if ( $params{payment_type} eq 'without_skonto' ) {
 | 
  
    |  |       $arap_amount = $rounded_params_amount;
 | 
  
    |  |     } elsif ( $params{payment_type} eq 'with_skonto_pt' ) {
 | 
  
    |  |       # this should be amount + sum(amount+skonto), but while we only allow
 | 
  
    |  |       # 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     => _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);
 | 
  
    |  | 
 | 
  
    |  |         $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;
 | 
  
    |  | 
 | 
  
    |  |     # 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 ) {
 | 
  
    |  | 
 | 
  
    |  |       my $datev = SL::DATEV->new(
 | 
  
    |  |         dbh        => $db->dbh,
 | 
  
    |  |         trans_id   => $self->{id},
 | 
  
    |  |       );
 | 
  
    |  | 
 | 
  
    |  |       $datev->generate_datev_data;
 | 
  
    |  | 
 | 
  
    |  |       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;
 | 
  
    |  |       }
 | 
  
    |  |     }
 | 
  
    |  | 
 | 
  
    |  |     1;
 | 
  
    |  | 
 | 
  
    |  |   }) || die t8('error while paying invoice #1 : ', $self->invnumber) . $db->error . "\n";
 | 
  
    |  | 
 | 
  
    |  |   $return_bank_amount *= -1;   # negative booking is positive bank transaction
 | 
  
    |  |                                # positive booking is negative bank transaction
 | 
  
    |  |   return wantarray ? ( { return_bank_amount => $return_bank_amount }, @new_acc_ids) : 1;
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub skonto_date {
 | 
  
    |  | 
 | 
  
    |  |   my $self = shift;
 | 
  
    |  | 
 | 
  
    |  |   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 {
 | 
  
    |  |   my $self = shift;
 | 
  
    |  | 
 | 
  
    |  |   my $is_sales = ref($self) eq 'SL::DB::Invoice';
 | 
  
    |  | 
 | 
  
    |  |   require SL::DB::Manager::AccTransaction;
 | 
  
    |  | 
 | 
  
    |  |   my $link_filter = $is_sales ? 'AR' : 'AP';
 | 
  
    |  | 
 | 
  
    |  |   my $acc_trans = SL::DB::Manager::AccTransaction->find_by(
 | 
  
    |  |      'trans_id'    => $self->id,
 | 
  
    |  |      '!chart_id'   => $::instance_conf->get_advance_payment_clearing_chart_id,
 | 
  
    |  |      SL::DB::Manager::AccTransaction->chart_link_filter("$link_filter")
 | 
  
    |  |   );
 | 
  
    |  | 
 | 
  
    |  |   return undef unless ref $acc_trans;
 | 
  
    |  | 
 | 
  
    |  |   my $reference_account = SL::DB::Manager::Chart->find_by(id => $acc_trans->chart_id);
 | 
  
    |  | 
 | 
  
    |  |   return $reference_account;
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub open_amount {
 | 
  
    |  |   my $self = shift;
 | 
  
    |  | 
 | 
  
    |  |   # in the future maybe calculate this from acc_trans
 | 
  
    |  | 
 | 
  
    |  |   # 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 // 0) - ($self->paid // 0);
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub skonto_amount {
 | 
  
    |  |   my $self = shift;
 | 
  
    |  | 
 | 
  
    |  |   return $self->amount - $self->amount_less_skonto;
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub percent_skonto {
 | 
  
    |  |   my $self = shift;
 | 
  
    |  | 
 | 
  
    |  |   my $percent_skonto = 0;
 | 
  
    |  | 
 | 
  
    |  |   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 $percent_skonto = $self->percent_skonto || 0;
 | 
  
    |  | 
 | 
  
    |  |   return _round($self->amount - ( $self->amount * $percent_skonto) );
 | 
  
    |  | 
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub _skonto_charts_and_tax_correction {
 | 
  
    |  |   my ($self, %params)   = @_;
 | 
  
    |  |   my $amount = $params{amount} || $self->skonto_amount;
 | 
  
    |  | 
 | 
  
    |  |   croak "no amount passed to skonto_charts"                    unless abs(_round($amount)) >= 0.01;
 | 
  
    |  |   croak "no banktransaction.id passed to skonto_charts"        unless $params{bt_id};
 | 
  
    |  |   croak "no banktransaction.transdate passed to skonto_charts" unless ref $params{transdate_obj} eq 'DateTime';
 | 
  
    |  | 
 | 
  
    |  |   $params{memo}   //= '';
 | 
  
    |  |   $params{source} //= '';
 | 
  
    |  | 
 | 
  
    |  | 
 | 
  
    |  |   my $is_sales = $self->is_sales;
 | 
  
    |  |   my (@skonto_charts, $inv_calc, $total_skonto_rounded);
 | 
  
    |  | 
 | 
  
    |  |   $inv_calc = $self->get_tax_and_amount_by_tax_chart_id();
 | 
  
    |  | 
 | 
  
    |  |   # foreach tax.chart_id || $entry->{ta..id}
 | 
  
    |  |   while (my ($tax_chart_id, $entry) = each %{ $inv_calc } ) {
 | 
  
    |  |     my $tax = SL::DB::Manager::Tax->find_by(id => $entry->{tax_id}) || die "Can't find tax with id " . $tax_chart_id;
 | 
  
    |  |     die t8('no skonto_chart configured for taxkey #1 : #2 : #3', $tax->taxkey, $tax->taxdescription , $tax->rate * 100)
 | 
  
    |  |       unless $is_sales ? ref $tax->skonto_sales_chart : ref $tax->skonto_purchase_chart;
 | 
  
    |  | 
 | 
  
    |  |     # percent net amount
 | 
  
    |  |     my $transaction_net_skonto_percent = abs($entry->{netamount} / $self->amount);
 | 
  
    |  |     my $skonto_netamount_unrounded     = abs($amount * $transaction_net_skonto_percent);
 | 
  
    |  | 
 | 
  
    |  |     # percent tax amount
 | 
  
    |  |     my $transaction_tax_skonto_percent = abs($entry->{tax} / $self->amount);
 | 
  
    |  |     my $skonto_taxamount_unrounded     = abs($amount * $transaction_tax_skonto_percent);
 | 
  
    |  | 
 | 
  
    |  |     my $skonto_taxamount_rounded   = _round($skonto_taxamount_unrounded);
 | 
  
    |  |     my $skonto_netamount_rounded   = _round($skonto_netamount_unrounded);
 | 
  
    |  |     my $chart_id                   = $is_sales ? $tax->skonto_sales_chart->id : $tax->skonto_purchase_chart->id;
 | 
  
    |  | 
 | 
  
    |  |     # entry net + tax for caller
 | 
  
    |  |     my $rec_net = {
 | 
  
    |  |       chart_id               => $chart_id,
 | 
  
    |  |       skonto_amount          => _round($skonto_netamount_unrounded + $skonto_taxamount_unrounded),
 | 
  
    |  |     };
 | 
  
    |  |     push @skonto_charts, $rec_net;
 | 
  
    |  |     $total_skonto_rounded += $rec_net->{skonto_amount};
 | 
  
    |  | 
 | 
  
    |  |     # add-on: correct tax with one linked gl booking
 | 
  
    |  | 
 | 
  
    |  |     # no skonto tax correction for dual tax (reverse charge) or rate = 0 or taxamount below 0.01
 | 
  
    |  |     next if ($tax->rate == 0 || $tax->reverse_charge_chart_id || $skonto_taxamount_rounded < 0.01);
 | 
  
    |  | 
 | 
  
    |  |     my ($credit, $debit);
 | 
  
    |  |     $credit = SL::DB::Manager::Chart->find_by(id => $chart_id);
 | 
  
    |  |     $debit  = SL::DB::Manager::Chart->find_by(id => $tax_chart_id);
 | 
  
    |  |     croak("No such Chart ID")  unless ref $credit eq 'SL::DB::Chart' && ref $debit eq 'SL::DB::Chart';
 | 
  
    |  |     my $notes = SL::HTML::Util->strip($self->notes);
 | 
  
    |  | 
 | 
  
    |  |     my $current_transaction = SL::DB::GLTransaction->new(
 | 
  
    |  |          employee_id    => $self->employee_id,
 | 
  
    |  |          transdate      => $params{transdate_obj},
 | 
  
    |  |          notes          => $params{source} . ' ' . $params{memo},
 | 
  
    |  |          description    => $notes || $self->invnumber,
 | 
  
    |  |          reference      => t8('Skonto Tax Correction for') . " " . $tax->rate * 100 . '% ' . $self->invnumber,
 | 
  
    |  |          department_id  => $self->department_id ? $self->department_id : undef,
 | 
  
    |  |          imported       => 0, # not imported
 | 
  
    |  |          taxincluded    => 0,
 | 
  
    |  |       )->add_chart_booking(
 | 
  
    |  |          chart  => $is_sales ? $debit : $credit,
 | 
  
    |  |          debit  => abs($skonto_taxamount_rounded),
 | 
  
    |  |          source => t8('Skonto Tax Correction for') . " " . $self->invnumber,
 | 
  
    |  |          memo   => $params{memo},
 | 
  
    |  |          tax_id => 0,
 | 
  
    |  |       )->add_chart_booking(
 | 
  
    |  |          chart  => $is_sales ? $credit : $debit,
 | 
  
    |  |          credit => abs($skonto_taxamount_rounded),
 | 
  
    |  |          source => t8('Skonto Tax Correction for') . " " . $self->invnumber,
 | 
  
    |  |          memo   => $params{memo},
 | 
  
    |  |          tax_id => 0,
 | 
  
    |  |       )->post;
 | 
  
    |  | 
 | 
  
    |  |     # add a stable link acc_trans_id to bank_transactions.id
 | 
  
    |  |     foreach my $transaction (@{ $current_transaction->transactions }) {
 | 
  
    |  |       my %props_acc = (
 | 
  
    |  |            acc_trans_id        => $transaction->acc_trans_id,
 | 
  
    |  |            bank_transaction_id => $params{bt_id},
 | 
  
    |  |            gl                  => $current_transaction->id,
 | 
  
    |  |       );
 | 
  
    |  |       SL::DB::BankTransactionAccTrans->new(%props_acc)->save;
 | 
  
    |  |     }
 | 
  
    |  |     # Record a record link from banktransactions to gl
 | 
  
    |  |     my %props_rl = (
 | 
  
    |  |          from_table => 'bank_transactions',
 | 
  
    |  |          from_id    => $params{bt_id},
 | 
  
    |  |          to_table   => 'gl',
 | 
  
    |  |          to_id      => $current_transaction->id,
 | 
  
    |  |     );
 | 
  
    |  |     SL::DB::RecordLink->new(%props_rl)->save;
 | 
  
    |  |     # Record a record link from arap to gl
 | 
  
    |  |     # linked gl booking will appear in tab linked records
 | 
  
    |  |     # this is just a link for convenience
 | 
  
    |  |     %props_rl = (
 | 
  
    |  |          from_table => $is_sales ? 'ar' : 'ap',
 | 
  
    |  |          from_id    => $self->id,
 | 
  
    |  |          to_table   => 'gl',
 | 
  
    |  |          to_id      => $current_transaction->id,
 | 
  
    |  |     );
 | 
  
    |  |     SL::DB::RecordLink->new(%props_rl)->save;
 | 
  
    |  | 
 | 
  
    |  |   }
 | 
  
    |  |   # check for rounding errors, at least for the payment chart
 | 
  
    |  |   # we ignore tax rounding errors as long as the amount (user input or calculated)
 | 
  
    |  |   # is fully assigned.
 | 
  
    |  |   # we simply alter one cent for the first skonto booking entry
 | 
  
    |  |   # should be correct for most of the cases (no invoices with mixed taxes)
 | 
  
    |  |   if (_round($total_skonto_rounded - $amount) >= 0.01) {
 | 
  
    |  |     # subtract one cent
 | 
  
    |  |     $skonto_charts[0]->{skonto_amount} -= 0.01;
 | 
  
    |  |   } elsif (_round($amount - $total_skonto_rounded) >= 0.01) {
 | 
  
    |  |     # add one cent
 | 
  
    |  |     $skonto_charts[0]->{skonto_amount} += 0.01;
 | 
  
    |  |   }
 | 
  
    |  | 
 | 
  
    |  |   # return same array of skonto charts as sub skonto_charts
 | 
  
    |  |   return @skonto_charts;
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | 
 | 
  
    |  | sub within_skonto_period {
 | 
  
    |  |   my $self = shift;
 | 
  
    |  |   validate(
 | 
  
    |  |     @_,
 | 
  
    |  |        { transdate => {
 | 
  
    |  |                         isa => 'DateTime',
 | 
  
    |  |                         callbacks => {
 | 
  
    |  |                           'self has a skonto date'  => sub { ref $self->skonto_date eq 'DateTime' },
 | 
  
    |  |                           'is within skonto period' => sub { return shift() <= $self->skonto_date },
 | 
  
    |  |                         },
 | 
  
    |  |                       },
 | 
  
    |  |        }
 | 
  
    |  |     );
 | 
  
    |  |   # then return true
 | 
  
    |  |   return 1;
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub valid_skonto_amount {
 | 
  
    |  |   my $self = shift;
 | 
  
    |  |   my $amount = shift || 0;
 | 
  
    |  |   my $max_skonto_percent = 0.10;
 | 
  
    |  | 
 | 
  
    |  |   return 0 unless $amount > 0;
 | 
  
    |  | 
 | 
  
    |  |   # does this work for other currencies?
 | 
  
    |  |   return ($self->amount*$max_skonto_percent) > $amount;
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub get_payment_select_options_for_bank_transaction {
 | 
  
    |  |   my ($self, $bt_id, %params) = @_;
 | 
  
    |  | 
 | 
  
    |  | 
 | 
  
    |  |   # 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(!$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(transdate => $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
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | # locales for payment type
 | 
  
    |  | #
 | 
  
    |  | # $main::locale->text('without_skonto')
 | 
  
    |  | # $main::locale->text('with_skonto_pt')
 | 
  
    |  | #
 | 
  
    |  | 
 | 
  
    |  | sub validate_payment_type {
 | 
  
    |  |   my $payment_type = shift;
 | 
  
    |  | 
 | 
  
    |  |   my %allowed_payment_types = map { $_ => 1 } qw(without_skonto with_skonto_pt 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;
 | 
  
    |  |   return $::form->round_amount($value, 2);
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | 1;
 | 
  
    |  | 
 | 
  
    |  | __END__
 | 
  
    |  | 
 | 
  
    |  | =pod
 | 
  
    |  | 
 | 
  
    |  | =head1 NAME
 | 
  
    |  | 
 | 
  
    |  | SL::DB::Helper::Payment  Mixin providing helper methods for paying C<Invoice>
 | 
  
    |  |                          and C<PurchaseInvoice> objects and using skonto
 | 
  
    |  | 
 | 
  
    |  | =head1 SYNOPSIS
 | 
  
    |  | 
 | 
  
    |  | In addition to actually causing a payment via pay_invoice this helper contains
 | 
  
    |  | many methods that help in determining information about the status of the
 | 
  
    |  | invoice, such as the remaining open amount, whether skonto applies, until which
 | 
  
    |  | date skonto applies, the skonto amount and relative percentages, what to do
 | 
  
    |  | with skonto, ...
 | 
  
    |  | 
 | 
  
    |  | To prevent duplicate code this was all added in this mixin rather than directly
 | 
  
    |  | in SL::DB::Invoice and SL::DB::PurchaseInvoice.
 | 
  
    |  | 
 | 
  
    |  | =over 4
 | 
  
    |  | 
 | 
  
    |  | =item C<pay_invoice %params>
 | 
  
    |  | 
 | 
  
    |  | 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>, C<amount> and C<chart_id> are mandantory.
 | 
  
    |  | 
 | 
  
    |  | For all valid skonto types ('free_skonto' or 'with_skonto_pt') the source of
 | 
  
    |  | the bank_transaction is needed, therefore pay_invoice expects the param
 | 
  
    |  | C<bt_id> with a valid bank_transactions.id.
 | 
  
    |  | 
 | 
  
    |  | If the payment type ('free_skonto') is used the number param skonto_amount is
 | 
  
    |  | as well mandantory and needs to be positive. Furthermore the skonto amount has
 | 
  
    |  | to be lower or equal than the open invoice amount.
 | 
  
    |  | Payments with only skonto and zero bank transaction amount are possible.
 | 
  
    |  | 
 | 
  
    |  | 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, and for the case 'free_skonto' might be zero.
 | 
  
    |  | 
 | 
  
    |  | CAVEAT! The helper tries to get the sign right and all calls from BankTransaction are
 | 
  
    |  | positive (abs($value)) values.
 | 
  
    |  | 
 | 
  
    |  | 
 | 
  
    |  | Example:
 | 
  
    |  | 
 | 
  
    |  |   my $ap   = SL::DB::Manager::PurchaseInvoice->find_by( invnumber => '1');
 | 
  
    |  |   my $bank = SL::DB::Manager::BankAccount->find_by( name => 'Bank');
 | 
  
    |  |   $ap->pay_invoice(chart_id      => $bank->chart_id,
 | 
  
    |  |                    amount        => $ap->open_amount,
 | 
  
    |  |                    transdate     => DateTime->now->to_kivitendo,
 | 
  
    |  |                    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',
 | 
  
    |  |                    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
 | 
  
    |  | 
 | 
  
    |  | The option C<payment_type> allows for a basic skonto mechanism.
 | 
  
    |  | 
 | 
  
    |  | C<without_skonto> is the default mode, "amount" is paid to the account in
 | 
  
    |  | chart_id. This can also be used for partial payments and corrections via
 | 
  
    |  | negative amounts.
 | 
  
    |  | 
 | 
  
    |  | C<with_skonto_pt> can't be used for partial payments. When used on unpaid
 | 
  
    |  | invoices the whole amount is paid, with the skonto part automatically being
 | 
  
    |  | booked according to the skonto chart configured in the tax settings for each
 | 
  
    |  | tax key. If an amount is passed it is ignored and the actual configured skonto
 | 
  
    |  | amount is used.
 | 
  
    |  | 
 | 
  
    |  | So passing amount doesn't have any effect for the case C<with_skonto_pt>.
 | 
  
    |  | 
 | 
  
    |  | The skonto modes automatically calculate the relative amounts for a mix of
 | 
  
    |  | taxes, e.g. items with 7% and 19% in one invoice. There is a helper method
 | 
  
    |  | _skonto_charts_and_tax_correction, which calculates the relative percentages
 | 
  
    |  | according to the amounts in acc_trans grouped by different tax rates.
 | 
  
    |  | 
 | 
  
    |  | The helper method also generates the tax correction for the skonto booking
 | 
  
    |  | and links this to the original bank transaction and the selected record.
 | 
  
    |  | 
 | 
  
    |  | There is currently no way of excluding certain items in an invoice from having
 | 
  
    |  | skonto applied to them.  If this feature was added to parts the calculation
 | 
  
    |  | method of relative skonto would have to be completely rewritten using the
 | 
  
    |  | invoice items rather than acc_trans.
 | 
  
    |  | 
 | 
  
    |  | Because of the way skonto_charts works the calculation doesn't work if there
 | 
  
    |  | are negative values in acc_trans. E.g. one invoice with a positive value for
 | 
  
    |  | 19% tax and a negative value for the acc_trans line with 7%
 | 
  
    |  | 
 | 
  
    |  | 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 or more (gl transaction for skonto tax correction) 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.
 | 
  
    |  | 
 | 
  
    |  | Example (1200 is the AR account for SKR04):
 | 
  
    |  |   my $invoice = invoice(invnumber => '144');
 | 
  
    |  |   $invoice->reference_account->accno
 | 
  
    |  |   # 1200
 | 
  
    |  | 
 | 
  
    |  | =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 settingssettings for ap.
 | 
  
    |  | 
 | 
  
    |  | =item C<amount_less_skonto>
 | 
  
    |  | 
 | 
  
    |  | 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.
 | 
  
    |  | 
 | 
  
    |  | The returned value is rounded to two decimals.
 | 
  
    |  | 
 | 
  
    |  | =item C<skonto_date>
 | 
  
    |  | 
 | 
  
    |  | The date up to which skonto may be taken. This is calculated from the invoice
 | 
  
    |  | date + the number of days configured in the payment terms.
 | 
  
    |  | 
 | 
  
    |  | This method can also be used to determine whether skonto applies for the
 | 
  
    |  | invoice, as it returns undef if there is no payment term or skonto days is set
 | 
  
    |  | to 0.
 | 
  
    |  | 
 | 
  
    |  | =item C<within_skonto_period [transdate =E<gt> DateTime]>
 | 
  
    |  | 
 | 
  
    |  | Returns 1 if skonto_date is in a skontoable period.
 | 
  
    |  | Needs the mandatory named param 'transdate' as a 'DateTime', usually a bank
 | 
  
    |  | transaction date for imported bank data.
 | 
  
    |  | 
 | 
  
    |  | Checks if the invoice has skontoable payment terms configured and whether the date
 | 
  
    |  | is within the skonto max date.
 | 
  
    |  | 
 | 
  
    |  | If one of the condition fails, a hopefully helpful error message is returned.
 | 
  
    |  | 
 | 
  
    |  | =item C<valid_skonto_amount>
 | 
  
    |  | 
 | 
  
    |  | Takes an amount as an argument and checks whether the amount is less than 10%
 | 
  
    |  | of the total amount of the invoice. The value of 10% is currently hardcoded in
 | 
  
    |  | the method. This method is currently used to check whether to offer the payment
 | 
  
    |  | option "difference as skonto".
 | 
  
    |  | 
 | 
  
    |  | Example:
 | 
  
    |  |  if ( $invoice->valid_skonto_amount($invoice->open_amount) ) {
 | 
  
    |  |    # ... do something
 | 
  
    |  |  }
 | 
  
    |  | 
 | 
  
    |  | =item C<_skonto_charts_and_tax_correction [amount => $amount, bt_id => $bank_transaction.id, transdate_ojb => DateTime]>
 | 
  
    |  | 
 | 
  
    |  | Needs a valid bank_transaction id and the transdate of the bank_transaction as
 | 
  
    |  | a DateTime object.
 | 
  
    |  | If no amout is passed, the currently open invoice amount will be used.
 | 
  
    |  | 
 | 
  
    |  | Returns a list of chart_ids and some calculated numbers that can be used for
 | 
  
    |  | paying the invoice with skonto. This function will automatically calculate the
 | 
  
    |  | relative skonto amounts even if the invoice contains several types of taxes
 | 
  
    |  | (e.g. 7% and 19%).
 | 
  
    |  | 
 | 
  
    |  | Example usage:
 | 
  
    |  |   my $invoice = SL::DB::Manager::Invoice->find_by(invnumber => '211');
 | 
  
    |  |   my @skonto_charts = $invoice->_skonto_charts_and_tax_correction(bt_id         => $bt_id,
 | 
  
    |  |                                                                   transdate_obj => $transdate_obj);
 | 
  
    |  | 
 | 
  
    |  | or with the total skonto amount as an argument:
 | 
  
    |  |   my @skonto_charts = $invoice->_skonto_charts_and_tax_correction(amount => $invoice->open_amount,
 | 
  
    |  |                                                                   bt_id  => $bt_id,
 | 
  
    |  |                                                                   transdate_obj => $transdate_obj);
 | 
  
    |  | 
 | 
  
    |  | The following values are generated for each chart:
 | 
  
    |  | 
 | 
  
    |  | =over 2
 | 
  
    |  | 
 | 
  
    |  | =item C<chart_id>
 | 
  
    |  | 
 | 
  
    |  | The chart id of the skonto amount to be booked.
 | 
  
    |  | 
 | 
  
    |  | =item C<skonto_amount>
 | 
  
    |  | 
 | 
  
    |  | The total amount to be paid to the account
 | 
  
    |  | 
 | 
  
    |  | =back
 | 
  
    |  | 
 | 
  
    |  | If the invoice contains several types of taxes then skonto_charts can be used
 | 
  
    |  | to calculate the relative amounts.
 | 
  
    |  | 
 | 
  
    |  | C<_skonto_charts_and_tax_correction> generates one entry for each tax type entry.
 | 
  
    |  | 
 | 
  
    |  | =item C<open_amount>
 | 
  
    |  | 
 | 
  
    |  | Unrounded total open amount of invoice (amount - paid).
 | 
  
    |  | Doesn't take into account pending SEPA transfers.
 | 
  
    |  | 
 | 
  
    |  | =item C<get_payment_suggestions %params>
 | 
  
    |  | 
 | 
  
    |  | Creates data intended for an L.select_tag dropdown that can be used in a
 | 
  
    |  | template. Depending on the rules it will choose from the options
 | 
  
    |  | without_skonto and with_skonto_pt and select the most
 | 
  
    |  | likely one.
 | 
  
    |  | 
 | 
  
    |  | If the parameter "sepa" is passed, the SEPA export payments that haven't been
 | 
  
    |  | executed yet are considered when determining the open amount of the invoice.
 | 
  
    |  | 
 | 
  
    |  | The current rules are:
 | 
  
    |  | 
 | 
  
    |  | =over 2
 | 
  
    |  | 
 | 
  
    |  | =item * without_skonto is always an option
 | 
  
    |  | 
 | 
  
    |  | =item * with_skonto_pt is only offered if there haven't been any payments yet and the current date is within the skonto period.
 | 
  
    |  | 
 | 
  
    |  | with_skonto_pt will only be offered, if all the AR_amount/AP_amount have a
 | 
  
    |  | taxkey with a configured skonto chart
 | 
  
    |  | 
 | 
  
    |  | =back
 | 
  
    |  | 
 | 
  
    |  | It will also fill $self->{invoice_amount_suggestion} with either the open
 | 
  
    |  | amount, or if with_skonto_pt is selected, with amount_less_skonto, so the
 | 
  
    |  | template can fill the input with the likely amount.
 | 
  
    |  | 
 | 
  
    |  | Example in console:
 | 
  
    |  |   my $ar = invoice( invnumber => '257');
 | 
  
    |  |   $ar->get_payment_suggestions;
 | 
  
    |  |   print $ar->{invoice_amount_suggestion} . "\n";
 | 
  
    |  |   # 97.23
 | 
  
    |  |   pp $ar->{payment_select_options}
 | 
  
    |  |   # $VAR1 = [
 | 
  
    |  |   #         {
 | 
  
    |  |   #           'display' => 'ohne Skonto',
 | 
  
    |  |   #           'payment_type' => 'without_skonto'
 | 
  
    |  |   #         },
 | 
  
    |  |   #         {
 | 
  
    |  |   #           'display' => 'mit Skonto nach ZB',
 | 
  
    |  |   #           'payment_type' => 'with_skonto_pt',
 | 
  
    |  |   #           'selected' => 1
 | 
  
    |  |   #         }
 | 
  
    |  |   #       ];
 | 
  
    |  | 
 | 
  
    |  | The resulting array $ar->{payment_select_options} can be used in a template
 | 
  
    |  | select_tag using value_key and title_key:
 | 
  
    |  | 
 | 
  
    |  | [% L.select_tag('payment_type_' _ loop.count, invoice.payment_select_options, value_key => 'payment_type', title_key => 'display', id => 'payment_type_' _ loop.count) %]
 | 
  
    |  | 
 | 
  
    |  | It would probably make sense to have different rules for the pre-selected items
 | 
  
    |  | for sales and purchase, and to also make these rules configurable in the
 | 
  
    |  | 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<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 and
 | 
  
    |  | template/webpages/bank_transactions/invoices.html
 | 
  
    |  | 
 | 
  
    |  | We are working with an existing payment, so (deprecated) 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
 | 
  
    |  | 
 | 
  
    |  | =over 4
 | 
  
    |  | 
 | 
  
    |  | =item *
 | 
  
    |  | 
 | 
  
    |  | when looking at open amount, maybe consider that there may already be queued
 | 
  
    |  | amounts in SEPA Export
 | 
  
    |  | 
 | 
  
    |  | =item * C<_skonto_charts_and_tax_correction>
 | 
  
    |  | 
 | 
  
    |  | Cannot handle negative skonto amounts, will always calculate the skonto amount
 | 
  
    |  | for credit notes or negative ap transactions with a positive sign.
 | 
  
    |  | 
 | 
  
    |  | 
 | 
  
    |  | =back
 | 
  
    |  | 
 | 
  
    |  | =head1 AUTHOR
 | 
  
    |  | 
 | 
  
    |  | G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
 | 
  
    |  | 
 | 
  
    |  | =cut
 |