Projekt

Allgemein

Profil

Herunterladen (51,3 KB) Statistiken
| Zweig: | Markierung: | Revision:
15f58ff3 Geoffrey Richardson
package SL::DB::Helper::Payment;

use strict;

use parent qw(Exporter);
our @EXPORT = qw(pay_invoice);
58dbf540 Jan Büren
our @EXPORT_OK = qw(skonto_date amount_less_skonto within_skonto_period percent_skonto reference_account open_amount skonto_amount valid_skonto_amount validate_payment_type get_payment_select_options_for_bank_transaction forex _skonto_charts_and_tax_correction get_exchangerate_for_bank_transaction get_exchangerate _add_bank_fx_fees open_amount_fx);
15f58ff3 Geoffrey Richardson
our %EXPORT_TAGS = (
"ALL" => [@EXPORT, @EXPORT_OK],
);

require SL::DB::Chart;
9a2a4c7f Jan Büren
use Carp;
15f58ff3 Geoffrey Richardson
use Data::Dumper;
use DateTime;
use List::Util qw(sum);
f8ea96a8 Jan Büren
use Params::Validate qw(:all);
9a2a4c7f Jan Büren
use SL::DATEV qw(:CONSTANTS);
dee8b29f Geoffrey Richardson
use SL::DB::Exchangerate;
use SL::DB::Currency;
da3cca7d Jan Büren
use SL::HTML::Util;
9a2a4c7f Jan Büren
use SL::Locale::String qw(t8);
15f58ff3 Geoffrey Richardson
#
# Public functions not exported by default
#

sub pay_invoice {
my ($self, %params) = @_;
4e795d54 Jan Büren
# todo named params
15f58ff3 Geoffrey Richardson
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
c5dccb51 Jan Büren
my @new_acc_ids;
ba68038e Geoffrey Richardson
my $paid_amount = 0; # the amount that will be later added to $self->paid, should be in default currency
15f58ff3 Geoffrey Richardson
# default values if not set
$params{payment_type} = 'without_skonto' unless $params{payment_type};
validate_payment_type($params{payment_type});

69e03937 Jan Büren
# check for required parameters and optional params depending on payment_type
23b40897 Jan Büren
Common::check_params(\%params, qw(chart_id transdate amount));
d8275f6e Jan Büren
Common::check_params(\%params, qw(bt_id)) unless $params{payment_type} eq 'without_skonto';
23b40897 Jan Büren
# three valid cases, test logical params in depth, before proceeding ...
a4bbff92 Jan Büren
if ( $params{'payment_type'} eq 'without_skonto' && abs($params{'amount'}) < 0) {
croak "invalid amount for payment_type 'without_skonto': $params{'amount'}\n";
23b40897 Jan Büren
} elsif ($params{'payment_type'} eq 'free_skonto') {
69e03937 Jan Büren
# we dont like too much automagic for this payment type.
# we force caller input for amount and skonto amount
23b40897 Jan Büren
Common::check_params(\%params, qw(skonto_amount));
69e03937 Jan Büren
# secondly we dont want to handle credit notes and purchase credit notes
e04af795 Jan Büren
croak("Cannot use 'free skonto' for credit or debit notes") if ($params{amount} < 0 || $params{skonto_amount} <= 0);
69e03937 Jan Büren
# both amount have to be rounded
$params{skonto_amount} = _round($params{skonto_amount});
$params{amount} = _round($params{amount});
e04af795 Jan Büren
# 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);
69e03937 Jan Büren
}
23b40897 Jan Büren
} 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;
69e03937 Jan Büren
}
15f58ff3 Geoffrey Richardson
ba68038e Geoffrey Richardson
my $transdate_obj;
bd218b67 Geoffrey Richardson
if (ref($params{transdate}) eq 'DateTime') {
ba68038e Geoffrey Richardson
$transdate_obj = $params{transdate};
} else {
bd218b67 Geoffrey Richardson
$transdate_obj = $::locale->parse_date_to_object($params{transdate});
ba68038e Geoffrey Richardson
};
15f58ff3 Geoffrey Richardson
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 );
};

