1 package SL::Controller::Reconciliation;
5 use parent qw(SL::Controller::Base);
7 use SL::Locale::String;
9 use SL::Controller::Helper::ParseFilter;
10 use SL::Helper::Flash;
12 use SL::DB::BankTransaction;
13 use SL::DB::Manager::BankAccount;
14 use SL::DB::AccTransaction;
15 use SL::DB::ReconciliationLink;
16 use List::Util qw(sum);
18 use Rose::Object::MakeMethods::Generic (
19 'scalar --get_set_init' => [ qw(cleared BANK_ACCOUNTS) ],
22 __PACKAGE__->run_before('check_auth');
23 __PACKAGE__->run_before('_bank_account');
32 $self->setup_search_action_bar;
33 $self->render('reconciliation/search',
34 title => t8('Reconciliation with bank'),
39 sub action_reconciliation {
42 $self->_get_proposals;
44 $self->_get_linked_transactions;
48 $self->setup_reconciliation_action_bar;
49 $self->render('reconciliation/form',
50 ui_tab => scalar(@{$self->{PROPOSALS}}) > 0?1:0,
51 title => t8('Reconciliation'),
55 sub action_load_overview {
58 $self->_get_proposals;
60 $self->_get_linked_transactions;
64 my $output = $self->render('reconciliation/tabs/overview', { output => 0 });
65 my %result = ( html => $output );
67 $self->render(\to_json(\%result), { type => 'json', process => 0 });
70 sub action_filter_overview {
73 $self->_get_linked_transactions;
76 my $output = $self->render('reconciliation/_linked_transactions', { output => 0 });
77 my %result = ( html => $output,
78 absolut_bt_balance => $::form->format_amount(\%::myconfig, $self->{absolut_bt_balance}, 2),
79 absolut_bb_balance => $::form->format_amount(\%::myconfig, -1 * $self->{absolut_bb_balance}, 2),
80 bt_balance => $::form->format_amount(\%::myconfig, $self->{bt_balance}, 2),
81 bb_balance => $::form->format_amount(\%::myconfig, -1 * $self->{bb_balance}, 2)
84 $self->render(\to_json(\%result), { type => 'json', process => 0 });
87 sub action_update_reconciliation_table {
90 my @errors = $self->_get_elements_and_validate();
92 my $output = $self->render('reconciliation/assigning_table', { output => 0 },
93 bt_sum => $::form->format_amount(\%::myconfig, $self->{bt_sum}, 2),
94 bb_sum => $::form->format_amount(\%::myconfig, -1 * $self->{bb_sum}, 2),
98 my %result = ( html => $output );
100 $self->render(\to_json(\%result), { type => 'json', process => 0 });
103 sub action_reconcile {
107 my @errors = $self->_get_elements_and_validate;
110 unshift(@errors, (t8('Could not reconcile chosen elements!')));
111 flash('error', @errors);
112 $self->action_reconciliation;
118 $self->action_reconciliation;
121 sub action_delete_reconciliation {
124 my $rec_links = SL::DB::Manager::ReconciliationLink->get_all(where => [ rec_group => $::form->{rec_group} ]);
126 foreach my $rec_link (@{ $rec_links }) {
127 my $bank_transaction = SL::DB::Manager::BankTransaction->find_by( id => $rec_link->bank_transaction_id );
128 my $acc_transaction = SL::DB::Manager::AccTransaction ->find_by( acc_trans_id => $rec_link->acc_trans_id );
130 $bank_transaction->cleared('0');
131 $acc_transaction->cleared('0');
133 $bank_transaction->save;
134 $acc_transaction->save;
139 $self->_get_linked_transactions;
140 $self->_get_balances;
142 my $output = $self->render('reconciliation/_linked_transactions', { output => 0 });
143 my %result = ( html => $output,
144 absolut_bt_balance => $::form->format_amount(\%::myconfig, $self ->{absolut_bt_balance}, 2),
145 absolut_bb_balance => $::form->format_amount(\%::myconfig, -1 * $self ->{absolut_bb_balance}, 2),
146 bt_balance => $::form->format_amount(\%::myconfig, $self ->{bt_balance}, 2),
147 bb_balance => $::form->format_amount(\%::myconfig, -1 * $self ->{bb_balance}, 2)
150 $self->render(\to_json(\%result), { type => 'json', process => 0 });
153 sub action_load_proposals {
156 $self->_get_proposals;
158 my $output = $self->render('reconciliation/tabs/automatic', { output => 0 });
159 my %result = ( html => $output );
161 $self->render(\to_json(\%result), { type => 'json', process => 0 });
164 sub action_filter_proposals {
167 $self->_get_balances;
168 $self->_get_proposals;
170 my $output = $self->render('reconciliation/proposals', { output => 0 });
171 my %result = ( html => $output,
172 absolut_bt_balance => $::form->format_amount(\%::myconfig, $self ->{absolut_bt_balance}, 2),
173 absolut_bb_balance => $::form->format_amount(\%::myconfig, -1 * $self ->{absolut_bb_balance}, 2),
174 bt_balance => $::form->format_amount(\%::myconfig, $self ->{bt_balance}, 2),
175 bb_balance => $::form->format_amount(\%::myconfig, -1 * $self ->{bb_balance}, 2)
178 $self->render(\to_json(\%result), { type => 'json', process => 0 });
181 sub action_reconcile_proposals {
186 # reconcile transaction safe
187 SL::DB->client->with_transaction(sub {
188 foreach my $bt_id ( @{ $::form->{bt_ids} }) {
189 my $rec_group = SL::DB::Manager::ReconciliationLink->get_new_rec_group();
190 my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
191 $bank_transaction->cleared('1');
192 $bank_transaction->save;
193 foreach my $acc_trans_id (@{ $::form->{proposal_list}->{$bt_id}->{BB} }) {
194 SL::DB::ReconciliationLink->new(
195 rec_group => $rec_group,
196 bank_transaction_id => $bt_id,
197 acc_trans_id => $acc_trans_id
199 my $acc_trans = SL::DB::Manager::AccTransaction->find_by(acc_trans_id => $acc_trans_id);
200 $acc_trans->cleared('1');
207 }) or die t8('Unable to reconcile, database transaction failure');
209 flash('ok', t8('#1 proposal(s) saved.', $counter));
211 $self->action_reconciliation;
219 $::auth->assert('bank_transaction');
224 $self->{bank_account} = SL::DB::Manager::BankAccount->find_by(id => $::form->{filter}->{"local_bank_account_id:number"});
234 # reconciliation suggestion is based on:
235 # * record_link exists (was paid by bank transaction)
236 # or acc_trans entry exists where
237 # * amount is exactly the same
239 # * IBAN or account number have to match exactly (cv details, no spaces)
241 # * there is exactly one match for all conditions
243 $self->_filter_to_where;
245 my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => [ @{ $self->{bt_where} }, cleared => '0' ]);
251 foreach my $bt (@{ $bank_transactions }) {
252 $check_sum = $bt->amount;
254 $proposal->{BT} = $bt;
255 $proposal->{BB} = [];
257 # first of all check if any of the bank_transactions are already linked (i.e. were paid via bank transactions)
258 my $linked_records = SL::DB::Manager::RecordLink->get_all(where => [ from_table => 'bank_transactions', from_id => $bt->id ]);
259 foreach my $linked_record (@{ $linked_records }) {
261 if ($linked_record->to_table eq 'ar') {
262 $invoice = SL::DB::Manager::Invoice->find_by(id => $linked_record->to_id);
264 my $payments = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, chart_id => $bt->local_bank_account->chart_id , transdate => $bt->transdate ]);
265 foreach my $payment (@{ $payments }) {
266 $check_sum += $payment->amount;
267 push @{ $proposal->{BB} }, $payment;
270 if ($linked_record->to_table eq 'ap') {
271 $invoice = SL::DB::Manager::PurchaseInvoice->find_by(id => $linked_record->to_id);
273 my $payments = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, chart_id => $bt->local_bank_account->chart_id, transdate => $bt->transdate ]);
274 foreach my $payment (@{ $payments }) {
275 $check_sum += $payment->amount;
276 push @{ $proposal->{BB} }, $payment;
281 #add proposal if something in acc_trans was found
282 #otherwise try to find another entry in acc_trans and add it
283 # for linked_records we allow a slight difference / imprecision, for acc_trans search we don't
284 if (scalar @{ $proposal->{BB} } and abs($check_sum) <= 0.01 ) {
285 push @proposals, $proposal;
286 } elsif (!scalar @{ $proposal->{BB} }) {
287 # use account_number and iban for matching remote account number
288 # don't suggest gl stornos (ar and ap stornos shouldn't have any payments)
290 my @account_number_match = (
291 ( 'ar.customer.iban' => $bt->remote_account_number ),
292 ( 'ar.customer.account_number' => $bt->remote_account_number ),
293 ( 'ap.vendor.iban' => $bt->remote_account_number ),
294 ( 'ap.vendor.account_number' => $bt->remote_account_number ),
295 ( 'gl.storno' => '0' ),
298 my $acc_transactions = SL::DB::Manager::AccTransaction->get_all(where => [ @{ $self->{bb_where} },
299 amount => -1 * $bt->amount,
301 'transdate' => $bt->transdate,
302 or => [ @account_number_match ]
304 with_objects => [ 'ar', 'ap', 'ar.customer', 'ap.vendor', 'gl' ]);
305 if (scalar @{ $acc_transactions } == 1) {
306 push @{ $proposal->{BB} }, @{ $acc_transactions }[0];
307 push @proposals, $proposal;
312 $self->{PROPOSALS} = \@proposals;
315 sub _get_elements_and_validate {
320 if ( not defined $::form->{bt_ids} ) {
321 push @errors, t8('No bank account chosen!');
324 if ( not defined $::form->{bb_ids} ) {
325 push @errors, t8('No transaction on chart bank chosen!');
329 if (scalar @{ $::form->{bt_ids} } > 1 and scalar @{ $::form->{bb_ids} } > 1) {
330 push @errors, t8('No 1:n or n:1 relation');
335 my ($bt_sum, $bb_sum) = (0,0);
337 foreach my $bt_id (@{ $::form->{bt_ids} }) {
338 my $bt = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
340 $bt_sum += $bt->amount;
344 foreach my $bb_id (@{ $::form->{bb_ids} }) {
345 my $bb = SL::DB::Manager::AccTransaction->find_by(acc_trans_id => $bb_id);
347 $bb->{id} = $bb->acc_trans_id;
348 $bb_sum += $bb->amount;
352 if ($::form->round_amount($bt_sum + $bb_sum, 2) != 0) {
353 push @errors, t8('Out of balance!'), t8('Sum of bank #1 and sum of bookings #2',$bt_sum, $bb_sum);
356 $self->{ELEMENTS} = \@elements;
357 $self->{bt_sum} = $bt_sum;
358 $self->{bb_sum} = $bb_sum;
366 # reconcile transaction safe
367 SL::DB->client->with_transaction(sub {
369 # 1. step: set AccTrans and BankTransactions to 'cleared'
370 foreach my $element (@{ $self->{ELEMENTS} }) {
371 $element->cleared('1');
372 # veto either invoice_amount is fully assigned or not! No state tricks in later workflow!
375 # 2. step: insert entry in reconciliation_links
376 my $rec_group = SL::DB::Manager::ReconciliationLink->get_new_rec_group();
377 #There is either a 1:n relation or a n:1 relation
378 if (scalar @{ $::form->{bt_ids} } == 1) {
379 my $bt_id = @{ $::form->{bt_ids} }[0];
380 foreach my $bb_id (@{ $::form->{bb_ids} }) {
381 my $rec_link = SL::DB::ReconciliationLink->new(bank_transaction_id => $bt_id,
382 acc_trans_id => $bb_id,
383 rec_group => $rec_group);
387 my $bb_id = @{ $::form->{bb_ids} }[0];
388 foreach my $bt_id (@{ $::form->{bt_ids} }) {
389 my $rec_link = SL::DB::ReconciliationLink->new(bank_transaction_id => $bt_id,
390 acc_trans_id => $bb_id,
391 rec_group => $rec_group);
397 }) or die t8('Unable to reconcile, database transaction failure');
400 sub _filter_to_where {
403 my %parse_filter = parse_filter($::form->{filter});
404 my %filter = @{ $parse_filter{query} };
406 my (@rl_where, @bt_where, @bb_where);
407 @rl_where = ('bank_transaction.local_bank_account_id' => $filter{local_bank_account_id});
408 @bt_where = (local_bank_account_id => $filter{local_bank_account_id});
409 @bb_where = (chart_id => $self->{bank_account}->chart_id);
411 if ($filter{fromdate} and $filter{todate}) {
413 push @rl_where, (or => [ and => [ 'acc_trans.transdate' => $filter{fromdate},
414 'acc_trans.transdate' => $filter{todate} ],
415 and => [ 'bank_transaction.transdate' => $filter{fromdate},
416 'bank_transaction.transdate' => $filter{todate} ] ] );
418 push @bt_where, (transdate => $filter{todate} );
419 push @bt_where, (transdate => $filter{fromdate} );
420 push @bb_where, (transdate => $filter{todate} );
421 push @bb_where, (transdate => $filter{fromdate} );
424 if ( $self->{bank_account}->reconciliation_starting_date ) {
425 push @bt_where, (transdate => { ge => $self->{bank_account}->reconciliation_starting_date });
426 push @bb_where, (transdate => { ge => $self->{bank_account}->reconciliation_starting_date });
429 # don't try to reconcile opening and closing balance transactions
430 push @bb_where, ('acc_trans.ob_transaction' => 0);
431 push @bb_where, ('acc_trans.cb_transaction' => 0);
433 if ($filter{fromdate} and not $filter{todate}) {
434 push @rl_where, (or => [ 'acc_trans.transdate' => $filter{fromdate},
435 'bank_transaction.transdate' => $filter{fromdate} ] );
436 push @bt_where, (transdate => $filter{fromdate} );
437 push @bb_where, (transdate => $filter{fromdate} );
440 if ($filter{todate} and not $filter{fromdate}) {
441 push @rl_where, ( or => [ 'acc_trans.transdate' => $filter{todate} ,
442 'bank_transaction.transdate' => $filter{todate} ] );
443 push @bt_where, (transdate => $filter{todate} );
444 push @bb_where, (transdate => $filter{todate} );
447 if ($filter{cleared}) {
448 $filter{cleared} = $filter{cleared} eq 'FALSE' ? '0' : '1';
449 push @rl_where, ('acc_trans.cleared' => $filter{cleared} );
451 push @bt_where, (cleared => $filter{cleared} );
452 push @bb_where, (cleared => $filter{cleared} );
455 $self->{rl_where} = \@rl_where;
456 $self->{bt_where} = \@bt_where;
457 $self->{bb_where} = \@bb_where;
460 sub _get_linked_transactions {
463 $self->_filter_to_where;
465 my (@where, @bt_where, @bb_where);
466 # don't try to reconcile opening and closing balances
467 # instead use an offset in configuration
469 @where = (@{ $self->{rl_where} });
470 @bt_where = (@{ $self->{bt_where} }, cleared => '0');
471 @bb_where = (@{ $self->{bb_where} }, cleared => '0');
475 my $reconciliation_groups = SL::DB::Manager::ReconciliationLink->get_all(distinct => 1,
476 select => ['rec_group'],
478 with_objects => ['bank_transaction', 'acc_trans']);
480 my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate_date__ge});
481 my $todate = $::locale->parse_date_to_object($::form->{filter}->{todate_date__le});
483 foreach my $rec_group (@{ $reconciliation_groups }) {
484 my $linked_transactions = SL::DB::Manager::ReconciliationLink->get_all(where => [rec_group => $rec_group->rec_group], with_objects => ['bank_transaction', 'acc_trans']);
486 my $first_transaction = shift @{ $linked_transactions };
487 my $first_bt = $first_transaction->bank_transaction;
488 my $first_bb = $first_transaction->acc_trans;
490 if (defined $fromdate) {
491 $first_bt->{class} = 'out_of_balance' if ( $first_bt->transdate lt $fromdate );
492 $first_bb->{class} = 'out_of_balance' if ( $first_bb->transdate lt $fromdate );
494 if (defined $todate) {
495 $first_bt->{class} = 'out_of_balance' if ( $first_bt->transdate gt $todate );
496 $first_bb->{class} = 'out_of_balance' if ( $first_bb->transdate gt $todate );
498 $line->{BT} = [ $first_bt ];
499 $line->{BB} = [ $first_bb ];
500 $line->{rec_group} = $first_transaction->rec_group;
501 $line->{type} = 'Link';
503 #add the rest of transaction of this group
504 my ($previous_bt_id, $previous_acc_trans_id) = ($first_transaction->bank_transaction_id, $first_transaction->acc_trans_id);
505 foreach my $linked_transaction (@{ $linked_transactions }) {
506 my $bank_transaction = $linked_transaction->bank_transaction;
507 my $acc_transaction = $linked_transaction->acc_trans;
508 if (defined $fromdate) {
509 $bank_transaction->{class} = 'out_of_balance' if ( $bank_transaction->transdate lt $fromdate );
510 $acc_transaction->{class} = 'out_of_balance' if ( $acc_transaction->transdate lt $fromdate );
512 if (defined $todate) {
513 $bank_transaction->{class} = 'out_of_balance' if ( $bank_transaction->transdate gt $todate );
514 $acc_transaction->{class} = 'out_of_balance' if ( $acc_transaction->transdate gt $todate );
516 if ($bank_transaction->id != $previous_bt_id) {
517 push @{ $line->{BT} }, $bank_transaction;
519 if ($acc_transaction->acc_trans_id != $previous_acc_trans_id) {
520 push @{ $line->{BB} }, $acc_transaction;
526 # add non-cleared bank transactions
527 my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => \@bt_where);
528 foreach my $bt (@{ $bank_transactions }) {
530 $line->{BT} = [ $bt ];
531 $line->{type} = 'BT';
532 $line->{id} = $bt->id;
536 # add non-cleared bookings on bank
537 my $bookings_on_bank = SL::DB::Manager::AccTransaction->get_all(where => \@bb_where);
538 foreach my $bb (@{ $bookings_on_bank }) {
539 if ($::form->{filter}->{show_stornos} or !$bb->record->storno) {
541 $line->{BB} = [ $bb ];
542 $line->{type} = 'BB';
543 $line->{id} = $bb->acc_trans_id;
549 @rows = sort sort_by_transdate @rows;
551 $self->{LINKED_TRANSACTIONS} = \@rows;
554 sub sort_by_transdate {
555 if ($a->{BT} and $b->{BT}) {
556 return $a->{BT}[0]->amount <=> $b->{BT}[0]->amount if $a->{BT}[0]->transdate eq $b->{BT}[0]->transdate;
557 return $a->{BT}[0]->transdate cmp $b->{BT}[0]->transdate;
560 return $a->{BT}[0]->amount <=> (-1 * $b->{BB}[0]->amount) if $a->{BT}[0]->transdate eq $b->{BB}[0]->transdate;
561 return $a->{BT}[0]->transdate cmp $b->{BB}[0]->transdate;
564 return (-1 * $a->{BB}[0]->amount) <=> $b->{BT}[0]->amount if $a->{BB}[0]->transdate eq $b->{BT}[0]->transdate;
565 return $a->{BB}[0]->transdate cmp $b->{BT}[0]->transdate;
567 return (-1 * $a->{BB}[0]->amount) <=> (-1 * $b->{BB}[0]->amount) if $a->{BB}[0]->transdate eq $b->{BB}[0]->transdate;
568 return $a->{BB}[0]->transdate cmp $b->{BB}[0]->transdate;
574 $self->_filter_to_where;
576 my (@bt_where, @bb_where);
577 @bt_where = @{ $self->{bt_where} };
578 @bb_where = @{ $self->{bb_where} };
580 my @all_bt_where = (local_bank_account_id => $self->{bank_account}->id);
581 my @all_bb_where = (chart_id => $self->{bank_account}->chart_id);
583 my ($bt_balance, $bb_balance) = (0,0);
584 my ($absolut_bt_balance, $absolut_bb_balance) = (0,0);
586 if ( $self->{bank_account}->reconciliation_starting_date ) {
587 $bt_balance = $self->{bank_account}->reconciliation_starting_balance;
588 $bb_balance = $self->{bank_account}->reconciliation_starting_balance * -1;
589 $absolut_bt_balance = $self->{bank_account}->reconciliation_starting_balance;
590 $absolut_bb_balance = $self->{bank_account}->reconciliation_starting_balance * -1;
592 push @all_bt_where, ( transdate => { gt => $self->{bank_account}->reconciliation_starting_date });
593 push @all_bb_where, ( transdate => { gt => $self->{bank_account}->reconciliation_starting_date });
596 my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => \@bt_where );
597 my $payments = SL::DB::Manager::AccTransaction ->get_all(where => \@bb_where );
599 # for absolute balance get all bookings until todate
600 my $todate = $::locale->parse_date_to_object($::form->{filter}->{todate_date__le});
601 my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate_date__le});
604 push @all_bt_where, (transdate => { le => $todate });
605 push @all_bb_where, (transdate => { le => $todate });
608 my $all_bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => \@all_bt_where);
609 my $all_payments = SL::DB::Manager::AccTransaction ->get_all(where => \@all_bb_where);
611 $bt_balance += sum map { $_->amount } @{ $bank_transactions };
612 $bb_balance += sum map { $_->amount if ($::form->{filter}->{show_stornos} or !$_->record->storno) } @{ $payments };
614 $absolut_bt_balance += sum map { $_->amount } @{ $all_bank_transactions };
615 $absolut_bb_balance += sum map { $_->amount } @{ $all_payments };
618 $self->{bt_balance} = $bt_balance || 0;
619 $self->{bb_balance} = $bb_balance || 0;
620 $self->{absolut_bt_balance} = $absolut_bt_balance || 0;
621 $self->{absolut_bb_balance} = $absolut_bb_balance || 0;
623 $self->{difference} = $bt_balance + $bb_balance;
627 [ { title => t8("all"), value => '' },
628 { title => t8("cleared"), value => 'TRUE' },
629 { title => t8("uncleared"), value => 'FALSE' }, ]
632 sub init_BANK_ACCOUNTS {
633 SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
636 sub setup_search_action_bar {
637 my ($self, %params) = @_;
639 for my $bar ($::request->layout->get('actionbar')) {
643 submit => [ '#search_form', { action => 'Reconciliation/reconciliation' } ],
644 accesskey => 'enter',
650 sub setup_reconciliation_action_bar {
651 my ($self, %params) = @_;
653 for my $bar ($::request->layout->get('actionbar')) {
657 call => [ 'filter_table' ],
658 accesskey => 'enter',