Projekt

Allgemein

Profil

Herunterladen (49 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 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);
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) = @_;
# todo named 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 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)
my ($exchangerate, $currency);
my $fx_gain_loss_amount = 0;
my $return_bank_amount = 0;
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});

die "No currency found" unless ref $currency eq 'SL::DB::Currency';
die "No exchange rate" unless $params{exchangerate} > 0;

$exchangerate = $params{exchangerate};

my $new_open_amount = ( $self->open_amount / $self->get_exchangerate ) * $exchangerate;
# caller wants to book fees and set a fee amount
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
$return_bank_amount += _round($params{fx_fee_amount}); # invoice_type needs negative bank_amount
}
} elsif (!$self->forex) { # invoices uses default currency. no exchangerate
$exchangerate = 1;
} else {
die "Cannot calculate exchange rate, if invoices uses the default currency";
}

# 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 $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;

my $amount = (-1 * $pay_amount) * $mult;

# total amount against bank, do we already know this by now?
# Yes, method requires this
$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 invoice exchangerate differs from exchangerate of payment
# add fxloss or fxgain
if ($exchangerate != 1 && $self->get_exchangerate and $self->get_exchangerate != 1 and $self->get_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";
#
# AMOUNT == EUR / fx rate payment * (fx rate invoice - fx rate payment)
# 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

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

my $gain_loss_chart = $fx_gain_loss_amount > 0 ? $fxgain_chart : $fxloss_chart;
# for sales add loss to ar.paid and subtract gain from ar.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)
# 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; # 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

$new_acc_trans = SL::DB::AccTransaction->new(trans_id => $self->id,
chart_id => $gain_loss_chart->id,
chart_link => $gain_loss_chart->link,
amount => $fx_gain_loss_amount,
transdate => $transdate_obj,
source => $source,
memo => $memo,
taxkey => 0,
fx_transaction => 0, # probably indicates a real bank account in foreign currency
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;
$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 and/or use this as running total for ar.paid|ap.paid
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 - $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;
}
}
$self->paid($self->paid + _round($paid_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 eq 'SL::DB::PaymentTerm';
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 open_amount_fx {
validate_pos(
@_,
{