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