7b349901 Jan Büren
# currency has to be passed and caller has to be sure to assign it for a forex invoice
# dies if called for a invoice with the default currency (TODO: Params::Validate before)
58dbf540 Jan Büren
my ($exchangerate, $currency, $return_bank_amount , $fx_gain_loss_amount);
92044939 Jan Büren
$return_bank_amount = 0;
7b349901 Jan Büren
if ($params{currency} || $params{currency_id} && $self->forex) { # currency was specified
$currency = SL::DB::Manager::Currency->find_by(name => $params{currency}) || SL::DB::Manager::Currency->find_by(id => $params{currency_id});
58dbf540 Jan Büren
die "No currency found" unless ref $currency eq 'SL::DB::Currency';
die "No exchange rate" unless $params{exchangerate} > 0;

7b349901 Jan Büren
$exchangerate = $params{exchangerate};
58dbf540 Jan Büren
7b349901 Jan Büren
my $new_open_amount = ( $self->open_amount / $self->get_exchangerate ) * $exchangerate;
# VORHER
# my $gain_loss_amount = _round($amount * ($exchangerate - $self->get_exchangerate ) * -1,2);
58dbf540 Jan Büren
# $fx_gain_loss_amount = _round( $self->open_amount - $new_open_amount);
# $fx_gain_loss_amount = _round($self->open_amount / $self->get_exchangerate - $new_open_amount / $exchangerate);
7b349901 Jan Büren
# works for ap, but change sign for ar (todo credit notes and negative ap transactions
58dbf540 Jan Büren
# $fx_gain_loss_amount *= -1 if $self->is_sales;
$main::lxdebug->message(0, 'h 1 ' . $new_open_amount . ' h 3 ' . $params{amount});
# if new open amount for payment booking is smaller than original amount use this
# assume that the rest are fees, if the user selected this
if ($params{fx_book} && $params{fx_fee_amount} > 0) {
die "Bank Fees can only be added for AP transactions or Sales Credit Notes"
unless $self->invoice_type =~ m/purchase_invoice|ap_transaction|credit_note/;

$self->_add_bank_fx_fees(fee => _round($params{fx_fee_amount}),
bt_id => $params{bt_id},
bank_chart_id => $params{chart_id},
memo => $params{memo},
source => $params{source},
transdate_obj => $transdate_obj );
# invoice_amount add gl booking
$main::lxdebug->message(0, 'fee ' . $params{fx_fee_amount});
$return_bank_amount += _round($params{fx_fee_amount}); # invoice_type needs negative bank_amount
$main::lxdebug->message(0, 'bank_amount' . $return_bank_amount);
#$fx_gain_loss_amount = _round($params{amount} - ($params{amount} / $self->get_exchangerate * $exchangerate) );
14824095 Jan Büren
}
7b349901 Jan Büren
} elsif (!$self->forex) { # invoices uses default currency. no exchangerate
ba68038e Geoffrey Richardson
$exchangerate = 1;
7b349901 Jan Büren
} else {
die "Cannot calculate exchange rate, if invoices uses the default currency";
}
ba68038e Geoffrey Richardson
15f58ff3 Geoffrey Richardson
# 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});
bbb58258 Geoffrey Richardson
croak "can't find bank account with id " . $params{chart_id} unless ref $account_bank;
15f58ff3 Geoffrey Richardson
my $reference_account = $self->reference_account;
croak "can't find reference account (link = AR/AP) for invoice" unless ref $reference_account;

