Payment-Helper - create_bank_transaction bekommt "purpose" param
[kivitendo-erp.git] / SL / DB / Helper / Payment.pm
index c5add7b..5b3a138 100644 (file)
@@ -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);
+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 create_bank_transaction exchangerate forex);
 our %EXPORT_TAGS = (
   "ALL" => [@EXPORT, @EXPORT_OK],
 );
@@ -15,6 +15,8 @@ use DateTime;
 use SL::DATEV qw(:CONSTANTS);
 use SL::Locale::String qw(t8);
 use List::Util qw(sum);
+use SL::DB::Exchangerate;
+use SL::DB::Currency;
 use Carp;
 
 #
@@ -29,7 +31,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 +40,12 @@ 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')) {
+    $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 +59,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 +115,8 @@ 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 $fx_gain_loss_amount = 0; # for fx_gain and fx_loss
 
   my $db = $self->db;
   $db->do_transaction(sub {
@@ -107,19 +140,61 @@ 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,
+                                                   project_id => $params{project_id} ? $params{project_id} : undef,
                                                    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));
+        $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 invoice exchangerate differs from exchangerate of payment
+        # deal with fxloss and fxamount
+        if ($self->exchangerate and $self->exchangerate != 1 and $self->exchangerate != $exchangerate) {
+          my $fxgain_chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxgain_accno_id) || die "Can't determine fxgain chart";
+          my $fxloss_chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_fxloss_accno_id) || die "Can't determine fxloss chart";
+          my $gain_loss_amount = _round($amount * ($exchangerate - $self->exchangerate ) * -1,2);
+          my $gain_loss_chart = $gain_loss_amount > 0 ? $fxgain_chart : $fxloss_chart;
+          $fx_gain_loss_amount = $gain_loss_amount;
+
+          $new_acc_trans = SL::DB::AccTransaction->new(trans_id       => $self->id,
+                                                       chart_id       => $gain_loss_chart->id,
+                                                       chart_link     => $gain_loss_chart->link,
+                                                       amount         => $gain_loss_amount,
+                                                       transdate      => $transdate_obj,
+                                                       source         => $source,
+                                                       memo           => $memo,
+                                                       taxkey         => 0,
+                                                       fx_transaction => 0,
+                                                       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,19 +223,23 @@ 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},
                                                      taxkey     => 0,
                                                      tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+
+        # the acc_trans entries are saved individually, not added to $self and then saved all at once
         $new_acc_trans->save;
 
         $reference_amount -= abs($amount);
-        $paid_amount      += -1 * $amount;
+        $paid_amount      += -1 * $amount * $exchangerate;
         $skonto_amount_check -= $skonto_booking->{'skonto_amount'};
       };
-      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;
+      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;
+      }
 
     };
 
@@ -182,17 +261,23 @@ 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 - $fx_gain_loss_amount),
                                                   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;
+    $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);
     $self->save;
 
