1 package SL::Controller::BankTransaction;
 
   3 # idee- möglichkeit bankdaten zu übernehmen in stammdaten
 
   4 # erst Kontenabgleich, um alle gl-Einträge wegzuhaben
 
   7 use parent qw(SL::Controller::Base);
 
   9 use SL::Controller::Helper::GetModels;
 
  10 use SL::Controller::Helper::ReportGenerator;
 
  11 use SL::ReportGenerator;
 
  13 use SL::DB::BankTransaction;
 
  14 use SL::Helper::Flash;
 
  15 use SL::Locale::String;
 
  18 use SL::DB::PurchaseInvoice;
 
  19 use SL::DB::RecordLink;
 
  22 use SL::DB::AccTransaction;
 
  24 use SL::DB::BankAccount;
 
  25 use SL::DB::RecordTemplate;
 
  26 use SL::DB::SepaExportItem;
 
  27 use SL::DBUtils qw(like);
 
  30 use List::MoreUtils qw(any);
 
  31 use List::Util qw(max);
 
  33 use Rose::Object::MakeMethods::Generic
 
  35   scalar                  => [ qw(callback transaction) ],
 
  36   'scalar --get_set_init' => [ qw(models problems) ],
 
  39 __PACKAGE__->run_before('check_auth');
 
  49   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
 
  51   $self->setup_search_action_bar;
 
  52   $self->render('bank_transactions/search',
 
  53                  BANK_ACCOUNTS => $bank_accounts);
 
  59   $self->make_filter_summary;
 
  60   $self->prepare_report;
 
  62   $self->setup_list_all_action_bar;
 
  63   $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
 
  69   if (!$::form->{filter}{bank_account}) {
 
  70     flash('error', t8('No bank account chosen!'));
 
  75   my $sort_by = $::form->{sort_by} || 'transdate';
 
  76   $sort_by = 'transdate' if $sort_by eq 'proposal';
 
  77   $sort_by .= $::form->{sort_dir} ? ' DESC' : ' ASC';
 
  79   my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
 
  80   my $todate   = $::locale->parse_date_to_object($::form->{filter}->{todate});
 
  81   $todate->add( days => 1 ) if $todate;
 
  84   push @where, (transdate => { ge => $fromdate }) if ($fromdate);
 
  85   push @where, (transdate => { lt => $todate })   if ($todate);
 
  86   my $bank_account = SL::DB::Manager::BankAccount->find_by( id => $::form->{filter}{bank_account} );
 
  87   # bank_transactions no younger than starting date,
 
  88   # including starting date (same search behaviour as fromdate)
 
  89   # but OPEN invoices to be matched may be from before
 
  90   if ( $bank_account->reconciliation_starting_date ) {
 
  91     push @where, (transdate => { ge => $bank_account->reconciliation_starting_date });
 
  94   my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
 
  95     with_objects => [ 'local_bank_account', 'currency' ],
 
  99       amount                => {ne => \'invoice_amount'},
 
 100       local_bank_account_id => $::form->{filter}{bank_account},
 
 104   # credit notes have a negative amount, treat differently
 
 105   my $all_open_ar_invoices = SL::DB::Manager::Invoice        ->get_all(where => [ or => [ amount => { gt => \'paid' },
 
 106                                                                                           and => [ type    => 'credit_note',
 
 107                                                                                                    amount  => { lt => \'paid' }
 
 111                                                                        with_objects => ['customer','payment_terms']);
 
 113   my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { ne => \'paid' }], with_objects => ['vendor'  ,'payment_terms']);
 
 114   my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $bank_account->chart_id ,
 
 115                                                                              'sepa_export.executed' => 0, 'sepa_export.closed' => 0 ], with_objects => ['sepa_export']);
 
 117   my @all_open_invoices;
 
 118   # filter out invoices with less than 1 cent outstanding
 
 119   push @all_open_invoices, map { $_->{is_ar}=1 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
 
 120   push @all_open_invoices, map { $_->{is_ar}=0 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
 
 123   # first collect sepa export items to open invoices
 
 124   foreach my $open_invoice (@all_open_invoices){
 
 125     $open_invoice->{realamount}  = $::form->format_amount(\%::myconfig,$open_invoice->amount,2);
 
 126     $open_invoice->{skonto_type} = 'without_skonto';
 
 127     foreach ( @{$all_open_sepa_export_items}) {
 
 128       if (($_->ap_id && $_->ap_id == $open_invoice->id) || ($_->ar_id && $_->ar_id == $open_invoice->id)) {
 
 129         my $factor                   = ($_->ar_id == $open_invoice->id ? 1 : -1);
 
 130         #$main::lxdebug->message(LXDebug->DEBUG2(),"sepa_exitem=".$_->id." for invoice ".$open_invoice->id." factor=".$factor);
 
 131         $open_invoice->{realamount}  = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);
 
 132         $open_invoice->{skonto_type} = $_->payment_type;
 
 133         $sepa_exports{$_->sepa_export_id} ||= { count => 0, is_ar => 0, amount => 0, proposed => 0, invoices => [], item => $_ };
 
 134         $sepa_exports{$_->sepa_export_id}->{count}++;
 
 135         $sepa_exports{$_->sepa_export_id}->{is_ar}++ if  $_->ar_id == $open_invoice->id;
 
 136         $sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor;
 
 137         push @{ $sepa_exports{$_->sepa_export_id}->{invoices} }, $open_invoice;
 
 142   # try to match each bank_transaction with each of the possible open invoices
 
 146   foreach my $bt (@{ $bank_transactions }) {
 
 147     ## 5 Stellen hinter dem Komma auf 2 Stellen reduzieren
 
 148     $bt->amount($bt->amount*1);
 
 149     $bt->invoice_amount($bt->invoice_amount*1);
 
 151     $bt->{proposals}    = [];
 
 152     $bt->{rule_matches} = [];
 
 154     $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
 
 156     if ( $bt->is_batch_transaction ) {
 
 157       foreach ( keys  %sepa_exports) {
 
 158         if ( abs(($sepa_exports{$_}->{amount} * 1) - ($bt->amount * 1)) < 0.01 ) {
 
 160           @{$bt->{proposals}} = @{$sepa_exports{$_}->{invoices}};
 
 161           $bt->{sepa_export_ok} = 1;
 
 162           $sepa_exports{$_}->{proposed}=1;
 
 163           push(@proposals, $bt);
 
 167       # batch transaction has no remotename !!
 
 169       next unless $bt->{remote_name};  # bank has no name, usually fees, use create invoice to assign
 
 172     # try to match the current $bt to each of the open_invoices, saving the
 
 173     # results of get_agreement_with_invoice in $open_invoice->{agreement} and
 
 174     # $open_invoice->{rule_matches}.
 
 176     # The values are overwritten each time a new bt is checked, so at the end
 
 177     # of each bt the likely results are filtered and those values are stored in
 
 178     # the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
 
 179     # score is stored in $bt->{agreement}
 
 181     foreach my $open_invoice (@all_open_invoices) {
 
 182       ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
 
 183       $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,
 
 184                                       $open_invoice->amount * ($open_invoice->{is_ar} ? 1 : -1), 2);
 
 188     my $min_agreement = 3; # suggestions must have at least this score
 
 190     my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
 
 192     # add open_invoices with highest agreement into array $bt->{proposals}
 
 193     if ( $max_agreement >= $min_agreement ) {
 
 194       $bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
 
 195       $bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
 
 197       # store the rule_matches in a separate array, so they can be displayed in template
 
 198       foreach ( @{ $bt->{proposals} } ) {
 
 199         push(@{$bt->{rule_matches}}, $_->{rule_matches});
 
 205   # separate filter for proposals (second tab, agreement >= 5 and exactly one match)
 
 206   # to qualify as a proposal there has to be
 
 207   # * agreement >= 5  TODO: make threshold configurable in configuration
 
 208   # * there must be only one exact match
 
 209   # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
 
 210   my $proposal_threshold = 5;
 
 211   my @otherproposals = grep {
 
 212        ($_->{agreement} >= $proposal_threshold)
 
 213     && (1 == scalar @{ $_->{proposals} })
 
 214     && (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01
 
 215                                           : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01)
 
 216   } @{ $bank_transactions };
 
 218   push @proposals, @otherproposals;
 
 220   # sort bank transaction proposals by quality (score) of proposal
 
 221   if ($::form->{sort_by} && $::form->{sort_by} eq 'proposal') {
 
 222     if ($::form->{sort_dir}) {
 
 223       $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ];
 
 225       $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ];
 
 229   # for testing with t/bank/banktransaction.t :
 
 230   if ( $::form->{dont_render_for_test} ) {
 
 231     return $bank_transactions;
 
 234   $::request->layout->add_javascripts("kivi.BankTransaction.js");
 
 235   $self->render('bank_transactions/list',
 
 236                 title             => t8('Bank transactions MT940'),
 
 237                 BANK_TRANSACTIONS => $bank_transactions,
 
 238                 PROPOSALS         => \@proposals,
 
 239                 bank_account      => $bank_account,
 
 240                 ui_tab            => scalar(@proposals) > 0?1:0,
 
 244 sub action_assign_invoice {
 
 247   $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
 
 249   $self->render('bank_transactions/assign_invoice',
 
 251                 title => t8('Assign invoice'),);
 
 254 sub action_create_invoice {
 
 256   my %myconfig = %main::myconfig;
 
 258   $self->transaction(SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id}));
 
 260   # This was dead code: We compared vendor.account_name with bank_transaction.iban.
 
 261   # This did never match (Kontonummer != IBAN). It's kivis 09/02 (2013) day
 
 262   # If refactored/improved, also consider that vendor.iban should be normalized
 
 263   # user may like to input strings like: 'AT 3333 3333 2222 1111' -> can be checked strictly
 
 264   # at Vendor code because we need the correct data for all sepa exports.
 
 266   my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(iban => $self->transaction->{remote_account_number});
 
 267   my $use_vendor_filter     = $self->transaction->{remote_account_number} && $vendor_of_transaction;
 
 269   my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
 
 270     where        => [ template_type => 'ap_transaction' ],
 
 271     with_objects => [ qw(employee vendor) ],
 
 273   my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
 
 274     query        => [ template_type => 'gl_transaction',
 
 275                       chart_id      => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
 
 277     with_objects => [ qw(employee record_template_items) ],
 
 280   # pre filter templates_ap, if we have a vendor match (IBAN eq IBAN) - show and allow user to edit this via gui!
 
 281   $templates_ap = [ grep { $_->vendor_id == $vendor_of_transaction->id } @{ $templates_ap } ] if $use_vendor_filter;
 
 283   $self->callback($self->url_for(
 
 285     'filter.bank_account' => $::form->{filter}->{bank_account},
 
 286     'filter.todate'       => $::form->{filter}->{todate},
 
 287     'filter.fromdate'     => $::form->{filter}->{fromdate},
 
 291     'bank_transactions/create_invoice',
 
 293     title        => t8('Create invoice'),
 
 294     TEMPLATES_GL => $use_vendor_filter && @{ $templates_ap } ? undef : $templates_gl,
 
 295     TEMPLATES_AP => $templates_ap,
 
 296     vendor_name  => $use_vendor_filter && @{ $templates_ap } ? $vendor_of_transaction->name : undef,
 
 300 sub action_ajax_payment_suggestion {
 
 303   # based on a BankTransaction ID and a Invoice or PurchaseInvoice ID passed via $::form,
 
 304   # create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
 
 305   # and return encoded as JSON
 
 307   my $bt      = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
 
 308   my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
 
 310   die unless $bt and $invoice;
 
 312   my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
 
 315   $html = $self->render(
 
 316     'bank_transactions/_payment_suggestion', { output => 0 },
 
 317     bt_id          => $::form->{bt_id},
 
 318     prop_id        => $::form->{prop_id},
 
 320     SELECT_OPTIONS => \@select_options,
 
 323   $self->render(\ SL::JSON::to_json( { 'html' => "$html" } ), { layout => 0, type => 'json', process => 0 });
 
 326 sub action_filter_templates {
 
 329   $self->{transaction}      = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
 
 331   my (@filter, @filter_ap);
 
 333   # filter => gl and ap | filter_ap = ap (i.e. vendorname)
 
 334   push @filter,    ('template_name' => { ilike => '%' . $::form->{template} . '%' })  if $::form->{template};
 
 335   push @filter,    ('reference'     => { ilike => '%' . $::form->{reference} . '%' }) if $::form->{reference};
 
 336   push @filter_ap, ('vendor.name'   => { ilike => '%' . $::form->{vendor} . '%' })    if $::form->{vendor};
 
 337   push @filter_ap, @filter;
 
 338   my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
 
 339     query        => [ template_type => 'gl_transaction',
 
 340                       chart_id      => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
 
 341                       (and => \@filter) x !!@filter
 
 343     with_objects => [ qw(employee record_template_items) ],
 
 346   my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
 
 347     where        => [ template_type => 'ap_transaction', (and => \@filter_ap) x !!@filter_ap ],
 
 348     with_objects => [ qw(employee vendor) ],
 
 350   $::form->{filter} //= {};
 
 352   $self->callback($self->url_for(
 
 354     'filter.bank_account' => $::form->{filter}->{bank_account},
 
 355     'filter.todate'       => $::form->{filter}->{todate},
 
 356     'filter.fromdate'     => $::form->{filter}->{fromdate},
 
 359   my $output  = $self->render(
 
 360     'bank_transactions/_template_list',
 
 362     TEMPLATES_AP => $templates_ap,
 
 363     TEMPLATES_GL => $templates_gl,
 
 366   $self->render(\to_json({ html => $output }), { type => 'json', process => 0 });
 
 369 sub action_ajax_add_list {
 
 372   my @where_sale     = (amount => { ne => \'paid' });
 
 373   my @where_purchase = (amount => { ne => \'paid' });
 
 375   if ($::form->{invnumber}) {
 
 376     push @where_sale,     (invnumber => { ilike => like($::form->{invnumber})});
 
 377     push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
 
 380   if ($::form->{amount}) {
 
 381     push @where_sale,     (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
 
 382     push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
 
 385   if ($::form->{vcnumber}) {
 
 386     push @where_sale,     ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
 
 387     push @where_purchase, ('vendor.vendornumber'     => { ilike => like($::form->{vcnumber})});
 
 390   if ($::form->{vcname}) {
 
 391     push @where_sale,     ('customer.name' => { ilike => like($::form->{vcname})});
 
 392     push @where_purchase, ('vendor.name'   => { ilike => like($::form->{vcname})});
 
 395   if ($::form->{transdatefrom}) {
 
 396     my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
 
 397     if ( ref($fromdate) eq 'DateTime' ) {
 
 398       push @where_sale,     ('transdate' => { ge => $fromdate});
 
 399       push @where_purchase, ('transdate' => { ge => $fromdate});
 
 403   if ($::form->{transdateto}) {
 
 404     my $todate = $::locale->parse_date_to_object($::form->{transdateto});
 
 405     if ( ref($todate) eq 'DateTime' ) {
 
 406       $todate->add(days => 1);
 
 407       push @where_sale,     ('transdate' => { lt => $todate});
 
 408       push @where_purchase, ('transdate' => { lt => $todate});
 
 412   my $all_open_ar_invoices = SL::DB::Manager::Invoice        ->get_all(where => \@where_sale,     with_objects => 'customer');
 
 413   my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
 
 415   my @all_open_invoices = @{ $all_open_ar_invoices };
 
 416   # add ap invoices, filtering out subcent open amounts
 
 417   push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
 
 419   @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
 
 421   my $output  = $self->render(
 
 422     'bank_transactions/add_list',
 
 424     INVOICES => \@all_open_invoices,
 
 427   my %result = ( count => 0, html => $output );
 
 429   $self->render(\to_json(\%result), { type => 'json', process => 0 });
 
 432 sub action_ajax_accept_invoices {
 
 435   my @selected_invoices;
 
 436   foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
 
 437     my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
 
 438     push @selected_invoices, $invoice_object;
 
 442     'bank_transactions/invoices',
 
 444     INVOICES => \@selected_invoices,
 
 445     bt_id    => $::form->{bt_id},
 
 452   return 0 if !$::form->{invoice_ids};
 
 454   my %invoice_hash = %{ delete $::form->{invoice_ids} };  # each key (the bt line with a bt_id) contains an array of invoice_ids
 
 456   # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
 
 469   # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
 
 471   #           '44' => [ '50', '51', 52' ]
 
 474   $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
 
 476   # a bank_transaction may be assigned to several invoices, i.e. a customer
 
 477   # might pay several open invoices with one transaction
 
 483   if ( $::form->{proposal_ids} ) {
 
 484     foreach (@{ $::form->{proposal_ids} }) {
 
 485       my  $bank_transaction_id = $_;
 
 486       my  $invoice_ids = $invoice_hash{$_};
 
 487       push @{ $self->problems }, $self->save_single_bank_transaction(
 
 488         bank_transaction_id => $bank_transaction_id,
 
 489         invoice_ids         => $invoice_ids,
 
 490         sources             => ($::form->{sources} // {})->{$_},
 
 491         memos               => ($::form->{memos}   // {})->{$_},
 
 493       $count += scalar( @{$invoice_ids} );
 
 496     while ( my ($bank_transaction_id, $invoice_ids) = each(%invoice_hash) ) {
 
 497       push @{ $self->problems }, $self->save_single_bank_transaction(
 
 498         bank_transaction_id => $bank_transaction_id,
 
 499         invoice_ids         => $invoice_ids,
 
 500         sources             => [  map { $::form->{"sources_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
 
 501         memos               => [  map { $::form->{"memos_${bank_transaction_id}_${_}"}   } @{ $invoice_ids } ],
 
 503       $count += scalar( @{$invoice_ids} );
 
 506   foreach (@{ $self->problems }) {
 
 507     $count-- if $_->{result} eq 'error';
 
 512 sub action_save_invoices {
 
 514   my $count = $self->save_invoices();
 
 516   flash('ok', t8('#1 invoice(s) saved.', $count));
 
 518   $self->action_list();
 
 521 sub action_save_proposals {
 
 524   if ( $::form->{proposal_ids} ) {
 
 525     my $propcount = scalar(@{ $::form->{proposal_ids} });
 
 526     if ( $propcount > 0 ) {
 
 527       my $count = $self->save_invoices();
 
 529       flash('ok', t8('#1 proposal(s) with #2 invoice(s) saved.',  $propcount, $count));
 
 532   $self->action_list();
 
 536 sub save_single_bank_transaction {
 
 537   my ($self, %params) = @_;
 
 541     bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
 
 545   if (!$data{bank_transaction}) {
 
 549       message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
 
 556     my $bt_id                 = $data{bank_transaction_id};
 
 557     my $bank_transaction      = $data{bank_transaction};
 
 558     my $sign                  = $bank_transaction->amount < 0 ? -1 : 1;
 
 559     my $amount_of_transaction = $sign * $bank_transaction->amount;
 
 560     my $payment_received      = $bank_transaction->amount > 0;
 
 561     my $payment_sent          = $bank_transaction->amount < 0;
 
 564     foreach my $invoice_id (@{ $params{invoice_ids} }) {
 
 565       my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
 
 570           message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
 
 573       push @{ $data{invoices} }, $invoice;
 
 576     if (   $payment_received
 
 577         && any {    ( $_->is_sales && ($_->amount < 0))
 
 578                  || (!$_->is_sales && ($_->amount > 0))
 
 579                } @{ $data{invoices} }) {
 
 583         message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
 
 588         && any {    ( $_->is_sales && ($_->amount > 0))
 
 589                  || (!$_->is_sales && ($_->amount < 0) && ($_->invoice_type eq 'purchase_invoice'))
 
 590                } @{ $data{invoices} }) {
 
 594         message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
 
 598     my $max_invoices = scalar(@{ $data{invoices} });
 
 601     foreach my $invoice (@{ $data{invoices} }) {
 
 602       my $source = ($data{sources} // [])->[$n_invoices];
 
 603       my $memo   = ($data{memos}   // [])->[$n_invoices];
 
 607       # Check if bank_transaction already has a link to the invoice, may only be linked once per invoice
 
 608       # This might be caused by the user reloading a page and resending the form
 
 609       if (_existing_record_link($bank_transaction, $invoice)) {
 
 613           message => $::locale->text("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name),
 
 617       if (!$amount_of_transaction && $invoice->open_amount) {
 
 621           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."),
 
 626       if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
 
 627         $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
 
 629         $payment_type = 'without_skonto';
 
 633       # pay invoice or go to the next bank transaction if the amount is not sufficiently high
 
 634       if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) {
 
 635         my $open_amount = ($payment_type eq 'with_skonto_pt'?$invoice->amount_less_skonto:$invoice->open_amount);
 
 636         # first calculate new bank transaction amount ...
 
 637         if ($invoice->is_sales) {
 
 638           $amount_of_transaction -= $sign * $open_amount;
 
 639           $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $open_amount);
 
 641           $amount_of_transaction += $sign * $open_amount;
 
 642           $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $open_amount);
 
 644         # ... and then pay the invoice
 
 645         $invoice->pay_invoice(chart_id     => $bank_transaction->local_bank_account->chart_id,
 
 646                               trans_id     => $invoice->id,
 
 647                               amount       => $open_amount,
 
 648                               payment_type => $payment_type,
 
 651                               transdate    => $bank_transaction->transdate->to_kivitendo);
 
 653         # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary
 
 655         # this catches credit_notes and negative sales invoices
 
 656         if ( $invoice->is_sales && $invoice->amount < 0 ) {
 
 657           # $invoice->open_amount     is negative for credit_notes
 
 658           # $bank_transaction->amount is negative for outgoing transactions
 
 659           # so $amount_of_transaction is negative but needs positive
 
 660           $amount_of_transaction *= -1;
 
 662         } elsif (!$invoice->is_sales && $invoice->invoice_type =~ m/ap_transaction|purchase_invoice/) {
 
 663           # $invoice->open_amount may be negative for ap_transaction but may be positiv for negativ ap_transaction
 
 664           # if $invoice->open_amount is negative $bank_transaction->amount is positve
 
 665           # if $invoice->open_amount is positive $bank_transaction->amount is negative
 
 666           # but amount of transaction is for both positive
 
 667           $amount_of_transaction *= -1 if $invoice->open_amount == - $amount_of_transaction;
 
 670         my $overpaid_amount = $amount_of_transaction - $invoice->open_amount;
 
 671         $invoice->pay_invoice(chart_id     => $bank_transaction->local_bank_account->chart_id,
 
 672                               trans_id     => $invoice->id,
 
 673                               amount       => $amount_of_transaction,
 
 674                               payment_type => $payment_type,
 
 677                               transdate    => $bank_transaction->transdate->to_kivitendo);
 
 678         $bank_transaction->invoice_amount($bank_transaction->amount);
 
 679         $amount_of_transaction = 0;
 
 681         if ($overpaid_amount >= 0.01) {
 
 685             message => $::locale->text('Invoice #1 was overpaid by #2.', $invoice->invnumber, $::form->format_amount(\%::myconfig, $overpaid_amount, 2)),
 
 689       # Record a record link from the bank transaction to the invoice
 
 691         from_table => 'bank_transactions',
 
 693         to_table   => $invoice->is_sales ? 'ar' : 'ap',
 
 694         to_id      => $invoice->id,
 
 697       SL::DB::RecordLink->new(@props)->save;
 
 699       # "close" a sepa_export_item if it exists
 
 700       # code duplicated in action_save_proposals!
 
 701       # currently only works, if there is only exactly one open sepa_export_item
 
 702       if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
 
 703         if ( scalar @$seis == 1 ) {
 
 704           # moved the execution and the check for sepa_export into a method,
 
 705           # this isn't part of a transaction, though
 
 706           $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
 
 711     $bank_transaction->save;
 
 713     # 'undef' means 'no error' here.
 
 718   my $rez = $data{bank_transaction}->db->with_transaction(sub {
 
 720       $error = $worker->();
 
 731     # Rollback Fehler nicht weiterreichen
 
 735   return grep { $_ } ($error, @warnings);
 
 743   $::auth->assert('bank_transaction');
 
 750 sub make_filter_summary {
 
 753   my $filter = $::form->{filter} || {};
 
 757     [ $filter->{"transdate:date::ge"},      $::locale->text('Transdate')  . " " . $::locale->text('From Date') ],
 
 758     [ $filter->{"transdate:date::le"},      $::locale->text('Transdate')  . " " . $::locale->text('To Date')   ],
 
 759     [ $filter->{"valutadate:date::ge"},     $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
 
 760     [ $filter->{"valutadate:date::le"},     $::locale->text('Valutadate') . " " . $::locale->text('To Date')   ],
 
 761     [ $filter->{"amount:number"},           $::locale->text('Amount')                                          ],
 
 762     [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account')                              ],
 
 766     push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
 
 769   $self->{filter_summary} = join ', ', @filter_strings;
 
 775   my $callback    = $self->models->get_callback;
 
 777   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
 
 778   $self->{report} = $report;
 
 780   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);
 
 781   my @sortable    = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount                                  purpose local_account_number local_bank_code);
 
 784     transdate             => { sub   => sub { $_[0]->transdate_as_date } },
 
 785     valutadate            => { sub   => sub { $_[0]->valutadate_as_date } },
 
 787     remote_account_number => { },
 
 788     remote_bank_code      => { },
 
 789     amount                => { sub   => sub { $_[0]->amount_as_number },
 
 791     invoice_amount        => { sub   => sub { $_[0]->invoice_amount_as_number },
 
 793     invoices              => { sub   => sub { $_[0]->linked_invoices } },
 
 794     currency              => { sub   => sub { $_[0]->currency->name } },
 
 796     local_account_number  => { sub   => sub { $_[0]->local_bank_account->account_number } },
 
 797     local_bank_code       => { sub   => sub { $_[0]->local_bank_account->bank_code } },
 
 798     local_bank_name       => { sub   => sub { $_[0]->local_bank_account->name } },
 
 802   map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
 
 804   $report->set_options(
 
 805     std_column_visibility => 1,
 
 806     controller_class      => 'BankTransaction',
 
 807     output_format         => 'HTML',
 
 808     top_info_text         => $::locale->text('Bank transactions'),
 
 809     title                 => $::locale->text('Bank transactions'),
 
 810     allow_pdf_export      => 1,
 
 811     allow_csv_export      => 1,
 
 813   $report->set_columns(%column_defs);
 
 814   $report->set_column_order(@columns);
 
 815   $report->set_export_options(qw(list_all filter));
 
 816   $report->set_options_from_form;
 
 817   $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
 
 818   $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
 
 820   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
 
 822   $report->set_options(
 
 823     raw_top_info_text     => $self->render('bank_transactions/report_top',    { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
 
 824     raw_bottom_info_text  => $self->render('bank_transactions/report_bottom', { output => 0 }),
 
 828 sub _existing_record_link {
 
 829   my ($bt, $invoice) = @_;
 
 831   # check whether a record link from banktransaction $bt already exists to
 
 832   # invoice $invoice, returns 1 if that is the case
 
 834   die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
 
 836   my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
 
 837   my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ]  );
 
 839   return @$linked_records ? 1 : 0;
 
 842 sub init_problems { [] }
 
 847   SL::Controller::Helper::GetModels->new(
 
 852         dir => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
 
 854       transdate             => t8('Transdate'),
 
 855       remote_name           => t8('Remote name'),
 
 856       amount                => t8('Amount'),
 
 857       invoice_amount        => t8('Assigned'),
 
 858       invoices              => t8('Linked invoices'),
 
 859       valutadate            => t8('Valutadate'),
 
 860       remote_account_number => t8('Remote account number'),
 
 861       remote_bank_code      => t8('Remote bank code'),
 
 862       currency              => t8('Currency'),
 
 863       purpose               => t8('Purpose'),
 
 864       local_account_number  => t8('Local account number'),
 
 865       local_bank_code       => t8('Local bank code'),
 
 866       local_bank_name       => t8('Bank account'),
 
 868     with_objects => [ 'local_bank_account', 'currency' ],
 
 872 sub load_ap_record_template_url {
 
 873   my ($self, $template) = @_;
 
 875   return $self->url_for(
 
 876     controller                           => 'ap.pl',
 
 877     action                               => 'load_record_template',
 
 879     'form_defaults.amount_1'             => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
 
 880     'form_defaults.transdate'            => $self->transaction->transdate_as_date,
 
 881     'form_defaults.duedate'              => $self->transaction->transdate_as_date,
 
 882     'form_defaults.no_payment_bookings'  => 1,
 
 883     'form_defaults.paid_1_suggestion'    => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
 
 884     'form_defaults.AP_paid_1_suggestion' => $self->transaction->local_bank_account->chart->accno,
 
 885     'form_defaults.callback'             => $self->callback,
 
 889 sub load_gl_record_template_url {
 
 890   my ($self, $template) = @_;
 
 892   return $self->url_for(
 
 893     controller                           => 'gl.pl',
 
 894     action                               => 'load_record_template',
 
 896     'form_defaults.amount_1'             => abs($self->transaction->amount), # always positive
 
 897     'form_defaults.transdate'            => $self->transaction->transdate_as_date,
 
 898     'form_defaults.callback'             => $self->callback,
 
 902 sub setup_search_action_bar {
 
 903   my ($self, %params) = @_;
 
 905   for my $bar ($::request->layout->get('actionbar')) {
 
 909         submit    => [ '#search_form', { action => 'BankTransaction/list' } ],
 
 910         accesskey => 'enter',
 
 916 sub setup_list_all_action_bar {
 
 917   my ($self, %params) = @_;
 
 919   for my $bar ($::request->layout->get('actionbar')) {
 
 923         submit    => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
 
 924         accesskey => 'enter',
 
 939 SL::Controller::BankTransaction - Posting payments to invoices from
 
 940 bank transactions imported earlier
 
 946 =item C<save_single_bank_transaction %params>
 
 948 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
 
 949 tries to post its amount to a certain number of invoices (parameter
 
 950 C<invoice_ids>, an array ref of database IDs to purchase or sales
 
 953 The whole function is wrapped in a database transaction. If an
 
 954 exception occurs the bank transaction is not posted at all. The same
 
 955 is true if the code detects an error during the execution, e.g. a bank
 
 956 transaction that's already been posted earlier. In both cases the
 
 957 database transaction will be rolled back.
 
 959 If warnings but not errors occur the database transaction is still
 
 962 The return value is an error object or C<undef> if the function
 
 963 succeeded. The calling function will collect all warnings and errors
 
 964 and display them in a nicely formatted table if any occurred.
 
 966 An error object is a hash reference containing the following members:
 
 970 =item * C<result> — can be either C<warning> or C<error>. Warnings are
 
 971 displayed slightly different than errors.
 
 973 =item * C<message> — a human-readable message included in the list of
 
 974 errors meant as the description of why the problem happened
 
 976 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
 
 977 that the function was called with
 
 979 =item * C<bank_transaction> — the database object
 
 980 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
 
 982 =item * C<invoices> — an array ref of the database objects (either
 
 983 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
 
 992 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
 
 993 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>