c4d3f82d Moritz Bunkus
my $memo = $params{memo} // '';
my $source = $params{source} // '';
15f58ff3 Geoffrey Richardson
ba68038e Geoffrey Richardson
my $rounded_params_amount = _round( $params{amount} ); # / $exchangerate);
15f58ff3 Geoffrey Richardson
my $db = $self->db;
58d09211 Moritz Bunkus
$db->with_transaction(sub {
15f58ff3 Geoffrey Richardson
my $new_acc_trans;

# all three payment type create 1 AR/AP booking (the paid part)
23b40897 Jan Büren
# with_skonto_pt creates 1 bank booking and n skonto bookings (1 for each tax type)
# and one tax correction as a gl booking
15f58ff3 Geoffrey Richardson
# without_skonto creates 1 bank booking

23b40897 Jan Büren
unless ( $rounded_params_amount == 0 ) {
69e03937 Jan Büren
# cases with_skonto_pt, free_skonto and without_skonto
15f58ff3 Geoffrey Richardson
# for case with_skonto_pt we need to know the corrected amount at this
23b40897 Jan Büren
# stage because we don't use $params{amount} ?!
15f58ff3 Geoffrey Richardson
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
7b349901 Jan Büren
$paid_amount += $pay_amount;
ba68038e Geoffrey Richardson
my $amount = (-1 * $pay_amount) * $mult;
58dbf540 Jan Büren
$main::lxdebug->message(0, 'bank pay amount:' . $pay_amount);
$main::lxdebug->message(0, 'paidamount:' . $paid_amount);
15f58ff3 Geoffrey Richardson
# total amount against bank, do we already know this by now?
705d2fe1 Jan Büren
# Yes, method requires this
15f58ff3 Geoffrey Richardson
$new_acc_trans = SL::DB::AccTransaction->new(trans_id => $self->id,
chart_id => $account_bank->id,
chart_link => $account_bank->link,
ba68038e Geoffrey Richardson
amount => $amount,
15f58ff3 Geoffrey Richardson
transdate => $transdate_obj,
source => $source,
memo => $memo,
2d8e82ac Geoffrey Richardson
project_id => $params{project_id} ? $params{project_id} : undef,
15f58ff3 Geoffrey Richardson
taxkey => 0,
tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
$new_acc_trans->save;
58dbf540 Jan Büren
$return_bank_amount += $amount;
$main::lxdebug->message(0, 'return 5 :' . $return_bank_amount);
$main::lxdebug->message(0, 'paid amount hier 1 :' . $paid_amount);
7dd42f87 Jan Büren
push @new_acc_ids, $new_acc_trans->acc_trans_id;
7b349901 Jan Büren
# deal with fxtransaction ...
# if invoice exchangerate differs from exchangerate of payment
# add fxloss or fxgain
58dbf540 Jan Büren
if ($exchangerate != 1 && $self->get_exchangerate and $self->get_exchangerate != 1 and $self->get_exchangerate != $exchangerate) {
7b349901 Jan Büren
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";
58dbf540 Jan Büren
# AMOUNT == EUR / fx rate pay * (fx rate invoice - fx rate pa)
# rate invoice = 2, fx rate paid = 1.75
# partial payment of 15000 EUR invtotal 20000
# 15000/1.75 * (2 - 1.75) = 2142. EUR gain if invoice is purchase # sql ledger (with fx amount):
# AR.pm
# $amount = $form->round_amount($form->{"paid_$i"} * ($form->{exchangerate} - $form->{"exchangerate_$i"}) * -1, 2);
# AP.pm
# exchangerate gain/loss
# $amount = $form->round_amount($form->{"paid_$i"} * ($form->{exchangerate} - $form->{"exchangerate_$i"}), 2);
# ==>
# my $fx_gain_loss_sign = $is_sales ? -1 : 1; # multiplier for getting the right sign depending on ar/ap

#my $fx_gain_loss_sign = $self->invoice_type =~ m/purchase_invoice|ap_transaction|credit_note/ ? 1
# : $self->invoice_type =~ m/invoice|ar_transaction|purchase_credit_note|invoice_for_advance_payment/ ? -1
# : die "invalid state";

$fx_gain_loss_amount = _round($amount / $exchangerate * ( $self->get_exchangerate - $exchangerate)); # * $fx_gain_loss_sign;

$main::lxdebug->message(0, 'was sagt gain loss 2 ' . $fx_gain_loss_amount);
# die "huchz" . $fx_gain_loss_amount;
7b349901 Jan Büren
my $gain_loss_chart = $fx_gain_loss_amount > 0 ? $fxgain_chart : $fxloss_chart;
5ef28dc4 Jan Büren
# $paid_amount += abs($fx_gain_loss_amount); # if $fx_gain_loss_amount < 0; # only add if we have fx_loss
58dbf540 Jan Büren
$main::lxdebug->message(0, 'paid hier 1 ' . $paid_amount);
# for sales add loss to ar.paid and subtract gain from ar.paid
# for purchase add gain to ap.paid and subtract loss from ap.paid
$paid_amount += abs($fx_gain_loss_amount) if $fx_gain_loss_amount < 0 && $self->is_sales; # extract if we have fx_loss
$paid_amount -= abs($fx_gain_loss_amount) if $fx_gain_loss_amount > 0 && $self->is_sales; # but add if to match original invoice amount (arap)
$paid_amount += abs($fx_gain_loss_amount) if $fx_gain_loss_amount > 0 && !$self->is_sales; # but add if to match original invoice amount (arap)
$paid_amount -= abs($fx_gain_loss_amount) if $fx_gain_loss_amount < 0 && !$self->is_sales; # extract if we have fx_loss
# (self->amount - self->paid) / $self->exchangerate
$main::lxdebug->message(0, 'paid dort 2 ' . $paid_amount);

d10ce474 Jan Büren
$main::lxdebug->message(0, 'return 1 ' . $return_bank_amount);
$main::lxdebug->message(0, 'paid amount hier 2 ' . $paid_amount);
# $return_bank_amount += $fx_gain_loss_amount if $fx_gain_loss_amount < 0; # only add if we have fx_loss
5ef28dc4 Jan Büren
7b349901 Jan Büren
$main::lxdebug->message(0, 'paid2chart ' . $fx_gain_loss_amount);
d10ce474 Jan Büren
$main::lxdebug->message(0, 'return 2 ' . $return_bank_amount);
7b349901 Jan Büren
# $fx_gain_loss_amount = $gain_loss_amount;

ba68038e Geoffrey Richardson
$new_acc_trans = SL::DB::AccTransaction->new(trans_id => $self->id,
7b349901 Jan Büren
chart_id => $gain_loss_chart->id,
chart_link => $gain_loss_chart->link,
amount => $fx_gain_loss_amount,
ba68038e Geoffrey Richardson
transdate => $transdate_obj,
source => $source,
memo => $memo,
taxkey => 0,
7b349901 Jan Büren
fx_transaction => 0, # probably indicates a real bank account in foreign currency
ba68038e Geoffrey Richardson
tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
$new_acc_trans->save;
7dd42f87 Jan Büren
push @new_acc_ids, $new_acc_trans->acc_trans_id;
356fec7e Jan Büren
}
}
23b40897 Jan Büren
# skonto cases
if ($params{payment_type} eq 'with_skonto_pt' or $params{payment_type} eq 'free_skonto' ) {
15f58ff3 Geoffrey Richardson
my $total_skonto_amount;
if ( $params{payment_type} eq 'with_skonto_pt' ) {
$total_skonto_amount = $self->skonto_amount;
69e03937 Jan Büren
} elsif ( $params{payment_type} eq 'free_skonto') {
$total_skonto_amount = $params{skonto_amount};
}
d8275f6e Jan Büren
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});
15f58ff3 Geoffrey Richardson
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'},
ba68038e Geoffrey Richardson
chart_link => SL::DB::Manager::Chart->find_by(id => $skonto_booking->{'chart_id'})->link,
15f58ff3 Geoffrey Richardson
amount => $amount * $mult,
transdate => $transdate_obj,
source => $params{source},
taxkey => 0,
tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
0d34b381 Geoffrey Richardson
# the acc_trans entries are saved individually, not added to $self and then saved all at once
15f58ff3 Geoffrey Richardson
$new_acc_trans->save;
7dd42f87 Jan Büren
push @new_acc_ids, $new_acc_trans->acc_trans_id;
15f58ff3 Geoffrey Richardson
$reference_amount -= abs($amount);
7b349901 Jan Büren
$paid_amount += -1 * $amount;
15f58ff3 Geoffrey Richardson
$skonto_amount_check -= $skonto_booking->{'skonto_amount'};
9a2a4c7f Jan Büren
}
356fec7e Jan Büren
}
15f58ff3 Geoffrey Richardson
my $arap_amount = 0;

23b40897 Jan Büren
if ( $params{payment_type} eq 'without_skonto' ) {
15f58ff3 Geoffrey Richardson
$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;
69e03937 Jan Büren
} 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};
356fec7e Jan Büren
}
15f58ff3 Geoffrey Richardson
# regardless of payment_type there is always only exactly one arap booking
7b349901 Jan Büren
# TODO: compare $arap_amount to running total and/or use this as running total for ar.paid|ap.paid
15f58ff3 Geoffrey Richardson
my $arap_booking= SL::DB::AccTransaction->new(trans_id => $self->id,
chart_id => $reference_account->id,
chart_link => $reference_account->link,
7b349901 Jan Büren
amount => _round($arap_amount * $mult - $fx_gain_loss_amount),
15f58ff3 Geoffrey Richardson
transdate => $transdate_obj,
source => '', #$params{source},
taxkey => 0,
tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
$arap_booking->save;
7dd42f87 Jan Büren
push @new_acc_ids, $arap_booking->acc_trans_id;
15f58ff3 Geoffrey Richardson
953b505f Jan Büren
# hook for invoice_for_advance_payment DATEV always pairs, acc_trans_id has to be higher than arap_booking ;-)
f4ebee3d Bernd Bleßmann
if ($self->invoice_type eq 'invoice_for_advance_payment') {
953b505f Jan Büren
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' : '';
4e33311d Jan Büren
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;

953b505f Jan Büren
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);
4e33311d Jan Büren
$tax_booking->save;
push @new_acc_ids, $tax_booking->acc_trans_id;
953b505f Jan Büren
}
}
7b349901 Jan Büren
# $fx_gain_loss_amount *= -1 if $self->is_sales;
$self->paid($self->paid + _round($paid_amount)) if $paid_amount;
15f58ff3 Geoffrey Richardson
$self->datepaid($transdate_obj);
$self->save;

