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;
20 use SL::DB::ReconciliationLink;
23 use SL::DB::AccTransaction;
24 use SL::DB::BankTransactionAccTrans;
26 use SL::DB::BankAccount;
27 use SL::DB::GLTransaction;
28 use SL::DB::RecordTemplate;
29 use SL::DB::SepaExportItem;
30 use SL::DBUtils qw(like do_query);
32 use SL::Presenter::Tag qw(checkbox_tag html_tag);
34 use List::UtilsBy qw(partition_by);
35 use List::MoreUtils qw(any);
36 use List::Util qw(max);
38 use Rose::Object::MakeMethods::Generic
40 scalar => [ qw(callback transaction) ],
41 'scalar --get_set_init' => [ qw(models problems) ],
44 __PACKAGE__->run_before('check_auth');
54 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
56 $self->setup_search_action_bar;
57 $self->render('bank_transactions/search',
58 BANK_ACCOUNTS => $bank_accounts,
59 title => t8('Search bank transactions'),
66 $self->make_filter_summary;
67 $self->prepare_report;
69 $self->setup_list_all_action_bar;
70 $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
73 sub gather_bank_transactions_and_proposals {
74 my ($self, %params) = @_;
76 my $sort_by = $params{sort_by} || 'transdate';
77 $sort_by = 'transdate' if $sort_by eq 'proposal';
78 $sort_by .= $params{sort_dir} ? ' DESC' : ' ASC';
81 push @where, (transdate => { ge => $params{fromdate} }) if $params{fromdate};
82 push @where, (transdate => { lt => $params{todate} }) if $params{todate};
83 # bank_transactions no younger than starting date,
84 # including starting date (same search behaviour as fromdate)
85 # but OPEN invoices to be matched may be from before
86 if ( $params{bank_account}->reconciliation_starting_date ) {
87 push @where, (transdate => { ge => $params{bank_account}->reconciliation_starting_date });
90 my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
91 with_objects => [ 'local_bank_account', 'currency' ],
95 amount => {ne => \'invoice_amount'}, # '} make emacs happy
96 local_bank_account_id => $params{bank_account}->id,
102 my $has_batch_transaction = (grep { $_->is_batch_transaction } @{ $bank_transactions }) ? 1 : undef;
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' }, # '} make emacs happy
106 and => [ type => 'credit_note',
107 amount => { lt => \'paid' } # '} make emacs happy
111 with_objects => ['customer','payment_terms']);
113 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { ne => \'paid' }], # '}] make emacs happy
114 with_objects => ['vendor' ,'payment_terms']);
116 my @all_open_invoices;
117 # filter out invoices with less than 1 cent outstanding
118 push @all_open_invoices, map { $_->{is_ar}=1 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
119 push @all_open_invoices, map { $_->{is_ar}=0 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
122 my (%sepa_exports, %sepa_export_items_by_id, $all_open_sepa_export_items);
123 if ($has_batch_transaction) {
124 $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $params{bank_account}->chart_id ,
125 'sepa_export.executed' => 0,
126 'sepa_export.closed' => 0
128 with_objects => ['sepa_export']);
129 %sepa_export_items_by_id = partition_by { $_->ar_id || $_->ap_id } @$all_open_sepa_export_items;
131 # first collect sepa export items to open invoices
132 foreach my $open_invoice (@all_open_invoices){
133 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount,2);
134 $open_invoice->{skonto_type} = 'without_skonto';
135 foreach (@{ $sepa_export_items_by_id{ $open_invoice->id } || [] }) {
136 my $factor = ($_->ar_id == $open_invoice->id ? 1 : -1);
137 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);
139 $open_invoice->{skonto_type} = $_->payment_type;
140 $sepa_exports{$_->sepa_export_id} ||= { count => 0, is_ar => 0, amount => 0, proposed => 0, invoices => [], item => $_ };
141 $sepa_exports{$_->sepa_export_id}->{count}++;
142 $sepa_exports{$_->sepa_export_id}->{is_ar}++ if $_->ar_id == $open_invoice->id;
143 $sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor;
144 push @{ $sepa_exports{$_->sepa_export_id}->{invoices} }, $open_invoice;
148 # try to match each bank_transaction with each of the possible open invoices
152 foreach my $bt (@{ $bank_transactions }) {
153 ## 5 Stellen hinter dem Komma auf 2 Stellen reduzieren
154 $bt->amount($bt->amount*1);
155 $bt->invoice_amount($bt->invoice_amount*1);
157 $bt->{proposals} = [];
158 $bt->{rule_matches} = [];
160 $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
162 if ($has_batch_transaction && $bt->is_batch_transaction ) {
164 foreach ( keys %sepa_exports) {
165 if ( abs(($sepa_exports{$_}->{amount} * 1) - ($bt->amount * 1)) < 0.01 ) {
167 @{$bt->{proposals}} = @{$sepa_exports{$_}->{invoices}};
168 $bt->{sepa_export_ok} = 1;
169 $sepa_exports{$_}->{proposed}=1;
170 push(@proposals, $bt);
176 # batch transaction has no remotename !!
179 # try to match the current $bt to each of the open_invoices, saving the
180 # results of get_agreement_with_invoice in $open_invoice->{agreement} and
181 # $open_invoice->{rule_matches}.
183 # The values are overwritten each time a new bt is checked, so at the end
184 # of each bt the likely results are filtered and those values are stored in
185 # the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
186 # score is stored in $bt->{agreement}
188 foreach my $open_invoice (@all_open_invoices) {
190 ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
192 $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,
193 $open_invoice->amount * ($open_invoice->{is_ar} ? 1 : -1), 2);
197 my $min_agreement = 3; # suggestions must have at least this score
199 my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
201 # add open_invoices with highest agreement into array $bt->{proposals}
202 if ( $max_agreement >= $min_agreement ) {
203 $bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
204 $bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
206 # store the rule_matches in a separate array, so they can be displayed in template
207 foreach ( @{ $bt->{proposals} } ) {
208 push(@{$bt->{rule_matches}}, $_->{rule_matches});
214 # separate filter for proposals (second tab, agreement >= 5 and exactly one match)
215 # to qualify as a proposal there has to be
216 # * agreement >= 5 TODO: make threshold configurable in configuration
217 # * there must be only one exact match
218 my $proposal_threshold = 5;
219 my @otherproposals = grep {
220 ($_->{agreement} >= $proposal_threshold)
221 && (1 == scalar @{ $_->{proposals} })
222 && ($_->{proposals}->[0]->forex == 0) # nyi forex invoices for automatic booking
223 } @{ $bank_transactions };
225 push @proposals, @otherproposals;
227 # sort bank transaction proposals by quality (score) of proposal
228 if ($params{sort_by} && $params{sort_by} eq 'proposal') {
229 my $dir = $params{sort_dir} ? 1 : -1;
230 $bank_transactions = [ sort { ($a->{agreement} <=> $b->{agreement}) * $dir } @{ $bank_transactions } ];
233 return ( $bank_transactions , \@proposals );
239 if (!$::form->{filter}{bank_account}) {
240 flash('error', t8('No bank account chosen!'));
241 $self->action_search;
245 my $bank_account = SL::DB::BankAccount->load_cached($::form->{filter}->{bank_account});
246 my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
247 my $todate = $::locale->parse_date_to_object($::form->{filter}->{todate});
248 $todate->add( days => 1 ) if $todate;
250 my ($bank_transactions, $proposals) = $self->gather_bank_transactions_and_proposals(
251 bank_account => $bank_account,
252 fromdate => $fromdate,
254 sort_by => $::form->{sort_by},
255 sort_dir => $::form->{sort_dir},
258 my $ui_tab = $::instance_conf->get_no_bank_proposals ? 0
259 : scalar(@{ $proposals }) > 0 ? 1
262 $::request->layout->add_javascripts("kivi.BankTransaction.js");
263 $self->render('bank_transactions/list',
264 title => t8('Bank transactions MT940'),
265 BANK_TRANSACTIONS => $bank_transactions,
266 PROPOSALS => $proposals,
267 bank_account => $bank_account,
272 sub action_assign_invoice {
275 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
277 $self->render('bank_transactions/assign_invoice',
279 title => t8('Assign invoice'),);
282 sub action_create_invoice {
284 my %myconfig = %main::myconfig;
286 $self->transaction(SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id}));
288 my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(iban => $self->transaction->{remote_account_number});
289 my $use_vendor_filter = $self->transaction->{remote_account_number} && $vendor_of_transaction;
291 my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
292 where => [ template_type => 'ap_transaction' ],
293 sort_by => [ qw(template_name) ],
294 with_objects => [ qw(employee vendor) ],
296 my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
297 query => [ template_type => 'gl_transaction',
298 chart_id => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
300 sort_by => [ qw(template_name) ],
301 with_objects => [ qw(employee record_template_items) ],
304 # pre filter templates_ap, if we have a vendor match (IBAN eq IBAN) - show and allow user to edit this via gui!
305 $templates_ap = [ grep { $_->vendor_id == $vendor_of_transaction->id } @{ $templates_ap } ] if $use_vendor_filter;
307 $self->callback($self->url_for(
309 'filter.bank_account' => $::form->{filter}->{bank_account},
310 'filter.todate' => $::form->{filter}->{todate},
311 'filter.fromdate' => $::form->{filter}->{fromdate},
314 # if we have exactly one ap match, use this directly
315 if ($use_vendor_filter && 1 == scalar @{ $templates_ap }) {
316 $self->redirect_to($self->load_ap_record_template_url($templates_ap->[0]));
319 my $dialog_html = $self->render(
320 'bank_transactions/create_invoice',
321 { layout => 0, output => 0 },
322 title => t8('Create invoice'),
323 TEMPLATES_GL => $use_vendor_filter && @{ $templates_ap } ? undef : $templates_gl,
324 TEMPLATES_AP => $templates_ap,
325 vendor_name => $use_vendor_filter && @{ $templates_ap } ? $vendor_of_transaction->name : undef,
327 $self->js->run('kivi.BankTransaction.show_create_invoice_dialog', $dialog_html)->render;
331 sub action_ajax_payment_suggestion {
334 # based on a BankTransaction ID and a Invoice or PurchaseInvoice ID passed via $::form,
335 # create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
336 # and return encoded as JSON
338 croak("Need bt_id") unless $::form->{bt_id};
340 my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
342 croak("No valid invoice found") unless $invoice;
344 my $html = $self->render(
345 'bank_transactions/_payment_suggestion', { output => 0 },
346 bt_id => $::form->{bt_id},
350 $self->render(\ SL::JSON::to_json( { 'html' => "$html" } ), { layout => 0, type => 'json', process => 0 });
353 sub action_filter_templates {
356 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
358 my (@filter, @filter_ap);
360 # filter => gl and ap | filter_ap = ap (i.e. vendorname)
361 push @filter, ('template_name' => { ilike => '%' . $::form->{template} . '%' }) if $::form->{template};
362 push @filter, ('reference' => { ilike => '%' . $::form->{reference} . '%' }) if $::form->{reference};
363 push @filter_ap, ('vendor.name' => { ilike => '%' . $::form->{vendor} . '%' }) if $::form->{vendor};
364 push @filter_ap, @filter;
365 my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
366 query => [ template_type => 'gl_transaction',
367 chart_id => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
368 (and => \@filter) x !!@filter
370 with_objects => [ qw(employee record_template_items) ],
373 my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
374 where => [ template_type => 'ap_transaction', (and => \@filter_ap) x !!@filter_ap ],
375 with_objects => [ qw(employee vendor) ],
377 $::form->{filter} //= {};
379 $self->callback($self->url_for(
381 'filter.bank_account' => $::form->{filter}->{bank_account},
382 'filter.todate' => $::form->{filter}->{todate},
383 'filter.fromdate' => $::form->{filter}->{fromdate},
386 my $output = $self->render(
387 'bank_transactions/_template_list',
389 TEMPLATES_AP => $templates_ap,
390 TEMPLATES_GL => $templates_gl,
393 $self->render(\to_json({ html => $output }), { type => 'json', process => 0 });
396 sub action_ajax_add_list {
399 my @where_sale = (amount => { ne => \'paid' });
400 my @where_purchase = (amount => { ne => \'paid' });
402 if ($::form->{invnumber}) {
403 push @where_sale, (invnumber => { ilike => like($::form->{invnumber})});
404 push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
407 if ($::form->{amount}) {
408 push @where_sale, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
409 push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
412 if ($::form->{vcnumber}) {
413 push @where_sale, ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
414 push @where_purchase, ('vendor.vendornumber' => { ilike => like($::form->{vcnumber})});
417 if ($::form->{vcname}) {
418 push @where_sale, ('customer.name' => { ilike => like($::form->{vcname})});
419 push @where_purchase, ('vendor.name' => { ilike => like($::form->{vcname})});
422 if ($::form->{transdatefrom}) {
423 my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
424 if ( ref($fromdate) eq 'DateTime' ) {
425 push @where_sale, ('transdate' => { ge => $fromdate});
426 push @where_purchase, ('transdate' => { ge => $fromdate});
430 if ($::form->{transdateto}) {
431 my $todate = $::locale->parse_date_to_object($::form->{transdateto});
432 if ( ref($todate) eq 'DateTime' ) {
433 $todate->add(days => 1);
434 push @where_sale, ('transdate' => { lt => $todate});
435 push @where_purchase, ('transdate' => { lt => $todate});
439 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => \@where_sale, with_objects => 'customer');
440 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
442 my @all_open_invoices = @{ $all_open_ar_invoices };
443 # add ap invoices, filtering out subcent open amounts
444 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.005 } @{ $all_open_ap_invoices };
446 @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
448 my $output = $self->render(
449 'bank_transactions/add_list',
451 INVOICES => \@all_open_invoices,
454 my %result = ( count => 0, html => $output );
456 $self->render(\to_json(\%result), { type => 'json', process => 0 });
459 sub action_ajax_accept_invoices {
462 my @selected_invoices;
463 foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
464 my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
465 push @selected_invoices, $invoice_object;
469 'bank_transactions/invoices',
471 INVOICES => \@selected_invoices,
472 bt_id => $::form->{bt_id},
479 return 0 if !$::form->{invoice_ids};
481 my %invoice_hash = %{ delete $::form->{invoice_ids} }; # each key (the bt line with a bt_id) contains an array of invoice_ids
483 # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
496 # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
498 # '44' => [ '50', '51', 52' ]
501 $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
503 # a bank_transaction may be assigned to several invoices, i.e. a customer
504 # might pay several open invoices with one transaction
510 if ( $::form->{proposal_ids} ) {
511 foreach (@{ $::form->{proposal_ids} }) {
512 my $bank_transaction_id = $_;
513 my $invoice_ids = $invoice_hash{$_};
514 push @{ $self->problems }, $self->save_single_bank_transaction(
515 bank_transaction_id => $bank_transaction_id,
516 invoice_ids => $invoice_ids,
517 sources => ($::form->{sources} // {})->{$_},
518 memos => ($::form->{memos} // {})->{$_},
520 $count += scalar( @{$invoice_ids} );
523 while ( my ($bank_transaction_id, $invoice_ids) = each(%invoice_hash) ) {
524 push @{ $self->problems }, $self->save_single_bank_transaction(
525 bank_transaction_id => $bank_transaction_id,
526 invoice_ids => $invoice_ids,
527 sources => [ map { $::form->{"sources_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
528 memos => [ map { $::form->{"memos_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
529 book_fx_bank_fees => [ map { $::form->{"book_fx_bank_fees_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
530 currency_ids => [ map { $::form->{"currency_id_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
531 exchangerates => [ map { $::form->parse_amount(\%::myconfig, $::form->{"exchangerate_${bank_transaction_id}_${_}"}) } @{ $invoice_ids } ],
533 $count += scalar( @{$invoice_ids} );
536 my $max_count = $count;
537 foreach (@{ $self->problems }) {
538 $count-- if $_->{result} eq 'error';
540 return ($count, $max_count);
543 sub action_save_invoices {
545 my ($success_count, $max_count) = $self->save_invoices();
547 if ($success_count == $max_count) {
548 flash('ok', t8('#1 invoice(s) saved.', $success_count));
550 flash('error', t8('At least #1 invoice(s) not saved', $max_count - $success_count));
553 $self->action_list();
556 sub action_save_proposals {
559 if ( $::form->{proposal_ids} ) {
560 my $propcount = scalar(@{ $::form->{proposal_ids} });
561 if ( $propcount > 0 ) {
562 my $count = $self->save_invoices();
564 flash('ok', t8('#1 proposal(s) with #2 invoice(s) saved.', $propcount, $count));
567 $self->action_list();
571 sub save_single_bank_transaction {
572 my ($self, %params) = @_;
576 bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
580 if (!$data{bank_transaction}) {
584 message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
588 my $bank_transaction = $data{bank_transaction};
590 if ($bank_transaction->closed_period) {
594 message => $::locale->text('Cannot post payment for a closed period!'),
599 my $transit_items_account = SL::DB::Manager::Chart->find_by(id => SL::DB::Default->get->transit_items_chart_id);
602 my $bt_id = $data{bank_transaction_id};
603 my $sign = $bank_transaction->amount < 0 ? -1 : 1;
604 my $payment_received = $bank_transaction->amount > 0;
605 my $payment_sent = $bank_transaction->amount < 0;
606 my ($has_negative_record, $has_positive_record);
609 foreach my $invoice_id (@{ $params{invoice_ids} }) {
610 my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
615 message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
618 $has_positive_record = 1 if $invoice->amount > 0; # invoice
619 $has_negative_record = 1 if $invoice->amount < 0; # credit_note
620 push @{ $data{invoices} }, $invoice;
623 if (ref $transit_items_account eq 'SL::DB::Chart' && $has_positive_record
624 && scalar @{ $data{invoices} } == 2 && $has_negative_record) {
626 $self->_check_and_book_credit_note(
627 invoices => $data{invoices},
628 chart_id => $transit_items_account->id,
630 transdate => $bank_transaction->valutadate,
631 transit_chart => $transit_items_account );
634 if ( $payment_received
635 && any { ( $_->is_sales && ($_->amount < 0))
636 || (!$_->is_sales && ($_->amount > 0))
637 } @{ $data{invoices} }) {
641 message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
646 && any { ( $_->is_sales && ($_->amount > 0))
647 || (!$_->is_sales && ($_->amount < 0) && ($_->invoice_type eq 'purchase_invoice'))
648 } @{ $data{invoices} }) {
652 message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
656 my $max_invoices = scalar(@{ $data{invoices} });
659 foreach my $invoice (@{ $data{invoices} }) {
660 my $source = ($data{sources} // [])->[$n_invoices];
661 my $memo = ($data{memos} // [])->[$n_invoices];
662 my $fx_rate = ($data{exchangerates} // [])->[$n_invoices];
663 my $fx_book = ($data{book_fx_bank_fees} // [])->[$n_invoices];
664 my $currency_id = ($data{currency_ids} // [])->[$n_invoices];
667 # safety check invoice open
668 croak("Invoice closed. Cannot proceed.") unless ($invoice->open_amount);
670 if ( ($payment_sent && $bank_transaction->not_assigned_amount >= 0)
671 || ($payment_received && $bank_transaction->not_assigned_amount <= 0)) {
675 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."),
679 my ($payment_type, $free_skonto_amount);
680 if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
681 $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} }) || '';
683 $payment_type = 'without_skonto';
685 # hack payment type use free_skonto for with_fuzzy_skonto
686 if ($payment_type eq 'with_fuzzy_skonto_pt') {
687 $free_skonto_amount = abs($invoice->open_amount - abs($bank_transaction->not_assigned_amount));
688 die "Invalid state for fuzzy skonto amount" unless $free_skonto_amount > 0;
689 $payment_type = 'free_skonto'; # we have a valid free_skonto amount, therefore go ...
690 } elsif ($payment_type eq 'free_skonto') {
691 # parse user input > 0
692 if ($::form->parse_amount(\%::myconfig, $::form->{"free_skonto_amount"}->{"$bt_id"}{$invoice->id}) > 0) {
693 $free_skonto_amount = $::form->parse_amount(\%::myconfig, $::form->{"free_skonto_amount"}->{"$bt_id"}{$invoice->id});
698 message => $::locale->text("Free skonto amount has to be a positive number."),
703 # TODO rewrite this: really booked amount should be a return value of Payment.pm
704 # -> quick and dirty done -> really booked amount is the first element of return array
705 # also this controller shouldnt care about how to calc skonto. we simply delegate the
706 # payment_type to the helper and get the corresponding bank_transaction values back
707 # hotfix to get the signs right - compare absolute values and later set the signs
708 # should be better done elsewhere - changing not_assigned_amount to abs feels seriously bogus
709 # default open amount
710 my $open_amount = $payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto : $invoice->open_amount;
711 # if fx calc new open amount with skonto pt and set new exchange rate (default or for bank_transaction)
713 # 1. set new open amount
714 die "Exchangerate without currency" unless $currency_id;
715 die "Invoice currency differs from user input currency" unless $currency_id == $invoice->currency->id;
716 $open_amount = $payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto_fx($fx_rate) : $invoice->open_amount_fx($fx_rate);
717 # 2. set daily default or custom record exchange rate
718 my $default_rate = $invoice->get_exchangerate_for_bank_transaction($bank_transaction->id);
719 if (!$default_rate) { # set new daily default
720 my $buysell = $invoice->is_sales ? 'buy' : 'sell';
721 my $ex = SL::DB::Manager::Exchangerate->find_by(currency_id => $currency_id,
722 transdate => $bank_transaction->valutadate)
723 || SL::DB::Exchangerate->new(currency_id => $currency_id,
724 transdate => $bank_transaction->valutadate);
725 $ex->update_attributes($buysell => $fx_rate);
726 $bank_transaction->exchangerate(undef); # maybe user reassigned bank_transaction
727 } elsif ($default_rate != $fx_rate) { # set record (banktransaction) exchangerate
728 $bank_transaction->exchangerate($fx_rate); # custom rate, will be displayed in ap, ir, is
729 } elsif (abs($default_rate - $fx_rate) < 0.001) {
730 # last valid state default rate is (nearly) the same as user input -> do nothing
731 } else { die "Invalid exchange rate state:" . $default_rate . " " . $fx_rate; }
734 # open amount is in default currency -> free_skonto is in default currency, no need to change
735 $open_amount = abs($open_amount);
736 $open_amount -= $free_skonto_amount if ($payment_type eq 'free_skonto');
737 my $not_assigned_amount = abs($bank_transaction->not_assigned_amount);
738 my $amount_for_booking = ($open_amount < $not_assigned_amount) ? $open_amount : $not_assigned_amount;
739 my $fx_fee_amount = $fx_book && ($open_amount < $not_assigned_amount) ? $not_assigned_amount - $open_amount : 0;
740 my $amount_for_payment = $amount_for_booking;
742 # $amount_for_booking
744 # get the right direction for the payment bookings (all amounts < 0 are stornos, credit notes or negative ap)
745 $amount_for_payment *= -1 if $invoice->amount < 0;
746 $free_skonto_amount *= -1 if ($free_skonto_amount && $invoice->amount < 0);
747 # get the right direction for the bank transaction
748 # sign is simply the sign of amount in bank_transactions: positive for increase and negative for decrease
749 $amount_for_booking *= $sign;
751 # ... and then pay the invoice
752 my @acc_ids = $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
753 trans_id => $invoice->id,
754 amount => $amount_for_payment,
755 payment_type => $payment_type,
758 skonto_amount => $free_skonto_amount,
759 exchangerate => $fx_rate,
761 fx_fee_amount => $fx_fee_amount,
762 currency_id => $currency_id,
764 transdate => $bank_transaction->valutadate->to_kivitendo);
765 # First element is the booked amount for accno bank
766 my $bank_amount = shift @acc_ids;
768 if (!$invoice->forex) {
769 # die "Invalid state, calculated invoice_amount differs from expected invoice amount" unless (abs($bank_amount->{return_bank_amount}) - abs($amount_for_booking) < 0.001);
770 $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking);
772 die "Invalid state, calculated invoice_amount differs from expected invoice amount: $amount_for_booking <> " . $bank_amount->{return_bank_amount}
773 unless $fx_book || (abs($bank_amount->{return_bank_amount}) - abs($amount_for_booking) < 0.005);
774 $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $bank_amount->{return_bank_amount});
775 #$bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking);
777 # ... and record the origin via BankTransactionAccTrans
778 if (scalar(@acc_ids) < 2) {
782 message => $::locale->text("Unable to book transactions for bank purpose #1", $bank_transaction->purpose),
785 foreach my $acc_trans_id (@acc_ids) {
786 my $id_type = $invoice->is_sales ? 'ar' : 'ap';
788 acc_trans_id => $acc_trans_id,
789 bank_transaction_id => $bank_transaction->id,
790 $id_type => $invoice->id,
792 SL::DB::BankTransactionAccTrans->new(%props_acc)->save;
794 # Record a record link from the bank transaction to the invoice
796 from_table => 'bank_transactions',
798 to_table => $invoice->is_sales ? 'ar' : 'ap',
799 to_id => $invoice->id,
801 SL::DB::RecordLink->new(%props)->save;
803 # "close" a sepa_export_item if it exists
804 # code duplicated in action_save_proposals!
805 # currently only works, if there is only exactly one open sepa_export_item
806 if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
807 if ( scalar @$seis == 1 ) {
808 # moved the execution and the check for sepa_export into a method,
809 # this isn't part of a transaction, though
810 $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
815 $bank_transaction->save;
817 # 'undef' means 'no error' here.
822 my $rez = $data{bank_transaction}->db->with_transaction(sub {
824 $error = $worker->();
835 # Rollback Fehler nicht weiterreichen
837 # aber einen rollback von hand
838 $::lxdebug->message(LXDebug->DEBUG2(),"finish worker with ". ($error ? $error->{result} : '-'));
839 $data{bank_transaction}->db->dbh->rollback if $error && $error->{result} eq 'error';
842 return grep { $_ } ($error, @warnings);
844 sub action_unlink_bank_transaction {
845 my ($self, %params) = @_;
847 croak("No bank transaction ids") unless scalar @{ $::form->{ids}} > 0;
851 foreach my $bt_id (@{ $::form->{ids}} ) {
853 my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
854 croak("No valid bank transaction found") unless (ref($bank_transaction) eq 'SL::DB::BankTransaction');
855 croak t8('Cannot unlink payment for a closed period!') if $bank_transaction->closed_period;
857 # everything in one transaction
858 my $rez = $bank_transaction->db->with_transaction(sub {
859 # 1. remove all reconciliations (due to underlying trigger, this has to be the first step)
860 my $rec_links = SL::DB::Manager::ReconciliationLink->get_all(where => [ bank_transaction_id => $bt_id ]);
861 $_->delete for @{ $rec_links };
864 foreach my $acc_trans_id_entry (@{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [bank_transaction_id => $bt_id ] )}) {
866 my $acc_trans = SL::DB::Manager::AccTransaction->get_all(where => [acc_trans_id => $acc_trans_id_entry->acc_trans_id]);
868 # save trans_id and type
869 die "no type" unless ($acc_trans_id_entry->ar_id || $acc_trans_id_entry->ap_id || $acc_trans_id_entry->gl_id);
870 $trans_ids{$acc_trans_id_entry->ar_id} = 'ar' if $acc_trans_id_entry->ar_id;
871 $trans_ids{$acc_trans_id_entry->ap_id} = 'ap' if $acc_trans_id_entry->ap_id;
872 $trans_ids{$acc_trans_id_entry->gl_id} = 'gl' if $acc_trans_id_entry->gl_id;
873 # 2. all good -> ready to delete acc_trans and bt_acc link
874 $acc_trans_id_entry->delete;
875 $_->delete for @{ $acc_trans };
877 # 3. update arap.paid (may not be 0, yet)
878 # or in case of gl, delete whole entry
879 while (my ($trans_id, $type) = each %trans_ids) {
881 SL::DB::Manager::GLTransaction->delete_all(where => [ id => $trans_id ]);
884 die ("invalid type") unless $type =~ m/^(ar|ap)$/;
886 # recalc and set paid via database query
887 my $query = qq|UPDATE $type SET paid =
888 (SELECT COALESCE(abs(sum(amount)),0) FROM acc_trans
890 AND (chart_link ilike '%paid%'
891 OR chart_id IN (SELECT fxgain_accno_id from defaults)
892 OR chart_id IN (SELECT fxloss_accno_id from defaults)
897 die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id, $trans_id) == -1);
899 # undo datepaid if no payment exists
900 $query = qq|UPDATE $type SET datepaid = null WHERE ID = ? AND paid = 0|;
901 die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id) == -1);
903 # 4. and delete all (if any) record links
904 my $rl = SL::DB::Manager::RecordLink->delete_all(where => [ from_id => $bt_id, from_table => 'bank_transactions' ]);
906 # 5. finally reset this bank transaction
907 $bank_transaction->invoice_amount(0);
908 $bank_transaction->exchangerate(undef);
909 $bank_transaction->cleared(0);
910 $bank_transaction->save;
911 # 6. and add a log entry in history_erp
912 SL::DB::History->new(
913 trans_id => $bank_transaction->id,
914 snumbers => 'bank_transaction_unlink_' . $bank_transaction->id,
915 employee_id => SL::DB::Manager::Employee->current->id,
916 what_done => 'bank_transaction',
917 addition => 'UNLINKED',
922 }) || die t8('error while unlinking payment #1 : ', $bank_transaction->purpose) . $bank_transaction->db->error . "\n";
927 flash('ok', t8('#1 bank transaction bookings undone.', $success_count));
928 $self->action_list_all() unless $params{testcase};
935 $::auth->assert('bank_transaction');
942 sub make_filter_summary {
945 my $filter = $::form->{filter} || {};
949 [ $filter->{"transdate:date::ge"}, $::locale->text('Transdate') . " " . $::locale->text('From Date') ],
950 [ $filter->{"transdate:date::le"}, $::locale->text('Transdate') . " " . $::locale->text('To Date') ],
951 [ $filter->{"valutadate:date::ge"}, $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
952 [ $filter->{"valutadate:date::le"}, $::locale->text('Valutadate') . " " . $::locale->text('To Date') ],
953 [ $filter->{"amount:number"}, $::locale->text('Amount') ],
954 [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account') ],
955 [ $filter->{"remote_name:substr::ilike"}, $::locale->text('Remote name') ],
956 [ $filter->{"remote_account_number:substr::ilike"}, $::locale->text('Remote account number') ],
957 [ $filter->{"remote_bank_code:substr::ilike"} , $::locale->text('Remote bank code') ],
958 [ $filter->{"purpose:substr::ilike"} , $::locale->text('Purpose') ],
962 push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
965 $self->{filter_summary} = join ', ', @filter_strings;
971 my $callback = $self->models->get_callback;
973 my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
974 $report->{title} = t8('Bank transactions');
975 $self->{report} = $report;
977 my @columns = qw(ids local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount invoice_amount invoices currency purpose end_to_end_id local_account_number local_bank_code id);
978 my @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code);
981 ids => { raw_header_data => checkbox_tag("", id => "check_all", checkall => "[data-checkall=1]"),
983 raw_data => sub { if (@{ $_[0]->linked_invoices }) {
984 if ($_[0]->closed_period) {
985 html_tag('text', "X"); #, tooltip => t8('Bank Transaction is in a closed period.')),
987 checkbox_tag("ids[]", value => $_[0]->id, "data-checkall" => 1);
990 transdate => { sub => sub { $_[0]->transdate_as_date } },
991 valutadate => { sub => sub { $_[0]->valutadate_as_date } },
993 remote_account_number => { },
994 remote_bank_code => { },
995 amount => { sub => sub { $_[0]->amount_as_number },
997 invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number },
999 invoices => { sub => sub { my @invnumbers; for my $obj (@{ $_[0]->linked_invoices }) {
1000 next unless $obj; push @invnumbers, $obj->invnumber } return \@invnumbers },
1001 obj_link => sub { my @links; for my $obj (@{ $_[0]->linked_invoices }) {
1002 next unless $obj; my $script = ref $obj eq 'SL::DB::GLTransaction' ? 'gl.pl'
1003 : $obj->is_sales && $obj->invoice ? 'is.pl'
1004 : $obj->is_sales && !$obj->invoice ? 'ar.pl'
1005 : !$obj->is_sales && $obj->invoice ? 'ir.pl'
1006 : !$obj->is_sales && !$obj->invoice ? 'ap.pl'
1007 : die "Invalid invoice state for link";
1008 push @links,$script . "?action=edit&id=" . $obj->id } return \@links }
1010 currency => { sub => sub { $_[0]->currency->name } },
1012 local_account_number => { sub => sub { $_[0]->local_bank_account->account_number } },
1013 local_bank_code => { sub => sub { $_[0]->local_bank_account->bank_code } },
1014 local_bank_name => { sub => sub { $_[0]->local_bank_account->name } },
1015 end_to_end_id => { sub => sub { $_[0]->end_to_end_id }, text => $::locale->text('End to end ID') },
1019 map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
1021 $report->set_options(
1022 std_column_visibility => 1,
1023 controller_class => 'BankTransaction',
1024 output_format => 'HTML',
1025 top_info_text => $::locale->text('Bank transactions'),
1026 title => $::locale->text('Bank transactions'),
1027 allow_pdf_export => 1,
1028 allow_csv_export => 1,
1030 $report->set_columns(%column_defs);
1031 $report->set_column_order(@columns);
1032 $report->set_export_options(qw(list_all filter));
1033 $report->set_options_from_form;
1034 $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
1035 $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
1037 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
1039 $report->set_options(
1040 raw_top_info_text => $self->render('bank_transactions/report_top', { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
1041 raw_bottom_info_text => $self->render('bank_transactions/report_bottom', { output => 0 }),
1045 sub _check_and_book_credit_note {
1048 Common::check_params(\%params, qw(chart_id transdate bt_id invoices transit_chart));
1050 croak "No invoice " unless (ref $params{invoices}->[0] eq 'SL::DB::PurchaseInvoice')
1051 || (ref $params{invoices}->[0] eq 'SL::DB::Invoice' );
1052 croak "Not a valid date" unless ref $params{transdate} eq 'DateTime';
1053 croak "Not a valid chart" unless ref $params{transit_chart} eq 'SL::DB::Chart';
1054 croak "Need exactly two records" unless scalar @{ $params{invoices} } == 2;
1057 my ($has_one_credit_note, $has_one_invoice, $amount, $credit_note_index, $credit_note_no, $invoice_no);
1059 foreach my $invoice (@{ $params{invoices} }) {
1060 if ( ( $invoice->is_sales && $invoice->type eq 'credit_note')
1061 || (!$invoice->is_sales && $invoice->invoice_type eq 'purchase_credit_note')) {
1062 # credit_notes | purchase_credit_note
1063 # -1397.11000 | AR | 504.74000 | AP
1064 # 1397.11000 | AR_paid | -504.74000 | AP_paid
1066 my $mult = $invoice->is_sales ? -1 : 1; # multiplier for getting the right sign for credit_notes
1067 $amount = ($invoice->amount - $invoice->paid) * $mult;
1068 # (-200 - (-10)) * $mult = AR_paid (positive) |AP_paid (negative)
1070 $has_one_credit_note += 1;
1071 $credit_note_index = $index;
1072 $credit_note_no = $invoice->invnumber;
1074 $has_one_invoice += 1;
1075 $invoice_no = $invoice->invnumber;
1079 die "Invalid state" unless ($has_one_credit_note == 1 && $has_one_invoice == 1);
1081 foreach my $invoice (@{ $params{invoices} }) {
1082 my $is_credit_note = $invoice->is_credit_note ? 1 : undef;
1083 my $sign = $invoice->is_credit_note ? 1 : -1; # correct sign for bookings
1084 my $paid_sign = $invoice->is_credit_note ? -1 : 1; # paid is always negative for credit_note
1086 my $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $invoice->id,
1087 chart_id => $params{transit_chart}->id,
1088 chart_link => $params{transit_chart}->link,
1089 amount => $amount * $sign,
1090 transdate => $params{transdate},
1091 source => $is_credit_note ? $invoice_no : $credit_note_no,
1092 memo => t8('Automatically assigned with bank transaction'),
1094 tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
1096 my $arap_booking= SL::DB::AccTransaction->new(trans_id => $invoice->id,
1097 chart_id => $invoice->reference_account->id,
1098 chart_link => $invoice->reference_account->link,
1099 amount => $amount * $sign * -1,
1100 transdate => $params{transdate},
1103 tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
1104 $new_acc_trans->save;
1105 $arap_booking->save;
1106 $invoice->update_attributes(paid => $invoice->paid + (abs($amount) * $paid_sign), datepaid => $params{transdate});
1108 # link both acc_trans transactions
1109 my $id_type = $invoice->is_sales ? 'ar' : 'ap';
1111 acc_trans_id => $new_acc_trans->acc_trans_id,
1112 bank_transaction_id => $params{bt_id},
1113 $id_type => $invoice->id,
1115 SL::DB::BankTransactionAccTrans->new(%props_acc)->save;
1117 acc_trans_id => $arap_booking->acc_trans_id,
1118 bank_transaction_id => $params{bt_id},
1119 $id_type => $invoice->id,
1121 SL::DB::BankTransactionAccTrans->new(%props_acc)->save;
1124 # Record a record link from the bank transaction to the credit note
1125 if ($invoice->invoice_type =~ m/credit_note/) {
1127 from_table => 'bank_transactions',
1128 from_id => $params{bt_id},
1129 to_table => $id_type,
1130 to_id => $invoice->id,
1132 SL::DB::RecordLink->new(%props)->save;
1135 # throw away the credit note
1136 splice @{ $params{invoices} }, $credit_note_index, 1;
1137 # and return nothing. hook is completely done
1140 sub init_problems { [] }
1145 SL::Controller::Helper::GetModels->new(
1146 controller => $self,
1150 dir => 0, # 1 = ASC, 0 = DESC : default sort is newest at top
1153 transdate => t8('Transdate'),
1154 remote_name => t8('Remote name'),
1155 amount => t8('Amount'),
1156 invoice_amount => t8('Assigned'),
1157 invoices => t8('Linked invoices'),
1158 valutadate => t8('Valutadate'),
1159 remote_account_number => t8('Remote account number'),
1160 remote_bank_code => t8('Remote bank code'),
1161 currency => t8('Currency'),
1162 purpose => t8('Purpose'),
1163 local_account_number => t8('Local account number'),
1164 local_bank_code => t8('Local bank code'),
1165 local_bank_name => t8('Bank account'),
1167 with_objects => [ 'local_bank_account', 'currency' ],
1171 sub load_ap_record_template_url {
1172 my ($self, $template) = @_;
1174 return $self->url_for(
1175 controller => 'ap.pl',
1176 action => 'load_record_template',
1177 id => $template->id,
1178 'form_defaults.amount_1' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
1179 'form_defaults.transdate' => $self->transaction->transdate_as_date,
1180 'form_defaults.duedate' => $self->transaction->transdate_as_date,
1181 'form_defaults.no_payment_bookings' => 1,
1182 'form_defaults.paid_1_suggestion' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
1183 'form_defaults.AP_paid_1_suggestion' => $self->transaction->local_bank_account->chart->accno,
1184 'form_defaults.callback' => $self->callback,
1185 'form_defaults.notes' => $self->convert_purpose_for_template($template, $self->transaction->purpose),
1189 sub load_gl_record_template_url {
1190 my ($self, $template) = @_;
1192 return $self->url_for(
1193 controller => 'gl.pl',
1194 action => 'load_record_template',
1195 id => $template->id,
1196 'form_defaults.amount_1' => abs($self->transaction->not_assigned_amount), # always positive
1197 'form_defaults.transdate' => $self->transaction->transdate_as_date,
1198 'form_defaults.callback' => $self->callback,
1199 'form_defaults.bt_id' => $self->transaction->id,
1200 'form_defaults.bt_chart_id' => $self->transaction->local_bank_account->chart->id,
1201 'form_defaults.description' => $self->convert_purpose_for_template($template, $self->transaction->purpose),
1205 sub convert_purpose_for_template {
1206 my ($self, $template, $purpose) = @_;
1208 # enter custom code here
1213 sub setup_search_action_bar {
1214 my ($self, %params) = @_;
1216 for my $bar ($::request->layout->get('actionbar')) {
1220 submit => [ '#search_form', { action => 'BankTransaction/list' } ],
1221 accesskey => 'enter',
1227 sub setup_list_all_action_bar {
1228 my ($self, %params) = @_;
1230 for my $bar ($::request->layout->get('actionbar')) {
1233 action => [ t8('Actions') ],
1235 t8('Unlink bank transactions'),
1236 submit => [ '#form', { action => 'BankTransaction/unlink_bank_transaction' } ],
1237 checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
1238 disabled => $::instance_conf->get_payments_changeable ? t8('Cannot safely unlink bank transactions, please set the posting configuration for payments to unchangeable.') : undef,
1243 submit => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
1244 accesskey => 'enter',
1259 SL::Controller::BankTransaction - Posting payments to invoices from
1260 bank transactions imported earlier
1266 =item C<save_single_bank_transaction %params>
1268 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
1269 tries to post its amount to a certain number of invoices (parameter
1270 C<invoice_ids>, an array ref of database IDs to purchase or sales
1273 This method handles already partly assigned bank transactions.
1275 This method cannot handle already partly assigned bank transactions, i.e.
1276 a bank transaction that has a invoice_amount <> 0 but not the fully
1277 transaction amount (invoice_amount == amount).
1279 If the amount of the bank transaction is higher than the sum of
1280 the assigned invoices (1 .. n) the bank transaction will only be
1283 The whole function is wrapped in a database transaction. If an
1284 exception occurs the bank transaction is not posted at all. The same
1285 is true if the code detects an error during the execution, e.g. a bank
1286 transaction that's already been posted earlier. In both cases the
1287 database transaction will be rolled back.
1289 If warnings but not errors occur the database transaction is still
1292 The return value is an error object or C<undef> if the function
1293 succeeded. The calling function will collect all warnings and errors
1294 and display them in a nicely formatted table if any occurred.
1296 An error object is a hash reference containing the following members:
1300 =item * C<result> — can be either C<warning> or C<error>. Warnings are
1301 displayed slightly different than errors.
1303 =item * C<message> — a human-readable message included in the list of
1304 errors meant as the description of why the problem happened
1306 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
1307 that the function was called with
1309 =item * C<bank_transaction> — the database object
1310 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
1312 =item * C<invoices> — an array ref of the database objects (either
1313 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
1318 =item C<action_unlink_bank_transaction>
1320 Takes one or more bank transaction ID (as parameter C<form::ids>) and
1321 tries to revert all payment bookings including already cleared bookings.
1323 This method won't undo payments that are in a closed period and assumes
1324 that payments are not manually changed, i.e. only imported payments.
1326 GL-records will be deleted completely if a bank transaction was the source.
1328 TODO: we still rely on linked_records for the check boxes
1330 =item C<convert_purpose_for_template>
1332 This method can be used to parse, filter and convert the bank transaction's
1333 purpose string before it will be assigned to the description field of a
1334 gl transaction or to the notes field of an ap transaction.
1335 You have to write your own custom code.
1337 =item C<_check_and_book_credit_note>
1339 This method takes a array of invoices with two entries one one valid credit note
1340 and books the amount of the credit note against the invoice via the default
1341 transfer items account (i.e. SKR04 1370) and adds a source and memo entry for the
1343 Logical and visual linking of the payment booking and credit note record to the bank
1344 transaction will also be done (necessary cond. for unlinking a bank transaction).
1345 If the methods success the credit note will be deleted from
1346 the original caller's array and he can further process the data without pondering
1347 about the removed credit note data.
1353 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
1354 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>