Bankauszug: Unterzahlung mehrerer Rechnungen verhindern
[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         $invoice->pay_invoice(chart_id     => $bank_transaction->local_bank_account->chart_id,
528                               trans_id     => $invoice->id,
529                               amount       => $amount_of_transaction,
530                               payment_type => $payment_type,
531                               transdate    => $bank_transaction->transdate->to_kivitendo);
532         $bank_transaction->invoice_amount($bank_transaction->amount);
533         $amount_of_transaction = 0;
534       }
535
536       # Record a record link from the bank transaction to the invoice
537       my @props = (
538         from_table => 'bank_transactions',
539         from_id    => $bt_id,
540         to_table   => $invoice->is_sales ? 'ar' : 'ap',
541         to_id      => $invoice->id,
542       );
543
544       SL::DB::RecordLink->new(@props)->save;
545
546       # "close" a sepa_export_item if it exists
547       # code duplicated in action_save_proposals!
548       # currently only works, if there is only exactly one open sepa_export_item
549       if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
550         if ( scalar @$seis == 1 ) {
551           # moved the execution and the check for sepa_export into a method,
552           # this isn't part of a transaction, though
553           $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
554         }
555       }
556
557     }
558     $bank_transaction->save;
559
560     # 'undef' means 'no error' here.
561     return undef;
562   };
563
564   my $error;
565   my $rez = $data{bank_transaction}->db->with_transaction(sub {
566     eval {
567       $error = $worker->();
568       1;
569
570     } or do {
571       $error = {
572         %data,
573         result  => 'error',
574         message => $@,
575       };
576     };
577
578     die if $error;
579   });
580
581   return grep { $_ } ($error, @warnings);
582 }
583
584 sub action_save_proposals {
585   my ($self) = @_;
586
587   foreach my $bt_id (@{ $::form->{proposal_ids} }) {
588     my $bt = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
589
590     my $arap = SL::DB::Manager::Invoice->find_by(id => $::form->{"proposed_invoice_$bt_id"});
591     $arap    = SL::DB::Manager::PurchaseInvoice->find_by(id => $::form->{"proposed_invoice_$bt_id"}) if not defined $arap;
592
593     # check for existing record_link for that $bt and $arap
594     # do this before any changes to $bt are made
595     die t8("Bank transaction with id #1 has already been linked to #2.", $bt->id, $arap->displayable_name)
596       if _existing_record_link($bt, $arap);
597
598     #mark bt as booked
599     $bt->invoice_amount($bt->amount);
600     $bt->save;
601
602     #pay invoice
603     $arap->pay_invoice(chart_id  => $bt->local_bank_account->chart_id,
604                        trans_id  => $arap->id,
605                        amount    => $arap->amount,
606                        transdate => $bt->transdate->to_kivitendo);
607     $arap->save;
608
609     #create record link
610     my @props = (
611       from_table => 'bank_transactions',
612       from_id    => $bt_id,
613       to_table   => $arap->is_sales ? 'ar' : 'ap',
614       to_id      => $arap->id,
615     );
616
617     SL::DB::RecordLink->new(@props)->save;
618
619     # code duplicated in action_save_invoices!
620     # "close" a sepa_export_item if it exists
621     # currently only works, if there is only exactly one open sepa_export_item
622     if ( my $seis = $arap->find_sepa_export_items({ executed => 0 }) ) {
623       if ( scalar @$seis == 1 ) {
624         # moved the execution and the check for sepa_export into a method,
625         # this isn't part of a transaction, though
626         $seis->[0]->set_executed if $arap->id == $seis->[0]->arap_id;
627       }
628     }
629   }
630
631   flash('ok', t8('#1 proposal(s) saved.', scalar @{ $::form->{proposal_ids} }));
632
633   $self->action_list();
634 }
635
636 #
637 # filters
638 #
639
640 sub check_auth {
641   $::auth->assert('bank_transaction');
642 }
643
644 #
645 # helpers
646 #
647
648 sub make_filter_summary {
649   my ($self) = @_;
650
651   my $filter = $::form->{filter} || {};
652   my @filter_strings;
653
654   my @filters = (
655     [ $filter->{"transdate:date::ge"},      $::locale->text('Transdate')  . " " . $::locale->text('From Date') ],
656     [ $filter->{"transdate:date::le"},      $::locale->text('Transdate')  . " " . $::locale->text('To Date')   ],
657     [ $filter->{"valutadate:date::ge"},     $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
658     [ $filter->{"valutadate:date::le"},     $::locale->text('Valutadate') . " " . $::locale->text('To Date')   ],
659     [ $filter->{"amount:number"},           $::locale->text('Amount')                                          ],
660     [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account')                              ],
661   );
662
663   for (@filters) {
664     push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
665   }
666
667   $self->{filter_summary} = join ', ', @filter_strings;
668 }
669
670 sub prepare_report {
671   my ($self)      = @_;
672
673   my $callback    = $self->models->get_callback;
674
675   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
676   $self->{report} = $report;
677
678   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);
679   my @sortable    = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount                                  purpose local_account_number local_bank_code);
680
681   my %column_defs = (
682     transdate             => { sub   => sub { $_[0]->transdate_as_date } },
683     valutadate            => { sub   => sub { $_[0]->valutadate_as_date } },
684     remote_name           => { },
685     remote_account_number => { },
686     remote_bank_code      => { },
687     amount                => { sub   => sub { $_[0]->amount_as_number },
688                                align => 'right' },
689     invoice_amount        => { sub   => sub { $_[0]->invoice_amount_as_number },
690                                align => 'right' },
691     invoices              => { sub   => sub { $_[0]->linked_invoices } },
692     currency              => { sub   => sub { $_[0]->currency->name } },
693     purpose               => { },
694     local_account_number  => { sub   => sub { $_[0]->local_bank_account->account_number } },
695     local_bank_code       => { sub   => sub { $_[0]->local_bank_account->bank_code } },
696     local_bank_name       => { sub   => sub { $_[0]->local_bank_account->name } },
697     id                    => {},
698   );
699
700   map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
701
702   $report->set_options(
703     std_column_visibility => 1,
704     controller_class      => 'BankTransaction',
705     output_format         => 'HTML',
706     top_info_text         => $::locale->text('Bank transactions'),
707     title                 => $::locale->text('Bank transactions'),
708     allow_pdf_export      => 1,
709     allow_csv_export      => 1,
710   );
711   $report->set_columns(%column_defs);
712   $report->set_column_order(@columns);
713   $report->set_export_options(qw(list_all filter));
714   $report->set_options_from_form;
715   $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
716   $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
717
718   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
719
720   $report->set_options(
721     raw_top_info_text     => $self->render('bank_transactions/report_top',    { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
722     raw_bottom_info_text  => $self->render('bank_transactions/report_bottom', { output => 0 }),
723   );
724 }
725
726 sub _existing_record_link {
727   my ($bt, $invoice) = @_;
728
729   # check whether a record link from banktransaction $bt already exists to
730   # invoice $invoice, returns 1 if that is the case
731
732   die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
733
734   my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
735   my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ]  );
736
737   return @$linked_records ? 1 : 0;
738 };
739
740 sub init_problems { [] }
741
742 sub init_models {
743   my ($self) = @_;
744
745   SL::Controller::Helper::GetModels->new(
746     controller => $self,
747     sorted     => {
748       _default => {
749         by  => 'transdate',
750         dir => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
751       },
752       transdate             => t8('Transdate'),
753       remote_name           => t8('Remote name'),
754       amount                => t8('Amount'),
755       invoice_amount        => t8('Assigned'),
756       invoices              => t8('Linked invoices'),
757       valutadate            => t8('Valutadate'),
758       remote_account_number => t8('Remote account number'),
759       remote_bank_code      => t8('Remote bank code'),
760       currency              => t8('Currency'),
761       purpose               => t8('Purpose'),
762       local_account_number  => t8('Local account number'),
763       local_bank_code       => t8('Local bank code'),
764       local_bank_name       => t8('Bank account'),
765     },
766     with_objects => [ 'local_bank_account', 'currency' ],
767   );
768 }
769
770 1;
771 __END__
772
773 =pod
774
775 =encoding utf8
776
777 =head1 NAME
778
779 SL::Controller::BankTransaction - Posting payments to invoices from
780 bank transactions imported earlier
781
782 =head1 FUNCTIONS
783
784 =over 4
785
786 =item C<save_single_bank_transaction %params>
787
788 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
789 tries to post its amount to a certain number of invoices (parameter
790 C<invoice_ids>, an array ref of database IDs to purchase or sales
791 invoice objects).
792
793 The whole function is wrapped in a database transaction. If an
794 exception occurs the bank transaction is not posted at all. The same
795 is true if the code detects an error during the execution, e.g. a bank
796 transaction that's already been posted earlier. In both cases the
797 database transaction will be rolled back.
798
799 If warnings but not errors occur the database transaction is still
800 committed.
801
802 The return value is an error object or C<undef> if the function
803 succeeded. The calling function will collect all warnings and errors
804 and display them in a nicely formatted table if any occurred.
805
806 An error object is a hash reference containing the following members:
807
808 =over 2
809
810 =item * C<result> — can be either C<warning> or C<error>. Warnings are
811 displayed slightly different than errors.
812
813 =item * C<message> — a human-readable message included in the list of
814 errors meant as the description of why the problem happened
815
816 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
817 that the function was called with
818
819 =item * C<bank_transaction> — the database object
820 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
821
822 =item * C<invoices> — an array ref of the database objects (either
823 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
824 C<invoice_ids>
825
826 =back
827
828 =back
829
830 =head1 AUTHOR
831
832 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
833 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>
834
835 =cut