Paymenthelper kann Fremdwährung mit Steuer inkl. und exkl.
[kivitendo-erp.git] / SL / DB / Helper / Payment.pm
1 package SL::DB::Helper::Payment;
2
3 use strict;
4
5 use parent qw(Exporter);
6 our @EXPORT = qw(pay_invoice);
7 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);
8 our %EXPORT_TAGS = (
9   "ALL" => [@EXPORT, @EXPORT_OK],
10 );
11
12 require SL::DB::Chart;
13 use Data::Dumper;
14 use DateTime;
15 use SL::DATEV qw(:CONSTANTS);
16 use SL::Locale::String qw(t8);
17 use List::Util qw(sum);
18 use Carp;
19
20 #
21 # Public functions not exported by default
22 #
23
24 sub pay_invoice {
25   my ($self, %params) = @_;
26
27   require SL::DB::Tax;
28
29   my $is_sales = ref($self) eq 'SL::DB::Invoice';
30   my $mult = $is_sales ? 1 : -1;  # multiplier for getting the right sign depending on ar/ap
31
32   my $paid_amount = 0; # the amount that will be later added to $self->paid, should be in default currency
33
34   # default values if not set
35   $params{payment_type} = 'without_skonto' unless $params{payment_type};
36   validate_payment_type($params{payment_type});
37
38   # check for required parameters
39   Common::check_params(\%params, qw(chart_id transdate));
40
41   my $transdate_obj;
42   if (ref($params{transdate} eq 'DateTime')) {
43     print "found transdate ref\n"; sleep 2;
44     $transdate_obj = $params{transdate};
45   } else {
46    $transdate_obj = $::locale->parse_date_to_object($params{transdate});
47   };
48   croak t8('Illegal date') unless ref $transdate_obj;
49
50   # check for closed period
51   my $closedto = $::locale->parse_date_to_object($::instance_conf->get_closedto);
52   if ( ref $closedto && $transdate_obj < $closedto ) {
53     croak t8('Cannot post payment for a closed period!');
54   };
55
56   # check for maximum number of future days
57   if ( $::instance_conf->get_max_future_booking_interval > 0 ) {
58     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 );
59   };
60
61   # currency is either passed or use the invoice currency if it differs from the default currency
62   my ($exchangerate,$currency);
63   if ($params{currency} || $params{currency_id} || $self->currency_id != $::instance_conf->get_currency_id) {
64     if ($params{currency} || $params{currency_id} ) { # currency was specified
65       $currency = SL::DB::Manager::Currency->find_by(name => $params{currency}) || SL::DB::Manager::Currency->find_by(id => $params{currency_id});
66     } else { # use invoice currency
67       $currency = SL::DB::Manager::Currency->find_by(id => $self->currency_id);
68     };
69     die "no currency" unless $currency;
70     if ($currency->id == $::instance_conf->get_currency_id) {
71       $exchangerate = 1;
72     } else {
73       my $rate = SL::DB::Manager::Exchangerate->find_by(currency_id => $currency->id,
74                                                         transdate   => $transdate_obj,
75                                                        );
76       if ($rate) {
77         $exchangerate = $is_sales ? $rate->buy : $rate->sell;
78       } else {
79         die "No exchange rate for " . $transdate_obj->to_kivitendo;
80       };
81     };
82   } else { # no currency param given or currency is the same as default_currency
83     $exchangerate = 1;
84   };
85
86   # input checks:
87   if ( $params{'payment_type'} eq 'without_skonto' ) {
88     croak "invalid amount for payment_type 'without_skonto': $params{'amount'}\n" unless abs($params{'amount'}) > 0;
89   };
90
91   # options with_skonto_pt and difference_as_skonto don't require the parameter
92   # amount, but if amount is passed, make sure it matches the expected value
93   if ( $params{'payment_type'} eq 'difference_as_skonto' ) {
94     croak "amount $params{amount} doesn't match open amount " . $self->open_amount . ", diff = " . ($params{amount}-$self->open_amount) if $params{amount} && abs($self->open_amount - $params{amount} ) > 0.0000001;
95   } elsif ( $params{'payment_type'} eq 'with_skonto_pt' ) {
96     croak "amount $params{amount} doesn't match amount less skonto: " . $self->open_amount . "\n" if $params{amount} && abs($self->amount_less_skonto - $params{amount} ) > 0.0000001;
97     croak "payment type with_skonto_pt can't be used if payments have already been made" if $self->paid != 0;
98   };
99
100   # absolute skonto amount for invoice, use as reference sum to see if the
101   # calculated skontos add up
102   # only needed for payment_term "with_skonto_pt"
103
104   my $skonto_amount_check = $self->skonto_amount; # variable should be zero after calculating all skonto
105   my $total_open_amount   = $self->open_amount;
106
107   # account where money is paid to/from: bank account or cash
108   my $account_bank = SL::DB::Manager::Chart->find_by(id => $params{chart_id});
109   croak "can't find bank account" unless ref $account_bank;
110
111   my $reference_account = $self->reference_account;
112   croak "can't find reference account (link = AR/AP) for invoice" unless ref $reference_account;
113
114   my $memo   = $params{'memo'}   || '';
115   my $source = $params{'source'} || '';
116
117   my $rounded_params_amount = _round( $params{amount} ); # / $exchangerate);
118
119   my $db = $self->db;
120   $db->do_transaction(sub {
121     my $new_acc_trans;
122
123     # all three payment type create 1 AR/AP booking (the paid part)
124     # difference_as_skonto creates n skonto bookings (1 for each tax type)
125     # with_skonto_pt creates 1 bank booking and n skonto bookings (1 for each tax type)
126     # without_skonto creates 1 bank booking
127
128     # as long as there is no automatic tax, payments are always booked with
129     # taxkey 0
130
131     unless ( $params{payment_type} eq 'difference_as_skonto' ) {
132       # cases with_skonto_pt and without_skonto
133
134       # for case with_skonto_pt we need to know the corrected amount at this
135       # stage if we are going to use $params{amount}
136
137       my $pay_amount = $rounded_params_amount;
138       $pay_amount = $self->amount_less_skonto if $params{payment_type} eq 'with_skonto_pt';
139
140       # bank account and AR/AP
141       $paid_amount += $pay_amount * $exchangerate;
142
143       my $amount = (-1 * $pay_amount) * $mult;
144
145
146       # total amount against bank, do we already know this by now?
147       $new_acc_trans = SL::DB::AccTransaction->new(trans_id   => $self->id,
148                                                    chart_id   => $account_bank->id,
149                                                    chart_link => $account_bank->link,
150                                                    amount     => $amount,
151                                                    transdate  => $transdate_obj,
152                                                    source     => $source,
153                                                    memo       => $memo,
154                                                    taxkey     => 0,
155                                                    tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
156       $new_acc_trans->save;
157
158       # deal with fxtransaction
159       if ( $self->currency_id != $::instance_conf->get_currency_id ) {
160         my $fxamount = _round($amount - ($amount * $exchangerate));
161         # print "amount: $amount, fxamount = $fxamount\n";
162         # print "amount - (amount * exchangerate) = " . $amount . " - (" . $amount . " - " . $exchangerate . ")\n";
163         $new_acc_trans = SL::DB::AccTransaction->new(trans_id       => $self->id,
164                                                      chart_id       => $account_bank->id,
165                                                      chart_link     => $account_bank->link,
166                                                      amount         => $fxamount * -1,
167                                                      transdate      => $transdate_obj,
168                                                      source         => $source,
169                                                      memo           => $memo,
170                                                      taxkey         => 0,
171                                                      fx_transaction => 1,
172                                                      tax_id         => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
173         $new_acc_trans->save;
174       };
175     };
176
177     if ( $params{payment_type} eq 'difference_as_skonto' or $params{payment_type} eq 'with_skonto_pt' ) {
178
179       my $total_skonto_amount;
180       if ( $params{payment_type} eq 'with_skonto_pt' ) {
181         $total_skonto_amount = $self->skonto_amount;
182       } elsif ( $params{payment_type} eq 'difference_as_skonto' ) {
183         $total_skonto_amount = $self->open_amount;
184       };
185
186       my @skonto_bookings = $self->skonto_charts($total_skonto_amount);
187
188       # error checking:
189       if ( $params{payment_type} eq 'difference_as_skonto' ) {
190         my $calculated_skonto_sum  = sum map { $_->{skonto_amount} } @skonto_bookings;
191         croak "calculated skonto for difference_as_skonto = $calculated_skonto_sum doesn't add up open amount: " . $self->open_amount unless _round($calculated_skonto_sum) == _round($self->open_amount);
192       };
193
194       my $reference_amount = $total_skonto_amount;
195
196       # create an acc_trans entry for each result of $self->skonto_charts
197       foreach my $skonto_booking ( @skonto_bookings ) {
198         next unless $skonto_booking->{'chart_id'};
199         next unless $skonto_booking->{'skonto_amount'} != 0;
200         my $amount = -1 * $skonto_booking->{skonto_amount};
201         $new_acc_trans = SL::DB::AccTransaction->new(trans_id   => $self->id,
202                                                      chart_id   => $skonto_booking->{'chart_id'},
203                                                      chart_link => SL::DB::Manager::Chart->find_by(id => $skonto_booking->{'chart_id'})->link,
204                                                      amount     => $amount * $mult,
205                                                      transdate  => $transdate_obj,
206                                                      source     => $params{source},
207                                                      taxkey     => 0,
208                                                      tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
209
210         # the acc_trans entries are saved individually, not added to $self and then saved all at once
211         $new_acc_trans->save;
212
213         $reference_amount -= abs($amount);
214         $paid_amount      += -1 * $amount * $exchangerate;
215         $skonto_amount_check -= $skonto_booking->{'skonto_amount'};
216       };
217       if ( $params{payment_type} eq 'difference_as_skonto' ) {
218           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;
219       }
220
221     };
222
223     my $arap_amount = 0;
224
225     if ( $params{payment_type} eq 'difference_as_skonto' ) {
226       $arap_amount = $total_open_amount;
227     } elsif ( $params{payment_type} eq 'without_skonto' ) {
228       $arap_amount = $rounded_params_amount;
229     } elsif ( $params{payment_type} eq 'with_skonto_pt' ) {
230       # this should be amount + sum(amount+skonto), but while we only allow
231       # with_skonto_pt for completely unpaid invoices we just use the value
232       # from the invoice
233       $arap_amount = $total_open_amount;
234     };
235
236     # regardless of payment_type there is always only exactly one arap booking
237     # TODO: compare $arap_amount to running total
238     my $arap_booking= SL::DB::AccTransaction->new(trans_id   => $self->id,
239                                                   chart_id   => $reference_account->id,
240                                                   chart_link => $reference_account->link,
241                                                   amount     => _round($arap_amount * $mult * $exchangerate),
242                                                   transdate  => $transdate_obj,
243                                                   source     => '', #$params{source},
244                                                   taxkey     => 0,
245                                                   tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
246     $arap_booking->save;
247
248     $self->paid($self->paid + _round($paid_amount)) if $paid_amount;
249     $self->datepaid($transdate_obj);
250     $self->save;
251
252     # make sure transactions will be reloaded the next time $self->transactions
253     # is called, as pay_invoice saves the acc_trans objects individually rather
254     # than adding them to the transaction relation array.
255     $self->forget_related('transactions');
256
257   my $datev_check = 0;
258   if ( $is_sales )  {
259     if ( (  $self->invoice && $::instance_conf->get_datev_check_on_sales_invoice  ) ||
260          ( !$self->invoice && $::instance_conf->get_datev_check_on_ar_transaction )) {
261       $datev_check = 1;
262     };
263   } else {
264     if ( (  $self->invoice && $::instance_conf->get_datev_check_on_purchase_invoice ) ||
265          ( !$self->invoice && $::instance_conf->get_datev_check_on_ap_transaction   )) {
266       $datev_check = 1;
267     };
268   };
269
270   if ( $datev_check ) {
271
272     my $datev = SL::DATEV->new(
273       exporttype => DATEV_ET_BUCHUNGEN,
274       format     => DATEV_FORMAT_KNE,
275       dbh        => $db->dbh,
276       trans_id   => $self->{id},
277     );
278
279     $datev->clean_temporary_directories;
280     $datev->export;
281
282     if ($datev->errors) {
283       # this exception should be caught by do_transaction, which handles the rollback
284       die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
285     }
286   };
287
288   }) || die t8('error while paying invoice #1 : ', $self->invnumber) . $db->error . "\n";
289
290   return 1;
291 };
292
293 sub skonto_date {
294
295   my $self = shift;
296
297   my $is_sales = ref($self) eq 'SL::DB::Invoice';
298
299   my $skonto_date;
300
301   if ( $is_sales ) {
302     return undef unless ref $self->payment_terms;
303     return undef unless $self->payment_terms->terms_skonto > 0;
304     $skonto_date = DateTime->from_object(object => $self->transdate)->add(days => $self->payment_terms->terms_skonto);
305   } else {
306     return undef unless ref $self->vendor->payment_terms;
307     return undef unless $self->vendor->payment_terms->terms_skonto > 0;
308     $skonto_date = DateTime->from_object(object => $self->transdate)->add(days => $self->vendor->payment_terms->terms_skonto);
309   };
310
311   return $skonto_date;
312 };
313
314 sub reference_account {
315   my $self = shift;
316
317   my $is_sales = ref($self) eq 'SL::DB::Invoice';
318
319   require SL::DB::Manager::AccTransaction;
320
321   my $link_filter = $is_sales ? 'AR' : 'AP';
322
323   my $acc_trans = SL::DB::Manager::AccTransaction->find_by(
324      trans_id   => $self->id,
325      SL::DB::Manager::AccTransaction->chart_link_filter("$link_filter")
326   );
327
328   return undef unless ref $acc_trans;
329
330   my $reference_account = SL::DB::Manager::Chart->find_by(id => $acc_trans->chart_id);
331
332   return $reference_account;
333 };
334
335 sub reference_amount {
336   my $self = shift;
337
338   my $is_sales = ref($self) eq 'SL::DB::Invoice';
339
340   require SL::DB::Manager::AccTransaction;
341
342   my $link_filter = $is_sales ? 'AR' : 'AP';
343
344   my $acc_trans = SL::DB::Manager::AccTransaction->find_by(
345      trans_id   => $self->id,
346      SL::DB::Manager::AccTransaction->chart_link_filter("$link_filter")
347   );
348
349   return undef unless ref $acc_trans;
350
351   # this should be the same as $self->amount
352   return $acc_trans->amount;
353 };
354
355
356 sub open_amount {
357   my $self = shift;
358
359   # in the future maybe calculate this from acc_trans
360
361   # if the difference is 0.01 Cent this may end up as 0.009999999999998
362   # numerically, so round this value when checking for cent threshold >= 0.01
363
364   return $self->amount - $self->paid;
365 };
366
367 sub open_percent {
368   my $self = shift;
369
370   return 0 if $self->amount == 0;
371   my $open_percent;
372   if ( $self->open_amount < 0 ) {
373     # overpaid, currently treated identically
374     $open_percent = $self->open_amount * 100 / $self->amount;
375   } else {
376     $open_percent = $self->open_amount * 100 / $self->amount;
377   };
378
379   return _round($open_percent) || 0;
380 };
381
382 sub skonto_amount {
383   my $self = shift;
384
385   return $self->amount - $self->amount_less_skonto;
386 };
387
388 sub remaining_skonto_days {
389   my $self = shift;
390
391   return undef unless ref $self->skonto_date;
392
393   my $dur = DateTime::Duration->new($self->skonto_date - DateTime->today);
394   return $dur->delta_days();
395
396 };
397
398 sub percent_skonto {
399   my $self = shift;
400
401   my $is_sales = ref($self) eq 'SL::DB::Invoice';
402
403   my $percent_skonto = 0;
404
405   if ( $is_sales ) {
406     return undef unless ref $self->payment_terms;
407     return undef unless $self->payment_terms->percent_skonto > 0;
408     $percent_skonto = $self->payment_terms->percent_skonto;
409   } else {
410     return undef unless ref $self->vendor->payment_terms;
411     return undef unless $self->vendor->payment_terms->terms_skonto > 0;
412     $percent_skonto = $self->vendor->payment_terms->percent_skonto;
413   };
414
415   return $percent_skonto;
416 };
417
418 sub amount_less_skonto {
419   # amount that has to be paid if skonto applies, always return positive rounded values
420   # the result is rounded so we can directly compare it with the user input
421   my $self = shift;
422
423   my $is_sales = ref($self) eq 'SL::DB::Invoice';
424
425   my $percent_skonto = $self->percent_skonto || 0;
426
427   return _round($self->amount - ( $self->amount * $percent_skonto) );
428
429 };
430
431 sub check_skonto_configuration {
432   my $self = shift;
433
434   my $is_sales = ref($self) eq 'SL::DB::Invoice';
435
436   my $skonto_configured = 1; # default is assume skonto works
437
438   # my $transactions = $self->transactions;
439   foreach my $transaction (@{ $self->transactions }) {
440     # find all transactions with an AR_amount or AP_amount link
441     my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->taxkey]);
442     croak "no tax for taxkey " . $transaction->{taxkey} unless ref $tax;
443
444     $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->chart_link) };
445     if ( $is_sales && $transaction->{chartlinks}->{AR_amount} ) {
446       $skonto_configured = 0 unless $tax->skonto_sales_chart_id;
447     } elsif ( !$is_sales && $transaction->{chartlinks}->{AP_amount}) {
448       $skonto_configured = 0 unless $tax->skonto_purchase_chart_id;
449     };
450   };
451
452   return $skonto_configured;
453 };
454
455 sub open_sepa_transfer_amount {
456   my $self = shift;
457
458   my ($vc, $key, $type);
459   if ( ref($self) eq 'SL::DB::Invoice' ) {
460     $vc   = 'customer';
461     $key  = 'ap_id';
462     $type = 'ar';
463   } else {
464     $vc   = 'vendor';
465     $key  = 'ap_id';
466     $type = 'ap';
467   };
468
469   my $sql = qq|SELECT SUM(sei.amount) AS amount FROM sepa_export_items sei | .
470             qq| LEFT JOIN sepa_export se ON (sei.sepa_export_id = se.id)   | .
471             qq| WHERE $key = ? AND NOT se.closed AND (se.vc = '$vc')       |;
472
473   my ($open_sepa_amount) = $self->db->dbh->selectrow_array($sql, undef, $self->id);
474
475   return $open_sepa_amount || 0;
476
477 };
478
479
480 sub skonto_charts {
481   my $self = shift;
482
483   # TODO: use param for amount, may also want to calculate skonto_amounts by
484   # passing percentage in the future
485
486   my $amount = shift || $self->skonto_amount;
487
488   croak "no amount passed to skonto_charts" unless abs(_round($amount)) >= 0.01;
489
490   # TODO: check whether there are negative values in invoice / acc_trans ... credited items
491
492   # don't check whether skonto applies, because user may want to override this
493   # return undef unless $self->percent_skonto;  # for is_sales
494   # return undef unless $self->vendor->payment_terms->percent_skonto;  # for purchase
495
496   my $is_sales = ref($self) eq 'SL::DB::Invoice';
497
498   my $mult = $is_sales ? 1 : -1;  # multiplier for getting the right sign
499
500   my @skonto_charts;  # resulting array with all income/expense accounts that have to be corrected
501
502   # calculate effective skonto (percentage) in difference_as_skonto mode
503   # only works if there are no negative acc_trans values
504   my $effective_skonto_rate = $amount ? $amount / $self->amount : 0;
505
506   # checks:
507   my $total_skonto_amount  = 0;
508   my $total_rounding_error = 0;
509
510   my $reference_ARAP_amount = 0;
511
512   # my $transactions = $self->transactions;
513   foreach my $transaction (@{ $self->transactions }) {
514     # find all transactions with an AR_amount or AP_amount link
515     $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->{chart_link}) };
516     # second condition is that we can determine an automatic Skonto account for each AR_amount entry
517
518     if ( ( $is_sales && $transaction->{chartlinks}->{AR_amount} ) or ( !$is_sales && $transaction->{chartlinks}->{AP_amount}) ) {
519         # $reference_ARAP_amount += $transaction->{amount} * $mult;
520
521         # quick hack that works around problem of non-unique tax keys in SKR04
522         my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->{taxkey}]);
523         croak "no tax for taxkey " . $transaction->{taxkey} unless ref $tax;
524
525         if ( $is_sales ) {
526           die t8('no skonto_chart configured for taxkey #1 : #2 : #3', $transaction->{taxkey} , $tax->taxdescription , $tax->rate*100) unless ref $tax->skonto_sales_chart;
527         } else {
528           die t8('no skonto_chart configured for taxkey #1 : #2 : #3', $transaction->{taxkey} , $tax->taxdescription , $tax->rate*100) unless ref $tax->skonto_purchase_chart;
529         };
530
531         my $skonto_amount_unrounded;
532
533         my $skonto_percent_abs = $self->amount ? abs($transaction->amount * (1 + $tax->rate) * 100 / $self->amount) : 0;
534
535         my $transaction_amount = abs($transaction->{amount} * (1 + $tax->rate));
536         my $transaction_skonto_percent = abs($transaction_amount/$self->amount); # abs($transaction->{amount} * (1 + $tax->rate));
537
538
539         $skonto_amount_unrounded   = abs($amount * $transaction_skonto_percent);
540         my $skonto_amount_rounded  = _round($skonto_amount_unrounded);
541         my $rounding_error         = $skonto_amount_unrounded - $skonto_amount_rounded;
542         my $rounded_rounding_error = _round($rounding_error);
543
544         $total_rounding_error += $rounding_error;
545         $total_skonto_amount  += $skonto_amount_rounded;
546
547         my $rec = {
548           # skonto_percent_abs: relative part of amount + tax to the total invoice amount
549           'skonto_percent_abs'     => $skonto_percent_abs,
550           'chart_id'               => $is_sales ? $tax->skonto_sales_chart->id : $tax->skonto_purchase_chart->id,
551           'skonto_amount'          => $skonto_amount_rounded,
552           # 'rounding_error'         => $rounding_error,
553           # 'rounded_rounding_error' => $rounded_rounding_error,
554         };
555
556         push @skonto_charts, $rec;
557       };
558   };
559
560   # if the rounded sum of all rounding_errors reaches 0.01 this sum is
561   # subtracted from the largest skonto_amount
562   my $rounded_total_rounding_error = abs(_round($total_rounding_error));
563
564   if ( $rounded_total_rounding_error > 0 ) {
565     my $highest_amount_pos = 0;
566     my $highest_amount = 0;
567     my $i = -1;
568     foreach my $ref ( @skonto_charts ) {
569       $i++;
570       if ( $ref->{skonto_amount} > $highest_amount ) {
571         $highest_amount     = $ref->{skonto_amount};
572         $highest_amount_pos = $i;
573       };
574     };
575     $skonto_charts[$i]->{skonto_amount} -= $rounded_total_rounding_error;
576   };
577
578   return @skonto_charts;
579 };
580
581
582 sub within_skonto_period {
583   my $self = shift;
584   my $dateref = shift || DateTime->now->truncate( to => 'day' );
585
586   return undef unless ref $dateref eq 'DateTime';
587   return 0 unless $self->skonto_date;
588
589   # return 1 if requested date (or today) is inside skonto period
590   # this will also return 1 if date is before the invoice date
591   return $dateref <= $self->skonto_date;
592 };
593
594 sub valid_skonto_amount {
595   my $self = shift;
596   my $amount = shift || 0;
597   my $max_skonto_percent = 0.10;
598
599   return 0 unless $amount > 0;
600
601   # does this work for other currencies?
602   return ($self->amount*$max_skonto_percent) > $amount;
603 };
604
605 sub get_payment_select_options_for_bank_transaction {
606   my ($self, $bt_id, %params) = @_;
607
608   my $bt = SL::DB::Manager::BankTransaction->find_by( id => $bt_id );
609   die unless $bt;
610
611   my $open_amount = $self->open_amount;
612
613   my @options;
614   if ( $open_amount &&                   # invoice amount not 0
615        $self->skonto_date &&             # check whether skonto applies
616        abs(abs($self->amount_less_skonto) - abs($bt->amount)) < 0.01 &&
617        $self->check_skonto_configuration) {
618          if ( $self->within_skonto_period($bt->transdate) ) {
619            push(@options, { payment_type => 'without_skonto', display => t8('without skonto') });
620            push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt'), selected => 1 });
621          } else {
622            push(@options, { payment_type => 'without_skonto', display => t8('without skonto') , selected => 1 });
623            push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt')});
624          };
625   };
626
627   return @options;
628
629 };
630
631
632 sub get_payment_suggestions {
633
634   my ($self, %params) = @_;
635
636   my $open_amount = $self->open_amount;
637   $open_amount   -= $self->open_sepa_transfer_amount if $params{sepa};
638
639   $self->{invoice_amount_suggestion} = $open_amount;
640   undef $self->{payment_select_options};
641   push(@{$self->{payment_select_options}} , { payment_type => 'without_skonto',  display => t8('without skonto') });
642   if ( $self->within_skonto_period ) {
643     # If there have been no payments yet suggest amount_less_skonto, otherwise the open amount
644     if ( $open_amount &&                   # invoice amount not 0
645          $open_amount == $self->amount &&  # no payments yet, or sum of payments and sepa export amounts is zero
646          $self->check_skonto_configuration) {
647       $self->{invoice_amount_suggestion} = $self->amount_less_skonto;
648       push(@{$self->{payment_select_options}} , { payment_type => 'with_skonto_pt',  display => t8('with skonto acc. to pt') , selected => 1 });
649     } else {
650       if ( ( $self->valid_skonto_amount($self->open_amount) || $self->valid_skonto_amount($open_amount) ) and not $params{sepa} ) {
651         $self->{invoice_amount_suggestion} = $open_amount;
652         # only suggest difference_as_skonto if open_amount exactly matches skonto_amount
653         # AND we aren't in SEPA mode
654         my $selected = 0;
655         $selected = 1 if _round($open_amount) == _round($self->skonto_amount);
656         push(@{$self->{payment_select_options}} , { payment_type => 'difference_as_skonto',  display => t8('difference as skonto') , selected => $selected });
657       };
658     };
659   } else {
660     # invoice was configured with skonto, but skonto date has passed, or no skonto available
661     $self->{invoice_amount_suggestion} = $open_amount;
662     # difference_as_skonto doesn't make any sense for SEPA transfer, as this doesn't cause any actual payment
663     if ( $self->valid_skonto_amount($self->open_amount) && not $params{sepa} ) {
664       push(@{$self->{payment_select_options}} , { payment_type => 'difference_as_skonto',  display => t8('difference as skonto') , selected => 0 });
665     };
666   };
667   return 1;
668 };
669
670 sub validate_payment_type {
671   my $payment_type = shift;
672
673   my %allowed_payment_types = map { $_ => 1 } qw(without_skonto with_skonto_pt difference_as_skonto);
674   croak "illegal payment type: $payment_type, must be one of: " . join(' ', keys %allowed_payment_types) unless $allowed_payment_types{ $payment_type };
675
676   return 1;
677 }
678
679 sub create_bank_transaction {
680   my ($self, %params) = @_;
681
682   require SL::DB::Chart;
683   require SL::DB::BankAccount;
684
685   my $bank_chart;
686   if ( $params{chart_id} ) {
687     $bank_chart = SL::DB::Manager::Chart->find_by(chart_id => $params{chart_id}) or die "Can't find bank chart";
688   } elsif ( $::instance_conf->get_ar_paid_accno_id ) {
689     $bank_chart   = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_paid_accno_id);
690   } else {
691     $bank_chart = SL::DB::Manager::Chart->find_by(description => 'Bank') or die "Can't find bank chart";
692   };
693   my $bank_account = SL::DB::Manager::BankAccount->find_by(chart_id => $bank_chart->id) or die "Can't find bank account for chart";
694
695   my $multiplier = $self->is_sales ? 1 : -1;
696   my $amount = ($params{amount} || $self->amount) * $multiplier;
697
698   my $transdate = $params{transdate} || DateTime->today;
699
700   my $bt = SL::DB::BankTransaction->new(
701     local_bank_account_id => $bank_account->id,
702     remote_bank_code      => $self->customervendor->bank_code,
703     remote_account_number => $self->customervendor->account_number,
704     transdate             => $transdate,
705     valutadate            => $transdate,
706     amount                => $::form->round_amount($amount, 2),
707     currency              => $self->currency->id,
708     remote_name           => $self->customervendor->depositor,
709     purpose               => $self->invnumber
710   )->save;
711 };
712
713
714 sub _round {
715   my $value = shift;
716   my $num_dec = 2;
717   return $::form->round_amount($value, 2);
718 }
719
720 1;
721
722 __END__
723
724 =pod
725
726 =head1 NAME
727
728 SL::DB::Helper::Payment  Mixin providing helper methods for paying C<Invoice>
729                          and C<PurchaseInvoice> objects and using skonto
730
731 =head1 SYNOPSIS
732
733 In addition to actually causing a payment via pay_invoice this helper contains
734 many methods that help in determining information about the status of the
735 invoice, such as the remaining open amount, whether skonto applies, until which
736 date skonto applies, the skonto amount and relative percentages, what to do
737 with skonto, ...
738
739 To prevent duplicate code this was all added in this mixin rather than directly
740 in SL::DB::Invoice and SL::DB::PurchaseInvoice.
741
742 =over 4
743
744 =item C<pay_invoice %params>
745
746 Create a payment booking for an existing invoice object (type ar/ap/is/ir) via
747 a configured bank account.
748
749 This function deals with all the acc_trans entries and also updates paid and datepaid.
750
751 Example:
752
753   my $ap   = SL::DB::Manager::PurchaseInvoice->find_by( invnumber => '1');
754   my $bank = SL::DB::Manager::BankAccount->find_by( name => 'Bank');
755   $ap->pay_invoice(chart_id      => $bank->chart_id,
756                    amount        => $ap->open_amount,
757                    transdate     => DateTime->now->to_kivitendo,
758                    memo          => 'foobar',
759                    source        => 'barfoo',
760                    payment_type  => 'without_skonto',  # default if not specified
761                   );
762
763 or with skonto:
764   $ap->pay_invoice(chart_id      => $bank->chart_id,
765                    amount        => $ap->amount,       # doesn't need to be specified
766                    transdate     => DateTime->now->to_kivitendo,
767                    memo          => 'foobar',
768                    source        => 'barfoo',
769                    payment_type  => 'with_skonto',
770                   );
771
772 or in a certain currency:
773   $ap->pay_invoice(chart_id      => $bank->chart_id,
774                    amount        => 500,
775                    currency      => 'USD',
776                    transdate     => DateTime->now->to_kivitendo,
777                    memo          => 'foobar',
778                    source        => 'barfoo',
779                    payment_type  => 'with_skonto',
780                   );
781
782 Allowed payment types are:
783   without_skonto with_skonto_pt difference_as_skonto
784
785 The option C<payment_type> allows for a basic skonto mechanism.
786
787 C<without_skonto> is the default mode, "amount" is paid to the account in
788 chart_id. This can also be used for partial payments and corrections via
789 negative amounts.
790
791 C<with_skonto_pt> can't be used for partial payments. When used on unpaid
792 invoices the whole amount is paid, with the skonto part automatically being
793 booked according to the skonto chart configured in the tax settings for each
794 tax key. If an amount is passed it is ignored and the actual configured skonto
795 amount is used.
796
797 C<difference_as_skonto> can only be used after partial payments have been made,
798 the whole specified amount is booked according to the skonto charts configured
799 in the tax settings for each tax key.
800
801 So passing amount doesn't have any effect for the cases C<with_skonto_pt> and
802 C<difference_as_skonto>, as all necessary values are taken from the stored
803 invoice.
804
805 The skonto modes automatically calculate the relative amounts for a mix of
806 taxes, e.g. items with 7% and 19% in one invoice. There is a helper method
807 skonto_charts, which calculates the relative percentages according to the
808 amounts in acc_trans (which are grouped by tax).
809
810 There is currently no way of excluding certain items in an invoice from having
811 skonto applied to them.  If this feature was added to parts the calculation
812 method of relative skonto would have to be completely rewritten using the
813 invoice items rather than acc_trans.
814
815 The skonto modes also still don't automatically correct the tax, this still has
816 to be done manually. Therefore all payments generated by pay_invoice have
817 taxkey 0.
818
819 There is currently no way to directly pay an invoice via this method if the
820 effective skonto differs from the skonto according to the payment terms
821 configured for the invoice/vendor.
822
823 In this case one has to pay in two steps: first the actual paid amount via
824 "without skonto", and then the remainder via "difference_as_skonto". The user
825 has to there actively decide whether to accept the differing skonto.
826
827 Because of the way skonto_charts works the calculation doesn't work if there
828 are negative values in acc_trans. E.g. one invoice with a positive value for
829 19% tax and a negative value for the acc_trans line with 7%
830
831 Skonto doesn't/shouldn't apply if the invoice contains credited items.
832
833 If no amount is given the whole open amout is paid.
834
835 If neither currency or currency_id are given as params, the currency of the
836 invoice is assumed to be the payment currency.
837
838 =item C<reference_account>
839
840 Returns a chart object which is the chart of the invoice with link AR or AP.
841
842 Example (1200 is the AR account for SKR04):
843   my $invoice = invoice(invnumber => '144');
844   $invoice->reference_account->accno
845   # 1200
846
847 =item C<percent_skonto>
848
849 Returns the configured skonto percentage of the payment terms of an invoice,
850 e.g. 0.02 for 2%. Payment terms come from invoice settings for ar, from vendor
851 settings for ap.
852
853 =item C<amount_less_skonto>
854
855 If the invoice has a payment term (via ar for sales, via vendor for purchase),
856 calculate the amount to be paid in the case of skonto.  This doesn't check,
857 whether skonto applies (i.e. skonto doesn't wasn't exceeded), it just subtracts
858 the configured percentage (e.g. 2%) from the total amount.
859
860 The returned value is rounded to two decimals.
861
862 =item C<skonto_date>
863
864 The date up to which skonto may be taken. This is calculated from the invoice
865 date + the number of days configured in the payment terms.
866
867 This method can also be used to determine whether skonto applies for the
868 invoice, as it returns undef if there is no payment term or skonto days is set
869 to 0.
870
871 =item C<within_skonto_period [DATE]>
872
873 Returns 0 or 1.
874
875 Checks whether the invoice has payment terms configured, and whether the date
876 is within the skonto max date. If no date is passed the current date is used.
877
878 You can also pass a dateref object as a parameter to check whether skonto
879 applies for that date rather than the current date.
880
881 =item C<valid_skonto_amount>
882
883 Takes an amount as an argument and checks whether the amount is less than 10%
884 of the total amount of the invoice. The value of 10% is currently hardcoded in
885 the method. This method is currently used to check whether to offer the payment
886 option "difference as skonto".
887
888 Example:
889  if ( $invoice->valid_skonto_amount($invoice->open_amount) ) {
890    # ... do something
891  }
892
893 =item C<skonto_charts [$amount]>
894
895 Returns a list of chart_ids and some calculated numbers that can be used for
896 paying the invoice with skonto. This function will automatically calculate the
897 relative skonto amounts even if the invoice contains several types of taxes
898 (e.g. 7% and 19%).
899
900 Example usage:
901   my $invoice = SL::DB::Manager::Invoice->find_by(invnumber => '211');
902   my @skonto_charts = $invoice->skonto_charts;
903
904 or with the total skonto amount as an argument:
905   my @skonto_charts = $invoice->skonto_charts($invoice->open_amount);
906
907 The following values are generated for each chart:
908
909 =over 2
910
911 =item C<chart_id>
912
913 The chart id of the skonto amount to be booked.
914
915 =item C<skonto_amount>
916
917 The total amount to be paid to the account
918
919 =item C<skonto_percent>
920
921 The relative percentage of that skonto chart. This can be useful if the actual
922 ekonto that is paid deviates from the granted skonto, e.g. customer effectively
923 pays 2.6% skonto instead of 2%, and we accept this. Then we can still calculate
924 the relative skonto amounts for different taxes based on the absolute
925 percentages. Used for case C<difference_as_skonto>.
926
927 =item C<skonto_percent_abs>
928
929 The absolute percentage of that skonto chart in relation to the total amount.
930 Used to calculate skonto_amount for case C<with_skonto_pt>.
931
932 =back
933
934 If the invoice contains several types of taxes then skonto_charts can be used
935 to calculate the relative amounts.
936
937 Example in console of an invoice with 100 Euro at 7% and 100 Euro at 19% with
938 tax not included:
939
940   my $invoice = invoice(invnumber => '144');
941   $invoice->amount
942   226.00000
943   $invoice->payment_terms->percent_skonto
944   0.02
945   $invoice->skonto_charts
946   pp $invoice->skonto_charts
947   #             $VAR1 = {
948   #               'chart_id'       => 128,
949   #               'skonto_amount'  => '2.14',
950   #               'skonto_percent' => '47.3451327433627'
951   #             };
952   #             $VAR2 = {
953   #               'chart_id'       => 130,
954   #               'skonto_amount'  => '2.38',
955   #               'skonto_percent' => '52.654867256637'
956   #             };
957
958 C<skonto_charts> always returns positive values (abs) for C<skonto_amount> and
959 C<skonto_percent>.
960
961 C<skonto_charts> generates one entry for each acc_trans entry. ar and ap
962 bookings only have one acc_trans entry for each taxkey (e.g. 7% and 19%).  This
963 is because all the items are grouped according to the Buchungsgruppen mechanism
964 and the totals are written to acc_trans.  For is and ir it is possible to have
965 several acc_trans entries with the same tax. In this case skonto_charts
966 generates a skonto booking for each acc_trans income/expense entry.
967
968 In the future this function may also be used to calculate the corrections for
969 the income tax.
970
971 =item C<open_amount>
972
973 Unrounded total open amount of invoice (amount - paid).
974 Doesn't take into account pending SEPA transfers.
975
976 =item C<open_percent>
977
978 Percentage of the invoice that is still unpaid, e.g. 100,00 if no payments have
979 been made yet, 0,00 if fully paid.
980
981 =item C<remaining_skonto_days>
982
983 How many days skonto can still be taken, calculated from current day. Returns 0
984 if current day is the max skonto date, and negative number if skonto date has
985 already passed.
986
987 Returns undef if skonto is not configured for that invoice.
988
989 =item C<get_payment_suggestions %params>
990
991 Creates data intended for an L.select_tag dropdown that can be used in a
992 template. Depending on the rules it will choose from the options
993 without_skonto, with_skonto_pt and difference_as_skonto, and select the most
994 likely one.
995
996 If the parameter "sepa" is passed, the SEPA export payments that haven't been
997 executed yet are considered when determining the open amount of the invoice.
998
999 The current rules are:
1000
1001 =over 2
1002
1003 =item * without_skonto is always an option
1004
1005 =item * with_skonto_pt is only offered if there haven't been any payments yet and the current date is within the skonto period.
1006
1007 =item * difference_as_skonto is only offered if there have already been payments made and the open amount is smaller than 10% of the total amount.
1008
1009 with_skonto_pt will only be offered, if all the AR_amount/AP_amount have a
1010 taxkey with a configured skonto chart
1011
1012 =back
1013
1014 It will also fill $self->{invoice_amount_suggestion} with either the open
1015 amount, or if with_skonto_pt is selected, with amount_less_skonto, so the
1016 template can fill the input with the likely amount.
1017
1018 Example in console:
1019   my $ar = invoice( invnumber => '257');
1020   $ar->get_payment_suggestions;
1021   print $ar->{invoice_amount_suggestion} . "\n";
1022   # 97.23
1023   pp $ar->{payment_select_options}
1024   # $VAR1 = [
1025   #         {
1026   #           'display' => 'ohne Skonto',
1027   #           'payment_type' => 'without_skonto'
1028   #         },
1029   #         {
1030   #           'display' => 'mit Skonto nach ZB',
1031   #           'payment_type' => 'with_skonto_pt',
1032   #           'selected' => 1
1033   #         }
1034   #       ];
1035
1036 The resulting array $ar->{payment_select_options} can be used in a template
1037 select_tag using value_key and title_key:
1038
1039 [% L.select_tag('payment_type_' _ loop.count, invoice.payment_select_options, value_key => 'payment_type', title_key => 'display', id => 'payment_type_' _ loop.count) %]
1040
1041 It would probably make sense to have different rules for the pre-selected items
1042 for sales and purchase, and to also make these rules configurable in the
1043 defaults. E.g. when creating a SEPA bank transfer for vendor invoices a company
1044 might always want to pay quickly making use of skonto, while another company
1045 might always want to pay as late as possible.
1046
1047 =item C<get_payment_select_options_for_bank_transaction $banktransaction_id %params>
1048
1049 Make suggestion for a skonto payment type by returning an HTML blob of the options
1050 of a HTML drop-down select with the most likely option preselected.
1051
1052 This is a helper function for BankTransaction/ajax_payment_suggestion.
1053
1054 We are working with an existing payment, so difference_as_skonto never makes sense.
1055
1056 If skonto is possible (skonto_date exists), add two possibilities:
1057 without_skonto and with_skonto_pt if payment date is within skonto_date,
1058 preselect with_skonto_pt, otherwise preselect without skonto.
1059
1060 =item C<create_bank_transaction %params>
1061
1062 Method used for testing purposes, allows you to quickly create bank
1063 transactions from invoices to have something to test payments against.
1064
1065  my $ap = SL::DB::Manager::Invoice->find_by(id => 41);
1066  $ap->create_bank_transaction(amount => $ap->amount/2, transdate => DateTime->today->add(days => 5));
1067
1068 Amount is always relative to the absolute amount of the invoice, use positive
1069 values for sales and purchases.
1070
1071 =back
1072
1073 =head1 TODO AND CAVEATS
1074
1075 =over 4
1076
1077 =item *
1078
1079 when looking at open amount, maybe consider that there may already be queued
1080 amounts in SEPA Export
1081
1082 =item *
1083
1084 Can only handle default currency.
1085
1086 =back
1087
1088 =head1 AUTHOR
1089
1090 G. Richardson E<lt>grichardson@kivitendo-premium.de<gt>
1091
1092 =cut