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