use parent qw(Exporter);
our @EXPORT = qw(pay_invoice);
-our @EXPORT_OK = qw(skonto_date skonto_charts amount_less_skonto within_skonto_period percent_skonto reference_account reference_amount open_amount open_percent remaining_skonto_days skonto_amount check_skonto_configuration valid_skonto_amount get_payment_suggestions validate_payment_type open_sepa_transfer_amount get_payment_select_options_for_bank_transaction create_bank_transaction);
+our @EXPORT_OK = qw(skonto_date skonto_charts amount_less_skonto within_skonto_period percent_skonto reference_account reference_amount open_amount open_percent remaining_skonto_days skonto_amount check_skonto_configuration valid_skonto_amount get_payment_suggestions validate_payment_type open_sepa_transfer_amount get_payment_select_options_for_bank_transaction create_bank_transaction exchangerate forex);
our %EXPORT_TAGS = (
"ALL" => [@EXPORT, @EXPORT_OK],
);
use SL::DATEV qw(:CONSTANTS);
use SL::Locale::String qw(t8);
use List::Util qw(sum);
+use SL::DB::Exchangerate;
+use SL::DB::Currency;
use Carp;
#
my $is_sales = ref($self) eq 'SL::DB::Invoice';
my $mult = $is_sales ? 1 : -1; # multiplier for getting the right sign depending on ar/ap
- my $paid_amount = 0; # the amount that will be later added to $self->paid
+ my $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};
# check for required parameters
Common::check_params(\%params, qw(chart_id transdate));
- my $transdate_obj = $::locale->parse_date_to_object($params{transdate});
+ my $transdate_obj;
+ if (ref($params{transdate} eq 'DateTime')) {
+ $transdate_obj = $params{transdate};
+ } else {
+ $transdate_obj = $::locale->parse_date_to_object($params{transdate});
+ };
croak t8('Illegal date') unless ref $transdate_obj;
# check for closed period
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
+ my ($exchangerate,$currency);
+ if ($params{currency} || $params{currency_id} || $self->currency_id != $::instance_conf->get_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;
+ };
+
# input checks:
if ( $params{'payment_type'} eq 'without_skonto' ) {
croak "invalid amount for payment_type 'without_skonto': $params{'amount'}\n" unless abs($params{'amount'}) > 0;
my $memo = $params{'memo'} || '';
my $source = $params{'source'} || '';
- my $rounded_params_amount = _round( $params{amount} );
+ my $rounded_params_amount = _round( $params{amount} ); # / $exchangerate);
+ my $fx_gain_loss_amount = 0; # for fx_gain and fx_loss
my $db = $self->db;
$db->do_transaction(sub {
$pay_amount = $self->amount_less_skonto if $params{payment_type} eq 'with_skonto_pt';
# bank account and AR/AP
- $paid_amount += $pay_amount;
+ $paid_amount += $pay_amount * $exchangerate;
+
+ my $amount = (-1 * $pay_amount) * $mult;
+
# total amount against bank, do we already know this by now?
$new_acc_trans = SL::DB::AccTransaction->new(trans_id => $self->id,
chart_id => $account_bank->id,
chart_link => $account_bank->link,
- amount => (-1 * $pay_amount) * $mult,
+ amount => $amount,
transdate => $transdate_obj,
source => $source,
memo => $memo,
+ project_id => $params{project_id} ? $params{project_id} : undef,
taxkey => 0,
tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
$new_acc_trans->save;
+
+ # deal with fxtransaction
+ if ( $self->currency_id != $::instance_conf->get_currency_id ) {
+ my $fxamount = _round($amount - ($amount * $exchangerate));
+ $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $self->id,
+ chart_id => $account_bank->id,
+ chart_link => $account_bank->link,
+ amount => $fxamount * -1,
+ transdate => $transdate_obj,
+ source => $source,
+ memo => $memo,
+ taxkey => 0,
+ fx_transaction => 1,
+ tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+ $new_acc_trans->save;
+ # if invoice exchangerate differs from exchangerate of payment
+ # deal with fxloss and fxamount
+ if ($self->exchangerate and $self->exchangerate != 1 and $self->exchangerate != $exchangerate) {
+ my $fxgain_chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxgain_accno_id) || die "Can't determine fxgain chart";
+ my $fxloss_chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxloss_accno_id) || die "Can't determine fxloss chart";
+ my $gain_loss_amount = _round($amount * ($exchangerate - $self->exchangerate ) * -1,2);
+ my $gain_loss_chart = $gain_loss_amount > 0 ? $fxgain_chart : $fxloss_chart;
+ $fx_gain_loss_amount = $gain_loss_amount;
+
+ $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $self->id,
+ chart_id => $gain_loss_chart->id,
+ chart_link => $gain_loss_chart->link,
+ amount => $gain_loss_amount,
+ transdate => $transdate_obj,
+ source => $source,
+ memo => $memo,
+ taxkey => 0,
+ fx_transaction => 0,
+ tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+ $new_acc_trans->save;
+
+ };
+ };
};
if ( $params{payment_type} eq 'difference_as_skonto' or $params{payment_type} eq 'with_skonto_pt' ) {
my $amount = -1 * $skonto_booking->{skonto_amount};
$new_acc_trans = SL::DB::AccTransaction->new(trans_id => $self->id,
chart_id => $skonto_booking->{'chart_id'},
- chart_link => SL::DB::Manager::Chart->find_by(id => $skonto_booking->{'chart_id'})->{'link'},
+ chart_link => SL::DB::Manager::Chart->find_by(id => $skonto_booking->{'chart_id'})->link,
amount => $amount * $mult,
transdate => $transdate_obj,
source => $params{source},
$new_acc_trans->save;
$reference_amount -= abs($amount);
- $paid_amount += -1 * $amount;
+ $paid_amount += -1 * $amount * $exchangerate;
$skonto_amount_check -= $skonto_booking->{'skonto_amount'};
};
if ( $params{payment_type} eq 'difference_as_skonto' ) {
my $arap_booking= SL::DB::AccTransaction->new(trans_id => $self->id,
chart_id => $reference_account->id,
chart_link => $reference_account->link,
- amount => $arap_amount * $mult,
+ amount => _round($arap_amount * $mult * $exchangerate - $fx_gain_loss_amount),
transdate => $transdate_obj,
source => '', #$params{source},
taxkey => 0,
tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
$arap_booking->save;
- $self->paid($self->paid+$paid_amount) if $paid_amount;
+ $fx_gain_loss_amount *= -1 if $self->is_sales;
+ $self->paid($self->paid + _round($paid_amount) + $fx_gain_loss_amount) if $paid_amount;
$self->datepaid($transdate_obj);
$self->save;
# my $transactions = $self->transactions;
foreach my $transaction (@{ $self->transactions }) {
# find all transactions with an AR_amount or AP_amount link
- my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->{taxkey}]);
+ my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->taxkey]);
croak "no tax for taxkey " . $transaction->{taxkey} unless ref $tax;
- $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->{chart_link}) };
+ $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->chart_link) };
if ( $is_sales && $transaction->{chartlinks}->{AR_amount} ) {
$skonto_configured = 0 unless $tax->skonto_sales_chart_id;
} elsif ( !$is_sales && $transaction->{chartlinks}->{AP_amount}) {
};
+sub exchangerate {
+ my ($self) = @_;
+
+ return 1 if $self->currency_id == $::instance_conf->get_currency_id;
+
+ die "transdate isn't a DateTime object:" . ref($self->transdate) unless ref($self->transdate) eq 'DateTime';
+ my $rate = SL::DB::Manager::Exchangerate->find_by(currency_id => $self->currency_id,
+ transdate => $self->transdate,
+ );
+ return undef unless $rate;
+
+ return $self->is_sales ? $rate->buy : $rate->sell; # also undef if not defined
+};
sub get_payment_suggestions {
};
+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;
memo => 'foobar',
source => 'barfoo',
payment_type => 'without_skonto', # default if not specified
+ project_id => 25,
);
or with skonto:
payment_type => 'with_skonto',
);
+or in a certain currency:
+ $ap->pay_invoice(chart_id => $bank->chart_id,
+ amount => 500,
+ currency => 'USD',
+ transdate => DateTime->now->to_kivitendo,
+ memo => 'foobar',
+ source => 'barfoo',
+ payment_type => 'with_skonto',
+ );
+
Allowed payment types are:
without_skonto with_skonto_pt difference_as_skonto
Skonto doesn't/shouldn't apply if the invoice contains credited items.
+If no amount is given the whole open amout is paid.
+
+If neither currency or currency_id are given as params, the currency of the
+invoice is assumed to be the payment currency.
+
=item C<reference_account>
Returns a chart object which is the chart of the invoice with link AR or AP.
Amount is always relative to the absolute amount of the invoice, use positive
values for sales and purchases.
+=item C<exchangerate>
+
+Returns 1 immediately if the record uses the default currency.
+
+Returns the exchangerate in database format for the invoice according to that
+invoice's transdate, returning 'buy' for sales, 'sell' for purchases.
+
+If no exchangerate can be found for that day undef is returned.
+
+=item C<forex>
+
+Returns 1 if record uses a different currency, 0 if the default currency is used.
+
=back
=head1 TODO AND CAVEATS
when looking at open amount, maybe consider that there may already be queued
amounts in SEPA Export
-=item *
-
-Can only handle default currency.
-
=back
=head1 AUTHOR