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 ? undef : $templates_gl,
295 TEMPLATES_AP => $templates_ap,
296 vendor_name => $use_vendor_filter ? $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});
332 push @filter, ('vendor.name' => { ilike => '%' . $::form->{vendor} . '%' }) if $::form->{vendor};
333 push @filter, ('template_name' => { ilike => '%' . $::form->{template} . '%' }) if $::form->{template};
334 push @filter, ('reference' => { ilike => '%' . $::form->{reference} . '%' }) if $::form->{reference};
336 my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
337 where => [ template_type => 'ap_transaction', (and => \@filter) x !!@filter ],
338 with_objects => [ qw(employee vendor) ],
340 my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
341 query => [ template_type => 'gl_transaction',
342 chart_id => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
343 (and => \@filter) x !!@filter
345 with_objects => [ qw(employee record_template_items) ],
348 $::form->{filter} //= {};
350 $self->callback($self->url_for(
352 'filter.bank_account' => $::form->{filter}->{bank_account},
353 'filter.todate' => $::form->{filter}->{todate},
354 'filter.fromdate' => $::form->{filter}->{fromdate},
357 my $output = $self->render(
358 'bank_transactions/_template_list',
360 TEMPLATES_AP => $templates_ap,
361 TEMPLATES_GL => $templates_gl,
364 $self->render(\to_json({ html => $output }), { type => 'json', process => 0 });
367 sub action_ajax_add_list {
370 my @where_sale = (amount => { ne => \'paid' });
371 my @where_purchase = (amount => { ne => \'paid' });
373 if ($::form->{invnumber}) {
374 push @where_sale, (invnumber => { ilike => like($::form->{invnumber})});
375 push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
378 if ($::form->{amount}) {
379 push @where_sale, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
380 push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
383 if ($::form->{vcnumber}) {
384 push @where_sale, ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
385 push @where_purchase, ('vendor.vendornumber' => { ilike => like($::form->{vcnumber})});
388 if ($::form->{vcname}) {
389 push @where_sale, ('customer.name' => { ilike => like($::form->{vcname})});
390 push @where_purchase, ('vendor.name' => { ilike => like($::form->{vcname})});
393 if ($::form->{transdatefrom}) {
394 my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
395 if ( ref($fromdate) eq 'DateTime' ) {
396 push @where_sale, ('transdate' => { ge => $fromdate});
397 push @where_purchase, ('transdate' => { ge => $fromdate});
401 if ($::form->{transdateto}) {
402 my $todate = $::locale->parse_date_to_object($::form->{transdateto});
403 if ( ref($todate) eq 'DateTime' ) {
404 $todate->add(days => 1);
405 push @where_sale, ('transdate' => { lt => $todate});
406 push @where_purchase, ('transdate' => { lt => $todate});
410 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => \@where_sale, with_objects => 'customer');
411 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
413 my @all_open_invoices = @{ $all_open_ar_invoices };
414 # add ap invoices, filtering out subcent open amounts
415 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
417 @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
419 my $output = $self->render(
420 'bank_transactions/add_list',
422 INVOICES => \@all_open_invoices,
425 my %result = ( count => 0, html => $output );
427 $self->render(\to_json(\%result), { type => 'json', process => 0 });
430 sub action_ajax_accept_invoices {
433 my @selected_invoices;
434 foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
435 my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
436 push @selected_invoices, $invoice_object;
440 'bank_transactions/invoices',
442 INVOICES => \@selected_invoices,
443 bt_id => $::form->{bt_id},
450 return 0 if !$::form->{invoice_ids};
452 my %invoice_hash = %{ delete $::form->{invoice_ids} }; # each key (the bt line with a bt_id) contains an array of invoice_ids
454 # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
467 # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
469 # '44' => [ '50', '51', 52' ]
472 $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
474 # a bank_transaction may be assigned to several invoices, i.e. a customer
475 # might pay several open invoices with one transaction
481 if ( $::form->{proposal_ids} ) {
482 foreach (@{ $::form->{proposal_ids} }) {
483 my $bank_transaction_id = $_;
484 my $invoice_ids = $invoice_hash{$_};
485 push @{ $self->problems }, $self->save_single_bank_transaction(
486 bank_transaction_id => $bank_transaction_id,
487 invoice_ids => $invoice_ids,
488 sources => ($::form->{sources} // {})->{$_},
489 memos => ($::form->{memos} // {})->{$_},
491 $count += scalar( @{$invoice_ids} );
494 while ( my ($bank_transaction_id, $invoice_ids) = each(%invoice_hash) ) {
495 push @{ $self->problems }, $self->save_single_bank_transaction(
496 bank_transaction_id => $bank_transaction_id,
497 invoice_ids => $invoice_ids,
498 sources => [ map { $::form->{"sources_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
499 memos => [ map { $::form->{"memos_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
501 $count += scalar( @{$invoice_ids} );
504 foreach (@{ $self->problems }) {
505 $count-- if $_->{result} eq 'error';
510 sub action_save_invoices {
512 my $count = $self->save_invoices();
514 flash('ok', t8('#1 invoice(s) saved.', $count));
516 $self->action_list();
519 sub action_save_proposals {
522 if ( $::form->{proposal_ids} ) {
523 my $propcount = scalar(@{ $::form->{proposal_ids} });
524 if ( $propcount > 0 ) {
525 my $count = $self->save_invoices();
527 flash('ok', t8('#1 proposal(s) with #2 invoice(s) saved.', $propcount, $count));
530 $self->action_list();
534 sub save_single_bank_transaction {
535 my ($self, %params) = @_;
539 bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
543 if (!$data{bank_transaction}) {
547 message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
554 my $bt_id = $data{bank_transaction_id};
555 my $bank_transaction = $data{bank_transaction};
556 my $sign = $bank_transaction->amount < 0 ? -1 : 1;
557 my $amount_of_transaction = $sign * $bank_transaction->amount;
558 my $payment_received = $bank_transaction->amount > 0;
559 my $payment_sent = $bank_transaction->amount < 0;
562 foreach my $invoice_id (@{ $params{invoice_ids} }) {
563 my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
568 message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
571 push @{ $data{invoices} }, $invoice;
574 if ( $payment_received
575 && any { ( $_->is_sales && ($_->amount < 0))
576 || (!$_->is_sales && ($_->amount > 0))
577 } @{ $data{invoices} }) {
581 message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
586 && any { ( $_->is_sales && ($_->amount > 0))
587 || (!$_->is_sales && ($_->amount < 0) && ($_->invoice_type eq 'purchase_invoice'))
588 } @{ $data{invoices} }) {
592 message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
596 my $max_invoices = scalar(@{ $data{invoices} });
599 foreach my $invoice (@{ $data{invoices} }) {
600 my $source = ($data{sources} // [])->[$n_invoices];
601 my $memo = ($data{memos} // [])->[$n_invoices];
605 # Check if bank_transaction already has a link to the invoice, may only be linked once per invoice
606 # This might be caused by the user reloading a page and resending the form
607 if (_existing_record_link($bank_transaction, $invoice)) {
611 message => $::locale->text("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name),
615 if (!$amount_of_transaction && $invoice->open_amount) {
619 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."),
624 if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
625 $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
627 $payment_type = 'without_skonto';
631 # pay invoice or go to the next bank transaction if the amount is not sufficiently high
632 if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) {
633 my $open_amount = ($payment_type eq 'with_skonto_pt'?$invoice->amount_less_skonto:$invoice->open_amount);
634 # first calculate new bank transaction amount ...
635 if ($invoice->is_sales) {
636 $amount_of_transaction -= $sign * $open_amount;
637 $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $open_amount);
639 $amount_of_transaction += $sign * $open_amount;
640 $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $open_amount);
642 # ... and then pay the invoice
643 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
644 trans_id => $invoice->id,
645 amount => $open_amount,
646 payment_type => $payment_type,
649 transdate => $bank_transaction->transdate->to_kivitendo);
651 # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary
653 # this catches credit_notes and negative sales invoices
654 if ( $invoice->is_sales && $invoice->amount < 0 ) {
655 # $invoice->open_amount is negative for credit_notes
656 # $bank_transaction->amount is negative for outgoing transactions
657 # so $amount_of_transaction is negative but needs positive
658 $amount_of_transaction *= -1;
660 } elsif (!$invoice->is_sales && $invoice->invoice_type =~ m/ap_transaction|purchase_invoice/) {
661 # $invoice->open_amount may be negative for ap_transaction but may be positiv for negativ ap_transaction
662 # if $invoice->open_amount is negative $bank_transaction->amount is positve
663 # if $invoice->open_amount is positive $bank_transaction->amount is negative
664 # but amount of transaction is for both positive
665 $amount_of_transaction *= -1 if $invoice->open_amount == - $amount_of_transaction;
668 my $overpaid_amount = $amount_of_transaction - $invoice->open_amount;
669 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
670 trans_id => $invoice->id,
671 amount => $amount_of_transaction,
672 payment_type => $payment_type,
675 transdate => $bank_transaction->transdate->to_kivitendo);
676 $bank_transaction->invoice_amount($bank_transaction->amount);
677 $amount_of_transaction = 0;
679 if ($overpaid_amount >= 0.01) {
683 message => $::locale->text('Invoice #1 was overpaid by #2.', $invoice->invnumber, $::form->format_amount(\%::myconfig, $overpaid_amount, 2)),
687 # Record a record link from the bank transaction to the invoice
689 from_table => 'bank_transactions',
691 to_table => $invoice->is_sales ? 'ar' : 'ap',
692 to_id => $invoice->id,
695 SL::DB::RecordLink->new(@props)->save;
697 # "close" a sepa_export_item if it exists
698 # code duplicated in action_save_proposals!
699 # currently only works, if there is only exactly one open sepa_export_item
700 if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
701 if ( scalar @$seis == 1 ) {
702 # moved the execution and the check for sepa_export into a method,
703 # this isn't part of a transaction, though
704 $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
709 $bank_transaction->save;
711 # 'undef' means 'no error' here.
716 my $rez = $data{bank_transaction}->db->with_transaction(sub {
718 $error = $worker->();
729 # Rollback Fehler nicht weiterreichen
733 return grep { $_ } ($error, @warnings);
741 $::auth->assert('bank_transaction');
748 sub make_filter_summary {
751 my $filter = $::form->{filter} || {};
755 [ $filter->{"transdate:date::ge"}, $::locale->text('Transdate') . " " . $::locale->text('From Date') ],
756 [ $filter->{"transdate:date::le"}, $::locale->text('Transdate') . " " . $::locale->text('To Date') ],
757 [ $filter->{"valutadate:date::ge"}, $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
758 [ $filter->{"valutadate:date::le"}, $::locale->text('Valutadate') . " " . $::locale->text('To Date') ],
759 [ $filter->{"amount:number"}, $::locale->text('Amount') ],
760 [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account') ],
764 push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
767 $self->{filter_summary} = join ', ', @filter_strings;
773 my $callback = $self->models->get_callback;
775 my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
776 $self->{report} = $report;
778 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);
779 my @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code);
782 transdate => { sub => sub { $_[0]->transdate_as_date } },
783 valutadate => { sub => sub { $_[0]->valutadate_as_date } },
785 remote_account_number => { },
786 remote_bank_code => { },
787 amount => { sub => sub { $_[0]->amount_as_number },
789 invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number },
791 invoices => { sub => sub { $_[0]->linked_invoices } },
792 currency => { sub => sub { $_[0]->currency->name } },
794 local_account_number => { sub => sub { $_[0]->local_bank_account->account_number } },
795 local_bank_code => { sub => sub { $_[0]->local_bank_account->bank_code } },
796 local_bank_name => { sub => sub { $_[0]->local_bank_account->name } },
800 map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
802 $report->set_options(
803 std_column_visibility => 1,
804 controller_class => 'BankTransaction',
805 output_format => 'HTML',
806 top_info_text => $::locale->text('Bank transactions'),
807 title => $::locale->text('Bank transactions'),
808 allow_pdf_export => 1,
809 allow_csv_export => 1,
811 $report->set_columns(%column_defs);
812 $report->set_column_order(@columns);
813 $report->set_export_options(qw(list_all filter));
814 $report->set_options_from_form;
815 $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
816 $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
818 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
820 $report->set_options(
821 raw_top_info_text => $self->render('bank_transactions/report_top', { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
822 raw_bottom_info_text => $self->render('bank_transactions/report_bottom', { output => 0 }),
826 sub _existing_record_link {
827 my ($bt, $invoice) = @_;
829 # check whether a record link from banktransaction $bt already exists to
830 # invoice $invoice, returns 1 if that is the case
832 die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
834 my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
835 my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ] );
837 return @$linked_records ? 1 : 0;
840 sub init_problems { [] }
845 SL::Controller::Helper::GetModels->new(
850 dir => 0, # 1 = ASC, 0 = DESC : default sort is newest at top
852 transdate => t8('Transdate'),
853 remote_name => t8('Remote name'),
854 amount => t8('Amount'),
855 invoice_amount => t8('Assigned'),
856 invoices => t8('Linked invoices'),
857 valutadate => t8('Valutadate'),
858 remote_account_number => t8('Remote account number'),
859 remote_bank_code => t8('Remote bank code'),
860 currency => t8('Currency'),
861 purpose => t8('Purpose'),
862 local_account_number => t8('Local account number'),
863 local_bank_code => t8('Local bank code'),
864 local_bank_name => t8('Bank account'),
866 with_objects => [ 'local_bank_account', 'currency' ],
870 sub load_ap_record_template_url {
871 my ($self, $template) = @_;
873 return $self->url_for(
874 controller => 'ap.pl',
875 action => 'load_record_template',
877 'form_defaults.amount_1' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
878 'form_defaults.transdate' => $self->transaction->transdate_as_date,
879 'form_defaults.duedate' => $self->transaction->transdate_as_date,
880 'form_defaults.no_payment_bookings' => 1,
881 'form_defaults.paid_1_suggestion' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
882 'form_defaults.AP_paid_1_suggestion' => $self->transaction->local_bank_account->chart->accno,
883 'form_defaults.callback' => $self->callback,
887 sub load_gl_record_template_url {
888 my ($self, $template) = @_;
890 return $self->url_for(
891 controller => 'gl.pl',
892 action => 'load_record_template',
894 'form_defaults.amount_1' => abs($self->transaction->amount), # always positive
895 'form_defaults.transdate' => $self->transaction->transdate_as_date,
896 'form_defaults.callback' => $self->callback,
900 sub setup_search_action_bar {
901 my ($self, %params) = @_;
903 for my $bar ($::request->layout->get('actionbar')) {
907 submit => [ '#search_form', { action => 'BankTransaction/list' } ],
908 accesskey => 'enter',
914 sub setup_list_all_action_bar {
915 my ($self, %params) = @_;
917 for my $bar ($::request->layout->get('actionbar')) {
921 submit => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
922 accesskey => 'enter',
937 SL::Controller::BankTransaction - Posting payments to invoices from
938 bank transactions imported earlier
944 =item C<save_single_bank_transaction %params>
946 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
947 tries to post its amount to a certain number of invoices (parameter
948 C<invoice_ids>, an array ref of database IDs to purchase or sales
951 The whole function is wrapped in a database transaction. If an
952 exception occurs the bank transaction is not posted at all. The same
953 is true if the code detects an error during the execution, e.g. a bank
954 transaction that's already been posted earlier. In both cases the
955 database transaction will be rolled back.
957 If warnings but not errors occur the database transaction is still
960 The return value is an error object or C<undef> if the function
961 succeeded. The calling function will collect all warnings and errors
962 and display them in a nicely formatted table if any occurred.
964 An error object is a hash reference containing the following members:
968 =item * C<result> — can be either C<warning> or C<error>. Warnings are
969 displayed slightly different than errors.
971 =item * C<message> — a human-readable message included in the list of
972 errors meant as the description of why the problem happened
974 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
975 that the function was called with
977 =item * C<bank_transaction> — the database object
978 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
980 =item * C<invoices> — an array ref of the database objects (either
981 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
990 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
991 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>