0d34b381 Geoffrey Richardson
# 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');

c6af9711 Moritz Bunkus
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;
}
}
15f58ff3 Geoffrey Richardson
c6af9711 Moritz Bunkus
if ( $datev_check ) {
15f58ff3 Geoffrey Richardson
c6af9711 Moritz Bunkus
my $datev = SL::DATEV->new(
dbh => $db->dbh,
trans_id => $self->{id},
);
15f58ff3 Geoffrey Richardson
0a64ac3d Geoffrey Richardson
$datev->generate_datev_data;
15f58ff3 Geoffrey Richardson
c6af9711 Moritz Bunkus
if ($datev->errors) {
58d09211 Moritz Bunkus
# this exception should be caught by with_transaction, which handles the rollback
c6af9711 Moritz Bunkus
die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
}
15f58ff3 Geoffrey Richardson
}

58d09211 Moritz Bunkus
1;

15f58ff3 Geoffrey Richardson
}) || die t8('error while paying invoice #1 : ', $self->invnumber) . $db->error . "\n";
58dbf540 Jan Büren
$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;
c5dccb51 Jan Büren
}
15f58ff3 Geoffrey Richardson
sub skonto_date {
my $self = shift;

5a485635 Jan Büren
return undef unless ref $self->payment_terms eq 'SL::DB::PaymentTerm';
a6a97a5f Bernd Bleßmann
return undef unless $self->payment_terms->terms_skonto > 0;
5a485635 Jan Büren
a6a97a5f Bernd Bleßmann
return DateTime->from_object(object => $self->transdate)->add(days => $self->payment_terms->terms_skonto);
1216d52e Jan Büren
}
15f58ff3 Geoffrey Richardson
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(
1fdb8bc7 Jan Büren
'trans_id' => $self->id,
'!chart_id' => $::instance_conf->get_advance_payment_clearing_chart_id,
15f58ff3 Geoffrey Richardson
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;
1216d52e Jan Büren
}
15f58ff3 Geoffrey Richardson
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

