X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FDB%2FHelper%2FPayment.pm;h=6db8951c911f7b3b114553306b26250a40159a1e;hb=293fb807d006f5c7dffdcd5f608964f0b6103ec9;hp=6dee0918f7d97d9bc36c5b669b94f6f9abf9c9c4;hpb=524bc23eb0c179bfa2acbf6c6f00dce3788fccc7;p=kivitendo-erp.git diff --git a/SL/DB/Helper/Payment.pm b/SL/DB/Helper/Payment.pm index 6dee0918f..6db8951c9 100644 --- a/SL/DB/Helper/Payment.pm +++ b/SL/DB/Helper/Payment.pm @@ -4,20 +4,22 @@ 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 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], ); require SL::DB::Chart; + +use Carp; use Data::Dumper; use DateTime; -use SL::DATEV qw(:CONSTANTS); -use SL::Locale::String qw(t8); use List::Util qw(sum); + +use SL::DATEV qw(:CONSTANTS); use SL::DB::Exchangerate; use SL::DB::Currency; -use Carp; +use SL::Locale::String qw(t8); # # Public functions not exported by default @@ -39,6 +41,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"; } @@ -77,8 +80,9 @@ sub pay_invoice { }; # currency is either passed or use the invoice currency if it differs from the default currency + # TODO remove my ($exchangerate,$currency); - if ($params{currency} || $params{currency_id} || $self->currency_id != $::instance_conf->get_currency_id) { + if ($params{currency} || $params{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 @@ -219,12 +223,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; @@ -234,6 +240,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; @@ -254,7 +261,7 @@ sub pay_invoice { $reference_amount -= abs($amount); $paid_amount += -1 * $amount * $exchangerate; $skonto_amount_check -= $skonto_booking->{'skonto_amount'}; - }; + } if ( $params{payment_type} eq 'difference_as_skonto' ) { die "difference_as_skonto calculated incorrectly, sum of calculated payments doesn't add up to open amount $total_open_amount, reference_amount = $reference_amount\n" unless _round($reference_amount) == 0; } @@ -290,6 +297,67 @@ sub pay_invoice { $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; + } + } $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); @@ -504,110 +572,125 @@ sub open_sepa_transfer_amount { return $open_sepa_amount || 0; -}; - - -sub skonto_charts { - my $self = shift; - - # TODO: use param for amount, may also want to calculate skonto_amounts by - # passing percentage in the future - - my $amount = shift || $self->skonto_amount; - - croak "no amount passed to skonto_charts" unless abs(_round($amount)) >= 0.01; - - # TODO: check whether there are negative values in invoice / acc_trans ... credited items - - # don't check whether skonto applies, because user may want to override this - # return undef unless $self->percent_skonto; - - my $is_sales = ref($self) eq 'SL::DB::Invoice'; - - my $mult = $is_sales ? 1 : -1; # multiplier for getting the right sign - - my @skonto_charts; # resulting array with all income/expense accounts that have to be corrected - - # calculate effective skonto (percentage) in difference_as_skonto mode - # only works if there are no negative acc_trans values - my $effective_skonto_rate = $amount ? $amount / $self->amount : 0; - - # checks: - my $total_skonto_amount = 0; - my $total_rounding_error = 0; - - my $reference_ARAP_amount = 0; - - # my $transactions = $self->transactions; - foreach my $transaction (@{ $self->transactions }) { - # find all transactions with an AR_amount or AP_amount link - $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->{chart_link}) }; - # second condition is that we can determine an automatic Skonto account for each AR_amount entry - - if ( ( $is_sales && $transaction->{chartlinks}->{AR_amount} ) or ( !$is_sales && $transaction->{chartlinks}->{AP_amount}) ) { - # $reference_ARAP_amount += $transaction->{amount} * $mult; - - # quick hack that works around problem of non-unique tax keys in SKR04 - # ? use tax_id in acc_trans - my $tax = SL::DB::Manager::Tax->get_first( where => [id => $transaction->{tax_id}]); - croak "no tax for taxkey " . $transaction->{taxkey} unless ref $tax; - - if ( $is_sales ) { - die t8('no skonto_chart configured for taxkey #1 : #2 : #3', $transaction->{taxkey} , $tax->taxdescription , $tax->rate*100) unless ref $tax->skonto_sales_chart; - } else { - die t8('no skonto_chart configured for taxkey #1 : #2 : #3', $transaction->{taxkey} , $tax->taxdescription , $tax->rate*100) unless ref $tax->skonto_purchase_chart; - }; +} - my $skonto_amount_unrounded; +sub _skonto_charts_and_tax_correction { + my ($self, %params) = @_; + my $amount = $params{amount} || $self->skonto_amount; - my $skonto_percent_abs = $self->amount ? abs($transaction->amount * (1 + $tax->rate) * 100 / $self->amount) : 0; + 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'; - my $transaction_amount = abs($transaction->{amount} * (1 + $tax->rate)); - my $transaction_skonto_percent = abs($transaction_amount/$self->amount); # abs($transaction->{amount} * (1 + $tax->rate)); + 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(); - $skonto_amount_unrounded = abs($amount * $transaction_skonto_percent); - my $skonto_amount_rounded = _round($skonto_amount_unrounded); - my $rounding_error = $skonto_amount_unrounded - $skonto_amount_rounded; - my $rounded_rounding_error = _round($rounding_error); + # foreach tax.chart_id || $entry->{ta..id} + while (my ($tax_chart_id, $entry) = each %{ $inv_calc } ) { + 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; - $total_rounding_error += $rounding_error; - $total_skonto_amount += $skonto_amount_rounded; + # percent net amount + my $transaction_net_skonto_percent = abs($entry->{netamount} / $self->amount); + my $skonto_netamount_unrounded = abs($amount * $transaction_net_skonto_percent); - my $rec = { - # skonto_percent_abs: relative part of amount + tax to the total invoice amount - 'skonto_percent_abs' => $skonto_percent_abs, - 'chart_id' => $is_sales ? $tax->skonto_sales_chart->id : $tax->skonto_purchase_chart->id, - 'skonto_amount' => $skonto_amount_rounded, - # 'rounding_error' => $rounding_error, - # 'rounded_rounding_error' => $rounded_rounding_error, - }; + # percent tax amount + my $transaction_tax_skonto_percent = abs($entry->{tax} / $self->amount); + my $skonto_taxamount_unrounded = abs($amount * $transaction_tax_skonto_percent); - push @skonto_charts, $rec; - }; - }; + 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; - # if the rounded sum of all rounding_errors reaches 0.01 this sum is - # subtracted from the largest skonto_amount - my $rounded_total_rounding_error = abs(_round($total_rounding_error)); - - if ( $rounded_total_rounding_error > 0 ) { - my $highest_amount_pos = 0; - my $highest_amount = 0; - my $i = -1; - foreach my $ref ( @skonto_charts ) { - $i++; - if ( $ref->{skonto_amount} > $highest_amount ) { - $highest_amount = $ref->{skonto_amount}; - $highest_amount_pos = $i; - }; + # entry net + tax for caller + my $rec_net = { + chart_id => $chart_id, + skonto_amount => _round($skonto_netamount_unrounded + $skonto_taxamount_unrounded), }; - $skonto_charts[$i]->{skonto_amount} -= $rounded_total_rounding_error; - }; + 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 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 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; - return @skonto_charts; -}; + } + # check for rounding errors, at least for the payment chart + # we ignore tax rounding errors as long as the amount (user input or calculated) + # 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 + $skonto_charts[0]->{skonto_amount} -= 0.01; + } elsif ($amount - $total_skonto_rounded > 0.01) { + # subtract one cent + $skonto_charts[0]->{skonto_amount} += 0.01; + } + # return same array of skonto charts as sub skonto_charts + return @skonto_charts; +} sub within_skonto_period { my $self = shift; @@ -693,6 +776,8 @@ sub get_payment_suggestions { push(@{$self->{payment_select_options}} , { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt') , selected => 1 }); } else { if ( ( $self->valid_skonto_amount($self->open_amount) || $self->valid_skonto_amount($open_amount) ) and not $params{sepa} ) { + # Will never be reached + die "This case is as dead as the dead cat. Go to start, don't pick 2,000 \$"; $self->{invoice_amount_suggestion} = $open_amount; # only suggest difference_as_skonto if open_amount exactly matches skonto_amount # AND we aren't in SEPA mode