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);
29 use List::UtilsBy qw(partition_by);
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 my %sepa_export_items_by_id = partition_by { $_->ar_id || $_->ap_id } @$all_open_sepa_export_items;
125 # first collect sepa export items to open invoices
126 foreach my $open_invoice (@all_open_invoices){
127 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount,2);
128 $open_invoice->{skonto_type} = 'without_skonto';
129 foreach (@{ $sepa_export_items_by_id{ $open_invoice->id } || [] }) {
130 my $factor = ($_->ar_id == $open_invoice->id ? 1 : -1);
131 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);
133 $open_invoice->{skonto_type} = $_->payment_type;
134 $sepa_exports{$_->sepa_export_id} ||= { count => 0, is_ar => 0, amount => 0, proposed => 0, invoices => [], item => $_ };
135 $sepa_exports{$_->sepa_export_id}->{count}++;
136 $sepa_exports{$_->sepa_export_id}->{is_ar}++ if $_->ar_id == $open_invoice->id;
137 $sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor;
138 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 ) {
158 foreach ( keys %sepa_exports) {
159 if ( abs(($sepa_exports{$_}->{amount} * 1) - ($bt->amount * 1)) < 0.01 ) {
161 @{$bt->{proposals}} = @{$sepa_exports{$_}->{invoices}};
162 $bt->{sepa_export_ok} = 1;
163 $sepa_exports{$_}->{proposed}=1;
164 push(@proposals, $bt);
170 # batch transaction has no remotename !!
172 next unless $bt->{remote_name}; # bank has no name, usually fees, use create invoice to assign
175 # try to match the current $bt to each of the open_invoices, saving the
176 # results of get_agreement_with_invoice in $open_invoice->{agreement} and
177 # $open_invoice->{rule_matches}.
179 # The values are overwritten each time a new bt is checked, so at the end
180 # of each bt the likely results are filtered and those values are stored in
181 # the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
182 # score is stored in $bt->{agreement}
184 foreach my $open_invoice (@all_open_invoices) {
185 ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
186 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,
187 $open_invoice->amount * ($open_invoice->{is_ar} ? 1 : -1), 2);
191 my $min_agreement = 3; # suggestions must have at least this score
193 my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
195 # add open_invoices with highest agreement into array $bt->{proposals}
196 if ( $max_agreement >= $min_agreement ) {
197 $bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
198 $bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
200 # store the rule_matches in a separate array, so they can be displayed in template
201 foreach ( @{ $bt->{proposals} } ) {
202 push(@{$bt->{rule_matches}}, $_->{rule_matches});
208 # separate filter for proposals (second tab, agreement >= 5 and exactly one match)
209 # to qualify as a proposal there has to be
210 # * agreement >= 5 TODO: make threshold configurable in configuration
211 # * there must be only one exact match
212 # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
213 my $proposal_threshold = 5;
214 my @otherproposals = grep {
215 ($_->{agreement} >= $proposal_threshold)
216 && (1 == scalar @{ $_->{proposals} })
217 && (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01
218 : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01)
219 } @{ $bank_transactions };
221 push @proposals, @otherproposals;
223 # sort bank transaction proposals by quality (score) of proposal
224 if ($::form->{sort_by} && $::form->{sort_by} eq 'proposal') {
225 if ($::form->{sort_dir}) {
226 $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ];
228 $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ];
232 # for testing with t/bank/banktransaction.t :
233 if ( $::form->{dont_render_for_test} ) {
234 return $bank_transactions;
237 $::request->layout->add_javascripts("kivi.BankTransaction.js");
238 $self->render('bank_transactions/list',
239 title => t8('Bank transactions MT940'),
240 BANK_TRANSACTIONS => $bank_transactions,
241 PROPOSALS => \@proposals,
242 bank_account => $bank_account,
243 ui_tab => scalar(@proposals) > 0?1:0,
247 sub action_assign_invoice {
250 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
252 $self->render('bank_transactions/assign_invoice',
254 title => t8('Assign invoice'),);
257 sub action_create_invoice {
259 my %myconfig = %main::myconfig;
261 $self->transaction(SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id}));
263 # This was dead code: We compared vendor.account_name with bank_transaction.iban.
264 # This did never match (Kontonummer != IBAN). It's kivis 09/02 (2013) day
265 # If refactored/improved, also consider that vendor.iban should be normalized
266 # user may like to input strings like: 'AT 3333 3333 2222 1111' -> can be checked strictly
267 # at Vendor code because we need the correct data for all sepa exports.
269 my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(iban => $self->transaction->{remote_account_number});
270 my $use_vendor_filter = $self->transaction->{remote_account_number} && $vendor_of_transaction;
272 my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
273 where => [ template_type => 'ap_transaction' ],
274 with_objects => [ qw(employee vendor) ],
276 my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
277 query => [ template_type => 'gl_transaction',
278 chart_id => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
280 with_objects => [ qw(employee record_template_items) ],
283 # pre filter templates_ap, if we have a vendor match (IBAN eq IBAN) - show and allow user to edit this via gui!
284 $templates_ap = [ grep { $_->vendor_id == $vendor_of_transaction->id } @{ $templates_ap } ] if $use_vendor_filter;
286 $self->callback($self->url_for(
288 'filter.bank_account' => $::form->{filter}->{bank_account},
289 'filter.todate' => $::form->{filter}->{todate},
290 'filter.fromdate' => $::form->{filter}->{fromdate},
294 'bank_transactions/create_invoice',
296 title => t8('Create invoice'),
297 TEMPLATES_GL => $use_vendor_filter && @{ $templates_ap } ? undef : $templates_gl,
298 TEMPLATES_AP => $templates_ap,
299 vendor_name => $use_vendor_filter && @{ $templates_ap } ? $vendor_of_transaction->name : undef,
303 sub action_ajax_payment_suggestion {
306 # based on a BankTransaction ID and a Invoice or PurchaseInvoice ID passed via $::form,
307 # create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
308 # and return encoded as JSON
310 my $bt = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
311 my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
313 die unless $bt and $invoice;
315 my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
318 $html = $self->render(
319 'bank_transactions/_payment_suggestion', { output => 0 },
320 bt_id => $::form->{bt_id},
321 prop_id => $::form->{prop_id},
323 SELECT_OPTIONS => \@select_options,
326 $self->render(\ SL::JSON::to_json( { 'html' => "$html" } ), { layout => 0, type => 'json', process => 0 });
329 sub action_filter_templates {
332 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
334 my (@filter, @filter_ap);
336 # filter => gl and ap | filter_ap = ap (i.e. vendorname)
337 push @filter, ('template_name' => { ilike => '%' . $::form->{template} . '%' }) if $::form->{template};
338 push @filter, ('reference' => { ilike => '%' . $::form->{reference} . '%' }) if $::form->{reference};
339 push @filter_ap, ('vendor.name' => { ilike => '%' . $::form->{vendor} . '%' }) if $::form->{vendor};
340 push @filter_ap, @filter;
341 my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
342 query => [ template_type => 'gl_transaction',
343 chart_id => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
344 (and => \@filter) x !!@filter
346 with_objects => [ qw(employee record_template_items) ],
349 my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
350 where => [ template_type => 'ap_transaction', (and => \@filter_ap) x !!@filter_ap ],
351 with_objects => [ qw(employee vendor) ],
353 $::form->{filter} //= {};
355 $self->callback($self->url_for(
357 'filter.bank_account' => $::form->{filter}->{bank_account},
358 'filter.todate' => $::form->{filter}->{todate},
359 'filter.fromdate' => $::form->{filter}->{fromdate},
362 my $output = $self->render(
363 'bank_transactions/_template_list',
365 TEMPLATES_AP => $templates_ap,
366 TEMPLATES_GL => $templates_gl,
369 $self->render(\to_json({ html => $output }), { type => 'json', process => 0 });
372 sub action_ajax_add_list {
375 my @where_sale = (amount => { ne => \'paid' });
376 my @where_purchase = (amount => { ne => \'paid' });
378 if ($::form->{invnumber}) {
379 push @where_sale, (invnumber => { ilike => like($::form->{invnumber})});
380 push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
383 if ($::form->{amount}) {
384 push @where_sale, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
385 push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
388 if ($::form->{vcnumber}) {
389 push @where_sale, ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
390 push @where_purchase, ('vendor.vendornumber' => { ilike => like($::form->{vcnumber})});
393 if ($::form->{vcname}) {
394 push @where_sale, ('customer.name' => { ilike => like($::form->{vcname})});
395 push @where_purchase, ('vendor.name' => { ilike => like($::form->{vcname})});
398 if ($::form->{transdatefrom}) {
399 my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
400 if ( ref($fromdate) eq 'DateTime' ) {
401 push @where_sale, ('transdate' => { ge => $fromdate});
402 push @where_purchase, ('transdate' => { ge => $fromdate});
406 if ($::form->{transdateto}) {
407 my $todate = $::locale->parse_date_to_object($::form->{transdateto});
408 if ( ref($todate) eq 'DateTime' ) {
409 $todate->add(days => 1);
410 push @where_sale, ('transdate' => { lt => $todate});
411 push @where_purchase, ('transdate' => { lt => $todate});
415 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => \@where_sale, with_objects => 'customer');
416 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
418 my @all_open_invoices = @{ $all_open_ar_invoices };
419 # add ap invoices, filtering out subcent open amounts
420 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
422 @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
424 my $output = $self->render(
425 'bank_transactions/add_list',
427 INVOICES => \@all_open_invoices,
430 my %result = ( count => 0, html => $output );
432 $self->render(\to_json(\%result), { type => 'json', process => 0 });
435 sub action_ajax_accept_invoices {
438 my @selected_invoices;
439 foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
440 my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
441 push @selected_invoices, $invoice_object;
445 'bank_transactions/invoices',
447 INVOICES => \@selected_invoices,
448 bt_id => $::form->{bt_id},
455 return 0 if !$::form->{invoice_ids};
457 my %invoice_hash = %{ delete $::form->{invoice_ids} }; # each key (the bt line with a bt_id) contains an array of invoice_ids
459 # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
472 # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
474 # '44' => [ '50', '51', 52' ]
477 $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
479 # a bank_transaction may be assigned to several invoices, i.e. a customer
480 # might pay several open invoices with one transaction
486 if ( $::form->{proposal_ids} ) {
487 foreach (@{ $::form->{proposal_ids} }) {
488 my $bank_transaction_id = $_;
489 my $invoice_ids = $invoice_hash{$_};
490 push @{ $self->problems }, $self->save_single_bank_transaction(
491 bank_transaction_id => $bank_transaction_id,
492 invoice_ids => $invoice_ids,
493 sources => ($::form->{sources} // {})->{$_},
494 memos => ($::form->{memos} // {})->{$_},
496 $count += scalar( @{$invoice_ids} );
499 while ( my ($bank_transaction_id, $invoice_ids) = each(%invoice_hash) ) {
500 push @{ $self->problems }, $self->save_single_bank_transaction(
501 bank_transaction_id => $bank_transaction_id,
502 invoice_ids => $invoice_ids,
503 sources => [ map { $::form->{"sources_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
504 memos => [ map { $::form->{"memos_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
506 $count += scalar( @{$invoice_ids} );
509 my $max_count = $count;
510 foreach (@{ $self->problems }) {
511 $count-- if $_->{result} eq 'error';
513 return ($count, $max_count);
516 sub action_save_invoices {
518 my ($success_count, $max_count) = $self->save_invoices();
520 if ($success_count == $max_count) {
521 flash('ok', t8('#1 invoice(s) saved.', $success_count));
523 flash('error', t8('At least #1 invoice(s) not saved', $max_count - $success_count));
526 $self->action_list();
529 sub action_save_proposals {
532 if ( $::form->{proposal_ids} ) {
533 my $propcount = scalar(@{ $::form->{proposal_ids} });
534 if ( $propcount > 0 ) {
535 my $count = $self->save_invoices();
537 flash('ok', t8('#1 proposal(s) with #2 invoice(s) saved.', $propcount, $count));
540 $self->action_list();
544 sub save_single_bank_transaction {
545 my ($self, %params) = @_;
549 bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
553 if (!$data{bank_transaction}) {
557 message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
564 my $bt_id = $data{bank_transaction_id};
565 my $bank_transaction = $data{bank_transaction};
566 my $sign = $bank_transaction->amount < 0 ? -1 : 1;
567 my $amount_of_transaction = $sign * $bank_transaction->amount;
568 my $payment_received = $bank_transaction->amount > 0;
569 my $payment_sent = $bank_transaction->amount < 0;
572 foreach my $invoice_id (@{ $params{invoice_ids} }) {
573 my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
578 message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
581 push @{ $data{invoices} }, $invoice;
584 if ( $payment_received
585 && any { ( $_->is_sales && ($_->amount < 0))
586 || (!$_->is_sales && ($_->amount > 0))
587 } @{ $data{invoices} }) {
591 message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
596 && any { ( $_->is_sales && ($_->amount > 0))
597 || (!$_->is_sales && ($_->amount < 0) && ($_->invoice_type eq 'purchase_invoice'))
598 } @{ $data{invoices} }) {
602 message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
606 my $max_invoices = scalar(@{ $data{invoices} });
609 foreach my $invoice (@{ $data{invoices} }) {
610 my $source = ($data{sources} // [])->[$n_invoices];
611 my $memo = ($data{memos} // [])->[$n_invoices];
615 # Check if bank_transaction already has a link to the invoice, may only be linked once per invoice
616 # This might be caused by the user reloading a page and resending the form
617 if (_existing_record_link($bank_transaction, $invoice)) {
621 message => $::locale->text("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name),
625 if (!$amount_of_transaction && $invoice->open_amount) {
629 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."),
634 if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
635 $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
637 $payment_type = 'without_skonto';
641 # pay invoice or go to the next bank transaction if the amount is not sufficiently high
642 if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) {
643 my $open_amount = ($payment_type eq 'with_skonto_pt'?$invoice->amount_less_skonto:$invoice->open_amount);
644 # first calculate new bank transaction amount ...
645 if ($invoice->is_sales) {
646 $amount_of_transaction -= $sign * $open_amount;
647 $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $open_amount);
649 $amount_of_transaction += $sign * $open_amount;
650 $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $open_amount);
652 # ... and then pay the invoice
653 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
654 trans_id => $invoice->id,
655 amount => $open_amount,
656 payment_type => $payment_type,
659 transdate => $bank_transaction->transdate->to_kivitendo);
661 # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary
663 # this catches credit_notes and negative sales invoices
664 if ( $invoice->is_sales && $invoice->amount < 0 ) {
665 # $invoice->open_amount is negative for credit_notes
666 # $bank_transaction->amount is negative for outgoing transactions
667 # so $amount_of_transaction is negative but needs positive
668 $amount_of_transaction *= -1;
670 } elsif (!$invoice->is_sales && $invoice->invoice_type =~ m/ap_transaction|purchase_invoice/) {
671 # $invoice->open_amount may be negative for ap_transaction but may be positiv for negativ ap_transaction
672 # if $invoice->open_amount is negative $bank_transaction->amount is positve
673 # if $invoice->open_amount is positive $bank_transaction->amount is negative
674 # but amount of transaction is for both positive
675 $amount_of_transaction *= -1 if $invoice->open_amount == - $amount_of_transaction;
678 my $overpaid_amount = $amount_of_transaction - $invoice->open_amount;
679 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
680 trans_id => $invoice->id,
681 amount => $amount_of_transaction,
682 payment_type => $payment_type,
685 transdate => $bank_transaction->transdate->to_kivitendo);
686 $bank_transaction->invoice_amount($bank_transaction->amount);
687 $amount_of_transaction = 0;
689 if ($overpaid_amount >= 0.01) {
693 message => $::locale->text('Invoice #1 was overpaid by #2.', $invoice->invnumber, $::form->format_amount(\%::myconfig, $overpaid_amount, 2)),
697 # Record a record link from the bank transaction to the invoice
699 from_table => 'bank_transactions',
701 to_table => $invoice->is_sales ? 'ar' : 'ap',
702 to_id => $invoice->id,
705 SL::DB::RecordLink->new(@props)->save;
707 # "close" a sepa_export_item if it exists
708 # code duplicated in action_save_proposals!
709 # currently only works, if there is only exactly one open sepa_export_item
710 if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
711 if ( scalar @$seis == 1 ) {
712 # moved the execution and the check for sepa_export into a method,
713 # this isn't part of a transaction, though
714 $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
719 $bank_transaction->save;
721 # 'undef' means 'no error' here.
726 my $rez = $data{bank_transaction}->db->with_transaction(sub {
728 $error = $worker->();
739 # Rollback Fehler nicht weiterreichen
743 return grep { $_ } ($error, @warnings);
751 $::auth->assert('bank_transaction');
758 sub make_filter_summary {
761 my $filter = $::form->{filter} || {};
765 [ $filter->{"transdate:date::ge"}, $::locale->text('Transdate') . " " . $::locale->text('From Date') ],
766 [ $filter->{"transdate:date::le"}, $::locale->text('Transdate') . " " . $::locale->text('To Date') ],
767 [ $filter->{"valutadate:date::ge"}, $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
768 [ $filter->{"valutadate:date::le"}, $::locale->text('Valutadate') . " " . $::locale->text('To Date') ],
769 [ $filter->{"amount:number"}, $::locale->text('Amount') ],
770 [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account') ],
774 push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
777 $self->{filter_summary} = join ', ', @filter_strings;
783 my $callback = $self->models->get_callback;
785 my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
786 $self->{report} = $report;
788 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);
789 my @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code);
792 transdate => { sub => sub { $_[0]->transdate_as_date } },
793 valutadate => { sub => sub { $_[0]->valutadate_as_date } },
795 remote_account_number => { },
796 remote_bank_code => { },
797 amount => { sub => sub { $_[0]->amount_as_number },
799 invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number },
801 invoices => { sub => sub { $_[0]->linked_invoices } },
802 currency => { sub => sub { $_[0]->currency->name } },
804 local_account_number => { sub => sub { $_[0]->local_bank_account->account_number } },
805 local_bank_code => { sub => sub { $_[0]->local_bank_account->bank_code } },
806 local_bank_name => { sub => sub { $_[0]->local_bank_account->name } },
810 map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
812 $report->set_options(
813 std_column_visibility => 1,
814 controller_class => 'BankTransaction',
815 output_format => 'HTML',
816 top_info_text => $::locale->text('Bank transactions'),
817 title => $::locale->text('Bank transactions'),
818 allow_pdf_export => 1,
819 allow_csv_export => 1,
821 $report->set_columns(%column_defs);
822 $report->set_column_order(@columns);
823 $report->set_export_options(qw(list_all filter));
824 $report->set_options_from_form;
825 $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
826 $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
828 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
830 $report->set_options(
831 raw_top_info_text => $self->render('bank_transactions/report_top', { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
832 raw_bottom_info_text => $self->render('bank_transactions/report_bottom', { output => 0 }),
836 sub _existing_record_link {
837 my ($bt, $invoice) = @_;
839 # check whether a record link from banktransaction $bt already exists to
840 # invoice $invoice, returns 1 if that is the case
842 die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
844 my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
845 my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ] );
847 return @$linked_records ? 1 : 0;
850 sub init_problems { [] }
855 SL::Controller::Helper::GetModels->new(
860 dir => 0, # 1 = ASC, 0 = DESC : default sort is newest at top
862 transdate => t8('Transdate'),
863 remote_name => t8('Remote name'),
864 amount => t8('Amount'),
865 invoice_amount => t8('Assigned'),
866 invoices => t8('Linked invoices'),
867 valutadate => t8('Valutadate'),
868 remote_account_number => t8('Remote account number'),
869 remote_bank_code => t8('Remote bank code'),
870 currency => t8('Currency'),
871 purpose => t8('Purpose'),
872 local_account_number => t8('Local account number'),
873 local_bank_code => t8('Local bank code'),
874 local_bank_name => t8('Bank account'),
876 with_objects => [ 'local_bank_account', 'currency' ],
880 sub load_ap_record_template_url {
881 my ($self, $template) = @_;
883 return $self->url_for(
884 controller => 'ap.pl',
885 action => 'load_record_template',
887 'form_defaults.amount_1' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
888 'form_defaults.transdate' => $self->transaction->transdate_as_date,
889 'form_defaults.duedate' => $self->transaction->transdate_as_date,
890 'form_defaults.no_payment_bookings' => 1,
891 'form_defaults.paid_1_suggestion' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
892 'form_defaults.AP_paid_1_suggestion' => $self->transaction->local_bank_account->chart->accno,
893 'form_defaults.callback' => $self->callback,
897 sub load_gl_record_template_url {
898 my ($self, $template) = @_;
900 return $self->url_for(
901 controller => 'gl.pl',
902 action => 'load_record_template',
904 'form_defaults.amount_1' => abs($self->transaction->amount), # always positive
905 'form_defaults.transdate' => $self->transaction->transdate_as_date,
906 'form_defaults.callback' => $self->callback,
910 sub setup_search_action_bar {
911 my ($self, %params) = @_;
913 for my $bar ($::request->layout->get('actionbar')) {
917 submit => [ '#search_form', { action => 'BankTransaction/list' } ],
918 accesskey => 'enter',
924 sub setup_list_all_action_bar {
925 my ($self, %params) = @_;
927 for my $bar ($::request->layout->get('actionbar')) {
931 submit => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
932 accesskey => 'enter',
947 SL::Controller::BankTransaction - Posting payments to invoices from
948 bank transactions imported earlier
954 =item C<save_single_bank_transaction %params>
956 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
957 tries to post its amount to a certain number of invoices (parameter
958 C<invoice_ids>, an array ref of database IDs to purchase or sales
961 The whole function is wrapped in a database transaction. If an
962 exception occurs the bank transaction is not posted at all. The same
963 is true if the code detects an error during the execution, e.g. a bank
964 transaction that's already been posted earlier. In both cases the
965 database transaction will be rolled back.
967 If warnings but not errors occur the database transaction is still
970 The return value is an error object or C<undef> if the function
971 succeeded. The calling function will collect all warnings and errors
972 and display them in a nicely formatted table if any occurred.
974 An error object is a hash reference containing the following members:
978 =item * C<result> — can be either C<warning> or C<error>. Warnings are
979 displayed slightly different than errors.
981 =item * C<message> — a human-readable message included in the list of
982 errors meant as the description of why the problem happened
984 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
985 that the function was called with
987 =item * C<bank_transaction> — the database object
988 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
990 =item * C<invoices> — an array ref of the database objects (either
991 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
1000 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
1001 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>