8d053869 Moritz Bunkus
return ($self->amount // 0) - ($self->paid // 0);
1216d52e Jan Büren
}
15f58ff3 Geoffrey Richardson
58dbf540 Jan Büren
sub open_amount_fx {
# validate shift == $self
validate_pos(
@_,
{ can => [ qw(forex get_exchangerate) ],
callbacks => { 'has forex' => sub { return $_[0]->forex } } },
{ callbacks => {
'is a positive real' => sub { return $_[0] =~ m/^[+]?\d+(\.\d+)?$/ }, },
}
);

my ($self, $fx_rate) = @_;

return ( $self->open_amount / $self->get_exchangerate ) * $fx_rate;

}

sub amount_less_skonto_fx {
# validate shift == $self
validate_pos(
@_,
{ can => [ qw(forex get_exchangerate percent_skonto) ],
callbacks => { 'has forex' => sub { return $_[0]->forex } } },
{ callbacks => {
'is a positive real' => sub { return $_[0] =~ m/^[+]?\d+(\.\d+)?$/ }, },
}
);

my ($self, $fx_rate) = @_;

return ( $self->amount_less_skonto / $self->get_exchangerate ) * $fx_rate;
}



15f58ff3 Geoffrey Richardson
sub skonto_amount {
my $self = shift;

return $self->amount - $self->amount_less_skonto;
1216d52e Jan Büren
}
15f58ff3 Geoffrey Richardson
sub percent_skonto {
my $self = shift;

my $percent_skonto = 0;

a6a97a5f Bernd Bleßmann
return undef unless ref $self->payment_terms;
return undef unless $self->payment_terms->percent_skonto > 0;
$percent_skonto = $self->payment_terms->percent_skonto;
15f58ff3 Geoffrey Richardson
return $percent_skonto;
1216d52e Jan Büren
}
15f58ff3 Geoffrey Richardson
sub amount_less_skonto {
# amount that has to be paid if skonto applies, always return positive rounded values
6c0095f1 Jan Büren
# no, rare case, but credit_notes and negative ap have negative amounts
# and therefore this comment may be misguiding
15f58ff3 Geoffrey Richardson
# the result is rounded so we can directly compare it with the user input
my $self = shift;

00451fb0 Sven Schöling
my $percent_skonto = $self->percent_skonto || 0;
15f58ff3 Geoffrey Richardson
return _round($self->amount - ( $self->amount * $percent_skonto) );

1216d52e Jan Büren
}
4e795d54 Jan Büren
sub _add_bank_fx_fees {
my ($self, %params) = @_;
my $amount = $params{fee};

croak "no amount passed for bank fx fees" unless abs(_round($amount)) >= 0.01;
croak "no banktransaction.id passed" unless $params{bt_id};
croak "no bank chart id passed" unless $params{bank_chart_id};
croak "no banktransaction.transdate passed" unless ref $params{transdate_obj} eq 'DateTime';

$params{memo} //= '';
$params{source} //= '';

my ($credit, $debit);
$credit = SL::DB::Chart->load_cached($params{bank_chart_id});
$debit = SL::DB::Manager::Chart->find_by(description => 'Nebenkosten des Geldverkehrs');
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('Automatic Foreign Exchange Bank Fees') . " " . $self->invnumber,
department_id => $self->department_id ? $self->department_id : undef,
imported => 0, # not imported
taxincluded => 0,
)->add_chart_booking(
chart => $debit,
debit => abs($amount),
source => t8('Automatic Foreign Exchange Bank Fees') . " " . $self->invnumber,
memo => $params{memo},
tax_id => 0,
)->add_chart_booking(
chart => $credit,
credit => abs($amount),
source => t8('Automatic Foreign Exchange Bank Fees') . " " . $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 ap to gl
# linked gl booking will appear in tab linked records
# this is just a link for convenience
%props_rl = (
58dbf540 Jan Büren
from_table => $self->is_sales ? 'ar' : 'ap', # yep sales credit notes
#from_table => 'ap',
4e795d54 Jan Büren
from_id => $self->id,
to_table => 'gl',
to_id => $current_transaction->id,
);
SL::DB::RecordLink->new(%props_rl)->save;
}
15f58ff3 Geoffrey Richardson
d8275f6e Jan Büren
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';
293fb807 Jan Büren
5a5ec009 Jan Büren
$params{memo} //= '';
$params{source} //= '';


