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);
29 use List::MoreUtils qw(any);
30 use List::Util qw(max);
32 use Rose::Object::MakeMethods::Generic
34 'scalar --get_set_init' => [ qw(models problems) ],
37 __PACKAGE__->run_before('check_auth');
47 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
49 $self->render('bank_transactions/search',
50 BANK_ACCOUNTS => $bank_accounts);
56 $self->make_filter_summary;
57 $self->prepare_report;
59 $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
65 if (!$::form->{filter}{bank_account}) {
66 flash('error', t8('No bank account chosen!'));
71 my $sort_by = $::form->{sort_by} || 'transdate';
72 $sort_by = 'transdate' if $sort_by eq 'proposal';
73 $sort_by .= $::form->{sort_dir} ? ' DESC' : ' ASC';
75 my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
76 my $todate = $::locale->parse_date_to_object($::form->{filter}->{todate});
77 $todate->add( days => 1 ) if $todate;
80 push @where, (transdate => { ge => $fromdate }) if ($fromdate);
81 push @where, (transdate => { lt => $todate }) if ($todate);
82 my $bank_account = SL::DB::Manager::BankAccount->find_by( id => $::form->{filter}{bank_account} );
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 ( $bank_account->reconciliation_starting_date ) {
87 push @where, (transdate => { ge => $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'},
96 local_bank_account_id => $::form->{filter}{bank_account},
101 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => [amount => { gt => \'paid' }], with_objects => 'customer');
102 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { gt => \'paid' }], with_objects => 'vendor');
104 my @all_open_invoices;
105 # filter out invoices with less than 1 cent outstanding
106 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
107 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
109 # try to match each bank_transaction with each of the possible open invoices
112 foreach my $bt (@{ $bank_transactions }) {
113 next unless $bt->{remote_name}; # bank has no name, usually fees, use create invoice to assign
115 $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
117 # try to match the current $bt to each of the open_invoices, saving the
118 # results of get_agreement_with_invoice in $open_invoice->{agreement} and
119 # $open_invoice->{rule_matches}.
121 # The values are overwritten each time a new bt is checked, so at the end
122 # of each bt the likely results are filtered and those values are stored in
123 # the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
124 # score is stored in $bt->{agreement}
126 foreach my $open_invoice (@all_open_invoices){
127 ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
130 $bt->{proposals} = [];
133 my $min_agreement = 3; # suggestions must have at least this score
135 my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
137 # add open_invoices with highest agreement into array $bt->{proposals}
138 if ( $max_agreement >= $min_agreement ) {
139 $bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
140 $bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
142 # store the rule_matches in a separate array, so they can be displayed in template
143 foreach ( @{ $bt->{proposals} } ) {
144 push(@{$bt->{rule_matches}}, $_->{rule_matches});
150 # separate filter for proposals (second tab, agreement >= 5 and exactly one match)
151 # to qualify as a proposal there has to be
152 # * agreement >= 5 TODO: make threshold configurable in configuration
153 # * there must be only one exact match
154 # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
155 my $proposal_threshold = 5;
156 my @proposals = grep {
157 ($_->{agreement} >= $proposal_threshold)
158 && (1 == scalar @{ $_->{proposals} })
159 && (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01
160 : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01)
161 } @{ $bank_transactions };
163 # sort bank transaction proposals by quality (score) of proposal
164 $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 1;
165 $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 0;
168 $self->render('bank_transactions/list',
169 title => t8('Bank transactions MT940'),
170 BANK_TRANSACTIONS => $bank_transactions,
171 PROPOSALS => \@proposals,
172 bank_account => $bank_account );
175 sub action_assign_invoice {
178 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
180 $self->render('bank_transactions/assign_invoice',
182 title => t8('Assign invoice'),);
185 sub action_create_invoice {
187 my %myconfig = %main::myconfig;
189 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
190 my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
192 my $use_vendor_filter = $self->{transaction}->{remote_account_number} && $vendor_of_transaction;
194 my $drafts = SL::DB::Manager::Draft->get_all(where => [ module => 'ap'] , with_objects => 'employee');
198 foreach my $draft ( @{ $drafts } ) {
199 my $draft_as_object = YAML::Load($draft->form);
200 my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
201 $draft->{vendor} = $vendor->name;
202 $draft->{vendor_id} = $vendor->id;
203 push @filtered_drafts, $draft;
207 @filtered_drafts = grep { $_->{vendor_id} == $vendor_of_transaction->id } @filtered_drafts if $use_vendor_filter;
209 my $all_vendors = SL::DB::Manager::Vendor->get_all();
210 my $callback = $self->url_for(action => 'list',
211 'filter.bank_account' => $::form->{filter}->{bank_account},
212 'filter.todate' => $::form->{filter}->{todate},
213 'filter.fromdate' => $::form->{filter}->{fromdate});
216 'bank_transactions/create_invoice',
218 title => t8('Create invoice'),
219 DRAFTS => \@filtered_drafts,
220 vendor_id => $use_vendor_filter ? $vendor_of_transaction->id : undef,
221 vendor_name => $use_vendor_filter ? $vendor_of_transaction->name : undef,
222 ALL_VENDORS => $all_vendors,
223 limit => $myconfig{vclimit},
224 callback => $callback,
228 sub action_ajax_payment_suggestion {
231 # based on a BankTransaction ID and a Invoice or PurchaseInvoice ID passed via $::form,
232 # create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
233 # and return encoded as JSON
235 my $bt = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
236 my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
238 die unless $bt and $invoice;
240 my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
243 $html .= SL::Presenter->input_tag('invoice_ids.' . $::form->{bt_id} . '[]', $::form->{prop_id} , type => 'hidden');
244 $html .= SL::Presenter->escape(t8('Invno.') . ': ' . $invoice->invnumber . ' ');
245 $html .= SL::Presenter->escape(t8('Open amount') . ': ' . $::form->format_amount(\%::myconfig, $invoice->open_amount, 2) . ' ');
246 $html .= SL::Presenter->select_tag('invoice_skontos.' . $::form->{bt_id} . '[]',
248 value_key => 'payment_type',
249 title_key => 'display' )
251 $html .= '<a href=# onclick="delete_invoice(' . $::form->{bt_id} . ',' . $::form->{prop_id} . ');">x</a>';
252 $html = SL::Presenter->html_tag('div', $html, id => $::form->{bt_id} . '.' . $::form->{prop_id});
254 $self->render(\ SL::JSON::to_json( { 'html' => $html } ), { layout => 0, type => 'json', process => 0 });
257 sub action_filter_drafts {
260 $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
261 my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
263 my $drafts = SL::DB::Manager::Draft->get_all(with_objects => 'employee');
267 foreach my $draft ( @{ $drafts } ) {
268 my $draft_as_object = YAML::Load($draft->form);
269 next unless $draft_as_object->{vendor_id}; # we cannot filter for vendor name, if this is a gl draft
271 my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
272 $draft->{vendor} = $vendor->name;
273 $draft->{vendor_id} = $vendor->id;
275 push @filtered_drafts, $draft;
278 my $vendor_name = $::form->{vendor};
279 my $vendor_id = $::form->{vendor_id};
282 @filtered_drafts = grep { $_->{vendor_id} == $vendor_id } @filtered_drafts if $vendor_id;
283 @filtered_drafts = grep { $_->{vendor} =~ /$vendor_name/i } @filtered_drafts if $vendor_name;
285 my $output = $self->render(
286 'bank_transactions/filter_drafts',
288 DRAFTS => \@filtered_drafts,
291 my %result = ( count => 0, html => $output );
293 $self->render(\to_json(\%result), { type => 'json', process => 0 });
296 sub action_ajax_add_list {
299 my @where_sale = (amount => { ne => \'paid' });
300 my @where_purchase = (amount => { ne => \'paid' });
302 if ($::form->{invnumber}) {
303 push @where_sale, (invnumber => { ilike => like($::form->{invnumber})});
304 push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
307 if ($::form->{amount}) {
308 push @where_sale, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
309 push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
312 if ($::form->{vcnumber}) {
313 push @where_sale, ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
314 push @where_purchase, ('vendor.vendornumber' => { ilike => like($::form->{vcnumber})});
317 if ($::form->{vcname}) {
318 push @where_sale, ('customer.name' => { ilike => like($::form->{vcname})});
319 push @where_purchase, ('vendor.name' => { ilike => like($::form->{vcname})});
322 if ($::form->{transdatefrom}) {
323 my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
324 if ( ref($fromdate) eq 'DateTime' ) {
325 push @where_sale, ('transdate' => { ge => $fromdate});
326 push @where_purchase, ('transdate' => { ge => $fromdate});
330 if ($::form->{transdateto}) {
331 my $todate = $::locale->parse_date_to_object($::form->{transdateto});
332 if ( ref($todate) eq 'DateTime' ) {
333 $todate->add(days => 1);
334 push @where_sale, ('transdate' => { lt => $todate});
335 push @where_purchase, ('transdate' => { lt => $todate});
339 my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => \@where_sale, with_objects => 'customer');
340 my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
342 my @all_open_invoices = @{ $all_open_ar_invoices };
343 # add ap invoices, filtering out subcent open amounts
344 push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
346 @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
348 my $output = $self->render(
349 'bank_transactions/add_list',
351 INVOICES => \@all_open_invoices,
354 my %result = ( count => 0, html => $output );
356 $self->render(\to_json(\%result), { type => 'json', process => 0 });
359 sub action_ajax_accept_invoices {
362 my @selected_invoices;
363 foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
364 my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
365 push @selected_invoices, $invoice_object;
369 'bank_transactions/invoices',
371 INVOICES => \@selected_invoices,
372 bt_id => $::form->{bt_id},
376 sub action_save_invoices {
379 my $invoice_hash = delete $::form->{invoice_ids}; # each key (the bt line with a bt_id) contains an array of invoice_ids
381 # e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
394 # or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
396 # '44' => [ '50', '51', 52' ]
399 $::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
401 # a bank_transaction may be assigned to several invoices, i.e. a customer
402 # might pay several open invoices with one transaction
406 while ( my ($bank_transaction_id, $invoice_ids) = each(%$invoice_hash) ) {
407 push @{ $self->problems }, $self->save_single_bank_transaction(
408 bank_transaction_id => $bank_transaction_id,
409 invoice_ids => $invoice_ids,
413 $self->action_list();
416 sub save_single_bank_transaction {
417 my ($self, %params) = @_;
421 bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
425 if (!$data{bank_transaction}) {
429 message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
436 my $bt_id = $data{bank_transaction_id};
437 my $bank_transaction = $data{bank_transaction};
438 my $sign = $bank_transaction->amount < 0 ? -1 : 1;
439 my $amount_of_transaction = $sign * $bank_transaction->amount;
440 my $payment_received = $bank_transaction->amount > 0;
441 my $payment_sent = $bank_transaction->amount < 0;
443 foreach my $invoice_id (@{ $params{invoice_ids} }) {
444 my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
449 message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
453 push @{ $data{invoices} }, $invoice;
456 if ( $payment_received
457 && any { ( $_->is_sales && ($_->amount < 0))
458 || (!$_->is_sales && ($_->amount > 0))
459 } @{ $data{invoices} }) {
463 message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
468 && any { ( $_->is_sales && ($_->amount > 0))
469 || (!$_->is_sales && ($_->amount < 0))
470 } @{ $data{invoices} }) {
474 message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
478 my $max_invoices = scalar(@{ $data{invoices} });
481 foreach my $invoice (@{ $data{invoices} }) {
485 # Check if bank_transaction already has a link to the invoice, may only be linked once per invoice
486 # This might be caused by the user reloading a page and resending the form
487 if (_existing_record_link($bank_transaction, $invoice)) {
491 message => $::locale->text("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name),
495 if (!$amount_of_transaction && $invoice->open_amount) {
499 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."),
504 if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
505 $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
507 $payment_type = 'without_skonto';
510 # pay invoice or go to the next bank transaction if the amount is not sufficiently high
511 if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) {
512 # first calculate new bank transaction amount ...
513 if ($invoice->is_sales) {
514 $amount_of_transaction -= $sign * $invoice->open_amount;
515 $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $invoice->open_amount);
517 $amount_of_transaction += $sign * $invoice->open_amount;
518 $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $invoice->open_amount);
520 # ... and then pay the invoice
521 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
522 trans_id => $invoice->id,
523 amount => $invoice->open_amount,
524 payment_type => $payment_type,
525 transdate => $bank_transaction->transdate->to_kivitendo);
526 } else { # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary
527 my $overpaid_amount = $amount_of_transaction - $invoice->open_amount;
528 $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
529 trans_id => $invoice->id,
530 amount => $amount_of_transaction,
531 payment_type => $payment_type,
532 transdate => $bank_transaction->transdate->to_kivitendo);
533 $bank_transaction->invoice_amount($bank_transaction->amount);
534 $amount_of_transaction = 0;
536 if ($overpaid_amount >= 0.01) {
540 message => $::locale->text('Invoice #1 was overpaid by #2.', $invoice->invnumber, $::form->format_amount(\%::myconfig, $overpaid_amount, 2)),
545 # Record a record link from the bank transaction to the invoice
547 from_table => 'bank_transactions',
549 to_table => $invoice->is_sales ? 'ar' : 'ap',
550 to_id => $invoice->id,
553 SL::DB::RecordLink->new(@props)->save;
555 # "close" a sepa_export_item if it exists
556 # code duplicated in action_save_proposals!
557 # currently only works, if there is only exactly one open sepa_export_item
558 if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
559 if ( scalar @$seis == 1 ) {
560 # moved the execution and the check for sepa_export into a method,
561 # this isn't part of a transaction, though
562 $seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
567 $bank_transaction->save;
569 # 'undef' means 'no error' here.
574 my $rez = $data{bank_transaction}->db->with_transaction(sub {
576 $error = $worker->();
590 return grep { $_ } ($error, @warnings);
593 sub action_save_proposals {
596 foreach my $bt_id (@{ $::form->{proposal_ids} }) {
597 my $bt = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
599 my $arap = SL::DB::Manager::Invoice->find_by(id => $::form->{"proposed_invoice_$bt_id"});
600 $arap = SL::DB::Manager::PurchaseInvoice->find_by(id => $::form->{"proposed_invoice_$bt_id"}) if not defined $arap;
602 # check for existing record_link for that $bt and $arap
603 # do this before any changes to $bt are made
604 die t8("Bank transaction with id #1 has already been linked to #2.", $bt->id, $arap->displayable_name)
605 if _existing_record_link($bt, $arap);
608 $bt->invoice_amount($bt->amount);
612 $arap->pay_invoice(chart_id => $bt->local_bank_account->chart_id,
613 trans_id => $arap->id,
614 amount => $arap->amount,
615 transdate => $bt->transdate->to_kivitendo);
620 from_table => 'bank_transactions',
622 to_table => $arap->is_sales ? 'ar' : 'ap',
626 SL::DB::RecordLink->new(@props)->save;
628 # code duplicated in action_save_invoices!
629 # "close" a sepa_export_item if it exists
630 # currently only works, if there is only exactly one open sepa_export_item
631 if ( my $seis = $arap->find_sepa_export_items({ executed => 0 }) ) {
632 if ( scalar @$seis == 1 ) {
633 # moved the execution and the check for sepa_export into a method,
634 # this isn't part of a transaction, though
635 $seis->[0]->set_executed if $arap->id == $seis->[0]->arap_id;
640 flash('ok', t8('#1 proposal(s) saved.', scalar @{ $::form->{proposal_ids} }));
642 $self->action_list();
650 $::auth->assert('bank_transaction');
657 sub make_filter_summary {
660 my $filter = $::form->{filter} || {};
664 [ $filter->{"transdate:date::ge"}, $::locale->text('Transdate') . " " . $::locale->text('From Date') ],
665 [ $filter->{"transdate:date::le"}, $::locale->text('Transdate') . " " . $::locale->text('To Date') ],
666 [ $filter->{"valutadate:date::ge"}, $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
667 [ $filter->{"valutadate:date::le"}, $::locale->text('Valutadate') . " " . $::locale->text('To Date') ],
668 [ $filter->{"amount:number"}, $::locale->text('Amount') ],
669 [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account') ],
673 push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
676 $self->{filter_summary} = join ', ', @filter_strings;
682 my $callback = $self->models->get_callback;
684 my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
685 $self->{report} = $report;
687 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);
688 my @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code);
691 transdate => { sub => sub { $_[0]->transdate_as_date } },
692 valutadate => { sub => sub { $_[0]->valutadate_as_date } },
694 remote_account_number => { },
695 remote_bank_code => { },
696 amount => { sub => sub { $_[0]->amount_as_number },
698 invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number },
700 invoices => { sub => sub { $_[0]->linked_invoices } },
701 currency => { sub => sub { $_[0]->currency->name } },
703 local_account_number => { sub => sub { $_[0]->local_bank_account->account_number } },
704 local_bank_code => { sub => sub { $_[0]->local_bank_account->bank_code } },
705 local_bank_name => { sub => sub { $_[0]->local_bank_account->name } },
709 map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
711 $report->set_options(
712 std_column_visibility => 1,
713 controller_class => 'BankTransaction',
714 output_format => 'HTML',
715 top_info_text => $::locale->text('Bank transactions'),
716 title => $::locale->text('Bank transactions'),
717 allow_pdf_export => 1,
718 allow_csv_export => 1,
720 $report->set_columns(%column_defs);
721 $report->set_column_order(@columns);
722 $report->set_export_options(qw(list_all filter));
723 $report->set_options_from_form;
724 $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
725 $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
727 my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
729 $report->set_options(
730 raw_top_info_text => $self->render('bank_transactions/report_top', { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
731 raw_bottom_info_text => $self->render('bank_transactions/report_bottom', { output => 0 }),
735 sub _existing_record_link {
736 my ($bt, $invoice) = @_;
738 # check whether a record link from banktransaction $bt already exists to
739 # invoice $invoice, returns 1 if that is the case
741 die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
743 my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
744 my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ] );
746 return @$linked_records ? 1 : 0;
749 sub init_problems { [] }
754 SL::Controller::Helper::GetModels->new(
759 dir => 0, # 1 = ASC, 0 = DESC : default sort is newest at top
761 transdate => t8('Transdate'),
762 remote_name => t8('Remote name'),
763 amount => t8('Amount'),
764 invoice_amount => t8('Assigned'),
765 invoices => t8('Linked invoices'),
766 valutadate => t8('Valutadate'),
767 remote_account_number => t8('Remote account number'),
768 remote_bank_code => t8('Remote bank code'),
769 currency => t8('Currency'),
770 purpose => t8('Purpose'),
771 local_account_number => t8('Local account number'),
772 local_bank_code => t8('Local bank code'),
773 local_bank_name => t8('Bank account'),
775 with_objects => [ 'local_bank_account', 'currency' ],
788 SL::Controller::BankTransaction - Posting payments to invoices from
789 bank transactions imported earlier
795 =item C<save_single_bank_transaction %params>
797 Takes a bank transaction ID (as parameter C<bank_transaction_id> and
798 tries to post its amount to a certain number of invoices (parameter
799 C<invoice_ids>, an array ref of database IDs to purchase or sales
802 The whole function is wrapped in a database transaction. If an
803 exception occurs the bank transaction is not posted at all. The same
804 is true if the code detects an error during the execution, e.g. a bank
805 transaction that's already been posted earlier. In both cases the
806 database transaction will be rolled back.
808 If warnings but not errors occur the database transaction is still
811 The return value is an error object or C<undef> if the function
812 succeeded. The calling function will collect all warnings and errors
813 and display them in a nicely formatted table if any occurred.
815 An error object is a hash reference containing the following members:
819 =item * C<result> — can be either C<warning> or C<error>. Warnings are
820 displayed slightly different than errors.
822 =item * C<message> — a human-readable message included in the list of
823 errors meant as the description of why the problem happened
825 =item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
826 that the function was called with
828 =item * C<bank_transaction> — the database object
829 (C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
831 =item * C<invoices> — an array ref of the database objects (either
832 C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
841 Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
842 Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>