Merge branch 'bank-transaction-improvements'
[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::JSON;
21 use SL::DB::Chart;
22 use SL::DB::AccTransaction;
23 use SL::DB::Tax;
24 use SL::DB::Draft;
25 use SL::DB::BankAccount;
26 use SL::DBUtils qw(like);
27 use SL::Presenter;
28
29 use List::MoreUtils qw(any);
30 use List::Util qw(max);
31
32 use Rose::Object::MakeMethods::Generic
33 (
34   'scalar --get_set_init' => [ qw(models problems) ],
35 );
36
37 __PACKAGE__->run_before('check_auth');
38
39
40 #
41 # actions
42 #
43
44 sub action_search {
45   my ($self) = @_;
46
47   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
48
49   $self->render('bank_transactions/search',
50                  BANK_ACCOUNTS => $bank_accounts);
51 }
52
53 sub action_list_all {
54   my ($self) = @_;
55
56   $self->make_filter_summary;
57   $self->prepare_report;
58
59   $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
60 }
61
62 sub action_list {
63   my ($self) = @_;
64
65   if (!$::form->{filter}{bank_account}) {
66     flash('error', t8('No bank account chosen!'));
67     $self->action_search;
68     return;
69   }
70
71   my $sort_by = $::form->{sort_by} || 'transdate';
72   $sort_by = 'transdate' if $sort_by eq 'proposal';
73   $sort_by .= $::form->{sort_dir} ? ' DESC' : ' ASC';
74
75   my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
76   my $todate   = $::locale->parse_date_to_object($::form->{filter}->{todate});
77   $todate->add( days => 1 ) if $todate;
78
79   my @where = ();
80   push @where, (transdate => { ge => $fromdate }) if ($fromdate);
81   push @where, (transdate => { lt => $todate })   if ($todate);
82   my $bank_account = SL::DB::Manager::BankAccount->find_by( id => $::form->{filter}{bank_account} );
83   # bank_transactions no younger than starting date,
84   # including starting date (same search behaviour as fromdate)
85   # but OPEN invoices to be matched may be from before
86   if ( $bank_account->reconciliation_starting_date ) {
87     push @where, (transdate => { ge => $bank_account->reconciliation_starting_date });
88   };
89
90   my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
91     with_objects => [ 'local_bank_account', 'currency' ],
92     sort_by      => $sort_by,
93     limit        => 10000,
94     where        => [
95       amount                => {ne => \'invoice_amount'},
96       local_bank_account_id => $::form->{filter}{bank_account},
97       @where
98     ],
99   );
100
101   my $all_open_ar_invoices = SL::DB::Manager::Invoice        ->get_all(where => [amount => { gt => \'paid' }], with_objects => 'customer');
102   my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { gt => \'paid' }], with_objects => 'vendor');
103
104   my @all_open_invoices;
105   # filter out invoices with less than 1 cent outstanding
106   push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
107   push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
108
109   # try to match each bank_transaction with each of the possible open invoices
110   # by awarding points
111
112   foreach my $bt (@{ $bank_transactions }) {
113     next unless $bt->{remote_name};  # bank has no name, usually fees, use create invoice to assign
114
115     $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
116
117     # try to match the current $bt to each of the open_invoices, saving the
118     # results of get_agreement_with_invoice in $open_invoice->{agreement} and
119     # $open_invoice->{rule_matches}.
120
121     # The values are overwritten each time a new bt is checked, so at the end
122     # of each bt the likely results are filtered and those values are stored in
123     # the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
124     # score is stored in $bt->{agreement}
125
126     foreach my $open_invoice (@all_open_invoices){
127       ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
128     };
129
130     $bt->{proposals} = [];
131
132     my $agreement = 15;
133     my $min_agreement = 3; # suggestions must have at least this score
134
135     my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
136
137     # add open_invoices with highest agreement into array $bt->{proposals}
138     if ( $max_agreement >= $min_agreement ) {
139       $bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
140       $bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
141
142       # store the rule_matches in a separate array, so they can be displayed in template
143       foreach ( @{ $bt->{proposals} } ) {
144         push(@{$bt->{rule_matches}}, $_->{rule_matches});
145       };
146     };
147   }  # finished one bt
148   # finished all bt
149
150   # separate filter for proposals (second tab, agreement >= 5 and exactly one match)
151   # to qualify as a proposal there has to be
152   # * agreement >= 5  TODO: make threshold configurable in configuration
153   # * there must be only one exact match
154   # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
155   my $proposal_threshold = 5;
156   my @proposals = grep {
157        ($_->{agreement} >= $proposal_threshold)
158     && (1 == scalar @{ $_->{proposals} })
159     && (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01
160                                           : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01)
161   } @{ $bank_transactions };
162
163   # sort bank transaction proposals by quality (score) of proposal
164   $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 1;
165   $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 0;
166
167
168   $self->render('bank_transactions/list',
169                 title             => t8('Bank transactions MT940'),
170                 BANK_TRANSACTIONS => $bank_transactions,
171                 PROPOSALS         => \@proposals,
172                 bank_account      => $bank_account );
173 }
174
175 sub action_assign_invoice {
176   my ($self) = @_;
177
178   $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
179
180   $self->render('bank_transactions/assign_invoice',
181                 { layout => 0 },
182                 title => t8('Assign invoice'),);
183 }
184
185 sub action_create_invoice {
186   my ($self) = @_;
187   my %myconfig = %main::myconfig;
188
189   $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
190   my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
191
192   my $use_vendor_filter = $self->{transaction}->{remote_account_number} && $vendor_of_transaction;
193
194   my $drafts = SL::DB::Manager::Draft->get_all(where => [ module => 'ap'] , with_objects => 'employee');
195
196   my @filtered_drafts;
197
198   foreach my $draft ( @{ $drafts } ) {
199     my $draft_as_object = YAML::Load($draft->form);
200     my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
201     $draft->{vendor} = $vendor->name;
202     $draft->{vendor_id} = $vendor->id;
203     push @filtered_drafts, $draft;
204   }
205
206   #Filter drafts
207   @filtered_drafts = grep { $_->{vendor_id} == $vendor_of_transaction->id } @filtered_drafts if $use_vendor_filter;
208
209   my $all_vendors = SL::DB::Manager::Vendor->get_all();
210   my $callback    = $self->url_for(action                => 'list',
211                                    'filter.bank_account' => $::form->{filter}->{bank_account},
212                                    'filter.todate'       => $::form->{filter}->{todate},
213                                    'filter.fromdate'     => $::form->{filter}->{fromdate});
214
215   $self->render(
216     'bank_transactions/create_invoice',
217     { layout => 0 },
218     title       => t8('Create invoice'),
219     DRAFTS      => \@filtered_drafts,
220     vendor_id   => $use_vendor_filter ? $vendor_of_transaction->id   : undef,
221     vendor_name => $use_vendor_filter ? $vendor_of_transaction->name : undef,
222     ALL_VENDORS => $all_vendors,
223     limit       => $myconfig{vclimit},
224     callback    => $callback,
225   );
226 }
227
228 sub action_ajax_payment_suggestion {
229   my ($self) = @_;
230
231   # based on a BankTransaction ID and a Invoice or PurchaseInvoice ID passed via $::form,
232   # create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
233   # and return encoded as JSON
234
235   my $bt      = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
236   my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
237
238   die unless $bt and $invoice;
239
240   my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
241
242   my $html;
243   $html .= SL::Presenter->input_tag('invoice_ids.' . $::form->{bt_id} . '[]', $::form->{prop_id} , type => 'hidden');
244   $html .= SL::Presenter->escape(t8('Invno.')      . ': ' . $invoice->invnumber . ' ');
245   $html .= SL::Presenter->escape(t8('Open amount') . ': ' . $::form->format_amount(\%::myconfig, $invoice->open_amount, 2) . ' ');
246   $html .= SL::Presenter->select_tag('invoice_skontos.' . $::form->{bt_id} . '[]',
247                                      \@select_options,
248                                      value_key => 'payment_type',
249                                      title_key => 'display' )
250     if @select_options;
251   $html .= '<a href=# onclick="delete_invoice(' . $::form->{bt_id} . ',' . $::form->{prop_id} . ');">x</a>';
252   $html = SL::Presenter->html_tag('div', $html, id => $::form->{bt_id} . '.' . $::form->{prop_id});
253
254   $self->render(\ SL::JSON::to_json( { 'html' => $html } ), { layout => 0, type => 'json', process => 0 });
255 };
256
257 sub action_filter_drafts {
258   my ($self) = @_;
259
260   $self->{transaction}      = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
261   my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
262
263   my $drafts                = SL::DB::Manager::Draft->get_all(with_objects => 'employee');
264
265   my @filtered_drafts;
266
267   foreach my $draft ( @{ $drafts } ) {
268     my $draft_as_object = YAML::Load($draft->form);
269     next unless $draft_as_object->{vendor_id};  # we cannot filter for vendor name, if this is a gl draft
270
271     my $vendor          = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
272     $draft->{vendor}    = $vendor->name;
273     $draft->{vendor_id} = $vendor->id;
274
275     push @filtered_drafts, $draft;
276   }
277
278   my $vendor_name = $::form->{vendor};
279   my $vendor_id   = $::form->{vendor_id};
280
281   #Filter drafts
282   @filtered_drafts = grep { $_->{vendor_id} == $vendor_id      } @filtered_drafts if $vendor_id;
283   @filtered_drafts = grep { $_->{vendor}    =~ /$vendor_name/i } @filtered_drafts if $vendor_name;
284
285   my $output  = $self->render(
286     'bank_transactions/filter_drafts',
287     { output => 0 },
288     DRAFTS => \@filtered_drafts,
289   );
290
291   my %result = ( count => 0, html => $output );
292
293   $self->render(\to_json(\%result), { type => 'json', process => 0 });
294 }
295
296 sub action_ajax_add_list {
297   my ($self) = @_;
298
299   my @where_sale     = (amount => { ne => \'paid' });
300   my @where_purchase = (amount => { ne => \'paid' });
301
302   if ($::form->{invnumber}) {
303     push @where_sale,     (invnumber => { ilike => like($::form->{invnumber})});
304     push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
305   }
306
307   if ($::form->{amount}) {
308     push @where_sale,     (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
309     push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
310   }
311
312   if ($::form->{vcnumber}) {
313     push @where_sale,     ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
314     push @where_purchase, ('vendor.vendornumber'     => { ilike => like($::form->{vcnumber})});
315   }
316
317   if ($::form->{vcname}) {
318     push @where_sale,     ('customer.name' => { ilike => like($::form->{vcname})});
319     push @where_purchase, ('vendor.name'   => { ilike => like($::form->{vcname})});
320   }
321
322   if ($::form->{transdatefrom}) {
323     my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
324     if ( ref($fromdate) eq 'DateTime' ) {
325       push @where_sale,     ('transdate' => { ge => $fromdate});
326       push @where_purchase, ('transdate' => { ge => $fromdate});
327     };
328   }
329
330   if ($::form->{transdateto}) {
331     my $todate = $::locale->parse_date_to_object($::form->{transdateto});
332     if ( ref($todate) eq 'DateTime' ) {
333       $todate->add(days => 1);
334       push @where_sale,     ('transdate' => { lt => $todate});
335       push @where_purchase, ('transdate' => { lt => $todate});
336     };
337   }
338
339   my $all_open_ar_invoices = SL::DB::Manager::Invoice        ->get_all(where => \@where_sale,     with_objects => 'customer');
340   my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
341
342   my @all_open_invoices = @{ $all_open_ar_invoices };
343   # add ap invoices, filtering out subcent open amounts
344   push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
345
346   @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
347
348   my $output  = $self->render(
349     'bank_transactions/add_list',
350     { output => 0 },
351     INVOICES => \@all_open_invoices,
352   );
353
354   my %result = ( count => 0, html => $output );
355
356   $self->render(\to_json(\%result), { type => 'json', process => 0 });
357 }
358
359 sub action_ajax_accept_invoices {
360   my ($self) = @_;
361
362   my @selected_invoices;
363   foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
364     my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
365     push @selected_invoices, $invoice_object;
366   }
367
368   $self->render(
369     'bank_transactions/invoices',
370     { layout => 0 },
371     INVOICES => \@selected_invoices,
372     bt_id    => $::form->{bt_id},
373   );
374 }
375
376 sub action_save_invoices {
377   my ($self) = @_;
378
379   my $invoice_hash = delete $::form->{invoice_ids}; # each key (the bt line with a bt_id) contains an array of invoice_ids
380
381   # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
382   # $invoice_hash = {
383   #         '55' => [
384   #                 '74'
385   #               ],
386   #         '54' => [
387   #                 '74'
388   #               ],
389   #         '56' => [
390   #                 '74'
391   #               ]
392   #       };
393   #
394   # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
395   # $invoice_hash = {
396   #           '44' => [ '50', '51', 52' ]
397   #         };
398
399   $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
400
401   # a bank_transaction may be assigned to several invoices, i.e. a customer
402   # might pay several open invoices with one transaction
403
404   $self->problems([]);
405
406   while ( my ($bank_transaction_id, $invoice_ids) = each(%$invoice_hash) ) {
407     push @{ $self->problems }, $self->save_single_bank_transaction(
408       bank_transaction_id => $bank_transaction_id,
409       invoice_ids         => $invoice_ids,
410     );
411   }
412
413   $self->action_list();
414 }
415
416 sub save_single_bank_transaction {
417   my ($self, %params) = @_;
418
419   my %data = (
420     %params,
421     bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
422     invoices         => [],
423   );
424
425   if (!$data{bank_transaction}) {
426     return {
427       %data,
428       result => 'error',
429       message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
430     };
431   }
432
433   my (@warnings);
434
435   my $worker = sub {
436     my $bt_id                 = $data{bank_transaction_id};
437     my $bank_transaction      = $data{bank_transaction};
438     my $sign                  = $bank_transaction->amount < 0 ? -1 : 1;
439     my $amount_of_transaction = $sign * $bank_transaction->amount;
440     my $payment_received      = $bank_transaction->amount > 0;
441     my $payment_sent          = $bank_transaction->amount < 0;
442
443     foreach my $invoice_id (@{ $params{invoice_ids} }) {
444       my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
445       if (!$invoice) {
446         return {
447           %data,
448           result  => 'error',
449           message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
450         };
451       }
452
453       push @{ $data{invoices} }, $invoice;
454     }
455
456     if (   $payment_received
457         && any {    ( $_->is_sales && ($_->amount < 0))
458                  || (!$_->is_sales && ($_->amount > 0))
459                } @{ $data{invoices} }) {
460       return {
461         %data,
462         result  => 'error',
463         message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
464       };
465     }
466
467     if (   $payment_sent
468         && any {    ( $_->is_sales && ($_->amount > 0))
469                  || (!$_->is_sales && ($_->amount < 0))
470                } @{ $data{invoices} }) {
471       return {
472         %data,
473         result  => 'error',
474         message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
475       };
476     }
477
478     my $max_invoices = scalar(@{ $data{invoices} });
479     my $n_invoices   = 0;
480
481     foreach my $invoice (@{ $data{invoices} }) {
482
483       $n_invoices++ ;
484
485       # Check if bank_transaction already has a link to the invoice, may only be linked once per invoice
486       # This might be caused by the user reloading a page and resending the form
487       if (_existing_record_link($bank_transaction, $invoice)) {
488         return {
489           %data,
490           result  => 'error',
491           message => $::locale->text("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name),
492         };
493       }
494
495       if (!$amount_of_transaction && $invoice->open_amount) {
496         return {
497           %data,
498           result  => 'error',
499           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."),
500         };
501       }
502
503       my $payment_type;
504       if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
505         $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
506       } else {
507         $payment_type = 'without_skonto';
508       };
509
510       # pay invoice or go to the next bank transaction if the amount is not sufficiently high
511       if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) {
512         # first calculate new bank transaction amount ...
513         if ($invoice->is_sales) {
514           $amount_of_transaction -= $sign * $invoice->open_amount;
515           $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $invoice->open_amount);
516         } else {
517           $amount_of_transaction += $sign * $invoice->open_amount;
518           $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $invoice->open_amount);
519         }
520         # ... and then pay the invoice
521         $invoice->pay_invoice(chart_id     => $bank_transaction->local_bank_account->chart_id,
522                               trans_id     => $invoice->id,
523                               amount       => $invoice->open_amount,
524                               payment_type => $payment_type,
525                               transdate    => $bank_transaction->transdate->to_kivitendo);
526       } else { # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary
527         my $overpaid_amount = $amount_of_transaction - $invoice->open_amount;
528         $invoice->pay_invoice(chart_id     => $bank_transaction->local_bank_account->chart_id,
529                               trans_id     => $invoice->id,
530                               amount       => $amount_of_transaction,
531                               payment_type => $payment_type,
532                               transdate    => $bank_transaction->transdate->to_kivitendo);
533         $bank_transaction->invoice_amount($bank_transaction->amount);
534         $amount_of_transaction = 0;
535
536         push @warnings, {
537           %data,
538           result  => 'warning',
539           message => $::locale->text('Invoice #1 was overpaid by #2.', $invoice->invnumber, $::form->format_amount(\%::myconfig, $overpaid_amount, 2)),
540         };
541       }
542
543       # Record a record link from the bank transaction to the invoice
544       my @props = (
545         from_table => 'bank_transactions',
546         from_id    => $bt_id,
547         to_table   => $invoice->is_sales ? 'ar' : 'ap',
548         to_id      => $invoice->id,
549       );
550
551       SL::DB::RecordLink->new(@props)->save;
552
553       # "close" a sepa_export_item if it exists
554       # code duplicated in action_save_proposals!
555       # currently only works, if there is only exactly one open sepa_export_item
556       if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
557         if ( scalar @$seis == 1 ) {
558           # moved the execution and the check for sepa_export into a method,
559           # this isn't part of a transaction, though
560           $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
561         }
562       }
563
564     }
565     $bank_transaction->save;
566
567     # 'undef' means 'no error' here.
568     return undef;
569   };
570
571   my $error;
572   my $rez = $data{bank_transaction}->db->with_transaction(sub {
573     eval {
574       $error = $worker->();
575       1;
576
577     } or do {
578       $error = {
579         %data,
580         result  => 'error',
581         message => $@,
582       };
583     };
584
585     die if $error;
586   });
587
588   return grep { $_ } ($error, @warnings);
589 }
590
591 sub action_save_proposals {
592   my ($self) = @_;
593
594   foreach my $bt_id (@{ $::form->{proposal_ids} }) {
595     my $bt = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
596
597     my $arap = SL::DB::Manager::Invoice->find_by(id => $::form->{"proposed_invoice_$bt_id"});
598     $arap    = SL::DB::Manager::PurchaseInvoice->find_by(id => $::form->{"proposed_invoice_$bt_id"}) if not defined $arap;
599
600     # check for existing record_link for that $bt and $arap
601     # do this before any changes to $bt are made
602     die t8("Bank transaction with id #1 has already been linked to #2.", $bt->id, $arap->displayable_name)
603       if _existing_record_link($bt, $arap);
604
605     #mark bt as booked
606     $bt->invoice_amount($bt->amount);
607     $bt->save;
608
609     #pay invoice
610     $arap->pay_invoice(chart_id  => $bt->local_bank_account->chart_id,
611                        trans_id  => $arap->id,
612                        amount    => $arap->amount,
613                        transdate => $bt->transdate->to_kivitendo);
614     $arap->save;
615
616     #create record link
617     my @props = (
618       from_table => 'bank_transactions',
619       from_id    => $bt_id,
620       to_table   => $arap->is_sales ? 'ar' : 'ap',
621       to_id      => $arap->id,
622     );
623
624     SL::DB::RecordLink->new(@props)->save;
625
626     # code duplicated in action_save_invoices!
627     # "close" a sepa_export_item if it exists
628     # currently only works, if there is only exactly one open sepa_export_item
629     if ( my $seis = $arap->find_sepa_export_items({ executed => 0 }) ) {
630       if ( scalar @$seis == 1 ) {
631         # moved the execution and the check for sepa_export into a method,
632         # this isn't part of a transaction, though
633         $seis->[0]->set_executed if $arap->id == $seis->[0]->arap_id;
634       }
635     }
636   }
637
638   flash('ok', t8('#1 proposal(s) saved.', scalar @{ $::form->{proposal_ids} }));
639
640   $self->action_list();
641 }
642
643 #
644 # filters
645 #
646
647 sub check_auth {
648   $::auth->assert('bank_transaction');
649 }
650
651 #
652 # helpers
653 #
654
655 sub make_filter_summary {
656   my ($self) = @_;
657
658   my $filter = $::form->{filter} || {};
659   my @filter_strings;
660
661   my @filters = (
662     [ $filter->{"transdate:date::ge"},      $::locale->text('Transdate')  . " " . $::locale->text('From Date') ],
663     [ $filter->{"transdate:date::le"},      $::locale->text('Transdate')  . " " . $::locale->text('To Date')   ],
664     [ $filter->{"valutadate:date::ge"},     $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
665     [ $filter->{"valutadate:date::le"},     $::locale->text('Valutadate') . " " . $::locale->text('To Date')   ],
666     [ $filter->{"amount:number"},           $::locale->text('Amount')                                          ],
667     [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account')                              ],
668   );
669
670   for (@filters) {
671     push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
672   }
673
674   $self->{filter_summary} = join ', ', @filter_strings;
675 }
676
677 sub prepare_report {
678   my ($self)      = @_;
679
680   my $callback    = $self->models->get_callback;
681
682   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
683   $self->{report} = $report;
684
685   my @columns     = qw(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);
686   my @sortable    = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount                                  purpose local_account_number local_bank_code);
687
688   my %column_defs = (
689     transdate             => { sub   => sub { $_[0]->transdate_as_date } },
690     valutadate            => { sub   => sub { $_[0]->valutadate_as_date } },
691     remote_name           => { },
692     remote_account_number => { },
693     remote_bank_code      => { },
694     amount                => { sub   => sub { $_[0]->amount_as_number },
695                                align => 'right' },
696     invoice_amount        => { sub   => sub { $_[0]->invoice_amount_as_number },
697                                align => 'right' },
698     invoices              => { sub   => sub { $_[0]->linked_invoices } },
699     currency              => { sub   => sub { $_[0]->currency->name } },
700     purpose               => { },
701     local_account_number  => { sub   => sub { $_[0]->local_bank_account->account_number } },
702     local_bank_code       => { sub   => sub { $_[0]->local_bank_account->bank_code } },
703     local_bank_name       => { sub   => sub { $_[0]->local_bank_account->name } },
704     id                    => {},
705   );
706
707   map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
708
709   $report->set_options(
710     std_column_visibility => 1,
711     controller_class      => 'BankTransaction',
712     output_format         => 'HTML',
713     top_info_text         => $::locale->text('Bank transactions'),
714     title                 => $::locale->text('Bank transactions'),
715     allow_pdf_export      => 1,
716     allow_csv_export      => 1,
717   );
718   $report->set_columns(%column_defs);
719   $report->set_column_order(@columns);
720   $report->set_export_options(qw(list_all filter));
721   $report->set_options_from_form;
722   $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
723   $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
724
725   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
726
727   $report->set_options(
728     raw_top_info_text     => $self->render('bank_transactions/report_top',    { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
729     raw_bottom_info_text  => $self->render('bank_transactions/report_bottom', { output => 0 }),
730   );
731 }
732
733 sub _existing_record_link {
734   my ($bt, $invoice) = @_;
735
736   # check whether a record link from banktransaction $bt already exists to
737   # invoice $invoice, returns 1 if that is the case
738
739   die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
740
741   my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
742   my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ]  );
743
744   return @$linked_records ? 1 : 0;
745 };
746
747 sub init_problems { [] }
748
749 sub init_models {
750   my ($self) = @_;
751
752   SL::Controller::Helper::GetModels->new(
753     controller => $self,
754     sorted     => {
755       _default => {
756         by  => 'transdate',
757         dir => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
758       },
759       transdate             => t8('Transdate'),
760       remote_name           => t8('Remote name'),
761       amount                => t8('Amount'),
762       invoice_amount        => t8('Assigned'),
763       invoices              => t8('Linked invoices'),
764       valutadate            => t8('Valutadate'),
765       remote_account_number => t8('Remote account number'),
766       remote_bank_code      => t8('Remote bank code'),
767       currency              => t8('Currency'),
768       purpose               => t8('Purpose'),
769       local_account_number  => t8('Local account number'),
770       local_bank_code       => t8('Local bank code'),
771       local_bank_name       => t8('Bank account'),
772     },
773     with_objects => [ 'local_bank_account', 'currency' ],
774   );
775 }
776
777 1;
778 __END__
779
780 =pod
781
782 =encoding utf8
783
784 =head1 NAME
785
786 SL::Controller::BankTransaction - Posting payments to invoices from
787 bank transactions imported earlier
788
789 =head1 FUNCTIONS
790
791 =over 4
792
793 =item C<save_single_bank_transaction %params>
794
795 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
796 tries to post its amount to a certain number of invoices (parameter
797 C<invoice_ids>, an array ref of database IDs to purchase or sales
798 invoice objects).
799
800 The whole function is wrapped in a database transaction. If an
801 exception occurs the bank transaction is not posted at all. The same
802 is true if the code detects an error during the execution, e.g. a bank
803 transaction that's already been posted earlier. In both cases the
804 database transaction will be rolled back.
805
806 If warnings but not errors occur the database transaction is still
807 committed.
808
809 The return value is an error object or C<undef> if the function
810 succeeded. The calling function will collect all warnings and errors
811 and display them in a nicely formatted table if any occurred.
812
813 An error object is a hash reference containing the following members:
814
815 =over 2
816
817 =item * C<result> — can be either C<warning> or C<error>. Warnings are
818 displayed slightly different than errors.
819
820 =item * C<message> — a human-readable message included in the list of
821 errors meant as the description of why the problem happened
822
823 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
824 that the function was called with
825
826 =item * C<bank_transaction> — the database object
827 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
828
829 =item * C<invoices> — an array ref of the database objects (either
830 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
831 C<invoice_ids>
832
833 =back
834
835 =back
836
837 =head1 AUTHOR
838
839 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
840 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>
841
842 =cut