1 package SL::Controller::BankTransaction;
3 # idee- möglichkeit bankdaten zu übernehmen in stammdaten
4 # erst Kontenabgleich, um alle gl-Einträge wegzuhaben
7 use parent qw(SL::Controller::Base);
9 use SL::Controller::Helper::GetModels;
10 use SL::Controller::Helper::ReportGenerator;
11 use SL::ReportGenerator;
13 use SL::DB::BankTransaction;
14 use SL::Helper::Flash;
15 use SL::Locale::String;
18 use SL::DB::PurchaseInvoice;
19 use SL::DB::RecordLink;
22 use SL::DB::AccTransaction;
25 use SL::DB::BankAccount;
26 use SL::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 --get_set_init' => [ qw(models problems) ],
38 __PACKAGE__->run_before('check_auth');
48 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
50 $self->render('bank_transactions/search',
51 BANK_ACCOUNTS => $bank_accounts);
57 $self->make_filter_summary;
58 $self->prepare_report;
60 $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
66 if (!$::form->{filter}{bank_account}) {
67 flash('error', t8('No bank account chosen!'));
72 my $sort_by = $::form->{sort_by} || 'transdate';
73 $sort_by = 'transdate' if $sort_by eq 'proposal';
74 $sort_by .= $::form->{sort_dir} ? ' DESC' : ' ASC';
76 my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
77 my $todate = $::locale->parse_date_to_object($::form->{filter}->{todate});
78 $todate->add( days => 1 ) if $todate;
81 push @where, (transdate => { ge => $fromdate }) if ($fromdate);
82 push @where, (transdate => { lt => $todate }) if ($todate);
83 my $bank_account = SL::DB::Manager::BankAccount->find_by( id => $::form->{filter}{bank_account} );
84 # bank_transactions no younger than starting date,
85 # including starting date (same search behaviour as fromdate)
86 # but OPEN invoices to be matched may be from before
87 if ( $bank_account->reconciliation_starting_date ) {
88 push @where, (transdate => { ge => $bank_account->reconciliation_starting_date });
91 my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
92 with_objects => [ 'local_bank_account', 'currency' ],
96 amount => {ne => \'invoice_amount'},
97 local_bank_account_id => $::form->{filter}{bank_account},
101 $main::lxdebug->message(LXDebug->DEBUG2(),"count bt=".scalar(@{$bank_transactions}." bank_account=".$bank_account->id." chart=".$bank_account->chart_id));
103 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => [amount => { gt => \'paid' }], with_objects => ['customer','payment_terms']);
104 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { gt => \'paid' }], with_objects => ['vendor' ,'payment_terms']);
105 my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $bank_account->chart_id ,
106 'sepa_export.executed' => 0, 'sepa_export.closed' => 0 ], with_objects => ['sepa_export']);
107 $main::lxdebug->message(LXDebug->DEBUG2(),"count sepaexport=".scalar(@{$all_open_sepa_export_items}));
109 my @all_open_invoices;
110 # filter out invoices with less than 1 cent outstanding
111 push @all_open_invoices, map { $_->{is_ar}=1 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
112 push @all_open_invoices, map { $_->{is_ar}=0 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
113 $main::lxdebug->message(LXDebug->DEBUG2(),"bank_account=".$::form->{filter}{bank_account}." invoices: ".scalar(@{ $all_open_ar_invoices }).
114 " + ".scalar(@{ $all_open_ap_invoices })." transactions=".scalar(@{ $bank_transactions }));
116 my @all_sepa_invoices;
117 my @all_non_sepa_invoices;
119 # first collect sepa export items to open invoices
120 foreach my $open_invoice (@all_open_invoices){
121 # my @items = grep { $_->ap_id == $open_invoice->id || $_->ar_id == $open_invoice->id } @{$all_open_sepa_export_items};
122 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount,2);
123 $open_invoice->{skonto_type} = 'without_skonto';
124 foreach ( @{$all_open_sepa_export_items}) {
125 if ( $_->ap_id == $open_invoice->id || $_->ar_id == $open_invoice->id ) {
126 my $factor = ( $_->ar_id == $open_invoice->id>0?1:-1);
127 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);
128 $main::lxdebug->message(LXDebug->DEBUG2(),"exitem=".$_->id." for invoice ".$open_invoice->id." factor=".$factor);
129 $open_invoice->{sepa_export_item} = $_ ;
130 $open_invoice->{skonto_type} = $_->payment_type;
131 $sepa_exports{$_->sepa_export_id} ||= { count => 0, is_ar => 0, amount => 0, proposed => 0, invoices => [], item => $_ };
132 $sepa_exports{$_->sepa_export_id}->{count}++ ;
133 $sepa_exports{$_->sepa_export_id}->{is_ar}++ if $_->ar_id == $open_invoice->id;
134 $sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor;
135 push ( @{ $sepa_exports{$_->sepa_export_id}->{invoices}} , $open_invoice );
136 #$main::lxdebug->message(LXDebug->DEBUG2(),"amount for export id ".$_->sepa_export_id." = ".
137 # $sepa_exports{$_->sepa_export_id}->{amount}." count = ".
138 # $sepa_exports{$_->sepa_export_id}->{count}." is_ar = ".
139 # $sepa_exports{$_->sepa_export_id}->{is_ar} );
140 push @all_sepa_invoices , $open_invoice;
143 push @all_non_sepa_invoices , $open_invoice if ! $open_invoice->{sepa_export_item};
146 # try to match each bank_transaction with each of the possible open invoices
148 @all_open_invoices = @all_non_sepa_invoices;
151 foreach my $bt (@{ $bank_transactions }) {
152 ## 5 Stellen hinter dem Komma auf 2 Stellen reduzieren
153 $bt->amount($bt->amount*1);
154 $bt->invoice_amount($bt->invoice_amount*1);
155 $main::lxdebug->message(LXDebug->DEBUG2(),"BT ".$bt->id." amount=".$bt->amount." invoice_amount=".$bt->invoice_amount." remote=". $bt->{remote_name});
157 $bt->{proposals} = [];
159 $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
161 if ( $self->is_collective_transaction($bt) ) {
162 foreach ( keys %sepa_exports) {
163 #$main::lxdebug->message(LXDebug->DEBUG2(),"Exp ID=".$_." compare sum amount ".($sepa_exports{$_}->{amount} *1) ." == ".($bt->amount * 1));
164 if ( $bt->transactioncode eq '191' && ($sepa_exports{$_}->{amount} * 1) eq ($bt->amount * 1) ) {
166 $bt->{proposals} = $sepa_exports{$_}->{invoices} ;
167 $bt->{agreement} = 20;
168 $bt->{rule_matches} = 'sepa_export_item(20)';
169 $sepa_exports{$_}->{proposed}=1;
170 #$main::lxdebug->message(LXDebug->DEBUG2(),"has ".scalar($bt->{proposals})." invoices");
171 push(@proposals, $bt);
176 next unless $bt->{remote_name}; # bank has no name, usually fees, use create invoice to assign
178 foreach ( @{$all_open_sepa_export_items}) {
179 last if scalar (@all_sepa_invoices) == 0;
180 foreach my $open_invoice (@all_sepa_invoices){
181 if ( $_->ap_id == $open_invoice->id || $_->ar_id == $open_invoice->id ) {
182 #$main::lxdebug->message(LXDebug->DEBUG2(),"exitem2=".$_->id." for invoice ".$open_invoice->id);
183 my $factor = ( $_->ar_id == $open_invoice->id?1:-1);
184 $_->amount($_->amount*1);
185 #$main::lxdebug->message(LXDebug->DEBUG2(),"remote account '".$bt->{remote_account_number}."' bt_amount=". ($bt->amount * $factor));
186 #$main::lxdebug->message(LXDebug->DEBUG2(),"compare with '".$_->vc_iban."' amount=".$_->amount);
187 if ( $bt->{remote_account_number} eq $_->vc_iban && $_->amount eq ($bt->amount * $factor)) {
188 push ($bt->{proposals},$open_invoice );
189 $bt->{agreement} = 20;
190 $bt->{rule_matches} = 'sepa_export_item(20)';
191 #$main::lxdebug->message(LXDebug->DEBUG2(),"found invoice");
192 @all_sepa_invoices = grep { $_ != $open_invoice } @all_sepa_invoices;
199 # try to match the current $bt to each of the open_invoices, saving the
200 # results of get_agreement_with_invoice in $open_invoice->{agreement} and
201 # $open_invoice->{rule_matches}.
203 # The values are overwritten each time a new bt is checked, so at the end
204 # of each bt the likely results are filtered and those values are stored in
205 # the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
206 # score is stored in $bt->{agreement}
208 foreach my $open_invoice (@all_open_invoices){
209 ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
210 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*($open_invoice->{is_ar}?1:-1),2);
211 # $main::lxdebug->message(LXDebug->DEBUG2(),"agreement=".$open_invoice->{agreement}." rules matches=".$open_invoice->{rule_matches});
215 my $min_agreement = 3; # suggestions must have at least this score
217 my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
219 # add open_invoices with highest agreement into array $bt->{proposals}
220 if ( $max_agreement >= $min_agreement ) {
221 $bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
222 $bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
224 # store the rule_matches in a separate array, so they can be displayed in template
225 foreach ( @{ $bt->{proposals} } ) {
226 push(@{$bt->{rule_matches}}, $_->{rule_matches});
232 # separate filter for proposals (second tab, agreement >= 5 and exactly one match)
233 # to qualify as a proposal there has to be
234 # * agreement >= 5 TODO: make threshold configurable in configuration
235 # * there must be only one exact match
236 # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
237 my $proposal_threshold = 5;
238 my @otherproposals = grep {
239 ($_->{agreement} >= $proposal_threshold)
240 && (1 == scalar @{ $_->{proposals} })
241 && (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01
242 : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01)
243 } @{ $bank_transactions };
245 push ( @proposals, @otherproposals);
247 # sort bank transaction proposals by quality (score) of proposal
248 $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 1;
249 $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 0;
252 $self->render('bank_transactions/list',
253 title => t8('Bank transactions MT940'),
254 BANK_TRANSACTIONS => $bank_transactions,
255 PROPOSALS => \@proposals,
256 bank_account => $bank_account,
257 ui_tab => scalar(@proposals) > 0?1:0,
261 sub action_assign_invoice {
264 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
266 $self->render('bank_transactions/assign_invoice',
268 title => t8('Assign invoice'),);
271 sub action_create_invoice {
273 my %myconfig = %main::myconfig;
275 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
276 my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
278 my $use_vendor_filter = $self->{transaction}->{remote_account_number} && $vendor_of_transaction;
280 my $drafts = SL::DB::Manager::Draft->get_all(where => [ module => 'ap'] , with_objects => 'employee');
284 foreach my $draft ( @{ $drafts } ) {
285 my $draft_as_object = YAML::Load($draft->form);
286 my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
287 $draft->{vendor} = $vendor->name;
288 $draft->{vendor_id} = $vendor->id;
289 push @filtered_drafts, $draft;
293 @filtered_drafts = grep { $_->{vendor_id} == $vendor_of_transaction->id } @filtered_drafts if $use_vendor_filter;
295 my $all_vendors = SL::DB::Manager::Vendor->get_all();
296 my $callback = $self->url_for(action => 'list',
297 'filter.bank_account' => $::form->{filter}->{bank_account},
298 'filter.todate' => $::form->{filter}->{todate},
299 'filter.fromdate' => $::form->{filter}->{fromdate});
302 'bank_transactions/create_invoice',
304 title => t8('Create invoice'),
305 DRAFTS => \@filtered_drafts,
306 vendor_id => $use_vendor_filter ? $vendor_of_transaction->id : undef,
307 vendor_name => $use_vendor_filter ? $vendor_of_transaction->name : undef,
308 ALL_VENDORS => $all_vendors,
309 limit => $myconfig{vclimit},
310 callback => $callback,
314 sub action_ajax_payment_suggestion {
317 # based on a BankTransaction ID and a Invoice or PurchaseInvoice ID passed via $::form,
318 # create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
319 # and return encoded as JSON
321 my $bt = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
322 my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
324 die unless $bt and $invoice;
326 my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
329 $html .= SL::Presenter->input_tag('invoice_ids.' . $::form->{bt_id} . '[]', $::form->{prop_id} , type => 'hidden');
330 $html .= SL::Presenter->escape(t8('Invno.') . ': ' . $invoice->invnumber . ' ');
331 $html .= SL::Presenter->escape(t8('Open amount') . ': ' . $::form->format_amount(\%::myconfig, $invoice->open_amount, 2) . ' ');
332 $html .= SL::Presenter->select_tag('invoice_skontos.' . $::form->{bt_id} . '[]',
334 value_key => 'payment_type',
335 title_key => 'display' )
337 $html .= '<a href=# onclick="delete_invoice(' . $::form->{bt_id} . ',' . $::form->{prop_id} . ');">x</a>';
338 $html = SL::Presenter->html_tag('div', $html, id => $::form->{bt_id} . '.' . $::form->{prop_id});
340 $self->render(\ SL::JSON::to_json( { 'html' => $html } ), { layout => 0, type => 'json', process => 0 });
343 sub action_filter_drafts {
346 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
347 my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
349 my $drafts = SL::DB::Manager::Draft->get_all(with_objects => 'employee');
353 foreach my $draft ( @{ $drafts } ) {
354 my $draft_as_object = YAML::Load($draft->form);
355 next unless $draft_as_object->{vendor_id}; # we cannot filter for vendor name, if this is a gl draft
357 my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
358 $draft->{vendor} = $vendor->name;
359 $draft->{vendor_id} = $vendor->id;
361 push @filtered_drafts, $draft;
364 my $vendor_name = $::form->{vendor};
365 my $vendor_id = $::form->{vendor_id};
368 @filtered_drafts = grep { $_->{vendor_id} == $vendor_id } @filtered_drafts if $vendor_id;
369 @filtered_drafts = grep { $_->{vendor} =~ /$vendor_name/i } @filtered_drafts if $vendor_name;
371 my $output = $self->render(
372 'bank_transactions/filter_drafts',
374 DRAFTS => \@filtered_drafts,
377 my %result = ( count => 0, html => $output );
379 $self->render(\to_json(\%result), { type => 'json', process => 0 });
382 sub action_ajax_add_list {
385 my @where_sale = (amount => { ne => \'paid' });
386 my @where_purchase = (amount => { ne => \'paid' });
388 if ($::form->{invnumber}) {
389 push @where_sale, (invnumber => { ilike => like($::form->{invnumber})});
390 push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
393 if ($::form->{amount}) {
394 push @where_sale, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
395 push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
398 if ($::form->{vcnumber}) {
399 push @where_sale, ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
400 push @where_purchase, ('vendor.vendornumber' => { ilike => like($::form->{vcnumber})});
403 if ($::form->{vcname}) {
404 push @where_sale, ('customer.name' => { ilike => like($::form->{vcname})});
405 push @where_purchase, ('vendor.name' => { ilike => like($::form->{vcname})});
408 if ($::form->{transdatefrom}) {
409 my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
410 if ( ref($fromdate) eq 'DateTime' ) {
411 push @where_sale, ('transdate' => { ge => $fromdate});
412 push @where_purchase, ('transdate' => { ge => $fromdate});
416 if ($::form->{transdateto}) {
417 my $todate = $::locale->parse_date_to_object($::form->{transdateto});
418 if ( ref($todate) eq 'DateTime' ) {
419 $todate->add(days => 1);
420 push @where_sale, ('transdate' => { lt => $todate});
421 push @where_purchase, ('transdate' => { lt => $todate});
425 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => \@where_sale, with_objects => 'customer');
426 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
428 my @all_open_invoices = @{ $all_open_ar_invoices };
429 # add ap invoices, filtering out subcent open amounts
430 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
432 @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
434 my $output = $self->render(
435 'bank_transactions/add_list',
437 INVOICES => \@all_open_invoices,
440 my %result = ( count => 0, html => $output );
442 $self->render(\to_json(\%result), { type => 'json', process => 0 });
445 sub action_ajax_accept_invoices {
448 my @selected_invoices;
449 foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
450 my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
451 push @selected_invoices, $invoice_object;
455 'bank_transactions/invoices',
457 INVOICES => \@selected_invoices,
458 bt_id => $::form->{bt_id},
465 return 0 if !$::form->{invoice_ids};
467 my %invoice_hash = %{ delete $::form->{invoice_ids} }; # each key (the bt line with a bt_id) contains an array of invoice_ids
469 # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
482 # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
484 # '44' => [ '50', '51', 52' ]
487 $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
489 # a bank_transaction may be assigned to several invoices, i.e. a customer
490 # might pay several open invoices with one transaction
496 if ( $::form->{proposal_ids} ) {
497 foreach (@{ $::form->{proposal_ids} }) {
498 my $bank_transaction_id = $_;
499 my $invoice_ids = $invoice_hash{$_};
500 push @{ $self->problems }, $self->save_single_bank_transaction(
501 bank_transaction_id => $bank_transaction_id,
502 invoice_ids => $invoice_ids,
504 $count += scalar( @{$invoice_ids} );
507 while ( my ($bank_transaction_id, $invoice_ids) = each(%invoice_hash) ) {
508 push @{ $self->problems }, $self->save_single_bank_transaction(
509 bank_transaction_id => $bank_transaction_id,
510 invoice_ids => $invoice_ids,
512 $count += scalar( @{$invoice_ids} );
518 sub action_save_invoices {
520 my $count = $self->save_invoices();
522 flash('ok', t8('#1 invoice(s) saved.', $count));
524 $self->action_list();
527 sub action_save_proposals {
529 if ( $::form->{proposal_ids} ) {
530 my $propcount = scalar(@{ $::form->{proposal_ids} });
531 if ( $propcount > 0 ) {
532 my $count = $self->save_invoices();
534 flash('ok', t8('#1 proposal(s) with #2 invoice(s) saved.', $propcount, $count));
537 $self->action_list();
541 sub is_collective_transaction {
542 my ($self, $bt) = @_;
543 return $bt->transactioncode eq "191";
546 sub save_single_bank_transaction {
547 my ($self, %params) = @_;
551 bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
555 if (!$data{bank_transaction}) {
559 message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
566 my $bt_id = $data{bank_transaction_id};
567 my $bank_transaction = $data{bank_transaction};
568 my $sign = $bank_transaction->amount < 0 ? -1 : 1;
569 my $amount_of_transaction = $sign * $bank_transaction->amount;
570 my $payment_received = $bank_transaction->amount > 0;
571 my $payment_sent = $bank_transaction->amount < 0;
573 foreach my $invoice_id (@{ $params{invoice_ids} }) {
574 my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
579 message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
583 push @{ $data{invoices} }, $invoice;
586 if ( $payment_received
587 && any { ( $_->is_sales && ($_->amount < 0))
588 || (!$_->is_sales && ($_->amount > 0))
589 } @{ $data{invoices} }) {
593 message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
598 && any { ( $_->is_sales && ($_->amount > 0))
599 || (!$_->is_sales && ($_->amount < 0))
600 } @{ $data{invoices} }) {
604 message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
608 my $max_invoices = scalar(@{ $data{invoices} });
611 foreach my $invoice (@{ $data{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';
640 # pay invoice or go to the next bank transaction if the amount is not sufficiently high
641 if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) {
642 my $open_amount = ($payment_type eq 'with_skonto_pt'?$invoice->amount_less_skonto:$invoice->open_amount);
643 # first calculate new bank transaction amount ...
644 if ($invoice->is_sales) {
645 $amount_of_transaction -= $sign * $open_amount;
646 $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $open_amount);
648 $amount_of_transaction += $sign * $open_amount;
649 $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $open_amount);
651 # ... and then pay the invoice
652 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
653 trans_id => $invoice->id,
654 amount => $open_amount,
655 payment_type => $payment_type,
656 transdate => $bank_transaction->transdate->to_kivitendo);
657 } else { # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary
658 my $overpaid_amount = $amount_of_transaction - $invoice->open_amount;
659 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
660 trans_id => $invoice->id,
661 amount => $amount_of_transaction,
662 payment_type => $payment_type,
663 transdate => $bank_transaction->transdate->to_kivitendo);
664 $bank_transaction->invoice_amount($bank_transaction->amount);
665 $amount_of_transaction = 0;
667 if ($overpaid_amount >= 0.01) {
671 message => $::locale->text('Invoice #1 was overpaid by #2.', $invoice->invnumber, $::form->format_amount(\%::myconfig, $overpaid_amount, 2)),
676 # Record a record link from the bank transaction to the invoice
678 from_table => 'bank_transactions',
680 to_table => $invoice->is_sales ? 'ar' : 'ap',
681 to_id => $invoice->id,
684 SL::DB::RecordLink->new(@props)->save;
686 # "close" a sepa_export_item if it exists
687 # code duplicated in action_save_proposals!
688 # currently only works, if there is only exactly one open sepa_export_item
689 if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
690 if ( scalar @$seis == 1 ) {
691 # moved the execution and the check for sepa_export into a method,
692 # this isn't part of a transaction, though
693 $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
698 $bank_transaction->save;
700 # 'undef' means 'no error' here.
705 my $rez = $data{bank_transaction}->db->with_transaction(sub {
707 $error = $worker->();
721 return grep { $_ } ($error, @warnings);
729 $::auth->assert('bank_transaction');
736 sub make_filter_summary {
739 my $filter = $::form->{filter} || {};
743 [ $filter->{"transdate:date::ge"}, $::locale->text('Transdate') . " " . $::locale->text('From Date') ],
744 [ $filter->{"transdate:date::le"}, $::locale->text('Transdate') . " " . $::locale->text('To Date') ],
745 [ $filter->{"valutadate:date::ge"}, $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
746 [ $filter->{"valutadate:date::le"}, $::locale->text('Valutadate') . " " . $::locale->text('To Date') ],
747 [ $filter->{"amount:number"}, $::locale->text('Amount') ],
748 [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account') ],
752 push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
755 $self->{filter_summary} = join ', ', @filter_strings;
761 my $callback = $self->models->get_callback;
763 my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
764 $self->{report} = $report;
766 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);
767 my @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code);
770 transdate => { sub => sub { $_[0]->transdate_as_date } },
771 valutadate => { sub => sub { $_[0]->valutadate_as_date } },
773 remote_account_number => { },
774 remote_bank_code => { },
775 amount => { sub => sub { $_[0]->amount_as_number },
777 invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number },
779 invoices => { sub => sub { $_[0]->linked_invoices } },
780 currency => { sub => sub { $_[0]->currency->name } },
782 local_account_number => { sub => sub { $_[0]->local_bank_account->account_number } },
783 local_bank_code => { sub => sub { $_[0]->local_bank_account->bank_code } },
784 local_bank_name => { sub => sub { $_[0]->local_bank_account->name } },
788 map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
790 $report->set_options(
791 std_column_visibility => 1,
792 controller_class => 'BankTransaction',
793 output_format => 'HTML',
794 top_info_text => $::locale->text('Bank transactions'),
795 title => $::locale->text('Bank transactions'),
796 allow_pdf_export => 1,
797 allow_csv_export => 1,
799 $report->set_columns(%column_defs);
800 $report->set_column_order(@columns);
801 $report->set_export_options(qw(list_all filter));
802 $report->set_options_from_form;
803 $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
804 $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
806 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
808 $report->set_options(
809 raw_top_info_text => $self->render('bank_transactions/report_top', { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
810 raw_bottom_info_text => $self->render('bank_transactions/report_bottom', { output => 0 }),
814 sub _existing_record_link {
815 my ($bt, $invoice) = @_;
817 # check whether a record link from banktransaction $bt already exists to
818 # invoice $invoice, returns 1 if that is the case
820 die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
822 my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
823 my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ] );
825 return @$linked_records ? 1 : 0;
828 sub init_problems { [] }
833 SL::Controller::Helper::GetModels->new(
838 dir => 0, # 1 = ASC, 0 = DESC : default sort is newest at top
840 transdate => t8('Transdate'),
841 remote_name => t8('Remote name'),
842 amount => t8('Amount'),
843 invoice_amount => t8('Assigned'),
844 invoices => t8('Linked invoices'),
845 valutadate => t8('Valutadate'),
846 remote_account_number => t8('Remote account number'),
847 remote_bank_code => t8('Remote bank code'),
848 currency => t8('Currency'),
849 purpose => t8('Purpose'),
850 local_account_number => t8('Local account number'),
851 local_bank_code => t8('Local bank code'),
852 local_bank_name => t8('Bank account'),
854 with_objects => [ 'local_bank_account', 'currency' ],
867 SL::Controller::BankTransaction - Posting payments to invoices from
868 bank transactions imported earlier
874 =item C<save_single_bank_transaction %params>
876 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
877 tries to post its amount to a certain number of invoices (parameter
878 C<invoice_ids>, an array ref of database IDs to purchase or sales
881 The whole function is wrapped in a database transaction. If an
882 exception occurs the bank transaction is not posted at all. The same
883 is true if the code detects an error during the execution, e.g. a bank
884 transaction that's already been posted earlier. In both cases the
885 database transaction will be rolled back.
887 If warnings but not errors occur the database transaction is still
890 The return value is an error object or C<undef> if the function
891 succeeded. The calling function will collect all warnings and errors
892 and display them in a nicely formatted table if any occurred.
894 An error object is a hash reference containing the following members:
898 =item * C<result> — can be either C<warning> or C<error>. Warnings are
899 displayed slightly different than errors.
901 =item * C<message> — a human-readable message included in the list of
902 errors meant as the description of why the problem happened
904 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
905 that the function was called with
907 =item * C<bank_transaction> — the database object
908 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
910 =item * C<invoices> — an array ref of the database objects (either
911 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
920 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
921 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>