d8275f6e Jan Büren
my $is_sales = $self->is_sales;
my (@skonto_charts, $inv_calc, $total_skonto_rounded);
293fb807 Jan Büren
d8275f6e Jan Büren
$inv_calc = $self->get_tax_and_amount_by_tax_chart_id();
293fb807 Jan Büren
# foreach tax.chart_id || $entry->{ta..id}
while (my ($tax_chart_id, $entry) = each %{ $inv_calc } ) {
d8275f6e Jan Büren
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;
293fb807 Jan Büren
# percent net amount
d8275f6e Jan Büren
my $transaction_net_skonto_percent = abs($entry->{netamount} / $self->amount);
my $skonto_netamount_unrounded = abs($amount * $transaction_net_skonto_percent);
293fb807 Jan Büren
# percent tax amount
d8275f6e Jan Büren
my $transaction_tax_skonto_percent = abs($entry->{tax} / $self->amount);
my $skonto_taxamount_unrounded = abs($amount * $transaction_tax_skonto_percent);
293fb807 Jan Büren
d8275f6e Jan Büren
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;

293fb807 Jan Büren
# entry net + tax for caller
d8275f6e Jan Büren
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

a3263b66 Jan Büren
# 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);
d8275f6e Jan Büren
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';
da3cca7d Jan Büren
my $notes = SL::HTML::Util->strip($self->notes);
d8275f6e Jan Büren
my $current_transaction = SL::DB::GLTransaction->new(
employee_id => $self->employee_id,
transdate => $params{transdate_obj},
notes => $params{source} . ' ' . $params{memo},
da3cca7d Jan Büren
description => $notes || $self->invnumber,
d8275f6e Jan Büren
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;

293fb807 Jan Büren
# add a stable link acc_trans_id to bank_transactions.id
d8275f6e Jan Büren
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
293fb807 Jan Büren
# we ignore tax rounding errors as long as the amount (user input or calculated)
# is fully assigned.
d8275f6e Jan Büren
# we simply alter one cent for the first skonto booking entry
# should be correct for most of the cases (no invoices with mixed taxes)
a3263b66 Jan Büren
if (_round($total_skonto_rounded - $amount) >= 0.01) {
d8275f6e Jan Büren
# subtract one cent
a3263b66 Jan Büren
$skonto_charts[0]->{skonto_amount} -= 0.01;
} elsif (_round($amount - $total_skonto_rounded) >= 0.01) {
# add one cent
d8275f6e Jan Büren
$skonto_charts[0]->{skonto_amount} += 0.01;
293fb807 Jan Büren
}
d8275f6e Jan Büren
# return same array of skonto charts as sub skonto_charts
return @skonto_charts;
}
15f58ff3 Geoffrey Richardson
sub within_skonto_period {
my $self = shift;
f8ea96a8 Jan Büren
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;
}
15f58ff3 Geoffrey Richardson
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;
1216d52e Jan Büren
}
15f58ff3 Geoffrey Richardson
sub get_payment_select_options_for_bank_transaction {
5a485635 Jan Büren
my ($self, $bt_id) = @_;
15f58ff3 Geoffrey Richardson
my @options;
5a485635 Jan Büren
b15c7890 Jan Büren
# 1. no sane skonto support for foreign currency yet
if ($self->forex) {
push(@options, { payment_type => 'without_skonto', display => t8('without skonto'), selected => 1 });
return @options;
}
# 2. no skonto available -> done
if (!$self->skonto_date) {
fca94606 Jan Büren
push(@options, { payment_type => 'without_skonto', display => t8('without skonto'), selected => 1 });
5a485635 Jan Büren
push(@options, { payment_type => 'free_skonto', display => t8('free skonto') });
fca94606 Jan Büren
return @options;
}
5a485635 Jan Büren
# 2. valid skonto date, check if date is within skonto period
# CAVEAT template code expects with_skonto_pt at position 1 for visual help
# [% is_skonto_pt = SELECT_OPTIONS.1.selected %]

fca94606 Jan Büren
my $bt = SL::DB::BankTransaction->new(id => $bt_id)->load;
5a485635 Jan Büren
croak "No Bank Transaction with ID $bt_id found" unless ref $bt eq 'SL::DB::BankTransaction';

if ($self->within_skonto_period(transdate => $bt->transdate)) {
4650c028 Jan Büren
push(@options, { payment_type => 'without_skonto', display => t8('without skonto') });
5a485635 Jan Büren
push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt'), selected => 1 });
4650c028 Jan Büren
} else {
push(@options, { payment_type => 'without_skonto', display => t8('without skonto') , selected => 1 });
5a485635 Jan Büren
push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt')});
0d5b91f1 Jan Büren
}
5a485635 Jan Büren
push(@options, { payment_type => 'free_skonto', display => t8('free skonto') });
0d5b91f1 Jan Büren
return @options;
}
15f58ff3 Geoffrey Richardson
4e795d54 Jan Büren
sub get_exchangerate {
dee8b29f Geoffrey Richardson
my ($self) = @_;

return 1 if $self->currency_id == $::instance_conf->get_currency_id;

4e795d54 Jan Büren
# return record exchange rate if set
return $self->exchangerate if $self->exchangerate > 0;

# none defined check daily exchangerate at records transdate
02ba4e7a Geoffrey Richardson
die "transdate isn't a DateTime object:" . ref($self->transdate) unless ref($self->transdate) eq 'DateTime';
4e795d54 Jan Büren
dee8b29f Geoffrey Richardson
my $rate = SL::DB::Manager::Exchangerate->find_by(currency_id => $self->currency_id,
transdate => $self->transdate,
);
return undef unless $rate;
02ba4e7a Geoffrey Richardson
return $self->is_sales ? $rate->buy : $rate->sell; # also undef if not defined
2cbf256e Jan Büren
}
15f58ff3 Geoffrey Richardson
07c884e5 Martin Helmling
# locales for payment type
#
# $main::locale->text('without_skonto')
# $main::locale->text('with_skonto_pt')
#

