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 $main::lxdebug->message(LXDebug->DEBUG2(),"count bt=".scalar(@{$bank_transactions}." bank_account=".$bank_account->id." chart=".$bank_account->chart_id));
106 # credit notes have a negative amount, treat differently
107 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => [ or => [ amount => { gt => \'paid' },
108 and => [ type => 'credit_note',
109 amount => { lt => \'paid' }
113 with_objects => ['customer','payment_terms']);
115 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { ne => \'paid' }], with_objects => ['vendor' ,'payment_terms']);
116 my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $bank_account->chart_id ,
117 'sepa_export.executed' => 0, 'sepa_export.closed' => 0 ], with_objects => ['sepa_export']);
118 $main::lxdebug->message(LXDebug->DEBUG2(),"count sepaexport=".scalar(@{$all_open_sepa_export_items}));
120 my @all_open_invoices;
121 # filter out invoices with less than 1 cent outstanding
122 push @all_open_invoices, map { $_->{is_ar}=1 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
123 push @all_open_invoices, map { $_->{is_ar}=0 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
124 $main::lxdebug->message(LXDebug->DEBUG2(),"bank_account=".$::form->{filter}{bank_account}." invoices: ".scalar(@{ $all_open_ar_invoices }).
125 " + ".scalar(@{ $all_open_ap_invoices })." non fully paid=".scalar(@all_open_invoices)." transactions=".scalar(@{ $bank_transactions }));
127 my @all_sepa_invoices;
128 my @all_non_sepa_invoices;
130 # first collect sepa export items to open invoices
131 foreach my $open_invoice (@all_open_invoices){
132 # my @items = grep { $_->ap_id == $open_invoice->id || $_->ar_id == $open_invoice->id } @{$all_open_sepa_export_items};
133 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount,2);
134 $open_invoice->{skonto_type} = 'without_skonto';
135 foreach ( @{$all_open_sepa_export_items}) {
136 if ( $_->ap_id == $open_invoice->id || $_->ar_id == $open_invoice->id ) {
137 my $factor = ($_->ar_id == $open_invoice->id?1:-1);
138 $main::lxdebug->message(LXDebug->DEBUG2(),"exitem=".$_->id." for invoice ".$open_invoice->id." factor=".$factor);
139 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);
140 $open_invoice->{sepa_export_item} = $_ ;
141 $open_invoice->{skonto_type} = $_->payment_type;
142 $sepa_exports{$_->sepa_export_id} ||= { count => 0, is_ar => 0, amount => 0, proposed => 0, invoices => [], item => $_ };
143 $sepa_exports{$_->sepa_export_id}->{count}++ ;
144 $sepa_exports{$_->sepa_export_id}->{is_ar}++ if $_->ar_id == $open_invoice->id;
145 $sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor;
146 push @{ $sepa_exports{$_->sepa_export_id}->{invoices} }, $open_invoice;
147 #$main::lxdebug->message(LXDebug->DEBUG2(),"amount for export id ".$_->sepa_export_id." = ".
148 # $sepa_exports{$_->sepa_export_id}->{amount}." count = ".
149 # $sepa_exports{$_->sepa_export_id}->{count}." is_ar = ".
150 # $sepa_exports{$_->sepa_export_id}->{is_ar} );
151 push @all_sepa_invoices , $open_invoice;
154 push @all_non_sepa_invoices , $open_invoice if ! $open_invoice->{sepa_export_item};
157 # try to match each bank_transaction with each of the possible open invoices
161 foreach my $bt (@{ $bank_transactions }) {
162 ## 5 Stellen hinter dem Komma auf 2 Stellen reduzieren
163 $bt->amount($bt->amount*1);
164 $bt->invoice_amount($bt->invoice_amount*1);
165 $main::lxdebug->message(LXDebug->DEBUG2(),"BT ".$bt->id." amount=".$bt->amount." invoice_amount=".$bt->invoice_amount." remote=". $bt->{remote_name});
167 $bt->{proposals} = [];
168 $bt->{rule_matches} = [];
170 $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
172 if ( $self->is_collective_transaction($bt) ) {
173 foreach ( keys %sepa_exports) {
174 #$main::lxdebug->message(LXDebug->DEBUG2(),"Exp ID=".$_." compare sum amount ".($sepa_exports{$_}->{amount} *1) ." == ".($bt->amount * 1));
175 if ( $bt->transaction_code eq '191' && abs(($sepa_exports{$_}->{amount} * 1) - ($bt->amount * 1)) < 0.01 ) {
177 @{$bt->{proposals}} = @{$sepa_exports{$_}->{invoices}};
178 $bt->{agreement} = 20;
179 push(@{$bt->{rule_matches}},'sepa_export_item(20)');
180 $sepa_exports{$_}->{proposed}=1;
181 #$main::lxdebug->message(LXDebug->DEBUG2(),"has ".scalar($bt->{proposals})." invoices");
182 push(@proposals, $bt);
187 next unless $bt->{remote_name}; # bank has no name, usually fees, use create invoice to assign
189 foreach ( @{$all_open_sepa_export_items}) {
190 last if scalar (@all_sepa_invoices) == 0;
191 foreach my $open_invoice (@all_sepa_invoices){
192 $open_invoice->{agreement} = 0;
193 $open_invoice->{rule_matches} ='';
194 if ( $_->ap_id == $open_invoice->id || $_->ar_id == $open_invoice->id ) {
195 #$main::lxdebug->message(LXDebug->DEBUG2(),"exitem2=".$_->id." for invoice ".$open_invoice->id);
196 my $factor = ( $_->ar_id == $open_invoice->id?1:-1);
197 $_->amount($_->amount*1);
198 #$main::lxdebug->message(LXDebug->DEBUG2(),"remote account '".$bt->{remote_account_number}."' bt_amount=".$bt->amount." factor=".$factor);
199 #$main::lxdebug->message(LXDebug->DEBUG2(),"compare with '".$_->vc_iban."' amount=".$_->amount);
200 if ( $bt->{remote_account_number} eq $_->vc_iban && abs(abs($_->amount) - abs($bt->amount)) < 0.01 ) {
202 $iban = $open_invoice->customer->iban if $open_invoice->is_sales;
203 $iban = $open_invoice->vendor->iban if ! $open_invoice->is_sales;
204 if($bt->{remote_account_number} eq $iban && abs(abs($open_invoice->amount) - abs($bt->amount)) < 0.01 ) {
205 ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
206 $open_invoice->{agreement} += 5;
207 $open_invoice->{rule_matches} .= 'sepa_export_item(5) ';
208 $main::lxdebug->message(LXDebug->DEBUG2(),"sepa invoice_id=".$open_invoice->id." agreement=".$open_invoice->{agreement}." rules matches=".$open_invoice->{rule_matches});
209 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);
216 # try to match the current $bt to each of the open_invoices, saving the
217 # results of get_agreement_with_invoice in $open_invoice->{agreement} and
218 # $open_invoice->{rule_matches}.
220 # The values are overwritten each time a new bt is checked, so at the end
221 # of each bt the likely results are filtered and those values are stored in
222 # the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
223 # score is stored in $bt->{agreement}
225 foreach my $open_invoice (@all_non_sepa_invoices){
226 ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
227 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*($open_invoice->{is_ar}?1:-1),2);
228 $main::lxdebug->message(LXDebug->DEBUG2(),"nons invoice_id=".$open_invoice->id." amount=".$open_invoice->amount." agreement=".$open_invoice->{agreement}." rules matches=".$open_invoice->{rule_matches}) if $open_invoice->{agreement} > 2;
232 my $min_agreement = 3; # suggestions must have at least this score
234 my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
236 # add open_invoices with highest agreement into array $bt->{proposals}
237 if ( $max_agreement >= $min_agreement ) {
238 $bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
239 $bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
241 # store the rule_matches in a separate array, so they can be displayed in template
242 foreach ( @{ $bt->{proposals} } ) {
243 push(@{$bt->{rule_matches}}, $_->{rule_matches});
249 # separate filter for proposals (second tab, agreement >= 5 and exactly one match)
250 # to qualify as a proposal there has to be
251 # * agreement >= 5 TODO: make threshold configurable in configuration
252 # * there must be only one exact match
253 # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
254 my $proposal_threshold = 5;
255 my @otherproposals = grep {
256 ($_->{agreement} >= $proposal_threshold)
257 && (1 == scalar @{ $_->{proposals} })
258 && (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01
259 : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01)
260 } @{ $bank_transactions };
262 push ( @proposals, @otherproposals);
264 # sort bank transaction proposals by quality (score) of proposal
265 $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 1;
266 $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 0;
268 $::request->layout->add_javascripts("kivi.BankTransaction.js");
269 $self->render('bank_transactions/list',
270 title => t8('Bank transactions MT940'),
271 BANK_TRANSACTIONS => $bank_transactions,
272 PROPOSALS => \@proposals,
273 bank_account => $bank_account,
274 ui_tab => scalar(@proposals) > 0?1:0,
278 sub action_assign_invoice {
281 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
283 $self->render('bank_transactions/assign_invoice',
285 title => t8('Assign invoice'),);
288 sub action_create_invoice {
290 my %myconfig = %main::myconfig;
292 $self->transaction(SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id}));
294 my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->transaction->{remote_account_number});
295 my $use_vendor_filter = $self->transaction->{remote_account_number} && $vendor_of_transaction;
297 my $templates = SL::DB::Manager::RecordTemplate->get_all(
298 where => [ template_type => 'ap_transaction' ],
299 with_objects => [ qw(employee vendor) ],
303 $templates = [ grep { $_->vendor_id == $vendor_of_transaction->id } @{ $templates } ] if $use_vendor_filter;
305 $self->callback($self->url_for(
307 'filter.bank_account' => $::form->{filter}->{bank_account},
308 'filter.todate' => $::form->{filter}->{todate},
309 'filter.fromdate' => $::form->{filter}->{fromdate},
313 'bank_transactions/create_invoice',
315 title => t8('Create invoice'),
316 TEMPLATES => $templates,
317 vendor_id => $use_vendor_filter ? $vendor_of_transaction->id : undef,
318 vendor_name => $use_vendor_filter ? $vendor_of_transaction->name : undef,
322 sub action_ajax_payment_suggestion {
325 # based on a BankTransaction ID and a Invoice or PurchaseInvoice ID passed via $::form,
326 # create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
327 # and return encoded as JSON
329 my $bt = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
330 my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
332 die unless $bt and $invoice;
334 my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
337 $html = $self->render(
338 'bank_transactions/_payment_suggestion', { output => 0 },
339 bt_id => $::form->{bt_id},
340 prop_id => $::form->{prop_id},
342 SELECT_OPTIONS => \@select_options,
345 $self->render(\ SL::JSON::to_json( { 'html' => "$html" } ), { layout => 0, type => 'json', process => 0 });
348 sub action_filter_templates {
351 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
352 my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
355 push @filter, ('vendor.id' => $::form->{vendor_id}) if $::form->{vendor_id};
356 push @filter, ('vendor.name' => { ilike => '%' . $::form->{vendor} . '%' }) if $::form->{vendor};
358 my $templates = SL::DB::Manager::RecordTemplate->get_all(
359 where => [ template_type => 'ap_transaction', (or => \@filter) x !!@filter ],
360 with_objects => [ qw(employee vendor) ],
363 $::form->{filter} //= {};
365 $self->callback($self->url_for(
367 'filter.bank_account' => $::form->{filter}->{bank_account},
368 'filter.todate' => $::form->{filter}->{todate},
369 'filter.fromdate' => $::form->{filter}->{fromdate},
372 my $output = $self->render(
373 'bank_transactions/_template_list',
375 TEMPLATES => $templates,
378 $self->render(\to_json({ html => $output }), { type => 'json', process => 0 });
381 sub action_ajax_add_list {
384 my @where_sale = (amount => { ne => \'paid' });
385 my @where_purchase = (amount => { ne => \'paid' });
387 if ($::form->{invnumber}) {
388 push @where_sale, (invnumber => { ilike => like($::form->{invnumber})});
389 push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
392 if ($::form->{amount}) {
393 push @where_sale, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
394 push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
397 if ($::form->{vcnumber}) {
398 push @where_sale, ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
399 push @where_purchase, ('vendor.vendornumber' => { ilike => like($::form->{vcnumber})});
402 if ($::form->{vcname}) {
403 push @where_sale, ('customer.name' => { ilike => like($::form->{vcname})});
404 push @where_purchase, ('vendor.name' => { ilike => like($::form->{vcname})});
407 if ($::form->{transdatefrom}) {
408 my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
409 if ( ref($fromdate) eq 'DateTime' ) {
410 push @where_sale, ('transdate' => { ge => $fromdate});
411 push @where_purchase, ('transdate' => { ge => $fromdate});
415 if ($::form->{transdateto}) {
416 my $todate = $::locale->parse_date_to_object($::form->{transdateto});
417 if ( ref($todate) eq 'DateTime' ) {
418 $todate->add(days => 1);
419 push @where_sale, ('transdate' => { lt => $todate});
420 push @where_purchase, ('transdate' => { lt => $todate});
424 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => \@where_sale, with_objects => 'customer');
425 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
427 my @all_open_invoices = @{ $all_open_ar_invoices };
428 # add ap invoices, filtering out subcent open amounts
429 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
431 @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
433 my $output = $self->render(
434 'bank_transactions/add_list',
436 INVOICES => \@all_open_invoices,
439 my %result = ( count => 0, html => $output );
441 $self->render(\to_json(\%result), { type => 'json', process => 0 });
444 sub action_ajax_accept_invoices {
447 my @selected_invoices;
448 foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
449 my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
450 push @selected_invoices, $invoice_object;
454 'bank_transactions/invoices',
456 INVOICES => \@selected_invoices,
457 bt_id => $::form->{bt_id},
464 return 0 if !$::form->{invoice_ids};
466 my %invoice_hash = %{ delete $::form->{invoice_ids} }; # each key (the bt line with a bt_id) contains an array of invoice_ids
468 # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
481 # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
483 # '44' => [ '50', '51', 52' ]
486 $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
488 # a bank_transaction may be assigned to several invoices, i.e. a customer
489 # might pay several open invoices with one transaction
495 if ( $::form->{proposal_ids} ) {
496 foreach (@{ $::form->{proposal_ids} }) {
497 my $bank_transaction_id = $_;
498 my $invoice_ids = $invoice_hash{$_};
499 push @{ $self->problems }, $self->save_single_bank_transaction(
500 bank_transaction_id => $bank_transaction_id,
501 invoice_ids => $invoice_ids,
502 sources => ($::form->{sources} // {})->{$_},
503 memos => ($::form->{memos} // {})->{$_},
505 $count += scalar( @{$invoice_ids} );
508 while ( my ($bank_transaction_id, $invoice_ids) = each(%invoice_hash) ) {
509 push @{ $self->problems }, $self->save_single_bank_transaction(
510 bank_transaction_id => $bank_transaction_id,
511 invoice_ids => $invoice_ids,
512 sources => [ map { $::form->{"sources_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
513 memos => [ map { $::form->{"memos_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
515 $count += scalar( @{$invoice_ids} );
518 foreach (@{ $self->problems }) {
519 $count-- if $_->{result} eq 'error';
524 sub action_save_invoices {
526 my $count = $self->save_invoices();
528 flash('ok', t8('#1 invoice(s) saved.', $count));
530 $self->action_list();
533 sub action_save_proposals {
536 if ( $::form->{proposal_ids} ) {
537 my $propcount = scalar(@{ $::form->{proposal_ids} });
538 if ( $propcount > 0 ) {
539 my $count = $self->save_invoices();
541 flash('ok', t8('#1 proposal(s) with #2 invoice(s) saved.', $propcount, $count));
544 $self->action_list();
548 sub is_collective_transaction {
549 my ($self, $bt) = @_;
550 return $bt->transaction_code eq "191";
553 sub save_single_bank_transaction {
554 my ($self, %params) = @_;
558 bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
562 if (!$data{bank_transaction}) {
566 message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
573 my $bt_id = $data{bank_transaction_id};
574 my $bank_transaction = $data{bank_transaction};
575 my $sign = $bank_transaction->amount < 0 ? -1 : 1;
576 my $amount_of_transaction = $sign * $bank_transaction->amount;
577 my $payment_received = $bank_transaction->amount > 0;
578 my $payment_sent = $bank_transaction->amount < 0;
581 foreach my $invoice_id (@{ $params{invoice_ids} }) {
582 my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
587 message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
590 push @{ $data{invoices} }, $invoice;
593 if ( $payment_received
594 && any { ( $_->is_sales && ($_->amount < 0))
595 || (!$_->is_sales && ($_->amount > 0))
596 } @{ $data{invoices} }) {
600 message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
605 && any { ( $_->is_sales && ($_->amount > 0))
606 || (!$_->is_sales && ($_->amount < 0) && ($_->invoice_type eq 'purchase_invoice'))
607 } @{ $data{invoices} }) {
611 message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
615 my $max_invoices = scalar(@{ $data{invoices} });
618 foreach my $invoice (@{ $data{invoices} }) {
619 my $source = ($data{sources} // [])->[$n_invoices];
620 my $memo = ($data{memos} // [])->[$n_invoices];
624 # Check if bank_transaction already has a link to the invoice, may only be linked once per invoice
625 # This might be caused by the user reloading a page and resending the form
626 if (_existing_record_link($bank_transaction, $invoice)) {
630 message => $::locale->text("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name),
634 if (!$amount_of_transaction && $invoice->open_amount) {
638 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."),
643 if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
644 $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
646 $payment_type = 'without_skonto';
650 # pay invoice or go to the next bank transaction if the amount is not sufficiently high
651 if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) {
652 my $open_amount = ($payment_type eq 'with_skonto_pt'?$invoice->amount_less_skonto:$invoice->open_amount);
653 # first calculate new bank transaction amount ...
654 if ($invoice->is_sales) {
655 $amount_of_transaction -= $sign * $open_amount;
656 $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $open_amount);
658 $amount_of_transaction += $sign * $open_amount;
659 $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $open_amount);
661 # ... and then pay the invoice
662 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
663 trans_id => $invoice->id,
664 amount => $open_amount,
665 payment_type => $payment_type,
668 transdate => $bank_transaction->transdate->to_kivitendo);
669 } elsif (( $invoice->is_sales && $invoice->invoice_type eq 'credit_note' ) ||
670 (!$invoice->is_sales && $invoice->invoice_type eq 'ap_transaction' )) {
671 # no check for overpayment/multiple payments
673 # 1. $invoice->open_amount is arap.amount - ararp.paid (always positive!)
674 # 2. $bank_transaction->amount is negative for outgoing transactions and positive for
675 # incoming transactions.
676 # 1. and 2. => we have to turn the sign for invoice_amount in bank_transactions
677 # for verifying expected data, check t/bank/bank_transactions.t
678 $bank_transaction->invoice_amount($invoice->open_amount * -1);
680 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
681 trans_id => $invoice->id,
682 amount => $invoice->open_amount,
683 payment_type => $payment_type,
686 transdate => $bank_transaction->transdate->to_kivitendo);
687 } else { # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary
688 my $overpaid_amount = $amount_of_transaction - $invoice->open_amount;
689 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
690 trans_id => $invoice->id,
691 amount => $amount_of_transaction,
692 payment_type => $payment_type,
695 transdate => $bank_transaction->transdate->to_kivitendo);
696 $bank_transaction->invoice_amount($bank_transaction->amount);
697 $amount_of_transaction = 0;
699 if ($overpaid_amount >= 0.01) {
703 message => $::locale->text('Invoice #1 was overpaid by #2.', $invoice->invnumber, $::form->format_amount(\%::myconfig, $overpaid_amount, 2)),
707 # Record a record link from the bank transaction to the invoice
709 from_table => 'bank_transactions',
711 to_table => $invoice->is_sales ? 'ar' : 'ap',
712 to_id => $invoice->id,
715 SL::DB::RecordLink->new(@props)->save;
717 # "close" a sepa_export_item if it exists
718 # code duplicated in action_save_proposals!
719 # currently only works, if there is only exactly one open sepa_export_item
720 if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
721 if ( scalar @$seis == 1 ) {
722 # moved the execution and the check for sepa_export into a method,
723 # this isn't part of a transaction, though
724 $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
729 $bank_transaction->save;
731 # 'undef' means 'no error' here.
736 my $rez = $data{bank_transaction}->db->with_transaction(sub {
738 $error = $worker->();
749 # Rollback Fehler nicht weiterreichen
753 return grep { $_ } ($error, @warnings);
761 $::auth->assert('bank_transaction');
768 sub make_filter_summary {
771 my $filter = $::form->{filter} || {};
775 [ $filter->{"transdate:date::ge"}, $::locale->text('Transdate') . " " . $::locale->text('From Date') ],
776 [ $filter->{"transdate:date::le"}, $::locale->text('Transdate') . " " . $::locale->text('To Date') ],
777 [ $filter->{"valutadate:date::ge"}, $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
778 [ $filter->{"valutadate:date::le"}, $::locale->text('Valutadate') . " " . $::locale->text('To Date') ],
779 [ $filter->{"amount:number"}, $::locale->text('Amount') ],
780 [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account') ],
784 push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
787 $self->{filter_summary} = join ', ', @filter_strings;
793 my $callback = $self->models->get_callback;
795 my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
796 $self->{report} = $report;
798 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);
799 my @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code);
802 transdate => { sub => sub { $_[0]->transdate_as_date } },
803 valutadate => { sub => sub { $_[0]->valutadate_as_date } },
805 remote_account_number => { },
806 remote_bank_code => { },
807 amount => { sub => sub { $_[0]->amount_as_number },
809 invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number },
811 invoices => { sub => sub { $_[0]->linked_invoices } },
812 currency => { sub => sub { $_[0]->currency->name } },
814 local_account_number => { sub => sub { $_[0]->local_bank_account->account_number } },
815 local_bank_code => { sub => sub { $_[0]->local_bank_account->bank_code } },
816 local_bank_name => { sub => sub { $_[0]->local_bank_account->name } },
820 map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
822 $report->set_options(
823 std_column_visibility => 1,
824 controller_class => 'BankTransaction',
825 output_format => 'HTML',
826 top_info_text => $::locale->text('Bank transactions'),
827 title => $::locale->text('Bank transactions'),
828 allow_pdf_export => 1,
829 allow_csv_export => 1,
831 $report->set_columns(%column_defs);
832 $report->set_column_order(@columns);
833 $report->set_export_options(qw(list_all filter));
834 $report->set_options_from_form;
835 $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
836 $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
838 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
840 $report->set_options(
841 raw_top_info_text => $self->render('bank_transactions/report_top', { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
842 raw_bottom_info_text => $self->render('bank_transactions/report_bottom', { output => 0 }),
846 sub _existing_record_link {
847 my ($bt, $invoice) = @_;
849 # check whether a record link from banktransaction $bt already exists to
850 # invoice $invoice, returns 1 if that is the case
852 die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
854 my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
855 my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ] );
857 return @$linked_records ? 1 : 0;
860 sub init_problems { [] }
865 SL::Controller::Helper::GetModels->new(
870 dir => 0, # 1 = ASC, 0 = DESC : default sort is newest at top
872 transdate => t8('Transdate'),
873 remote_name => t8('Remote name'),
874 amount => t8('Amount'),
875 invoice_amount => t8('Assigned'),
876 invoices => t8('Linked invoices'),
877 valutadate => t8('Valutadate'),
878 remote_account_number => t8('Remote account number'),
879 remote_bank_code => t8('Remote bank code'),
880 currency => t8('Currency'),
881 purpose => t8('Purpose'),
882 local_account_number => t8('Local account number'),
883 local_bank_code => t8('Local bank code'),
884 local_bank_name => t8('Bank account'),
886 with_objects => [ 'local_bank_account', 'currency' ],
890 sub load_ap_record_template_url {
891 my ($self, $template) = @_;
893 return $self->url_for(
894 controller => 'ap.pl',
895 action => 'load_record_template',
897 'form_defaults.amount_1' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
898 'form_defaults.transdate' => $self->transaction->transdate_as_date,
899 'form_defaults.duedate' => $self->transaction->transdate_as_date,
900 'form_defaults.datepaid_1' => $self->transaction->transdate_as_date,
901 'form_defaults.paid_1' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
902 'form_defaults.currency' => $self->transaction->currency->name,
903 'form_defaults.AP_paid_1' => $self->transaction->local_bank_account->chart->accno,
904 'form_defaults.callback' => $self->callback,
908 sub setup_search_action_bar {
909 my ($self, %params) = @_;
911 for my $bar ($::request->layout->get('actionbar')) {
915 submit => [ '#search_form', { action => 'BankTransaction/list' } ],
916 accesskey => 'enter',
922 sub setup_list_all_action_bar {
923 my ($self, %params) = @_;
925 for my $bar ($::request->layout->get('actionbar')) {
929 submit => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
930 accesskey => 'enter',
945 SL::Controller::BankTransaction - Posting payments to invoices from
946 bank transactions imported earlier
952 =item C<save_single_bank_transaction %params>
954 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
955 tries to post its amount to a certain number of invoices (parameter
956 C<invoice_ids>, an array ref of database IDs to purchase or sales
959 The whole function is wrapped in a database transaction. If an
960 exception occurs the bank transaction is not posted at all. The same
961 is true if the code detects an error during the execution, e.g. a bank
962 transaction that's already been posted earlier. In both cases the
963 database transaction will be rolled back.
965 If warnings but not errors occur the database transaction is still
968 The return value is an error object or C<undef> if the function
969 succeeded. The calling function will collect all warnings and errors
970 and display them in a nicely formatted table if any occurred.
972 An error object is a hash reference containing the following members:
976 =item * C<result> — can be either C<warning> or C<error>. Warnings are
977 displayed slightly different than errors.
979 =item * C<message> — a human-readable message included in the list of
980 errors meant as the description of why the problem happened
982 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
983 that the function was called with
985 =item * C<bank_transaction> — the database object
986 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
988 =item * C<invoices> — an array ref of the database objects (either
989 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
998 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
999 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>