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
# 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";
}
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;
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;
$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;
}
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;
+sub _skonto_charts_and_tax_correction {
+ my ($self, %params) = @_;
+ my $amount = $params{amount} || $self->skonto_amount;
- 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;
- };
+ 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 $skonto_amount_unrounded;
+ my $is_sales = $self->is_sales;
+ my (@skonto_charts, $inv_calc, $total_skonto_rounded);
- my $skonto_percent_abs = $self->amount ? abs($transaction->amount * (1 + $tax->rate) * 100 / $self->amount) : 0;
+ $inv_calc = $self->get_tax_and_amount_by_tax_chart_id();
- my $transaction_amount = abs($transaction->{amount} * (1 + $tax->rate));
- my $transaction_skonto_percent = abs($transaction_amount/$self->amount); # abs($transaction->{amount} * (1 + $tax->rate));
+ # 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;
+ # percent net amount
+ my $transaction_net_skonto_percent = abs($entry->{netamount} / $self->amount);
+ my $skonto_netamount_unrounded = abs($amount * $transaction_net_skonto_percent);
- $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);
+ # percent tax amount
+ my $transaction_tax_skonto_percent = abs($entry->{tax} / $self->amount);
+ my $skonto_taxamount_unrounded = abs($amount * $transaction_tax_skonto_percent);
- $total_rounding_error += $rounding_error;
- $total_skonto_amount += $skonto_amount_rounded;
+ 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 = {
- # 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,
- };
+ # entry net + tax for caller
+ my $rec_net = {
+ chart_id => $chart_id,
+ skonto_amount => _round($skonto_netamount_unrounded + $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 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;
- push @skonto_charts, $rec;
- }
+ }
+ # 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;
}
- # 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;
- };
- };
- $skonto_charts[$i]->{skonto_amount} -= $rounded_total_rounding_error;
- };
-
+ # return same array of skonto charts as sub skonto_charts
return @skonto_charts;
-};
-
+}
sub within_skonto_period {
my $self = shift;
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