BankTransaction: Neue Funktion um Bankverbuchungen wieder rückgängig zu machen
[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   my (@warnings);
565
566   my $worker = sub {
567     my $bt_id                 = $data{bank_transaction_id};
568     my $sign                  = $bank_transaction->amount < 0 ? -1 : 1;
569     my $not_assigned_amount   = $bank_transaction->not_assigned_amount;
570     my $payment_received      = $bank_transaction->amount > 0;
571     my $payment_sent          = $bank_transaction->amount < 0;
572
573
574     foreach my $invoice_id (@{ $params{invoice_ids} }) {
575       my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
576       if (!$invoice) {
577         return {
578           %data,
579           result  => 'error',
580           message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
581         };
582       }
583       push @{ $data{invoices} }, $invoice;
584     }
585
586     if (   $payment_received
587         && any {    ( $_->is_sales && ($_->amount < 0))
588                  || (!$_->is_sales && ($_->amount > 0))
589                } @{ $data{invoices} }) {
590       return {
591         %data,
592         result  => 'error',
593         message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
594       };
595     }
596
597     if (   $payment_sent
598         && any {    ( $_->is_sales && ($_->amount > 0))
599                  || (!$_->is_sales && ($_->amount < 0) && ($_->invoice_type eq 'purchase_invoice'))
600                } @{ $data{invoices} }) {
601       return {
602         %data,
603         result  => 'error',
604         message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
605       };
606     }
607
608     my $max_invoices = scalar(@{ $data{invoices} });
609     my $n_invoices   = 0;
610
611     foreach my $invoice (@{ $data{invoices} }) {
612       my $source = ($data{sources} // [])->[$n_invoices];
613       my $memo   = ($data{memos}   // [])->[$n_invoices];
614
615       $n_invoices++ ;
616
617
618       if (!$not_assigned_amount && $invoice->open_amount) {
619         return {
620           %data,
621           result  => 'error',
622           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."),
623         };
624       }
625
626       my $payment_type;
627       if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
628         $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
629       } else {
630         $payment_type = 'without_skonto';
631       };
632     # pay invoice
633     # TODO rewrite this: really booked amount should be a return value of Payment.pm
634     # also this controller shouldnt care about how to calc skonto. we simply delegate the
635     # payment_type to the helper and get the corresponding bank_transaction values back
636
637     my $open_amount = ($payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto : $invoice->open_amount);
638     my $amount_for_booking = abs(($open_amount < $not_assigned_amount) ? $open_amount : $not_assigned_amount);
639     $amount_for_booking *= $sign;
640     $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking);
641
642     # ... and then pay the invoice
643     my @acc_ids = $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
644                           trans_id     => $invoice->id,
645                           amount       => ($open_amount < $not_assigned_amount) ? $open_amount : $not_assigned_amount,
646                           payment_type => $payment_type,
647                           source       => $source,
648                           memo         => $memo,
649                           transdate    => $bank_transaction->transdate->to_kivitendo);
650     # ... and record the origin via BankTransactionAccTrans
651     if (scalar(@acc_ids) != 2) {
652       return {
653         %data,
654         result  => 'error',
655         message => $::locale->text("Unable to book transactions for bank purpose #1", $bank_transaction->purpose),
656       };
657     }
658     foreach my $acc_trans_id (@acc_ids) {
659         my $id_type = $invoice->is_sales ? 'ar' : 'ap';
660         my  %props_acc = (
661           acc_trans_id        => $acc_trans_id,
662           bank_transaction_id => $bank_transaction->id,
663           $id_type            => $invoice->id,
664         );
665         SL::DB::BankTransactionAccTrans->new(%props_acc)->save;
666     }
667       # Record a record link from the bank transaction to the invoice
668       my %props = (
669         from_table => 'bank_transactions',
670         from_id    => $bt_id,
671         to_table   => $invoice->is_sales ? 'ar' : 'ap',
672         to_id      => $invoice->id,
673       );
674       SL::DB::RecordLink->new(%props)->save;
675
676       # "close" a sepa_export_item if it exists
677       # code duplicated in action_save_proposals!
678       # currently only works, if there is only exactly one open sepa_export_item
679       if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
680         if ( scalar @$seis == 1 ) {
681           # moved the execution and the check for sepa_export into a method,
682           # this isn't part of a transaction, though
683           $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
684         }
685       }
686
687     }
688     $bank_transaction->save;
689
690     # 'undef' means 'no error' here.
691     return undef;
692   };
693
694   my $error;
695   my $rez = $data{bank_transaction}->db->with_transaction(sub {
696     eval {
697       $error = $worker->();
698       1;
699
700     } or do {
701       $error = {
702         %data,
703         result  => 'error',
704         message => $@,
705       };
706     };
707
708     # Rollback Fehler nicht weiterreichen
709     # die if $error;
710     # aber einen rollback von hand
711     $::lxdebug->message(LXDebug->DEBUG2(),"finish worker with ". ($error ? $error->{result} : '-'));
712     $data{bank_transaction}->db->dbh->rollback if $error && $error->{result} eq 'error';
713   });
714
715   return grep { $_ } ($error, @warnings);
716 }
717 sub action_unlink_bank_transaction {
718   my ($self) = @_;
719
720   croak("No bank transaction ids") unless scalar @{ $::form->{ids}} > 0;
721
722   my $closedto = $::locale->parse_date_to_object($::instance_conf->get_closedto);
723   my $success_count;
724
725   foreach my $bt_id (@{ $::form->{ids}} )  {
726
727     my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
728     croak("No valid bank transaction found") unless (ref($bank_transaction)  eq 'SL::DB::BankTransaction');
729
730     # everything in one transaction
731     my $rez = $bank_transaction->db->with_transaction(sub {
732       # 1. remove all reconciliations (due to underlying trigger, this has to be the first step)
733       my $rec_links = SL::DB::Manager::ReconciliationLink->get_all(where => [ bank_transaction_id => $bt_id ]);
734       $_->delete for @{ $rec_links };
735
736       my %trans_ids;
737       foreach my $acc_trans_id_entry (@{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [bank_transaction_id => $bt_id ] )}) {
738
739         my $acc_trans = SL::DB::Manager::AccTransaction->get_all(where => [acc_trans_id => $acc_trans_id_entry->acc_trans_id]);
740         # check closedto for acc trans entries
741         croak t8('Cannot unlink payment for a closed period!') if (ref $closedto && grep { $_->transdate < $closedto } @{ $acc_trans } );
742
743         # save trans_id and type
744         die "no type" unless ($acc_trans_id_entry->ar_id || $acc_trans_id_entry->ap_id || $acc_trans_id_entry->gl_id);
745         $trans_ids{$acc_trans_id_entry->ar_id} = 'ar' if $acc_trans_id_entry->ar_id;
746         $trans_ids{$acc_trans_id_entry->ap_id} = 'ap' if $acc_trans_id_entry->ap_id;
747         $trans_ids{$acc_trans_id_entry->gl_id} = 'gl' if $acc_trans_id_entry->gl_id;
748
749         # 2. all good -> ready to delete acc_trans and bt_acc link
750         $acc_trans_id_entry->delete;
751         $_->delete for @{ $acc_trans };
752       }
753       # 3. update arap.paid (may not be 0, yet)
754       while (my ($trans_id, $type) = each %trans_ids) {
755         next if $type eq 'gl';
756         die ("invalid type") unless $type =~ m/^(ar|ap)$/;
757
758         # recalc and set paid via database query
759         my $query = qq|UPDATE $type SET paid =
760                         (SELECT COALESCE(abs(sum(amount)),0) FROM acc_trans
761                          WHERE trans_id = ?
762                          AND chart_link ilike '%paid%')|;
763
764         die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id) == -1);
765       }
766       # 4. and delete all (if any) record links
767       my $rl = SL::DB::Manager::RecordLink->delete_all(where => [ from_id => $bt_id, from_table => 'bank_transactions' ]);
768
769       # 5. finally reset  this bank transaction
770       $bank_transaction->invoice_amount(0);
771       $bank_transaction->cleared(0);
772       $bank_transaction->save;
773
774       1;
775
776     }) || die t8('error while unlinking payment #1 : ', $bank_transaction->purpose) . $bank_transaction->db->error . "\n";
777
778     $success_count++;
779   }
780
781   flash('ok', t8('#1 bank transaction bookings undone.', $success_count));
782   $self->action_list_all();
783 }
784 #
785 # filters
786 #
787
788 sub check_auth {
789   $::auth->assert('bank_transaction');
790 }
791
792 #
793 # helpers
794 #
795
796 sub make_filter_summary {
797   my ($self) = @_;
798
799   my $filter = $::form->{filter} || {};
800   my @filter_strings;
801
802   my @filters = (
803     [ $filter->{"transdate:date::ge"},      $::locale->text('Transdate')  . " " . $::locale->text('From Date') ],
804     [ $filter->{"transdate:date::le"},      $::locale->text('Transdate')  . " " . $::locale->text('To Date')   ],
805     [ $filter->{"valutadate:date::ge"},     $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
806     [ $filter->{"valutadate:date::le"},     $::locale->text('Valutadate') . " " . $::locale->text('To Date')   ],
807     [ $filter->{"amount:number"},           $::locale->text('Amount')                                          ],
808     [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account')                              ],
809   );
810
811   for (@filters) {
812     push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
813   }
814
815   $self->{filter_summary} = join ', ', @filter_strings;
816 }
817
818 sub prepare_report {
819   my ($self)      = @_;
820
821   my $callback    = $self->models->get_callback;
822
823   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
824   $self->{report} = $report;
825
826   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);
827   my @sortable    = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount                                  purpose local_account_number local_bank_code);
828
829   my %column_defs = (
830     ids                 => { raw_header_data => checkbox_tag("", id => "check_all", checkall  => "[data-checkall=1]"),
831                              'align'         => 'center',
832                              raw_data        => sub { if (@{ $_[0]->linked_invoices } && !(grep {ref ($_) eq 'SL::DB::GLTransaction' } @{ $_[0]->linked_invoices })) {
833                                                          checkbox_tag("ids[]", value => $_[0]->id, "data-checkall" => 1); } } },
834     transdate             => { sub   => sub { $_[0]->transdate_as_date } },
835     valutadate            => { sub   => sub { $_[0]->valutadate_as_date } },
836     remote_name           => { },
837     remote_account_number => { },
838     remote_bank_code      => { },
839     amount                => { sub   => sub { $_[0]->amount_as_number },
840                                align => 'right' },
841     invoice_amount        => { sub   => sub { $_[0]->invoice_amount_as_number },
842                                align => 'right' },
843     invoices              => { sub   => sub { my @invnumbers; for my $obj (@{ $_[0]->linked_invoices }) {
844                                                                 next unless $obj; push @invnumbers, $obj->invnumber } return \@invnumbers } },
845     currency              => { sub   => sub { $_[0]->currency->name } },
846     purpose               => { },
847     local_account_number  => { sub   => sub { $_[0]->local_bank_account->account_number } },
848     local_bank_code       => { sub   => sub { $_[0]->local_bank_account->bank_code } },
849     local_bank_name       => { sub   => sub { $_[0]->local_bank_account->name } },
850     id                    => {},
851   );
852
853   map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
854
855   $report->set_options(
856     std_column_visibility => 1,
857     controller_class      => 'BankTransaction',
858     output_format         => 'HTML',
859     top_info_text         => $::locale->text('Bank transactions'),
860     title                 => $::locale->text('Bank transactions'),
861     allow_pdf_export      => 1,
862     allow_csv_export      => 1,
863   );
864   $report->set_columns(%column_defs);
865   $report->set_column_order(@columns);
866   $report->set_export_options(qw(list_all filter));
867   $report->set_options_from_form;
868   $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
869   $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
870
871   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
872
873   $report->set_options(
874     raw_top_info_text     => $self->render('bank_transactions/report_top',    { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
875     raw_bottom_info_text  => $self->render('bank_transactions/report_bottom', { output => 0 }),
876   );
877 }
878
879 sub init_problems { [] }
880
881 sub init_models {
882   my ($self) = @_;
883
884   SL::Controller::Helper::GetModels->new(
885     controller => $self,
886     sorted     => {
887       _default => {
888         by  => 'transdate',
889         dir => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
890       },
891       id                    => t8('ID'),
892       transdate             => t8('Transdate'),
893       remote_name           => t8('Remote name'),
894       amount                => t8('Amount'),
895       invoice_amount        => t8('Assigned'),
896       invoices              => t8('Linked invoices'),
897       valutadate            => t8('Valutadate'),
898       remote_account_number => t8('Remote account number'),
899       remote_bank_code      => t8('Remote bank code'),
900       currency              => t8('Currency'),
901       purpose               => t8('Purpose'),
902       local_account_number  => t8('Local account number'),
903       local_bank_code       => t8('Local bank code'),
904       local_bank_name       => t8('Bank account'),
905     },
906     with_objects => [ 'local_bank_account', 'currency' ],
907   );
908 }
909
910 sub load_ap_record_template_url {
911   my ($self, $template) = @_;
912
913   return $self->url_for(
914     controller                           => 'ap.pl',
915     action                               => 'load_record_template',
916     id                                   => $template->id,
917     'form_defaults.amount_1'             => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
918     'form_defaults.transdate'            => $self->transaction->transdate_as_date,
919     'form_defaults.duedate'              => $self->transaction->transdate_as_date,
920     'form_defaults.no_payment_bookings'  => 1,
921     'form_defaults.paid_1_suggestion'    => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
922     'form_defaults.AP_paid_1_suggestion' => $self->transaction->local_bank_account->chart->accno,
923     'form_defaults.callback'             => $self->callback,
924   );
925 }
926
927 sub load_gl_record_template_url {
928   my ($self, $template) = @_;
929
930   return $self->url_for(
931     controller                           => 'gl.pl',
932     action                               => 'load_record_template',
933     id                                   => $template->id,
934     'form_defaults.amount_1'             => abs($self->transaction->not_assigned_amount), # always positive
935     'form_defaults.transdate'            => $self->transaction->transdate_as_date,
936     'form_defaults.callback'             => $self->callback,
937     'form_defaults.bt_id'                => $self->transaction->id,
938     'form_defaults.bt_chart_id'          => $self->transaction->local_bank_account->chart->id,
939   );
940 }
941
942 sub setup_search_action_bar {
943   my ($self, %params) = @_;
944
945   for my $bar ($::request->layout->get('actionbar')) {
946     $bar->add(
947       action => [
948         t8('Filter'),
949         submit    => [ '#search_form', { action => 'BankTransaction/list' } ],
950         accesskey => 'enter',
951       ],
952     );
953   }
954 }
955
956 sub setup_list_all_action_bar {
957   my ($self, %params) = @_;
958
959   for my $bar ($::request->layout->get('actionbar')) {
960     $bar->add(
961       combobox => [
962         action => [ t8('Actions') ],
963         action => [
964           t8('Unlink bank transactions'),
965             submit => [ '#form', { action => 'BankTransaction/unlink_bank_transaction' } ],
966             checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
967             disabled  => $::instance_conf->get_payments_changeable ? t8('Cannot safely unlink bank transactions, please set the posting configuration for payments to unchangeable.') : undef,
968           ],
969         ],
970         action => [
971           t8('Filter'),
972           submit    => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
973         accesskey => 'enter',
974       ],
975     );
976   }
977 }
978
979 1;
980 __END__
981
982 =pod
983
984 =encoding utf8
985
986 =head1 NAME
987
988 SL::Controller::BankTransaction - Posting payments to invoices from
989 bank transactions imported earlier
990
991 =head1 FUNCTIONS
992
993 =over 4
994
995 =item C<save_single_bank_transaction %params>
996
997 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
998 tries to post its amount to a certain number of invoices (parameter
999 C<invoice_ids>, an array ref of database IDs to purchase or sales
1000 invoice objects).
1001
1002 This method handles already partly assigned bank transactions.
1003
1004 This method cannot handle already partly assigned bank transactions, i.e.
1005 a bank transaction that has a invoice_amount <> 0 but not the fully
1006 transaction amount (invoice_amount == amount).
1007
1008 If the amount of the bank transaction is higher than the sum of
1009 the assigned invoices (1 .. n) the bank transaction will only be
1010 partly assigned.
1011
1012 The whole function is wrapped in a database transaction. If an
1013 exception occurs the bank transaction is not posted at all. The same
1014 is true if the code detects an error during the execution, e.g. a bank
1015 transaction that's already been posted earlier. In both cases the
1016 database transaction will be rolled back.
1017
1018 If warnings but not errors occur the database transaction is still
1019 committed.
1020
1021 The return value is an error object or C<undef> if the function
1022 succeeded. The calling function will collect all warnings and errors
1023 and display them in a nicely formatted table if any occurred.
1024
1025 An error object is a hash reference containing the following members:
1026
1027 =over 2
1028
1029 =item * C<result> — can be either C<warning> or C<error>. Warnings are
1030 displayed slightly different than errors.
1031
1032 =item * C<message> — a human-readable message included in the list of
1033 errors meant as the description of why the problem happened
1034
1035 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
1036 that the function was called with
1037
1038 =item * C<bank_transaction> — the database object
1039 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
1040
1041 =item * C<invoices> — an array ref of the database objects (either
1042 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
1043 C<invoice_ids>
1044
1045 =back
1046
1047 =item C<action_unlink_bank_transaction>
1048
1049 Takes one or more bank transaction ID (as parameter C<form::ids>) and
1050 tries to revert all payment bookings including already cleared bookings.
1051
1052 This method won't undo payments that are in a closed period and assumes
1053 that payments are not manually changed, i.e. only imported payments.
1054
1055 GL-records will be deleted completely if a bank transaction was the source.
1056
1057 TODO: we still rely on linked_records for the check boxes
1058
1059 =back
1060
1061 =head1 AUTHOR
1062
1063 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
1064 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>
1065
1066 =cut