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;
 
  25 use SL::DB::BankAccount;
 
  26 use SL::DBUtils qw(like);
 
  29 use List::MoreUtils qw(any);
 
  30 use List::Util qw(max);
 
  32 use Rose::Object::MakeMethods::Generic
 
  34   'scalar --get_set_init' => [ qw(models problems) ],
 
  37 __PACKAGE__->run_before('check_auth');
 
  47   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
 
  49   $self->render('bank_transactions/search',
 
  50                  BANK_ACCOUNTS => $bank_accounts);
 
  56   $self->make_filter_summary;
 
  57   $self->prepare_report;
 
  59   $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
 
  65   if (!$::form->{filter}{bank_account}) {
 
  66     flash('error', t8('No bank account chosen!'));
 
  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';
 
  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;
 
  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 });
 
  90   my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
 
  91     with_objects => [ 'local_bank_account', 'currency' ],
 
  95       amount                => {ne => \'invoice_amount'},
 
  96       local_bank_account_id => $::form->{filter}{bank_account},
 
 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');
 
 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 };
 
 109   # try to match each bank_transaction with each of the possible open invoices
 
 112   foreach my $bt (@{ $bank_transactions }) {
 
 113     next unless $bt->{remote_name};  # bank has no name, usually fees, use create invoice to assign
 
 115     $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
 
 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}.
 
 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}
 
 126     foreach my $open_invoice (@all_open_invoices){
 
 127       ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
 
 130     $bt->{proposals} = [];
 
 133     my $min_agreement = 3; # suggestions must have at least this score
 
 135     my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
 
 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 : '';
 
 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});
 
 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 };
 
 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;
 
 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 );
 
 175 sub action_assign_invoice {
 
 178   $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
 
 180   $self->render('bank_transactions/assign_invoice',
 
 182                 title => t8('Assign invoice'),);
 
 185 sub action_create_invoice {
 
 187   my %myconfig = %main::myconfig;
 
 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});
 
 192   my $use_vendor_filter = $self->{transaction}->{remote_account_number} && $vendor_of_transaction;
 
 194   my $drafts = SL::DB::Manager::Draft->get_all(where => [ module => 'ap'] , with_objects => 'employee');
 
 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;
 
 207   @filtered_drafts = grep { $_->{vendor_id} == $vendor_of_transaction->id } @filtered_drafts if $use_vendor_filter;
 
 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});
 
 216     'bank_transactions/create_invoice',
 
 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,
 
 228 sub action_ajax_payment_suggestion {
 
 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
 
 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} );
 
 238   die unless $bt and $invoice;
 
 240   my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
 
 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} . '[]',
 
 248                                      value_key => 'payment_type',
 
 249                                      title_key => 'display' )
 
 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});
 
 254   $self->render(\ SL::JSON::to_json( { 'html' => $html } ), { layout => 0, type => 'json', process => 0 });
 
 257 sub action_filter_drafts {
 
 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});
 
 263   my $drafts                = SL::DB::Manager::Draft->get_all(with_objects => 'employee');
 
 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
 
 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;
 
 275     push @filtered_drafts, $draft;
 
 278   my $vendor_name = $::form->{vendor};
 
 279   my $vendor_id   = $::form->{vendor_id};
 
 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;
 
 285   my $output  = $self->render(
 
 286     'bank_transactions/filter_drafts',
 
 288     DRAFTS => \@filtered_drafts,
 
 291   my %result = ( count => 0, html => $output );
 
 293   $self->render(\to_json(\%result), { type => 'json', process => 0 });
 
 296 sub action_ajax_add_list {
 
 299   my @where_sale     = (amount => { ne => \'paid' });
 
 300   my @where_purchase = (amount => { ne => \'paid' });
 
 302   if ($::form->{invnumber}) {
 
 303     push @where_sale,     (invnumber => { ilike => like($::form->{invnumber})});
 
 304     push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
 
 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}));
 
 312   if ($::form->{vcnumber}) {
 
 313     push @where_sale,     ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
 
 314     push @where_purchase, ('vendor.vendornumber'     => { ilike => like($::form->{vcnumber})});
 
 317   if ($::form->{vcname}) {
 
 318     push @where_sale,     ('customer.name' => { ilike => like($::form->{vcname})});
 
 319     push @where_purchase, ('vendor.name'   => { ilike => like($::form->{vcname})});
 
 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});
 
 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});
 
 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');
 
 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 };
 
 346   @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
 
 348   my $output  = $self->render(
 
 349     'bank_transactions/add_list',
 
 351     INVOICES => \@all_open_invoices,
 
 354   my %result = ( count => 0, html => $output );
 
 356   $self->render(\to_json(\%result), { type => 'json', process => 0 });
 
 359 sub action_ajax_accept_invoices {
 
 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;
 
 369     'bank_transactions/invoices',
 
 371     INVOICES => \@selected_invoices,
 
 372     bt_id    => $::form->{bt_id},
 
 376 sub action_save_invoices {
 
 379   my $invoice_hash = delete $::form->{invoice_ids}; # each key (the bt line with a bt_id) contains an array of invoice_ids
 
 381   # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
 
 394   # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
 
 396   #           '44' => [ '50', '51', 52' ]
 
 399   $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
 
 401   # a bank_transaction may be assigned to several invoices, i.e. a customer
 
 402   # might pay several open invoices with one transaction
 
 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,
 
 413   $self->action_list();
 
 416 sub save_single_bank_transaction {
 
 417   my ($self, %params) = @_;
 
 421     bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
 
 425   if (!$data{bank_transaction}) {
 
 429       message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
 
 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;
 
 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);
 
 449           message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
 
 453       push @{ $data{invoices} }, $invoice;
 
 456     if (   $payment_received
 
 457         && any {    ( $_->is_sales && ($_->amount < 0))
 
 458                  || (!$_->is_sales && ($_->amount > 0))
 
 459                } @{ $data{invoices} }) {
 
 463         message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
 
 468         && any {    ( $_->is_sales && ($_->amount > 0))
 
 469                  || (!$_->is_sales && ($_->amount < 0))
 
 470                } @{ $data{invoices} }) {
 
 474         message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
 
 478     my $max_invoices = scalar(@{ $data{invoices} });
 
 481     foreach my $invoice (@{ $data{invoices} }) {
 
 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)) {
 
 491           message => $::locale->text("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name),
 
 495       if (!$amount_of_transaction && $invoice->open_amount) {
 
 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."),
 
 504       if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
 
 505         $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
 
 507         $payment_type = 'without_skonto';
 
 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);
 
 517           $amount_of_transaction += $sign * $invoice->open_amount;
 
 518           $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $invoice->open_amount);
 
 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;
 
 536         if ($overpaid_amount >= 0.01) {
 
 540             message => $::locale->text('Invoice #1 was overpaid by #2.', $invoice->invnumber, $::form->format_amount(\%::myconfig, $overpaid_amount, 2)),
 
 545       # Record a record link from the bank transaction to the invoice
 
 547         from_table => 'bank_transactions',
 
 549         to_table   => $invoice->is_sales ? 'ar' : 'ap',
 
 550         to_id      => $invoice->id,
 
 553       SL::DB::RecordLink->new(@props)->save;
 
 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;
 
 567     $bank_transaction->save;
 
 569     # 'undef' means 'no error' here.
 
 574   my $rez = $data{bank_transaction}->db->with_transaction(sub {
 
 576       $error = $worker->();
 
 590   return grep { $_ } ($error, @warnings);
 
 593 sub action_save_proposals {
 
 596   foreach my $bt_id (@{ $::form->{proposal_ids} }) {
 
 597     my $bt = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
 
 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;
 
 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);
 
 608     $bt->invoice_amount($bt->amount);
 
 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);
 
 620       from_table => 'bank_transactions',
 
 622       to_table   => $arap->is_sales ? 'ar' : 'ap',
 
 626     SL::DB::RecordLink->new(@props)->save;
 
 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;
 
 640   flash('ok', t8('#1 proposal(s) saved.', scalar @{ $::form->{proposal_ids} }));
 
 642   $self->action_list();
 
 650   $::auth->assert('bank_transaction');
 
 657 sub make_filter_summary {
 
 660   my $filter = $::form->{filter} || {};
 
 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')                              ],
 
 673     push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
 
 676   $self->{filter_summary} = join ', ', @filter_strings;
 
 682   my $callback    = $self->models->get_callback;
 
 684   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
 
 685   $self->{report} = $report;
 
 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);
 
 691     transdate             => { sub   => sub { $_[0]->transdate_as_date } },
 
 692     valutadate            => { sub   => sub { $_[0]->valutadate_as_date } },
 
 694     remote_account_number => { },
 
 695     remote_bank_code      => { },
 
 696     amount                => { sub   => sub { $_[0]->amount_as_number },
 
 698     invoice_amount        => { sub   => sub { $_[0]->invoice_amount_as_number },
 
 700     invoices              => { sub   => sub { $_[0]->linked_invoices } },
 
 701     currency              => { sub   => sub { $_[0]->currency->name } },
 
 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 } },
 
 709   map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
 
 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,
 
 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);
 
 727   my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
 
 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 }),
 
 735 sub _existing_record_link {
 
 736   my ($bt, $invoice) = @_;
 
 738   # check whether a record link from banktransaction $bt already exists to
 
 739   # invoice $invoice, returns 1 if that is the case
 
 741   die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
 
 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 ]  );
 
 746   return @$linked_records ? 1 : 0;
 
 749 sub init_problems { [] }
 
 754   SL::Controller::Helper::GetModels->new(
 
 759         dir => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
 
 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'),
 
 775     with_objects => [ 'local_bank_account', 'currency' ],
 
 788 SL::Controller::BankTransaction - Posting payments to invoices from
 
 789 bank transactions imported earlier
 
 795 =item C<save_single_bank_transaction %params>
 
 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
 
 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.
 
 808 If warnings but not errors occur the database transaction is still
 
 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.
 
 815 An error object is a hash reference containing the following members:
 
 819 =item * C<result> — can be either C<warning> or C<error>. Warnings are
 
 820 displayed slightly different than errors.
 
 822 =item * C<message> — a human-readable message included in the list of
 
 823 errors meant as the description of why the problem happened
 
 825 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
 
 826 that the function was called with
 
 828 =item * C<bank_transaction> — the database object
 
 829 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
 
 831 =item * C<invoices> — an array ref of the database objects (either
 
 832 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
 
 841 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
 
 842 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>