Payment-Helper _skonto_charts... debugs und kommentare aufgerÀumt
[kivitendo-erp.git] / SL / DB / Helper / Payment.pm
index 6dee091..6db8951 100644 (file)
@@ -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