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::DBUtils qw(like);
28 use List::Util qw(max);
30 use Rose::Object::MakeMethods::Generic
32 'scalar --get_set_init' => [ qw(models problems) ],
35 __PACKAGE__->run_before('check_auth');
45 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
47 $self->render('bank_transactions/search',
48 BANK_ACCOUNTS => $bank_accounts);
54 $self->make_filter_summary;
55 $self->prepare_report;
57 $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
63 if (!$::form->{filter}{bank_account}) {
64 flash('error', t8('No bank account chosen!'));
69 my $sort_by = $::form->{sort_by} || 'transdate';
70 $sort_by = 'transdate' if $sort_by eq 'proposal';
71 $sort_by .= $::form->{sort_dir} ? ' DESC' : ' ASC';
73 my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
74 my $todate = $::locale->parse_date_to_object($::form->{filter}->{todate});
75 $todate->add( days => 1 ) if $todate;
78 push @where, (transdate => { ge => $fromdate }) if ($fromdate);
79 push @where, (transdate => { lt => $todate }) if ($todate);
80 my $bank_account = SL::DB::Manager::BankAccount->find_by( id => $::form->{filter}{bank_account} );
81 # bank_transactions no younger than starting date,
82 # including starting date (same search behaviour as fromdate)
83 # but OPEN invoices to be matched may be from before
84 if ( $bank_account->reconciliation_starting_date ) {
85 push @where, (transdate => { ge => $bank_account->reconciliation_starting_date });
88 my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
89 with_objects => [ 'local_bank_account', 'currency' ],
93 amount => {ne => \'invoice_amount'},
94 local_bank_account_id => $::form->{filter}{bank_account},
99 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => [amount => { gt => \'paid' }], with_objects => 'customer');
100 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { gt => \'paid' }], with_objects => 'vendor');
102 my @all_open_invoices;
103 # filter out invoices with less than 1 cent outstanding
104 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
105 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
107 # try to match each bank_transaction with each of the possible open invoices
110 foreach my $bt (@{ $bank_transactions }) {
111 next unless $bt->{remote_name}; # bank has no name, usually fees, use create invoice to assign
113 $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
115 # try to match the current $bt to each of the open_invoices, saving the
116 # results of get_agreement_with_invoice in $open_invoice->{agreement} and
117 # $open_invoice->{rule_matches}.
119 # The values are overwritten each time a new bt is checked, so at the end
120 # of each bt the likely results are filtered and those values are stored in
121 # the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
122 # score is stored in $bt->{agreement}
124 foreach my $open_invoice (@all_open_invoices){
125 ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
128 $bt->{proposals} = [];
131 my $min_agreement = 3; # suggestions must have at least this score
133 my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
135 # add open_invoices with highest agreement into array $bt->{proposals}
136 if ( $max_agreement >= $min_agreement ) {
137 $bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
138 $bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
140 # store the rule_matches in a separate array, so they can be displayed in template
141 foreach ( @{ $bt->{proposals} } ) {
142 push(@{$bt->{rule_matches}}, $_->{rule_matches});
148 # separate filter for proposals (second tab, agreement >= 5 and exactly one match)
149 # to qualify as a proposal there has to be
150 # * agreement >= 5 TODO: make threshold configurable in configuration
151 # * there must be only one exact match
152 # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
153 my $proposal_threshold = 5;
154 my @proposals = grep {
155 ($_->{agreement} >= $proposal_threshold)
156 && (1 == scalar @{ $_->{proposals} })
157 && (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01
158 : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01)
159 } @{ $bank_transactions };
161 # sort bank transaction proposals by quality (score) of proposal
162 $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 1;
163 $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 0;
166 $self->render('bank_transactions/list',
167 title => t8('Bank transactions MT940'),
168 BANK_TRANSACTIONS => $bank_transactions,
169 PROPOSALS => \@proposals,
170 bank_account => $bank_account );
173 sub action_assign_invoice {
176 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
178 $self->render('bank_transactions/assign_invoice',
180 title => t8('Assign invoice'),);
183 sub action_create_invoice {
185 my %myconfig = %main::myconfig;
187 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
188 my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
190 my $use_vendor_filter = $self->{transaction}->{remote_account_number} && $vendor_of_transaction;
192 my $drafts = SL::DB::Manager::Draft->get_all(where => [ module => 'ap'] , with_objects => 'employee');
196 foreach my $draft ( @{ $drafts } ) {
197 my $draft_as_object = YAML::Load($draft->form);
198 my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
199 $draft->{vendor} = $vendor->name;
200 $draft->{vendor_id} = $vendor->id;
201 push @filtered_drafts, $draft;
205 @filtered_drafts = grep { $_->{vendor_id} == $vendor_of_transaction->id } @filtered_drafts if $use_vendor_filter;
207 my $all_vendors = SL::DB::Manager::Vendor->get_all();
208 my $callback = $self->url_for(action => 'list',
209 'filter.bank_account' => $::form->{filter}->{bank_account},
210 'filter.todate' => $::form->{filter}->{todate},
211 'filter.fromdate' => $::form->{filter}->{fromdate});
214 'bank_transactions/create_invoice',
216 title => t8('Create invoice'),
217 DRAFTS => \@filtered_drafts,
218 vendor_id => $use_vendor_filter ? $vendor_of_transaction->id : undef,
219 vendor_name => $use_vendor_filter ? $vendor_of_transaction->name : undef,
220 ALL_VENDORS => $all_vendors,
221 limit => $myconfig{vclimit},
222 callback => $callback,
226 sub action_ajax_payment_suggestion {
229 # based on a BankTransaction ID and a Invoice or PurchaseInvoice ID passed via $::form,
230 # create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
231 # and return encoded as JSON
233 my $bt = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
234 my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
236 die unless $bt and $invoice;
238 my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
241 $html .= SL::Presenter->input_tag('invoice_ids.' . $::form->{bt_id} . '[]', $::form->{prop_id} , type => 'hidden');
242 $html .= SL::Presenter->escape(t8('Invno.') . ': ' . $invoice->invnumber . ' ');
243 $html .= SL::Presenter->escape(t8('Open amount') . ': ' . $::form->format_amount(\%::myconfig, $invoice->open_amount, 2) . ' ');
244 $html .= SL::Presenter->select_tag('invoice_skontos.' . $::form->{bt_id} . '[]',
246 value_key => 'payment_type',
247 title_key => 'display' )
249 $html .= '<a href=# onclick="delete_invoice(' . $::form->{bt_id} . ',' . $::form->{prop_id} . ');">x</a>';
250 $html = SL::Presenter->html_tag('div', $html, id => $::form->{bt_id} . '.' . $::form->{prop_id});
252 $self->render(\ SL::JSON::to_json( { 'html' => $html } ), { layout => 0, type => 'json', process => 0 });
255 sub action_filter_drafts {
258 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
259 my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
261 my $drafts = SL::DB::Manager::Draft->get_all(with_objects => 'employee');
265 foreach my $draft ( @{ $drafts } ) {
266 my $draft_as_object = YAML::Load($draft->form);
267 next unless $draft_as_object->{vendor_id}; # we cannot filter for vendor name, if this is a gl draft
269 my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
270 $draft->{vendor} = $vendor->name;
271 $draft->{vendor_id} = $vendor->id;
273 push @filtered_drafts, $draft;
276 my $vendor_name = $::form->{vendor};
277 my $vendor_id = $::form->{vendor_id};
280 @filtered_drafts = grep { $_->{vendor_id} == $vendor_id } @filtered_drafts if $vendor_id;
281 @filtered_drafts = grep { $_->{vendor} =~ /$vendor_name/i } @filtered_drafts if $vendor_name;
283 my $output = $self->render(
284 'bank_transactions/filter_drafts',
286 DRAFTS => \@filtered_drafts,
289 my %result = ( count => 0, html => $output );
291 $self->render(\to_json(\%result), { type => 'json', process => 0 });
294 sub action_ajax_add_list {
297 my @where_sale = (amount => { ne => \'paid' });
298 my @where_purchase = (amount => { ne => \'paid' });
300 if ($::form->{invnumber}) {
301 push @where_sale, (invnumber => { ilike => like($::form->{invnumber})});
302 push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
305 if ($::form->{amount}) {
306 push @where_sale, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
307 push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
310 if ($::form->{vcnumber}) {
311 push @where_sale, ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
312 push @where_purchase, ('vendor.vendornumber' => { ilike => like($::form->{vcnumber})});
315 if ($::form->{vcname}) {
316 push @where_sale, ('customer.name' => { ilike => like($::form->{vcname})});
317 push @where_purchase, ('vendor.name' => { ilike => like($::form->{vcname})});
320 if ($::form->{transdatefrom}) {
321 my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
322 if ( ref($fromdate) eq 'DateTime' ) {
323 push @where_sale, ('transdate' => { ge => $fromdate});
324 push @where_purchase, ('transdate' => { ge => $fromdate});
328 if ($::form->{transdateto}) {
329 my $todate = $::locale->parse_date_to_object($::form->{transdateto});
330 if ( ref($todate) eq 'DateTime' ) {
331 $todate->add(days => 1);
332 push @where_sale, ('transdate' => { lt => $todate});
333 push @where_purchase, ('transdate' => { lt => $todate});
337 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => \@where_sale, with_objects => 'customer');
338 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
340 my @all_open_invoices = @{ $all_open_ar_invoices };
341 # add ap invoices, filtering out subcent open amounts
342 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
344 @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
346 my $output = $self->render(
347 'bank_transactions/add_list',
349 INVOICES => \@all_open_invoices,
352 my %result = ( count => 0, html => $output );
354 $self->render(\to_json(\%result), { type => 'json', process => 0 });
357 sub action_ajax_accept_invoices {
360 my @selected_invoices;
361 foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
362 my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
363 push @selected_invoices, $invoice_object;
367 'bank_transactions/invoices',
369 INVOICES => \@selected_invoices,
370 bt_id => $::form->{bt_id},
374 sub action_save_invoices {
377 my $invoice_hash = delete $::form->{invoice_ids}; # each key (the bt line with a bt_id) contains an array of invoice_ids
379 # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
392 # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
394 # '44' => [ '50', '51', 52' ]
397 $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
399 # a bank_transaction may be assigned to several invoices, i.e. a customer
400 # might pay several open invoices with one transaction
404 while ( my ($bank_transaction_id, $invoice_ids) = each(%$invoice_hash) ) {
405 push @{ $self->problems }, $self->save_single_bank_transaction(
406 bank_transaction_id => $bank_transaction_id,
407 invoice_ids => $invoice_ids,
411 $self->action_list();
414 sub save_single_bank_transaction {
415 my ($self, %params) = @_;
419 bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
423 if (!$data{bank_transaction}) {
427 message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
434 my $bt_id = $data{bank_transaction_id};
435 my $bank_transaction = $data{bank_transaction};
436 my $sign = $bank_transaction->amount < 0 ? -1 : 1;
437 my $amount_of_transaction = $sign * $bank_transaction->amount;
440 foreach my $invoice_id (@{ $params{invoice_ids} }) {
441 my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
446 message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
450 push @invoices, $invoice;
453 $data{invoices} = \@invoices;
455 @invoices = sort { return 1 if ( $a->is_sales and $a->amount > 0);
456 return 1 if (!$a->is_sales and $a->amount < 0);
458 } @invoices if $bank_transaction->amount > 0;
459 @invoices = sort { return -1 if ( $a->is_sales and $a->amount > 0);
460 return -1 if (!$a->is_sales and $a->amount < 0);
462 } @invoices if $bank_transaction->amount < 0;
464 my $max_invoices = scalar(@invoices);
467 foreach my $invoice (@invoices) {
470 # Check if bank_transaction already has a link to the invoice, may only be linked once per invoice
471 # This might be caused by the user reloading a page and resending the form
472 if (_existing_record_link($bank_transaction, $invoice)) {
476 message => $::locale->text("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name),
480 if ($amount_of_transaction == 0) {
484 message => $::locale->text('There are invoices which could not be paid by bank transaction #1 (Account number: #2, bank code: #3)!',
485 $bank_transaction->purpose, $bank_transaction->remote_account_number, $bank_transaction->remote_bank_code),
491 if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
492 $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
494 $payment_type = 'without_skonto';
497 # pay invoice or go to the next bank transaction if the amount is not sufficiently high
498 if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) {
499 # first calculate new bank transaction amount ...
500 if ($invoice->is_sales) {
501 $amount_of_transaction -= $sign * $invoice->open_amount;
502 $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $invoice->open_amount);
504 $amount_of_transaction += $sign * $invoice->open_amount;
505 $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $invoice->open_amount);
507 # ... and then pay the invoice
508 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
509 trans_id => $invoice->id,
510 amount => $invoice->open_amount,
511 payment_type => $payment_type,
512 transdate => $bank_transaction->transdate->to_kivitendo);
513 } else { # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary
514 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
515 trans_id => $invoice->id,
516 amount => $amount_of_transaction,
517 payment_type => $payment_type,
518 transdate => $bank_transaction->transdate->to_kivitendo);
519 $bank_transaction->invoice_amount($bank_transaction->amount);
520 $amount_of_transaction = 0;
523 # Record a record link from the bank transaction to the invoice
525 from_table => 'bank_transactions',
527 to_table => $invoice->is_sales ? 'ar' : 'ap',
528 to_id => $invoice->id,
531 SL::DB::RecordLink->new(@props)->save;
533 # "close" a sepa_export_item if it exists
534 # code duplicated in action_save_proposals!
535 # currently only works, if there is only exactly one open sepa_export_item
536 if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
537 if ( scalar @$seis == 1 ) {
538 # moved the execution and the check for sepa_export into a method,
539 # this isn't part of a transaction, though
540 $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
545 $bank_transaction->save;
547 # 'undef' means 'no error' here.
552 my $rez = $data{bank_transaction}->db->with_transaction(sub {
554 $error = $worker->();
568 return grep { $_ } ($error, @warnings);
571 sub action_save_proposals {
574 foreach my $bt_id (@{ $::form->{proposal_ids} }) {
575 my $bt = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
577 my $arap = SL::DB::Manager::Invoice->find_by(id => $::form->{"proposed_invoice_$bt_id"});
578 $arap = SL::DB::Manager::PurchaseInvoice->find_by(id => $::form->{"proposed_invoice_$bt_id"}) if not defined $arap;
580 # check for existing record_link for that $bt and $arap
581 # do this before any changes to $bt are made
582 die t8("Bank transaction with id #1 has already been linked to #2.", $bt->id, $arap->displayable_name)
583 if _existing_record_link($bt, $arap);
586 $bt->invoice_amount($bt->amount);
590 $arap->pay_invoice(chart_id => $bt->local_bank_account->chart_id,
591 trans_id => $arap->id,
592 amount => $arap->amount,
593 transdate => $bt->transdate->to_kivitendo);
598 from_table => 'bank_transactions',
600 to_table => $arap->is_sales ? 'ar' : 'ap',
604 SL::DB::RecordLink->new(@props)->save;
606 # code duplicated in action_save_invoices!
607 # "close" a sepa_export_item if it exists
608 # currently only works, if there is only exactly one open sepa_export_item
609 if ( my $seis = $arap->find_sepa_export_items({ executed => 0 }) ) {
610 if ( scalar @$seis == 1 ) {
611 # moved the execution and the check for sepa_export into a method,
612 # this isn't part of a transaction, though
613 $seis->[0]->set_executed if $arap->id == $seis->[0]->arap_id;
618 flash('ok', t8('#1 proposal(s) saved.', scalar @{ $::form->{proposal_ids} }));
620 $self->action_list();
628 $::auth->assert('bank_transaction');
635 sub make_filter_summary {
638 my $filter = $::form->{filter} || {};
642 [ $filter->{"transdate:date::ge"}, $::locale->text('Transdate') . " " . $::locale->text('From Date') ],
643 [ $filter->{"transdate:date::le"}, $::locale->text('Transdate') . " " . $::locale->text('To Date') ],
644 [ $filter->{"valutadate:date::ge"}, $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
645 [ $filter->{"valutadate:date::le"}, $::locale->text('Valutadate') . " " . $::locale->text('To Date') ],
646 [ $filter->{"amount:number"}, $::locale->text('Amount') ],
647 [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account') ],
651 push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
654 $self->{filter_summary} = join ', ', @filter_strings;
660 my $callback = $self->models->get_callback;
662 my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
663 $self->{report} = $report;
665 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);
666 my @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code);
669 transdate => { sub => sub { $_[0]->transdate_as_date } },
670 valutadate => { sub => sub { $_[0]->valutadate_as_date } },
672 remote_account_number => { },
673 remote_bank_code => { },
674 amount => { sub => sub { $_[0]->amount_as_number },
676 invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number },
678 invoices => { sub => sub { $_[0]->linked_invoices } },
679 currency => { sub => sub { $_[0]->currency->name } },
681 local_account_number => { sub => sub { $_[0]->local_bank_account->account_number } },
682 local_bank_code => { sub => sub { $_[0]->local_bank_account->bank_code } },
683 local_bank_name => { sub => sub { $_[0]->local_bank_account->name } },
687 map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
689 $report->set_options(
690 std_column_visibility => 1,
691 controller_class => 'BankTransaction',
692 output_format => 'HTML',
693 top_info_text => $::locale->text('Bank transactions'),
694 title => $::locale->text('Bank transactions'),
695 allow_pdf_export => 1,
696 allow_csv_export => 1,
698 $report->set_columns(%column_defs);
699 $report->set_column_order(@columns);
700 $report->set_export_options(qw(list_all filter));
701 $report->set_options_from_form;
702 $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
703 $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
705 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
707 $report->set_options(
708 raw_top_info_text => $self->render('bank_transactions/report_top', { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
709 raw_bottom_info_text => $self->render('bank_transactions/report_bottom', { output => 0 }),
713 sub _existing_record_link {
714 my ($bt, $invoice) = @_;
716 # check whether a record link from banktransaction $bt already exists to
717 # invoice $invoice, returns 1 if that is the case
719 die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
721 my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
722 my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ] );
724 return @$linked_records ? 1 : 0;
727 sub init_problems { [] }
732 SL::Controller::Helper::GetModels->new(
737 dir => 0, # 1 = ASC, 0 = DESC : default sort is newest at top
739 transdate => t8('Transdate'),
740 remote_name => t8('Remote name'),
741 amount => t8('Amount'),
742 invoice_amount => t8('Assigned'),
743 invoices => t8('Linked invoices'),
744 valutadate => t8('Valutadate'),
745 remote_account_number => t8('Remote account number'),
746 remote_bank_code => t8('Remote bank code'),
747 currency => t8('Currency'),
748 purpose => t8('Purpose'),
749 local_account_number => t8('Local account number'),
750 local_bank_code => t8('Local bank code'),
751 local_bank_name => t8('Bank account'),
753 with_objects => [ 'local_bank_account', 'currency' ],
766 SL::Controller::BankTransaction - Posting payments to invoices from
767 bank transactions imported earlier
773 =item C<save_single_bank_transaction %params>
775 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
776 tries to post its amount to a certain number of invoices (parameter
777 C<invoice_ids>, an array ref of database IDs to purchase or sales
780 The whole function is wrapped in a database transaction. If an
781 exception occurs the bank transaction is not posted at all. The same
782 is true if the code detects an error during the execution, e.g. a bank
783 transaction that's already been posted earlier. In both cases the
784 database transaction will be rolled back.
786 If warnings but not errors occur the database transaction is still
789 The return value is an error object or C<undef> if the function
790 succeeded. The calling function will collect all warnings and errors
791 and display them in a nicely formatted table if any occurred.
793 An error object is a hash reference containing the following members:
797 =item * C<result> — can be either C<warning> or C<error>. Warnings are
798 displayed slightly different than errors.
800 =item * C<message> — a human-readable message included in the list of
801 errors meant as the description of why the problem happened
803 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
804 that the function was called with
806 =item * C<bank_transaction> — the database object
807 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
809 =item * C<invoices> — an array ref of the database objects (either
810 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
819 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
820 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>