15f58ff3 Geoffrey Richardson
sub validate_payment_type {
my $payment_type = shift;

23b40897 Jan Büren
my %allowed_payment_types = map { $_ => 1 } qw(without_skonto with_skonto_pt free_skonto);
15f58ff3 Geoffrey Richardson
croak "illegal payment type: $payment_type, must be one of: " . join(' ', keys %allowed_payment_types) unless $allowed_payment_types{ $payment_type };

return 1;
}

784c2880 Geoffrey Richardson
sub forex {
my ($self) = @_;
$self->currency_id == $::instance_conf->get_currency_id ? return 0 : return 1;
1216d52e Jan Büren
}
784c2880 Geoffrey Richardson
4e795d54 Jan Büren
sub get_exchangerate_for_bank_transaction {
validate_pos(
@_,
{ can => [ qw(forex is_sales) ],
callbacks => { 'has forex' => sub { return $_[0]->forex } } },
{ callbacks => {
'is an integer' => sub { return $_[0] =~ /^[1-9][0-9]*$/ },
'is a valid bank transaction' => sub { ref SL::DB::BankTransaction->load_cached($_[0]) eq 'SL::DB::BankTransaction' },
'has a valid valuta date' => sub { ref SL::DB::BankTransaction->load_cached($_[0])->valutadate eq 'DateTime' },
},
}
);

my ($self, $bt_id) = @_;

my $bt = SL::DB::BankTransaction->load_cached($bt_id);
my $rate = SL::DB::Manager::Exchangerate->find_by(currency_id => $self->currency_id,
transdate => $bt->valutadate,
);
return undef unless $rate;

return $self->is_sales ? $rate->buy : $rate->sell; # also undef if not defined
}

