BT(save_single_invoice) valutadate anstelle transdate an pay_invoice
[kivitendo-erp.git] / SL / Controller / BankTransaction.pm
1 package SL::Controller::BankTransaction;
2
3 # idee- möglichkeit bankdaten zu übernehmen in stammdaten
4 # erst Kontenabgleich, um alle gl-Einträge wegzuhaben
5 use strict;
6
7 use parent qw(SL::Controller::Base);
8
9 use SL::Controller::Helper::GetModels;
10 use SL::Controller::Helper::ReportGenerator;
11 use SL::ReportGenerator;
12
13 use SL::DB::BankTransaction;
14 use SL::Helper::Flash;
15 use SL::Locale::String;
16 use SL::SEPA;
17 use SL::DB::Invoice;
18 use SL::DB::PurchaseInvoice;
19 use SL::DB::RecordLink;
20 use SL::DB::ReconciliationLink;
21 use SL::JSON;
22 use SL::DB::Chart;
23 use SL::DB::AccTransaction;
24 use SL::DB::BankTransactionAccTrans;
25 use SL::DB::Tax;
26 use SL::DB::BankAccount;
27 use SL::DB::RecordTemplate;
28 use SL::DB::SepaExportItem;
29 use SL::DBUtils qw(like do_query);
30
31 use SL::Presenter::Tag qw(checkbox_tag);
32 use Carp;
33 use List::UtilsBy qw(partition_by);
34 use List::MoreUtils qw(any);
35 use List::Util qw(max);
36
37 use Rose::Object::MakeMethods::Generic
38 (
39   scalar                  => [ qw(callback transaction) ],
40   'scalar --get_set_init' => [ qw(models problems) ],
41 );
42
43 __PACKAGE__->run_before('check_auth');
44
45
46 #
47 # actions
48 #
49
50 sub action_search {
51   my ($self) = @_;
52
53   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
54
55   $self->setup_search_action_bar;
56   $self->render('bank_transactions/search',
57                  BANK_ACCOUNTS => $bank_accounts);
58 }
59
60 sub action_list_all {
61   my ($self) = @_;
62
63   $self->make_filter_summary;
64   $self->prepare_report;
65
66   $self->setup_list_all_action_bar;
67   $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
68 }
69
70 sub action_list {
71   my ($self) = @_;
72
73   if (!$::form->{filter}{bank_account}) {
74     flash('error', t8('No bank account chosen!'));
75     $self->action_search;
76     return;
77   }
78
79   my $sort_by = $::form->{sort_by} || 'transdate';
80   $sort_by = 'transdate' if $sort_by eq 'proposal';
81   $sort_by .= $::form->{sort_dir} ? ' DESC' : ' ASC';
82
83   my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
84   my $todate   = $::locale->parse_date_to_object($::form->{filter}->{todate});
85   $todate->add( days => 1 ) if $todate;
86
87   my @where = ();
88   push @where, (transdate => { ge => $fromdate }) if ($fromdate);
89   push @where, (transdate => { lt => $todate })   if ($todate);
90   my $bank_account = SL::DB::Manager::BankAccount->find_by( id => $::form->{filter}{bank_account} );
91   # bank_transactions no younger than starting date,
92   # including starting date (same search behaviour as fromdate)
93   # but OPEN invoices to be matched may be from before
94   if ( $bank_account->reconciliation_starting_date ) {
95     push @where, (transdate => { ge => $bank_account->reconciliation_starting_date });
96   };
97
98   my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
99     with_objects => [ 'local_bank_account', 'currency' ],
100     sort_by      => $sort_by,
101     limit        => 10000,
102     where        => [
103       amount                => {ne => \'invoice_amount'},
104       local_bank_account_id => $::form->{filter}{bank_account},
105       cleared               => 0,
106       @where
107     ],
108   );
109   # credit notes have a negative amount, treat differently
110   my $all_open_ar_invoices = SL::DB::Manager::Invoice        ->get_all(where => [ or => [ amount => { gt => \'paid' },
111                                                                                           and => [ type    => 'credit_note',
112                                                                                                    amount  => { lt => \'paid' }
113                                                                                                  ],
114                                                                                         ],
115                                                                                 ],
116                                                                        with_objects => ['customer','payment_terms']);
117
118   my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { ne => \'paid' }], with_objects => ['vendor'  ,'payment_terms']);
119   my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $bank_account->chart_id ,
120                                                                              'sepa_export.executed' => 0, 'sepa_export.closed' => 0 ], with_objects => ['sepa_export']);
121
122   my @all_open_invoices;
123   # filter out invoices with less than 1 cent outstanding
124   push @all_open_invoices, map { $_->{is_ar}=1 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
125   push @all_open_invoices, map { $_->{is_ar}=0 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
126
127   my %sepa_exports;
128   my %sepa_export_items_by_id = partition_by { $_->ar_id || $_->ap_id } @$all_open_sepa_export_items;
129
130   # first collect sepa export items to open invoices
131   foreach my $open_invoice (@all_open_invoices){
132     $open_invoice->{realamount}  = $::form->format_amount(\%::myconfig,$open_invoice->amount,2);
133     $open_invoice->{skonto_type} = 'without_skonto';
134     foreach (@{ $sepa_export_items_by_id{ $open_invoice->id } || [] }) {
135       my $factor                   = ($_->ar_id == $open_invoice->id ? 1 : -1);
136       $open_invoice->{realamount}  = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);
137
138       $open_invoice->{skonto_type} = $_->payment_type;
139       $sepa_exports{$_->sepa_export_id} ||= { count => 0, is_ar => 0, amount => 0, proposed => 0, invoices => [], item => $_ };
140       $sepa_exports{$_->sepa_export_id}->{count}++;
141       $sepa_exports{$_->sepa_export_id}->{is_ar}++ if  $_->ar_id == $open_invoice->id;
142       $sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor;
143       push @{ $sepa_exports{$_->sepa_export_id}->{invoices} }, $open_invoice;
144     }
145   }
146
147   # try to match each bank_transaction with each of the possible open invoices
148   # by awarding points
149   my @proposals;
150
151   foreach my $bt (@{ $bank_transactions }) {
152     ## 5 Stellen hinter dem Komma auf 2 Stellen reduzieren
153     $bt->amount($bt->amount*1);
154     $bt->invoice_amount($bt->invoice_amount*1);
155
156     $bt->{proposals}    = [];
157     $bt->{rule_matches} = [];
158
159     $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
160
161     if ( $bt->is_batch_transaction ) {
162       my $found=0;
163       foreach ( keys  %sepa_exports) {
164         if ( abs(($sepa_exports{$_}->{amount} * 1) - ($bt->amount * 1)) < 0.01 ) {
165           ## jupp
166           @{$bt->{proposals}} = @{$sepa_exports{$_}->{invoices}};
167           $bt->{sepa_export_ok} = 1;
168           $sepa_exports{$_}->{proposed}=1;
169           push(@proposals, $bt);
170           $found=1;
171           last;
172         }
173       }
174       next if $found;
175       # batch transaction has no remotename !!
176     } else {
177       next unless $bt->{remote_name};  # bank has no name, usually fees, use create invoice to assign
178     }
179
180     # try to match the current $bt to each of the open_invoices, saving the
181     # results of get_agreement_with_invoice in $open_invoice->{agreement} and
182     # $open_invoice->{rule_matches}.
183
184     # The values are overwritten each time a new bt is checked, so at the end
185     # of each bt the likely results are filtered and those values are stored in
186     # the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
187     # score is stored in $bt->{agreement}
188
189     foreach my $open_invoice (@all_open_invoices) {
190       ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice,
191         sepa_export_items => $all_open_sepa_export_items,
192       );
193       $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,
194                                       $open_invoice->amount * ($open_invoice->{is_ar} ? 1 : -1), 2);
195     }
196
197     my $agreement = 15;
198     my $min_agreement = 3; # suggestions must have at least this score
199
200     my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
201
202     # add open_invoices with highest agreement into array $bt->{proposals}
203     if ( $max_agreement >= $min_agreement ) {
204       $bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
205       $bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
206
207       # store the rule_matches in a separate array, so they can be displayed in template
208       foreach ( @{ $bt->{proposals} } ) {
209         push(@{$bt->{rule_matches}}, $_->{rule_matches});
210       };
211     };
212   }  # finished one bt
213   # finished all bt
214
215   # separate filter for proposals (second tab, agreement >= 5 and exactly one match)
216   # to qualify as a proposal there has to be
217   # * agreement >= 5  TODO: make threshold configurable in configuration
218   # * there must be only one exact match
219   # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
220   my $proposal_threshold = 5;
221   my @otherproposals = grep {
222        ($_->{agreement} >= $proposal_threshold)
223     && (1 == scalar @{ $_->{proposals} })
224     && (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01
225                                           : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01)
226   } @{ $bank_transactions };
227
228   push @proposals, @otherproposals;
229
230   # sort bank transaction proposals by quality (score) of proposal
231   if ($::form->{sort_by} && $::form->{sort_by} eq 'proposal') {
232     if ($::form->{sort_dir}) {
233       $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ];
234     } else {
235       $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ];
236     }
237   }
238
239   # for testing with t/bank/banktransaction.t :
240   if ( $::form->{dont_render_for_test} ) {
241     return ( $bank_transactions , \@proposals );
242   }
243
244   $::request->layout->add_javascripts("kivi.BankTransaction.js");
245   $self->render('bank_transactions/list',
246                 title             => t8('Bank transactions MT940'),
247                 BANK_TRANSACTIONS => $bank_transactions,
248                 PROPOSALS         => \@proposals,
249                 bank_account      => $bank_account,
250                 ui_tab            => scalar(@proposals) > 0?1:0,
251               );
252 }
253
254 sub action_assign_invoice {
255   my ($self) = @_;
256
257   $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
258
259   $self->render('bank_transactions/assign_invoice',
260                 { layout => 0 },
261                 title => t8('Assign invoice'),);
262 }
263
264 sub action_create_invoice {
265   my ($self) = @_;
266   my %myconfig = %main::myconfig;
267
268   $self->transaction(SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id}));
269
270   my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(iban => $self->transaction->{remote_account_number});
271   my $use_vendor_filter     = $self->transaction->{remote_account_number} && $vendor_of_transaction;
272
273   my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
274     where        => [ template_type => 'ap_transaction' ],
275     with_objects => [ qw(employee vendor) ],
276   );
277   my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
278     query        => [ template_type => 'gl_transaction',
279                       chart_id      => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
280                     ],
281     with_objects => [ qw(employee record_template_items) ],
282   );
283
284   # pre filter templates_ap, if we have a vendor match (IBAN eq IBAN) - show and allow user to edit this via gui!
285   $templates_ap = [ grep { $_->vendor_id == $vendor_of_transaction->id } @{ $templates_ap } ] if $use_vendor_filter;
286
287   $self->callback($self->url_for(
288     action                => 'list',
289     'filter.bank_account' => $::form->{filter}->{bank_account},
290     'filter.todate'       => $::form->{filter}->{todate},
291     'filter.fromdate'     => $::form->{filter}->{fromdate},
292   ));
293
294   $self->render(
295     'bank_transactions/create_invoice',
296     { layout => 0 },
297     title        => t8('Create invoice'),
298     TEMPLATES_GL => $use_vendor_filter && @{ $templates_ap } ? undef : $templates_gl,
299     TEMPLATES_AP => $templates_ap,
300     vendor_name  => $use_vendor_filter && @{ $templates_ap } ? $vendor_of_transaction->name : undef,
301   );
302 }
303
304 sub action_ajax_payment_suggestion {
305   my ($self) = @_;
306
307   # based on a BankTransaction ID and a Invoice or PurchaseInvoice ID passed via $::form,
308   # create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
309   # and return encoded as JSON
310
311   my $bt      = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
312   my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
313
314   die unless $bt and $invoice;
315
316   my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
317
318   my $html;
319   $html = $self->render(
320     'bank_transactions/_payment_suggestion', { output => 0 },
321     bt_id          => $::form->{bt_id},
322     prop_id        => $::form->{prop_id},
323     invoice        => $invoice,
324     SELECT_OPTIONS => \@select_options,
325   );
326
327   $self->render(\ SL::JSON::to_json( { 'html' => "$html" } ), { layout => 0, type => 'json', process => 0 });
328 };
329
330 sub action_filter_templates {
331   my ($self) = @_;
332
333   $self->{transaction}      = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
334
335   my (@filter, @filter_ap);
336
337   # filter => gl and ap | filter_ap = ap (i.e. vendorname)
338   push @filter,    ('template_name' => { ilike => '%' . $::form->{template} . '%' })  if $::form->{template};
339   push @filter,    ('reference'     => { ilike => '%' . $::form->{reference} . '%' }) if $::form->{reference};
340   push @filter_ap, ('vendor.name'   => { ilike => '%' . $::form->{vendor} . '%' })    if $::form->{vendor};
341   push @filter_ap, @filter;
342   my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
343     query        => [ template_type => 'gl_transaction',
344                       chart_id      => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
345                       (and => \@filter) x !!@filter
346                     ],
347     with_objects => [ qw(employee record_template_items) ],
348   );
349
350   my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
351     where        => [ template_type => 'ap_transaction', (and => \@filter_ap) x !!@filter_ap ],
352     with_objects => [ qw(employee vendor) ],
353   );
354   $::form->{filter} //= {};
355
356   $self->callback($self->url_for(
357     action                => 'list',
358     'filter.bank_account' => $::form->{filter}->{bank_account},
359     'filter.todate'       => $::form->{filter}->{todate},
360     'filter.fromdate'     => $::form->{filter}->{fromdate},
361   ));
362
363   my $output  = $self->render(
364     'bank_transactions/_template_list',
365     { output => 0 },
366     TEMPLATES_AP => $templates_ap,
367     TEMPLATES_GL => $templates_gl,
368   );
369
370   $self->render(\to_json({ html => $output }), { type => 'json', process => 0 });
371 }
372
373 sub action_ajax_add_list {
374   my ($self) = @_;
375
376   my @where_sale     = (amount => { ne => \'paid' });
377   my @where_purchase = (amount => { ne => \'paid' });
378
379   if ($::form->{invnumber}) {
380     push @where_sale,     (invnumber => { ilike => like($::form->{invnumber})});
381     push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
382   }
383
384   if ($::form->{amount}) {
385     push @where_sale,     (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
386     push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
387   }
388
389   if ($::form->{vcnumber}) {
390     push @where_sale,     ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
391     push @where_purchase, ('vendor.vendornumber'     => { ilike => like($::form->{vcnumber})});
392   }
393
394   if ($::form->{vcname}) {
395     push @where_sale,     ('customer.name' => { ilike => like($::form->{vcname})});
396     push @where_purchase, ('vendor.name'   => { ilike => like($::form->{vcname})});
397   }
398
399   if ($::form->{transdatefrom}) {
400     my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
401     if ( ref($fromdate) eq 'DateTime' ) {
402       push @where_sale,     ('transdate' => { ge => $fromdate});
403       push @where_purchase, ('transdate' => { ge => $fromdate});
404     };
405   }
406
407   if ($::form->{transdateto}) {
408     my $todate = $::locale->parse_date_to_object($::form->{transdateto});
409     if ( ref($todate) eq 'DateTime' ) {
410       $todate->add(days => 1);
411       push @where_sale,     ('transdate' => { lt => $todate});
412       push @where_purchase, ('transdate' => { lt => $todate});
413     };
414   }
415
416   my $all_open_ar_invoices = SL::DB::Manager::Invoice        ->get_all(where => \@where_sale,     with_objects => 'customer');
417   my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
418
419   my @all_open_invoices = @{ $all_open_ar_invoices };
420   # add ap invoices, filtering out subcent open amounts
421   push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
422
423   @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
424
425   my $output  = $self->render(
426     'bank_transactions/add_list',
427     { output => 0 },
428     INVOICES => \@all_open_invoices,
429   );
430
431   my %result = ( count => 0, html => $output );
432
433   $self->render(\to_json(\%result), { type => 'json', process => 0 });
434 }
435
436 sub action_ajax_accept_invoices {
437   my ($self) = @_;
438
439   my @selected_invoices;
440   foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
441     my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
442     push @selected_invoices, $invoice_object;
443   }
444
445   $self->render(
446     'bank_transactions/invoices',
447     { layout => 0 },
448     INVOICES => \@selected_invoices,
449     bt_id    => $::form->{bt_id},
450   );
451 }
452
453 sub save_invoices {
454   my ($self) = @_;
455
456   return 0 if !$::form->{invoice_ids};
457
458   my %invoice_hash = %{ delete $::form->{invoice_ids} };  # each key (the bt line with a bt_id) contains an array of invoice_ids
459
460   # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
461   # $invoice_hash = {
462   #         '55' => [
463   #                 '74'
464   #               ],
465   #         '54' => [
466   #                 '74'
467   #               ],
468   #         '56' => [
469   #                 '74'
470   #               ]
471   #       };
472   #
473   # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
474   # $invoice_hash = {
475   #           '44' => [ '50', '51', 52' ]
476   #         };
477
478   $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
479
480   # a bank_transaction may be assigned to several invoices, i.e. a customer
481   # might pay several open invoices with one transaction
482
483   $self->problems([]);
484
485   my $count = 0;
486
487   if ( $::form->{proposal_ids} ) {
488     foreach (@{ $::form->{proposal_ids} }) {
489       my  $bank_transaction_id = $_;
490       my  $invoice_ids = $invoice_hash{$_};
491       push @{ $self->problems }, $self->save_single_bank_transaction(
492         bank_transaction_id => $bank_transaction_id,
493         invoice_ids         => $invoice_ids,
494         sources             => ($::form->{sources} // {})->{$_},
495         memos               => ($::form->{memos}   // {})->{$_},
496       );
497       $count += scalar( @{$invoice_ids} );
498     }
499   } else {
500     while ( my ($bank_transaction_id, $invoice_ids) = each(%invoice_hash) ) {
501       push @{ $self->problems }, $self->save_single_bank_transaction(
502         bank_transaction_id => $bank_transaction_id,
503         invoice_ids         => $invoice_ids,
504         sources             => [  map { $::form->{"sources_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
505         memos               => [  map { $::form->{"memos_${bank_transaction_id}_${_}"}   } @{ $invoice_ids } ],
506       );
507       $count += scalar( @{$invoice_ids} );
508     }
509   }
510   my $max_count = $count;
511   foreach (@{ $self->problems }) {
512     $count-- if $_->{result} eq 'error';
513   }
514   return ($count, $max_count);
515 }
516
517 sub action_save_invoices {
518   my ($self) = @_;
519   my ($success_count, $max_count) = $self->save_invoices();
520
521   if ($success_count == $max_count) {
522     flash('ok', t8('#1 invoice(s) saved.', $success_count));
523   } else {
524     flash('error', t8('At least #1 invoice(s) not saved', $max_count - $success_count));
525   }
526
527   $self->action_list();
528 }
529
530 sub action_save_proposals {
531   my ($self) = @_;
532
533   if ( $::form->{proposal_ids} ) {
534     my $propcount = scalar(@{ $::form->{proposal_ids} });
535     if ( $propcount > 0 ) {
536       my $count = $self->save_invoices();
537
538       flash('ok', t8('#1 proposal(s) with #2 invoice(s) saved.',  $propcount, $count));
539     }
540   }
541   $self->action_list();
542
543 }
544
545 sub save_single_bank_transaction {
546   my ($self, %params) = @_;
547
548   my %data = (
549     %params,
550     bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
551     invoices         => [],
552   );
553
554   if (!$data{bank_transaction}) {
555     return {
556       %data,
557       result => 'error',
558       message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
559     };
560   }
561
562   my $bank_transaction = $data{bank_transaction};
563
564   if ($bank_transaction->closed_period) {
565     return {
566       %data,
567       result => 'error',
568       message => $::locale->text('Cannot post payment for a closed period!'),
569     };
570   }
571   my (@warnings);
572
573   my $worker = sub {
574     my $bt_id                 = $data{bank_transaction_id};
575     my $sign                  = $bank_transaction->amount < 0 ? -1 : 1;
576     my $not_assigned_amount   = $bank_transaction->not_assigned_amount;
577     my $payment_received      = $bank_transaction->amount > 0;
578     my $payment_sent          = $bank_transaction->amount < 0;
579
580
581     foreach my $invoice_id (@{ $params{invoice_ids} }) {
582       my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
583       if (!$invoice) {
584         return {
585           %data,
586           result  => 'error',
587           message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
588         };
589       }
590       push @{ $data{invoices} }, $invoice;
591     }
592
593     if (   $payment_received
594         && any {    ( $_->is_sales && ($_->amount < 0))
595                  || (!$_->is_sales && ($_->amount > 0))
596                } @{ $data{invoices} }) {
597       return {
598         %data,
599         result  => 'error',
600         message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
601       };
602     }
603
604     if (   $payment_sent
605         && any {    ( $_->is_sales && ($_->amount > 0))
606                  || (!$_->is_sales && ($_->amount < 0) && ($_->invoice_type eq 'purchase_invoice'))
607                } @{ $data{invoices} }) {
608       return {
609         %data,
610         result  => 'error',
611         message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
612       };
613     }
614
615     my $max_invoices = scalar(@{ $data{invoices} });
616     my $n_invoices   = 0;
617
618     foreach my $invoice (@{ $data{invoices} }) {
619       my $source = ($data{sources} // [])->[$n_invoices];
620       my $memo   = ($data{memos}   // [])->[$n_invoices];
621
622       $n_invoices++ ;
623
624
625       if (!$not_assigned_amount && $invoice->open_amount) {
626         return {
627           %data,
628           result  => 'error',
629           message => $::locale->text("A payment can only be posted for multiple invoices if the amount to post is equal to or bigger than the sum of the open amounts of the affected invoices."),
630         };
631       }
632
633       my $payment_type;
634       if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
635         $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
636       } else {
637         $payment_type = 'without_skonto';
638       };
639     # pay invoice
640     # TODO rewrite this: really booked amount should be a return value of Payment.pm
641     # also this controller shouldnt care about how to calc skonto. we simply delegate the
642     # payment_type to the helper and get the corresponding bank_transaction values back
643     # hotfix to get the signs right - compare absolute values and later set the signs
644     # should be better done elsewhere - changing not_assigned_amount to abs feels seriously bogus
645
646     my $open_amount = $payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto : $invoice->open_amount;
647     $open_amount         = abs($open_amount);
648     $not_assigned_amount = abs($not_assigned_amount);
649     my $amount_for_booking = ($open_amount < $not_assigned_amount) ? $open_amount : $not_assigned_amount;
650     my $amount_for_payment = $amount_for_booking;
651
652     # get the right direction for the payment bookings (all amounts < 0 are stornos, credit notes or negative ap)
653     $amount_for_payment *= -1 if $invoice->amount < 0;
654     # get the right direction for the bank transaction
655     $amount_for_booking *= $sign;
656
657     $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking);
658
659     # ... and then pay the invoice
660     my @acc_ids = $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
661                           trans_id     => $invoice->id,
662                           amount       => $amount_for_payment,
663                           payment_type => $payment_type,
664                           source       => $source,
665                           memo         => $memo,
666                           transdate    => $bank_transaction->valutadate->to_kivitendo);
667     # ... and record the origin via BankTransactionAccTrans
668     if (scalar(@acc_ids) < 2) {
669       return {
670         %data,
671         result  => 'error',
672         message => $::locale->text("Unable to book transactions for bank purpose #1", $bank_transaction->purpose),
673       };
674     }
675     foreach my $acc_trans_id (@acc_ids) {
676         my $id_type = $invoice->is_sales ? 'ar' : 'ap';
677         my  %props_acc = (
678           acc_trans_id        => $acc_trans_id,
679           bank_transaction_id => $bank_transaction->id,
680           $id_type            => $invoice->id,
681         );
682         SL::DB::BankTransactionAccTrans->new(%props_acc)->save;
683     }
684       # Record a record link from the bank transaction to the invoice
685       my %props = (
686         from_table => 'bank_transactions',
687         from_id    => $bt_id,
688         to_table   => $invoice->is_sales ? 'ar' : 'ap',
689         to_id      => $invoice->id,
690       );
691       SL::DB::RecordLink->new(%props)->save;
692
693       # "close" a sepa_export_item if it exists
694       # code duplicated in action_save_proposals!
695       # currently only works, if there is only exactly one open sepa_export_item
696       if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
697         if ( scalar @$seis == 1 ) {
698           # moved the execution and the check for sepa_export into a method,
699           # this isn't part of a transaction, though
700           $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
701         }
702       }
703
704     }
705     $bank_transaction->save;
706
707     # 'undef' means 'no error' here.
708     return undef;
709   };
710
711   my $error;
712   my $rez = $data{bank_transaction}->db->with_transaction(sub {
713     eval {
714       $error = $worker->();
715       1;
716
717     } or do {
718       $error = {
719         %data,
720         result  => 'error',
721         message => $@,
722       };
723     };
724
725     # Rollback Fehler nicht weiterreichen
726     # die if $error;
727     # aber einen rollback von hand
728     $::lxdebug->message(LXDebug->DEBUG2(),"finish worker with ". ($error ? $error->{result} : '-'));
729     $data{bank_transaction}->db->dbh->rollback if $error && $error->{result} eq 'error';
730   });
731
732   return grep { $_ } ($error, @warnings);
733 }
734 sub action_unlink_bank_transaction {
735   my ($self, %params) = @_;
736
737   croak("No bank transaction ids") unless scalar @{ $::form->{ids}} > 0;
738
739   my $success_count;
740
741   foreach my $bt_id (@{ $::form->{ids}} )  {
742
743     my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
744     croak("No valid bank transaction found") unless (ref($bank_transaction)  eq 'SL::DB::BankTransaction');
745     croak t8('Cannot unlink payment for a closed period!') if $bank_transaction->closed_period;
746
747     # everything in one transaction
748     my $rez = $bank_transaction->db->with_transaction(sub {
749       # 1. remove all reconciliations (due to underlying trigger, this has to be the first step)
750       my $rec_links = SL::DB::Manager::ReconciliationLink->get_all(where => [ bank_transaction_id => $bt_id ]);
751       $_->delete for @{ $rec_links };
752
753       my %trans_ids;
754       foreach my $acc_trans_id_entry (@{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [bank_transaction_id => $bt_id ] )}) {
755
756         my $acc_trans = SL::DB::Manager::AccTransaction->get_all(where => [acc_trans_id => $acc_trans_id_entry->acc_trans_id]);
757
758         # save trans_id and type
759         die "no type" unless ($acc_trans_id_entry->ar_id || $acc_trans_id_entry->ap_id || $acc_trans_id_entry->gl_id);
760         $trans_ids{$acc_trans_id_entry->ar_id} = 'ar' if $acc_trans_id_entry->ar_id;
761         $trans_ids{$acc_trans_id_entry->ap_id} = 'ap' if $acc_trans_id_entry->ap_id;
762         $trans_ids{$acc_trans_id_entry->gl_id} = 'gl' if $acc_trans_id_entry->gl_id;
763         # 2. all good -> ready to delete acc_trans and bt_acc link
764         $acc_trans_id_entry->delete;
765         $_->delete for @{ $acc_trans };
766       }
767       # 3. update arap.paid (may not be 0, yet)
768       while (my ($trans_id, $type) = each %trans_ids) {
769         next if $type eq 'gl';
770         die ("invalid type") unless $type =~ m/^(ar|ap)$/;
771
772         # recalc and set paid via database query
773         my $query = qq|UPDATE $type SET paid =
774                         (SELECT COALESCE(abs(sum(amount)),0) FROM acc_trans
775                          WHERE trans_id = ?
776                          AND chart_link ilike '%paid%')|;
777
778         die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id) == -1);
779       }
780       # 4. and delete all (if any) record links
781       my $rl = SL::DB::Manager::RecordLink->delete_all(where => [ from_id => $bt_id, from_table => 'bank_transactions' ]);
782
783       # 5. finally reset  this bank transaction
784       $bank_transaction->invoice_amount(0);
785       $bank_transaction->cleared(0);
786       $bank_transaction->save;
787
788       1;
789
790     }) || die t8('error while unlinking payment #1 : ', $bank_transaction->purpose) . $bank_transaction->db->error . "\n";
791
792     $success_count++;
793   }
794
795   flash('ok', t8('#1 bank transaction bookings undone.', $success_count));
796   $self->action_list_all() unless $params{testcase};
797 }
798 #
799 # filters
800 #
801
802 sub check_auth {
803   $::auth->assert('bank_transaction');
804 }
805
806 #
807 # helpers
808 #
809
810 sub make_filter_summary {
811   my ($self) = @_;
812
813   my $filter = $::form->{filter} || {};
814   my @filter_strings;
815
816   my @filters = (
817     [ $filter->{"transdate:date::ge"},      $::locale->text('Transdate')  . " " . $::locale->text('From Date') ],
818     [ $filter->{"transdate:date::le"},      $::locale->text('Transdate')  . " " . $::locale->text('To Date')   ],
819     [ $filter->{"valutadate:date::ge"},     $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
820     [ $filter->{"valutadate:date::le"},     $::locale->text('Valutadate') . " " . $::locale->text('To Date')   ],
821     [ $filter->{"amount:number"},           $::locale->text('Amount')                                          ],
822     [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account')                              ],
823   );
824
825   for (@filters) {
826     push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
827   }
828
829   $self->{filter_summary} = join ', ', @filter_strings;
830 }
831
832 sub prepare_report {
833   my ($self)      = @_;
834
835   my $callback    = $self->models->get_callback;
836
837   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
838   $self->{report} = $report;
839
840   my @columns     = qw(ids local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount invoice_amount invoices currency purpose local_account_number local_bank_code id);
841   my @sortable    = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount                                  purpose local_account_number local_bank_code);
842
843   my %column_defs = (
844     ids                 => { raw_header_data => checkbox_tag("", id => "check_all", checkall  => "[data-checkall=1]"),
845                              'align'         => 'center',
846                              raw_data        => sub { if (@{ $_[0]->linked_invoices } && !(grep {ref ($_) eq 'SL::DB::GLTransaction' } @{ $_[0]->linked_invoices })) {
847                                                          checkbox_tag("ids[]", value => $_[0]->id, "data-checkall" => 1); } } },
848     transdate             => { sub   => sub { $_[0]->transdate_as_date } },
849     valutadate            => { sub   => sub { $_[0]->valutadate_as_date } },
850     remote_name           => { },
851     remote_account_number => { },
852     remote_bank_code      => { },
853     amount                => { sub   => sub { $_[0]->amount_as_number },
854                                align => 'right' },
855     invoice_amount        => { sub   => sub { $_[0]->invoice_amount_as_number },
856                                align => 'right' },
857     invoices              => { sub   => sub { my @invnumbers; for my $obj (@{ $_[0]->linked_invoices }) {
858                                                                 next unless $obj; push @invnumbers, $obj->invnumber } return \@invnumbers } },
859     currency              => { sub   => sub { $_[0]->currency->name } },
860     purpose               => { },
861     local_account_number  => { sub   => sub { $_[0]->local_bank_account->account_number } },
862     local_bank_code       => { sub   => sub { $_[0]->local_bank_account->bank_code } },
863     local_bank_name       => { sub   => sub { $_[0]->local_bank_account->name } },
864     id                    => {},
865   );
866
867   map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
868
869   $report->set_options(
870     std_column_visibility => 1,
871     controller_class      => 'BankTransaction',
872     output_format         => 'HTML',
873     top_info_text         => $::locale->text('Bank transactions'),
874     title                 => $::locale->text('Bank transactions'),
875     allow_pdf_export      => 1,
876     allow_csv_export      => 1,
877   );
878   $report->set_columns(%column_defs);
879   $report->set_column_order(@columns);
880   $report->set_export_options(qw(list_all filter));
881   $report->set_options_from_form;
882   $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
883   $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
884
885   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
886
887   $report->set_options(
888     raw_top_info_text     => $self->render('bank_transactions/report_top',    { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
889     raw_bottom_info_text  => $self->render('bank_transactions/report_bottom', { output => 0 }),
890   );
891 }
892
893 sub init_problems { [] }
894
895 sub init_models {
896   my ($self) = @_;
897
898   SL::Controller::Helper::GetModels->new(
899     controller => $self,
900     sorted     => {
901       _default => {
902         by  => 'transdate',
903         dir => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
904       },
905       id                    => t8('ID'),
906       transdate             => t8('Transdate'),
907       remote_name           => t8('Remote name'),
908       amount                => t8('Amount'),
909       invoice_amount        => t8('Assigned'),
910       invoices              => t8('Linked invoices'),
911       valutadate            => t8('Valutadate'),
912       remote_account_number => t8('Remote account number'),
913       remote_bank_code      => t8('Remote bank code'),
914       currency              => t8('Currency'),
915       purpose               => t8('Purpose'),
916       local_account_number  => t8('Local account number'),
917       local_bank_code       => t8('Local bank code'),
918       local_bank_name       => t8('Bank account'),
919     },
920     with_objects => [ 'local_bank_account', 'currency' ],
921   );
922 }
923
924 sub load_ap_record_template_url {
925   my ($self, $template) = @_;
926
927   return $self->url_for(
928     controller                           => 'ap.pl',
929     action                               => 'load_record_template',
930     id                                   => $template->id,
931     'form_defaults.amount_1'             => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
932     'form_defaults.transdate'            => $self->transaction->transdate_as_date,
933     'form_defaults.duedate'              => $self->transaction->transdate_as_date,
934     'form_defaults.no_payment_bookings'  => 1,
935     'form_defaults.paid_1_suggestion'    => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
936     'form_defaults.AP_paid_1_suggestion' => $self->transaction->local_bank_account->chart->accno,
937     'form_defaults.callback'             => $self->callback,
938   );
939 }
940
941 sub load_gl_record_template_url {
942   my ($self, $template) = @_;
943
944   return $self->url_for(
945     controller                           => 'gl.pl',
946     action                               => 'load_record_template',
947     id                                   => $template->id,
948     'form_defaults.amount_1'             => abs($self->transaction->not_assigned_amount), # always positive
949     'form_defaults.transdate'            => $self->transaction->transdate_as_date,
950     'form_defaults.callback'             => $self->callback,
951     'form_defaults.bt_id'                => $self->transaction->id,
952     'form_defaults.bt_chart_id'          => $self->transaction->local_bank_account->chart->id,
953   );
954 }
955
956 sub setup_search_action_bar {
957   my ($self, %params) = @_;
958
959   for my $bar ($::request->layout->get('actionbar')) {
960     $bar->add(
961       action => [
962         t8('Filter'),
963         submit    => [ '#search_form', { action => 'BankTransaction/list' } ],
964         accesskey => 'enter',
965       ],
966     );
967   }
968 }
969
970 sub setup_list_all_action_bar {
971   my ($self, %params) = @_;
972
973   for my $bar ($::request->layout->get('actionbar')) {
974     $bar->add(
975       combobox => [
976         action => [ t8('Actions') ],
977         action => [
978           t8('Unlink bank transactions'),
979             submit => [ '#form', { action => 'BankTransaction/unlink_bank_transaction' } ],
980             checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
981             disabled  => $::instance_conf->get_payments_changeable ? t8('Cannot safely unlink bank transactions, please set the posting configuration for payments to unchangeable.') : undef,
982           ],
983         ],
984         action => [
985           t8('Filter'),
986           submit    => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
987         accesskey => 'enter',
988       ],
989     );
990   }
991 }
992
993 1;
994 __END__
995
996 =pod
997
998 =encoding utf8
999
1000 =head1 NAME
1001
1002 SL::Controller::BankTransaction - Posting payments to invoices from
1003 bank transactions imported earlier
1004
1005 =head1 FUNCTIONS
1006
1007 =over 4
1008
1009 =item C<save_single_bank_transaction %params>
1010
1011 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
1012 tries to post its amount to a certain number of invoices (parameter
1013 C<invoice_ids>, an array ref of database IDs to purchase or sales
1014 invoice objects).
1015
1016 This method handles already partly assigned bank transactions.
1017
1018 This method cannot handle already partly assigned bank transactions, i.e.
1019 a bank transaction that has a invoice_amount <> 0 but not the fully
1020 transaction amount (invoice_amount == amount).
1021
1022 If the amount of the bank transaction is higher than the sum of
1023 the assigned invoices (1 .. n) the bank transaction will only be
1024 partly assigned.
1025
1026 The whole function is wrapped in a database transaction. If an
1027 exception occurs the bank transaction is not posted at all. The same
1028 is true if the code detects an error during the execution, e.g. a bank
1029 transaction that's already been posted earlier. In both cases the
1030 database transaction will be rolled back.
1031
1032 If warnings but not errors occur the database transaction is still
1033 committed.
1034
1035 The return value is an error object or C<undef> if the function
1036 succeeded. The calling function will collect all warnings and errors
1037 and display them in a nicely formatted table if any occurred.
1038
1039 An error object is a hash reference containing the following members:
1040
1041 =over 2
1042
1043 =item * C<result> — can be either C<warning> or C<error>. Warnings are
1044 displayed slightly different than errors.
1045
1046 =item * C<message> — a human-readable message included in the list of
1047 errors meant as the description of why the problem happened
1048
1049 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
1050 that the function was called with
1051
1052 =item * C<bank_transaction> — the database object
1053 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
1054
1055 =item * C<invoices> — an array ref of the database objects (either
1056 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
1057 C<invoice_ids>
1058
1059 =back
1060
1061 =item C<action_unlink_bank_transaction>
1062
1063 Takes one or more bank transaction ID (as parameter C<form::ids>) and
1064 tries to revert all payment bookings including already cleared bookings.
1065
1066 This method won't undo payments that are in a closed period and assumes
1067 that payments are not manually changed, i.e. only imported payments.
1068
1069 GL-records will be deleted completely if a bank transaction was the source.
1070
1071 TODO: we still rely on linked_records for the check boxes
1072
1073 =back
1074
1075 =head1 AUTHOR
1076
1077 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
1078 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>
1079
1080 =cut