+    # make sure transactions will be reloaded the next time $self->transactions
+    # is called, as pay_invoice saves the acc_trans objects individually rather
+    # than adding them to the transaction relation array.
+    $self->forget_related('transactions');
+
   my $datev_check = 0;
   if ( $is_sales )  {
     if ( (  $self->invoice && $::instance_conf->get_datev_check_on_sales_invoice  ) ||
@@ -361,7 +446,7 @@ sub amount_less_skonto {
 
   my $is_sales = ref($self) eq 'SL::DB::Invoice';
 
-  my $percent_skonto = $self->percent_skonto;
+  my $percent_skonto = $self->percent_skonto || 0;
 
   return _round($self->amount - ( $self->amount * $percent_skonto) );
 
@@ -374,13 +459,13 @@ sub check_skonto_configuration {
 
   my $skonto_configured = 1; # default is assume skonto works
 
-  my $transactions = $self->transactions;
-  foreach my $transaction (@{ $transactions }) {
+  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}) {
@@ -448,8 +533,8 @@ sub skonto_charts {
 
   my $reference_ARAP_amount = 0;
 
-  my $transactions = $self->transactions;
-  foreach my $transaction (@{ $transactions }) {
+  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
@@ -567,6 +652,19 @@ sub get_payment_select_options_for_bank_transaction {
 
 };
 
+sub exchangerate {
+  my ($self) = @_;
+
+  return 1 if $self->currency_id == $::instance_conf->get_currency_id;
+
+  die "transdate isn't a DateTime object:" . ref($self->transdate) unless ref($self->transdate) eq 'DateTime';
+  my $rate = SL::DB::Manager::Exchangerate->find_by(currency_id => $self->currency_id,
+                                                    transdate   => $self->transdate,
+                                                   );
+  return undef unless $rate;
+
+  return $self->is_sales ? $rate->buy : $rate->sell; # also undef if not defined
+};
 
 sub get_payment_suggestions {
 
@@ -615,6 +713,46 @@ sub validate_payment_type {
   return 1;
 }
 
+sub create_bank_transaction {
+  my ($self, %params) = @_;
+
+  require SL::DB::Chart;
+  require SL::DB::BankAccount;
+
+  my $bank_chart;
+  if ( $params{chart_id} ) {
+    $bank_chart = SL::DB::Manager::Chart->find_by(chart_id => $params{chart_id}) or die "Can't find bank chart";
+  } elsif ( $::instance_conf->get_ar_paid_accno_id ) {
+    $bank_chart   = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_paid_accno_id);
+  } else {
+    $bank_chart = SL::DB::Manager::Chart->find_by(description => 'Bank') or die "Can't find bank chart";
+  };
+  my $bank_account = SL::DB::Manager::BankAccount->find_by(chart_id => $bank_chart->id) or die "Can't find bank account for chart";
+
+  my $multiplier = $self->is_sales ? 1 : -1;
+  my $amount = ($params{amount} || $self->amount) * $multiplier;
+
+  my $transdate = $params{transdate} || DateTime->today;
+
+  my $bt = SL::DB::BankTransaction->new(
+    local_bank_account_id => $bank_account->id,
+    remote_bank_code      => $self->customervendor->bank_code,
+    remote_account_number => $self->customervendor->account_number,
+    transdate             => $transdate,
+    valutadate            => $transdate,
+    amount                => $::form->round_amount($amount, 2),
+    currency              => $self->currency->id,
+    remote_name           => $self->customervendor->depositor,
+    purpose               => $params{purpose} || $self->invnumber
+  )->save;
+};
+
+
+sub forex {
+  my ($self) = @_;
+  $self->currency_id == $::instance_conf->get_currency_id ? return 0 : return 1;
+};
+
 sub _round {
   my $value = shift;
   my $num_dec = 2;
@@ -659,17 +797,28 @@ Example:
   $ap->pay_invoice(chart_id      => $bank->chart_id,
                    amount        => $ap->open_amount,
                    transdate     => DateTime->now->to_kivitendo,
-                   memo          => 'foobar;
-                   source        => 'barfoo;
+                   memo          => 'foobar',
+                   source        => 'barfoo',
                    payment_type  => 'without_skonto',  # default if not specified
+                   project_id    => 25,
                   );
 
 or with skonto:
   $ap->pay_invoice(chart_id      => $bank->chart_id,
                    amount        => $ap->amount,       # doesn't need to be specified
                    transdate     => DateTime->now->to_kivitendo,
-                   memo          => 'foobar;
-                   source        => 'barfoo;
+                   memo          => 'foobar',
+                   source        => 'barfoo',
+                   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',
                   );
 
@@ -724,6 +873,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<reference_account>
 
 Returns a chart object which is the chart of the invoice with link AR or AP.
@@ -933,23 +1087,6 @@ defaults. E.g. when creating a SEPA bank transfer for vendor invoices a company
 might always want to pay quickly making use of skonto, while another company
 might always want to pay as late as possible.
 
-=item C<transactions>
-
-Returns all acc_trans Objects of an ar/ap object.
-
-Example in console to print account numbers and booked amounts of an invoice:
-  my $invoice = invoice(invnumber => '144');
-  foreach my $acc_trans ( @{ $invoice->transactions } ) {
-    print $acc_trans->chart->accno . " : " . $acc_trans->amount_as_number . "\n"
-  };
-  # 1200 : 226,00000
-  # 1800 : -226,00000
-  # 4300 : 100,00000
-  # 3801 : 7,00000
-  # 3806 : 19,00000
-  # 4400 : 100,00000
-  # 1200 : -226,00000
-
 =item C<get_payment_select_options_for_bank_transaction $banktransaction_id %params>
 
 Make suggestion for a skonto payment type by returning an HTML blob of the options
@@ -963,6 +1100,52 @@ If skonto is possible (skonto_date exists), add two possibilities:
 without_skonto and with_skonto_pt if payment date is within skonto_date,
 preselect with_skonto_pt, otherwise preselect without skonto.
 
+=item C<create_bank_transaction %params>
+
+Method used for testing purposes, allows you to quickly create bank
+transactions from invoices to have something to test payments against.
+
+ my $ap = SL::DB::Manager::Invoice->find_by(id => 41);
+ $ap->create_bank_transaction(amount => $ap->amount/2, transdate => DateTime->today->add(days => 5));
+
+To create a payment for 3 invoices that were all paid together, all with skonto:
+ my $ar1 = SL::DB::Manager::Invoice->find_by(invnumber=>'20');
+ my $ar2 = SL::DB::Manager::Invoice->find_by(invnumber=>'21');
+ my $ar3 = SL::DB::Manager::Invoice->find_by(invnumber=>'22');
+ $ar1->create_bank_transaction(amount  => ($ar1->amount_less_skonto + $ar2->amount_less_skonto + $ar2->amount_less_skonto),
+                               purpose => 'Rechnungen 20, 21, 22',
+                              );
+
+Amount is always relative to the absolute amount of the invoice, use positive
+values for sales and purchases.
+
+The following params can be passed to override the defaults:
+
+=over 2
+
+=item * amount
+
+=item * purpose
+
+=item * chart_id (the chart the amount is to be paid to)
+
+=item * transdate
+
+=back
+
+=item C<exchangerate>
+
+Returns 1 immediately if the record uses the default currency.
+
+Returns the exchangerate in database format for the invoice according to that
+invoice's transdate, returning 'buy' for sales, 'sell' for purchases.
+
+If no exchangerate can be found for that day undef is returned.
+
+=item C<forex>
+
+Returns 1 if record uses a different currency, 0 if the default currency is used.
+
 =back
 
 =head1 TODO AND CAVEATS
@@ -974,10 +1157,6 @@ preselect with_skonto_pt, otherwise preselect without skonto.
 when looking at open amount, maybe consider that there may already be queued
 amounts in SEPA Export
 
-=item *
-
-Can only handle default currency.
-
 =back
 
 =head1 AUTHOR