15f58ff3 Geoffrey Richardson
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.
23b40897 Jan Büren
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.
a4bbff92 Jan Büren
Transdate can either be a date object or a date string.
Chart_id is the id of the payment booking chart.
23b40897 Jan Büren
Amount is either a positive or negative number, and for the case 'free_skonto' might be zero.
524bc23e Jan Büren
CAVEAT! The helper tries to get the sign right and all calls from BankTransaction are
positive (abs($value)) values.
a4bbff92 Jan Büren
15f58ff3 Geoffrey Richardson
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,
0d34b381 Geoffrey Richardson
memo => 'foobar',
source => 'barfoo',
15f58ff3 Geoffrey Richardson
payment_type => 'without_skonto', # default if not specified
2d8e82ac Geoffrey Richardson
project_id => 25,
15f58ff3 Geoffrey Richardson
);

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,
0d34b381 Geoffrey Richardson
memo => 'foobar',
source => 'barfoo',
15f58ff3 Geoffrey Richardson
payment_type => 'with_skonto',
);

ba68038e Geoffrey Richardson
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',
5c1faed0 Geoffrey Richardson
payment_type => 'with_skonto_pt',
ba68038e Geoffrey Richardson
);

15f58ff3 Geoffrey Richardson
Allowed payment types are:
23b40897 Jan Büren
without_skonto with_skonto_pt
15f58ff3 Geoffrey Richardson
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.

23b40897 Jan Büren
So passing amount doesn't have any effect for the case C<with_skonto_pt>.
15f58ff3 Geoffrey Richardson
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
23b40897 Jan Büren
_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.
15f58ff3 Geoffrey Richardson
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.

ba68038e Geoffrey Richardson
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.

c5dccb51 Jan Büren
If successful the return value will be 1 in scalar context or in list context
23b40897 Jan Büren
the two or more (gl transaction for skonto tax correction) ids (acc_trans_id)
of the newly created bookings.

9d262289 Geoffrey Richardson
15f58ff3 Geoffrey Richardson
=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,
a6a97a5f Bernd Bleßmann
e.g. 0.02 for 2%. Payment terms come from invoice settingssettings for ap.
15f58ff3 Geoffrey Richardson
=item C<amount_less_skonto>

a6a97a5f Bernd Bleßmann
If the invoice has a payment term,
15f58ff3 Geoffrey Richardson
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.

356d8bda Jan Büren
=item C<within_skonto_period [transdate =E<gt> DateTime]>
15f58ff3 Geoffrey Richardson
356d8bda Jan Büren
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.
15f58ff3 Geoffrey Richardson
356d8bda Jan Büren
Checks if the invoice has skontoable payment terms configured and whether the date
is within the skonto max date.
15f58ff3 Geoffrey Richardson
356d8bda Jan Büren
If one of the condition fails, a hopefully helpful error message is returned.
15f58ff3 Geoffrey Richardson
=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
}

23b40897 Jan Büren
=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.
15f58ff3 Geoffrey Richardson
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');
23b40897 Jan Büren
my @skonto_charts = $invoice->_skonto_charts_and_tax_correction(bt_id => $bt_id,
transdate_obj => $transdate_obj);
15f58ff3 Geoffrey Richardson
or with the total skonto amount as an argument:
23b40897 Jan Büren
my @skonto_charts = $invoice->_skonto_charts_and_tax_correction(amount => $invoice->open_amount,
bt_id => $bt_id,
transdate_obj => $transdate_obj);
15f58ff3 Geoffrey Richardson
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.

23b40897 Jan Büren
C<_skonto_charts_and_tax_correction> generates one entry for each tax type entry.
15f58ff3 Geoffrey Richardson
=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
23b40897 Jan Büren
without_skonto and with_skonto_pt and select the most
15f58ff3 Geoffrey Richardson
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.

f72a365d Jan Büren
This is a helper function for BankTransaction/ajax_payment_suggestion and
template/webpages/bank_transactions/invoices.html
15f58ff3 Geoffrey Richardson
23b40897 Jan Büren
We are working with an existing payment, so (deprecated) difference_as_skonto never makes sense.
15f58ff3 Geoffrey Richardson
f72a365d Jan Büren
If skonto is not possible (skonto_date does not exists) simply return
the single 'no skonto' option as a visual hint.

15f58ff3 Geoffrey Richardson
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.

dee8b29f Geoffrey Richardson
=item C<exchangerate>

02ba4e7a Geoffrey Richardson
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.
dee8b29f Geoffrey Richardson
784c2880 Geoffrey Richardson
=item C<forex>

Returns 1 if record uses a different currency, 0 if the default currency is used.

15f58ff3 Geoffrey Richardson
=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

23b40897 Jan Büren
=item * C<_skonto_charts_and_tax_correction>
6c0095f1 Jan Büren
Cannot handle negative skonto amounts, will always calculate the skonto amount
for credit notes or negative ap transactions with a positive sign.


15f58ff3 Geoffrey Richardson
=back

=head1 AUTHOR

356d8bda Jan Büren
G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
15f58ff3 Geoffrey Richardson
=cut