Projekt

Allgemein

Profil

Herunterladen (44,3 KB) Statistiken
| Zweig: | Markierung: | Revision:
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_skonto_configuration 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 check_skonto_configuration {
my $self = shift;

my $is_sales = ref($self) eq 'SL::DB::Invoice';

my $skonto_configured = 1; # default is assume skonto works

# 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, 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) };
if ( $is_sales && $transaction->{chartlinks}->{AR_amount} ) {
$skonto_configured = 0 unless $tax->skonto_sales_chart_id;
} elsif ( !$is_sales && $transaction->{chartlinks}->{AP_amount}) {
$skonto_configured = 0 unless $tax->skonto_purchase_chart_id;
};
};

return $skonto_configured;
}

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($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