From ba68038e4208fc04ad63cd783c527a15ab845026 Mon Sep 17 00:00:00 2001 From: "G. Richardson" Date: Mon, 13 Jun 2016 14:12:34 +0200 Subject: [PATCH] =?utf8?q?Paymenthelper=20kann=20Fremdw=C3=A4hrung=20=20mi?= =?utf8?q?t=20Steuer=20inkl.=20und=20exkl.?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- SL/DB/Helper/Payment.pm | 89 ++++++++++++++-- t/db_helper/payment.t | 228 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 298 insertions(+), 19 deletions(-) diff --git a/SL/DB/Helper/Payment.pm b/SL/DB/Helper/Payment.pm index 6ab869116..3c34dce11 100644 --- a/SL/DB/Helper/Payment.pm +++ b/SL/DB/Helper/Payment.pm @@ -29,7 +29,7 @@ sub pay_invoice { 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}; @@ -38,7 +38,13 @@ sub pay_invoice { # 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')) { + print "found transdate ref\n"; sleep 2; + $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 @@ -52,6 +58,31 @@ sub pay_invoice { 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; @@ -83,7 +114,7 @@ sub pay_invoice { my $memo = $params{'memo'} || ''; my $source = $params{'source'} || ''; - my $rounded_params_amount = _round( $params{amount} ); + my $rounded_params_amount = _round( $params{amount} ); # / $exchangerate); my $db = $self->db; $db->do_transaction(sub { @@ -107,19 +138,40 @@ sub pay_invoice { $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, 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)); + # print "amount: $amount, fxamount = $fxamount\n"; + # print "amount - (amount * exchangerate) = " . $amount . " - (" . $amount . " - " . $exchangerate . ")\n"; + $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 ( $params{payment_type} eq 'difference_as_skonto' or $params{payment_type} eq 'with_skonto_pt' ) { @@ -148,7 +200,7 @@ sub pay_invoice { 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}, @@ -159,7 +211,7 @@ sub pay_invoice { $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' ) { @@ -186,14 +238,14 @@ sub pay_invoice { 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), 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; + $self->paid($self->paid + _round($paid_amount)) if $paid_amount; $self->datepaid($transdate_obj); $self->save; @@ -386,10 +438,10 @@ sub check_skonto_configuration { # 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}) { @@ -717,6 +769,16 @@ 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 @@ -768,6 +830,11 @@ are negative values in acc_trans. E.g. one invoice with a positive value for 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 Returns a chart object which is the chart of the invoice with link AR or AP. diff --git a/t/db_helper/payment.t b/t/db_helper/payment.t index 26880f4a1..2d0f566a5 100644 --- a/t/db_helper/payment.t +++ b/t/db_helper/payment.t @@ -12,6 +12,7 @@ use List::Util qw(sum); use SL::DB::Buchungsgruppe; use SL::DB::Currency; +use SL::DB::Exchangerate; use SL::DB::Customer; use SL::DB::Vendor; use SL::DB::Employee; @@ -21,8 +22,11 @@ use SL::DB::Unit; use SL::DB::TaxZone; use SL::DB::BankAccount; use SL::DB::PaymentTerm; +use Data::Dumper; -my ($customer, $vendor, $currency_id, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $tax, $tax7, $taxzone, $payment_terms, $bank_account); +my ($customer, $vendor, $currency_id, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $tax, $tax7, $tax_9, $taxzone, $payment_terms, $bank_account); +my ($transdate, $transdate2, $currency, $exchangerate, $exchangerate2); +my ($ar_chart,$bank,$ar_amount_chart, $ap_chart, $ap_amount_chart); my $ALWAYS_RESET = 1; @@ -39,6 +43,8 @@ sub clear_up { SL::DB::Manager::Vendor->delete_all(all => 1); SL::DB::Manager::BankAccount->delete_all(all => 1); SL::DB::Manager::PaymentTerm->delete_all(all => 1); + SL::DB::Manager::Exchangerate->delete_all(all => 1); + SL::DB::Manager::Currency->delete_all(where => [ name => 'CUR' ]); }; sub reset_state { @@ -50,6 +56,8 @@ sub reset_state { clear_up(); + $transdate = DateTime->today; + $transdate2 = DateTime->today->add(days => 1); $buchungsgruppe = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%', %{ $params{buchungsgruppe} }) || croak "No accounting group"; $buchungsgruppe7 = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%') || croak "No accounting group for 7\%"; @@ -58,9 +66,23 @@ sub reset_state { $tax = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{tax} }) || croak "No tax"; $tax7 = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07) || croak "No tax for 7\%"; $taxzone = SL::DB::Manager::TaxZone->find_by( description => 'Inland') || croak "No taxzone"; + $tax_9 = SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.19, %{ $params{tax} }) || croak "No tax"; + # $tax7 = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07) || croak "No tax for 7\%"; $currency_id = $::instance_conf->get_currency_id; + $currency = SL::DB::Currency->new(name => 'CUR')->save; + $exchangerate = SL::DB::Exchangerate->new(transdate => $transdate, + buy => '1.33333', + sell => '1.33333', + currency_id => $currency->id, + )->save; + $exchangerate2 = SL::DB::Exchangerate->new(transdate => $transdate2, + buy => '1.55555', + sell => '1.55555', + currency_id => $currency->id, + )->save; + $customer = SL::DB::Customer->new( name => 'Test Customer', currency_id => $currency_id, @@ -135,6 +157,12 @@ sub reset_state { %{ $params{part4} } )->save; + $ar_chart = SL::DB::Manager::Chart->find_by( accno => '1400' ); # Forderungen + $ap_chart = SL::DB::Manager::Chart->find_by( accno => '1600' ); # Verbindlichkeiten + $bank = SL::DB::Manager::Chart->find_by( accno => '1200' ); # Bank + $ar_amount_chart = SL::DB::Manager::Chart->find_by( accno => '8400' ); # Erlöse + $ap_amount_chart = SL::DB::Manager::Chart->find_by( accno => '3400' ); # Wareneingang 19% + $reset_state_counter++; } @@ -179,14 +207,13 @@ sub new_purchase_invoice { # %params, )->save; - my $today = DateTime->today_local->to_kivitendo; my $expense_chart = SL::DB::Manager::Chart->find_by(accno => '3400'); my $expense_chart_booking= SL::DB::AccTransaction->new( trans_id => $purchase_invoice->id, chart_id => $expense_chart->id, chart_link => $expense_chart->link, amount => '-100', - transdate => $today, + transdate => $transdate, source => '', taxkey => 9, tax_id => SL::DB::Manager::Tax->find_by(taxkey => 9)->id); @@ -198,7 +225,7 @@ sub new_purchase_invoice { chart_id => $tax_chart->id, chart_link => $tax_chart->link, amount => '-19', - transdate => $today, + transdate => $transdate, source => '', taxkey => 0, tax_id => SL::DB::Manager::Tax->find_by(taxkey => 9)->id); @@ -209,7 +236,7 @@ sub new_purchase_invoice { chart_id => $expense_chart->id, chart_link => $expense_chart->link, amount => '-100', - transdate => $today, + transdate => $transdate, source => '', taxkey => 8, tax_id => SL::DB::Manager::Tax->find_by(taxkey => 8)->id); @@ -222,7 +249,7 @@ sub new_purchase_invoice { chart_id => $tax_chart->id, chart_link => $tax_chart->link, amount => '-7', - transdate => $today, + transdate => $transdate, source => '', taxkey => 0, tax_id => SL::DB::Manager::Tax->find_by(taxkey => 8)->id); @@ -232,7 +259,7 @@ sub new_purchase_invoice { chart_id => $arap_chart->id, chart_link => $arap_chart->link, amount => '226', - transdate => $today, + transdate => $transdate, source => '', taxkey => 0, tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id); @@ -623,7 +650,7 @@ sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_fi } -sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto_2cent() { +sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto_2cent() { reset_state() if $ALWAYS_RESET; # if there are two cents left there will be two skonto bookings, 1 cent each @@ -998,6 +1025,185 @@ sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_multiple() { is($total, 0, "${title}: even balance: this will fail due to rounding error in invoice post, not the skonto"); } +sub test_ar_currency_tax_not_included_and_payment { + my $netamount = $::form->round_amount(75 * $exchangerate->sell,2); # 75 in CUR, 100.00 in EUR + my $amount = $::form->round_amount($netamount * 1.19,2); # 100 in CUR, 119.00 in EUR + my $invoice = SL::DB::Invoice->new( + invoice => 0, + amount => $amount, + netamount => $netamount, + transdate => $transdate, + taxincluded => 0, + customer_id => $customer->id, + taxzone_id => $customer->taxzone_id, + currency_id => $currency->id, + transactions => [], + notes => 'test_ar_currency_tax_not_included_and_payment', + ); + $invoice->add_ar_amount_row( + amount => $invoice->netamount, + chart => $ar_amount_chart, + tax_id => $tax->id, + ); + + $invoice->create_ar_row(chart => $ar_chart); + $invoice->save; + + is(SL::DB::Manager::Invoice->get_all_count(where => [ invoice => 0 ]), 1, 'there is one ar transaction'); + is($invoice->currency_id , $currency->id , 'currency_id has been saved'); + is($invoice->netamount , 100 , 'ar amount has been converted'); + is($invoice->amount , 119 , 'ar amount has been converted'); + is($invoice->taxincluded , 0 , 'ar transaction doesn\'t have taxincluded'); + is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_amount_chart->id, trans_id => $invoice->id)->amount, '100.00000', $ar_amount_chart->accno . ': has been converted for currency'); + is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_chart->id, trans_id => $invoice->id)->amount, '-119.00000', $ar_chart->accno . ': has been converted for currency'); + + $invoice->pay_invoice(chart_id => $bank->id, + amount => 50, + currency => 'CUR', + transdate => $transdate->to_kivitendo, + ); + $invoice->pay_invoice(chart_id => $bank->id, + amount => 39.25, + currency => 'CUR', + transdate => $transdate->to_kivitendo, + ); + # $invoice->pay_invoice(chart_id => $bank->id, + # amount => 30, + # transdate => $transdate2->to_kivitendo, + # ); + is(scalar @{$invoice->transactions}, 9, 'ar transaction has 9 transactions (incl. fxtransactions)'); + is($invoice->paid, $invoice->amount, 'ar transaction paid = amount in default currency'); +}; + +sub test_ar_currency_tax_included { + # we want the acc_trans amount to be 100 + my $amount = $::form->round_amount(75 * $exchangerate->sell * 1.19); + my $netamount = $::form->round_amount($amount / 1.19,2); + my $invoice = SL::DB::Invoice->new( + invoice => 0, + amount => 119, #$amount, + netamount => 100, #$netamount, + transdate => $transdate, + taxincluded => 1, + customer_id => $customer->id, + taxzone_id => $customer->taxzone_id, + currency_id => $currency->id, + notes => 'test_ar_currency_tax_included', + transactions => [], + ); + $invoice->add_ar_amount_row( # should take care of taxincluded + amount => $invoice->amount, # tax included in local currency + chart => $ar_amount_chart, + tax_id => $tax->id, + ); + + $invoice->create_ar_row( chart => $ar_chart ); + $invoice->save; + is(SL::DB::Manager::Invoice->get_all_count(where => [ invoice => 0 ]), 2, 'there are now two ar transactions'); + is($invoice->currency_id , $currency->id , 'currency_id has been saved'); + is($invoice->amount , $amount , 'amount ok'); + is($invoice->netamount , $netamount , 'netamount ok'); + is($invoice->taxincluded , 1 , 'ar transaction has taxincluded'); + is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_amount_chart->id, trans_id => $invoice->id)->amount, '100.00000', $ar_amount_chart->accno . ': has been converted for currency'); + is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ar_chart->id, trans_id => $invoice->id)->amount, '-119.00000', $ar_chart->accno . ': has been converted for currency'); + $invoice->pay_invoice(chart_id => $bank->id, + amount => 89.25, + currency => 'CUR', + transdate => $transdate->to_kivitendo, + ); + +}; + +sub test_ap_currency_tax_not_included_and_payment { + my $netamount = $::form->round_amount(75 * $exchangerate->sell,2); # 75 in CUR, 100.00 in EUR + my $amount = $::form->round_amount($netamount * 1.19,2); # 100 in CUR, 119.00 in EUR + my $invoice = SL::DB::PurchaseInvoice->new( + invoice => 0, + invnumber => 'test_ap_currency_tax_not_included_and_payment', + amount => $amount, + netamount => $netamount, + transdate => $transdate, + taxincluded => 0, + vendor_id => $vendor->id, + taxzone_id => $vendor->taxzone_id, + currency_id => $currency->id, + transactions => [], + notes => 'test_ap_currency_tax_not_included_and_payment', + ); + $invoice->add_ap_amount_row( + amount => $invoice->netamount, + chart => $ap_amount_chart, + tax_id => $tax_9->id, + ); + + $invoice->create_ap_row(chart => $ap_chart); + $invoice->save; + + is($invoice->currency_id, $currency->id, 'currency_id has been saved'); + is($invoice->netamount, 100, 'ap amount has been converted'); + is($invoice->amount, 119, 'ap amount has been converted'); + is($invoice->taxincluded, 0, 'ap transaction doesn\'t have taxincluded'); + is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_amount_chart->id, trans_id => $invoice->id)->amount, '-100.00000', $ap_amount_chart->accno . ': has been converted for currency'); + is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_chart->id, trans_id => $invoice->id)->amount, '119.00000', $ap_chart->accno . ': has been converted for currency'); + + $invoice->pay_invoice(chart_id => $bank->id, + amount => 50, + currency => 'CUR', + transdate => $transdate->to_kivitendo, + ); + $invoice->pay_invoice(chart_id => $bank->id, + amount => 39.25, + currency => 'CUR', + transdate => $transdate->to_kivitendo, + ); + # $invoice->pay_invoice(chart_id => $bank->id, + # amount => 30, + # transdate => $transdate2->to_kivitendo, + # ); + is(scalar @{$invoice->transactions}, 9, 'ap transaction has 9 transactions (incl. fxtransactions)'); + is($invoice->paid, $invoice->amount, 'ap transaction paid = amount in default currency'); +}; + +sub test_ap_currency_tax_included { + # we want the acc_trans amount to be 100 + my $amount = $::form->round_amount(75 * $exchangerate->sell * 1.19); + my $netamount = $::form->round_amount($amount / 1.19,2); + my $invoice = SL::DB::PurchaseInvoice->new( + invoice => 0, + amount => 119, #$amount, + netamount => 100, #$netamount, + transdate => $transdate, + taxincluded => 1, + vendor_id => $vendor->id, + taxzone_id => $vendor->taxzone_id, + currency_id => $currency->id, + notes => 'test_ap_currency_tax_included', + invnumber => 'test_ap_currency_tax_included', + transactions => [], + ); + $invoice->add_ap_amount_row( # should take care of taxincluded + amount => $invoice->amount, # tax included in local currency + chart => $ap_amount_chart, + tax_id => $tax_9->id, + ); + + $invoice->create_ap_row( chart => $ap_chart ); + $invoice->save; + is($invoice->currency_id , $currency->id , 'currency_id has been saved'); + is($invoice->amount , $amount , 'amount ok'); + is($invoice->netamount , $netamount , 'netamount ok'); + is($invoice->taxincluded , 1 , 'ap transaction has taxincluded'); + is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_amount_chart->id, trans_id => $invoice->id)->amount, '-100.00000', $ap_amount_chart->accno . ': has been converted for currency'); + is(SL::DB::Manager::AccTransaction->find_by(chart_id => $ap_chart->id, trans_id => $invoice->id)->amount, '119.00000', $ap_chart->accno . ': has been converted for currency'); + + $invoice->pay_invoice(chart_id => $bank->id, + amount => 89.25, + currency => 'CUR', + transdate => $transdate->to_kivitendo, + ); + +}; + Support::TestSetup::login(); # die; @@ -1027,6 +1233,12 @@ Support::TestSetup::login(); test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_tax_included(); test_default_invoice_two_items_19_7_tax_with_skonto_tax_included(); +# test payment of ar and ap transactions with currency and tax included/not included + test_ar_currency_tax_not_included_and_payment(); + test_ar_currency_tax_included(); + test_ap_currency_tax_not_included_and_payment(); + test_ap_currency_tax_included(); + # remove all created data at end of test clear_up(); -- 2.20.1