From d8275f6e998ac5f51c6edd786f6e4ae9db6f577f Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jan=20B=C3=BCren?= Date: Tue, 22 Mar 2022 08:54:09 +0100 Subject: [PATCH] Payment-Helper Skonto verbuchen mit Steuerkorrektur MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit tax_and_amount_by_tax_id ausgelagert für ar und ap in SalesPurchaseInvoice. pay_invoice mit skonto erwartet die banktransaction.id Invoice und PurchaseInvoice bindet den SPI Helper ein Alte Methode skonto_charts noch im Payment-Helper drin. Ferner auskommentierte Debug-Statements und auskommtiert ArGl, ApGl stabile Anbindung an ARAP (nicht notwendig, da mit BankTransationAccTrans verknüpft). --- SL/Controller/BankTransaction.pm | 1 + SL/DB/Helper/Payment.pm | 148 ++++++++++++++++++++++++++- SL/DB/Helper/SalesPurchaseInvoice.pm | 99 ++++++++++++++++++ SL/DB/Invoice.pm | 1 + SL/DB/PurchaseInvoice.pm | 1 + 5 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 SL/DB/Helper/SalesPurchaseInvoice.pm diff --git a/SL/Controller/BankTransaction.pm b/SL/Controller/BankTransaction.pm index 7f3174031..8263588bb 100644 --- a/SL/Controller/BankTransaction.pm +++ b/SL/Controller/BankTransaction.pm @@ -680,6 +680,7 @@ sub save_single_bank_transaction { source => $source, memo => $memo, skonto_amount => $free_skonto_amount, + bt_id => $bt_id, transdate => $bank_transaction->valutadate->to_kivitendo); # ... and record the origin via BankTransactionAccTrans if (scalar(@acc_ids) < 2) { diff --git a/SL/DB/Helper/Payment.pm b/SL/DB/Helper/Payment.pm index 028f76f3f..28a981768 100644 --- a/SL/DB/Helper/Payment.pm +++ b/SL/DB/Helper/Payment.pm @@ -4,7 +4,7 @@ use strict; 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 exchangerate forex); +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 exchangerate forex _skonto_charts_and_tax_correction); our %EXPORT_TAGS = ( "ALL" => [@EXPORT, @EXPORT_OK], ); @@ -39,6 +39,7 @@ sub pay_invoice { # check for required parameters and optional params depending on payment_type Common::check_params(\%params, qw(chart_id transdate)); + Common::check_params(\%params, qw(bt_id)) unless $params{payment_type} eq 'without_skonto'; if ( $params{'payment_type'} eq 'without_skonto' && abs($params{'amount'}) < 0) { croak "invalid amount for payment_type 'without_skonto': $params{'amount'}\n"; } @@ -220,12 +221,14 @@ sub pay_invoice { if ( $params{payment_type} eq 'with_skonto_pt' ) { $total_skonto_amount = $self->skonto_amount; } elsif ( $params{payment_type} eq 'difference_as_skonto' ) { + # only used for tests. no real code calls this payment_type! $total_skonto_amount = $self->open_amount; } elsif ( $params{payment_type} eq 'free_skonto') { $total_skonto_amount = $params{skonto_amount}; } - my @skonto_bookings = $self->skonto_charts($total_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}); # error checking: if ( $params{payment_type} eq 'difference_as_skonto' ) { my $calculated_skonto_sum = sum map { $_->{skonto_amount} } @skonto_bookings; @@ -235,6 +238,7 @@ sub pay_invoice { my $reference_amount = $total_skonto_amount; # create an acc_trans entry for each result of $self->skonto_charts + # TODO create internal sub _skonto_bookings foreach my $skonto_booking ( @skonto_bookings ) { next unless $skonto_booking->{'chart_id'}; next unless $skonto_booking->{'skonto_amount'} != 0; @@ -566,8 +570,144 @@ sub open_sepa_transfer_amount { return $open_sepa_amount || 0; -}; +} + +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'; + #$main::lxdebug->message(0, 'id der transaktion' . $params{bt_id}); + #$main::lxdebug->message(0, 'wert des skontos:' . $amount); + my $is_sales = $self->is_sales; + my (@skonto_charts, $inv_calc, $total_skonto_rounded); + $inv_calc = $self->get_tax_and_amount_by_tax_chart_id(); + #$main::lxdebug->message(0, 'lulu' . Dumper($inv_calc)); + while (my ($tax_chart_id, $entry) = each %{ $inv_calc } ) { # foreach tax key = tax.id + #$main::lxdebug->message(0, 'was hier:' . $tax_chart_id); + 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; + #$main::lxdebug->message(0, 'was dort:' . $tax->id); + my $transaction_net_skonto_percent = abs($entry->{netamount} / $self->amount); + my $skonto_netamount_unrounded = abs($amount * $transaction_net_skonto_percent); + #$main::lxdebug->message(0, 'ungerundet netto:' . $skonto_netamount_unrounded); + # divide for tax + my $transaction_tax_skonto_percent = abs($entry->{tax} / $self->amount); + my $skonto_taxamount_unrounded = abs($amount * $transaction_tax_skonto_percent); + #$main::lxdebug->message(0, 'ungerundet steuer:' . $skonto_taxamount_unrounded); + 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; + + my $rec_net = { + chart_id => $chart_id, + skonto_amount => _round($skonto_netamount_unrounded + $skonto_taxamount_unrounded), + # skonto_amount => _round($skonto_netamount_unrounded) + _round($skonto_taxamount_unrounded), + }; + push @skonto_charts, $rec_net; + $total_skonto_rounded += $rec_net->{skonto_amount}; + + # add-on: correct tax with one linked gl booking + + # no skonto tax correction for dual tax (reverse charge) or rate = 0 + next if ($tax->rate == 0 || $tax->reverse_charge_chart_id); + + my ($credit, $debit); + $credit = SL::DB::Manager::Chart->find_by(id => $chart_id); + $debit = SL::DB::Manager::Chart->find_by(id => $tax_chart_id); + croak("No such Chart ID") unless ref $credit eq 'SL::DB::Chart' && ref $debit eq 'SL::DB::Chart'; + + my $current_transaction = SL::DB::GLTransaction->new( + employee_id => $self->employee_id, + transdate => $params{transdate_obj}, + notes => $params{source} . ' ' . $params{memo}, + description => $self->notes || $self->invnumber, + reference => t8('Skonto Tax Correction for') . " " . $tax->rate * 100 . '% ' . $self->invnumber, + department_id => $self->department_id ? $self->department_id : undef, + imported => 0, # not imported + taxincluded => 0, + )->add_chart_booking( + chart => $is_sales ? $debit : $credit, + debit => abs($skonto_taxamount_rounded), + source => t8('Skonto Tax Correction for') . " " . $self->invnumber, + memo => $params{memo}, + tax_id => 0, + )->add_chart_booking( + chart => $is_sales ? $credit : $debit, + credit => abs($skonto_taxamount_rounded), + source => t8('Skonto Tax Correction for') . " " . $self->invnumber, + memo => $params{memo}, + tax_id => 0, + )->post; + + ## add a stable link from ap to gl + # not needed, BankTransactionAccTrans is already stable + # furthermore the origin of the booking is the bank_transaction + #my $arap = $self->is_sales ? 'ar' : 'ap'; + #my %props_gl = ( + # $arap . _id => $self->id, + # gl_id => $current_transaction->id, + # datev_export => 1, + #); + #if ($arap eq 'ap') { + # require SL::DB::ApGl; + # SL::DB::ApGl->new(%props_gl)->save; + #} elsif ($arap eq 'ar') { + # require SL::DB::ArGl; + # SL::DB::ArGl->new(%props_gl)->save; + #} else { die "Invalid state"; } + #push @new_acc_ids, map { $_->acc_trans_id } @{ $current_transaction->transactions }; + + 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 + # caller has to assign param bt_id + my %props_rl = ( + from_table => 'bank_transactions', + from_id => $params{bt_id}, + to_table => 'gl', + to_id => $current_transaction->id, + ); + SL::DB::RecordLink->new(%props_rl)->save; + # Record a record link from arap to gl + # linked gl booking will appear in tab linked records + # this is just a link for convenience + %props_rl = ( + from_table => $is_sales ? 'ar' : 'ap', + from_id => $self->id, + to_table => 'gl', + to_id => $current_transaction->id, + ); + SL::DB::RecordLink->new(%props_rl)->save; + } + # check for rounding errors, at least for the payment chart + # we ignore tax rounding errors as long as the user or calculated + # amount of skonto is fully assigned + # we simply alter one cent for the first skonto booking entry + # should be correct for most of the cases (no invoices with mixed taxes) + if ($total_skonto_rounded - $amount > 0.01) { + # add one cent + $main::lxdebug->message(0, 'Una mas!' . $total_skonto_rounded); + $skonto_charts[0]->{skonto_amount} -= 0.01; + } elsif ($amount - $total_skonto_rounded > 0.01) { + # subtract one cent + $main::lxdebug->message(0, 'Una menos!' . $total_skonto_rounded); + $skonto_charts[0]->{skonto_amount} += 0.01; + } else { $main::lxdebug->message(0, 'No rounding error'); } + + # return same array of skonto charts as sub skonto_charts + return @skonto_charts; +} sub skonto_charts { my $self = shift; diff --git a/SL/DB/Helper/SalesPurchaseInvoice.pm b/SL/DB/Helper/SalesPurchaseInvoice.pm new file mode 100644 index 000000000..ed1ba32d2 --- /dev/null +++ b/SL/DB/Helper/SalesPurchaseInvoice.pm @@ -0,0 +1,99 @@ +package SL::DB::Helper::SalesPurchaseInvoice; + +use strict; +use utf8; + +use parent qw(Exporter); +our @EXPORT = qw(get_tax_and_amount_by_tax_chart_id); + +sub get_tax_and_amount_by_tax_chart_id { + my ($self) = @_; + + my $ARAP = $self->is_sales ? 'AR' : 'AP'; + my ($tax_and_amount_by_tax_id, $total); + + foreach my $transaction (@{ $self->transactions }) { + next if $transaction->chart_link =~ m/(^${ARAP}$|paid)/; + + my $tax_or_netamount = $transaction->chart_link =~ m/tax/ ? 'tax' + : $transaction->chart_link =~ m/(${ARAP}_amount|IC_cogs)/ ? 'netamount' + : undef; + if ($tax_or_netamount eq 'netamount') { + $tax_and_amount_by_tax_id->{ $transaction->tax->chart_id }->{$tax_or_netamount} ||= 0; + $tax_and_amount_by_tax_id->{ $transaction->tax->chart_id }->{$tax_or_netamount} += $transaction->amount; + # die "Invalid state" unless $tax_and_amount_by_tax_id->{ $transaction->tax->chart_id }->{tax_id} == 0 + $tax_and_amount_by_tax_id->{ $transaction->tax->chart_id }->{tax_id} = $transaction->tax_id; + } elsif ($tax_or_netamount eq 'tax') { + $tax_and_amount_by_tax_id->{ $transaction->chart_id }->{$tax_or_netamount} ||= 0; + $tax_and_amount_by_tax_id->{ $transaction->chart_id }->{$tax_or_netamount} += $transaction->amount; + } else { + die "Invalid chart link at: " . $transaction->chart_link unless $tax_or_netamount; + } + $total ||= 0; + $total += $transaction->amount; + } + die "Invalid calculated amount" if abs($total) - abs($self->amount) > 0.001; + return $tax_and_amount_by_tax_id; +} + + + +1; + +__END__ + +=pod + +=encoding utf8 + +=head1 NAME + +SL::DB::Helper::SalesPurchaseInvoice - Helper functions for Sales or Purchase bookings (mirrored) + +Delivers the booked amounts split by net amount and tax amount for one ar or ap transaction +as persisted in the table acc_trans. +Should be rounding or calculation error prone because all values are already computed before +the values are written in the acc_trans table. + +That is the main purpose for this helper class. +=head1 FUNCTIONS + +=over 4 + +=item C + +Iterates over all transactions for one distinct ar or ap transaction (trans_id in acc_trans) and +groups the amounts in relation to distinct tax (tax.id) and net amounts (sums all bookings with +_cogs or _amount chart links). +Returns a hashref with the chart_id of the tax entry as key like this: + + '775' => { + 'tax_id' => 777 + 'tax' => '332.18', + 'netamount' => '1748.32', + }, + + '194' => { + 'tax_id' => 378, + 'netamount' => '20', + 'tax' => '1.4' + } + +C is the id of the used tax. C ist the amount of tax booked for the whole transaction. +C is the netamount booked with this tax. +TODO: Please note the hash key chart_id may not be unique but the entry tax_id is always unique. + +As additional safety method the functions dies if the calculated sums do not match the +the whole amount of the transaction with an accuracy of two decimal places. + +=back + +=head1 BUGS + +Nothing here yet. + +=head1 AUTHOR + +Jan Büren Ejan@kivitendo.deE + +=cut diff --git a/SL/DB/Invoice.pm b/SL/DB/Invoice.pm index 45259a064..2b4fd59ef 100644 --- a/SL/DB/Invoice.pm +++ b/SL/DB/Invoice.pm @@ -16,6 +16,7 @@ use SL::DB::Helper::LinkedRecords; use SL::DB::Helper::PDF_A; use SL::DB::Helper::PriceTaxCalculator; use SL::DB::Helper::PriceUpdater; +use SL::DB::Helper::SalesPurchaseInvoice; use SL::DB::Helper::TransNumberGenerator; use SL::DB::Helper::ZUGFeRD; use SL::Locale::String qw(t8); diff --git a/SL/DB/PurchaseInvoice.pm b/SL/DB/PurchaseInvoice.pm index dd49433c6..3eb320ee3 100644 --- a/SL/DB/PurchaseInvoice.pm +++ b/SL/DB/PurchaseInvoice.pm @@ -11,6 +11,7 @@ use SL::DB::Helper::AttrHTML; use SL::DB::Helper::AttrSorted; use SL::DB::Helper::LinkedRecords; use SL::DB::Helper::Payment qw(:ALL); +use SL::DB::Helper::SalesPurchaseInvoice; use SL::Locale::String qw(t8); use Rose::DB::Object::Helpers qw(has_loaded_related forget_related); -- 2.20.1