X-Git-Url: http://wagnertech.de/gitweb/gitweb.cgi/mfinanz.git/blobdiff_plain/07181f33712819fddc1dbb460f2fd4b86f441d56..9af3e1927c6fe3c631b63310888f1909039f24b6:/SL/Controller/BankTransaction.pm diff --git a/SL/Controller/BankTransaction.pm b/SL/Controller/BankTransaction.pm index bb9d07140..69bc63114 100644 --- a/SL/Controller/BankTransaction.pm +++ b/SL/Controller/BankTransaction.pm @@ -17,6 +17,7 @@ use SL::SEPA; use SL::DB::Invoice; use SL::DB::PurchaseInvoice; use SL::DB::RecordLink; +use SL::DB::ReconciliationLink; use SL::JSON; use SL::DB::Chart; use SL::DB::AccTransaction; @@ -25,8 +26,10 @@ use SL::DB::Tax; use SL::DB::BankAccount; use SL::DB::RecordTemplate; use SL::DB::SepaExportItem; -use SL::DBUtils qw(like); +use SL::DBUtils qw(like do_query); +use SL::Presenter::Tag qw(checkbox_tag); +use Carp; use List::UtilsBy qw(partition_by); use List::MoreUtils qw(any); use List::Util qw(max); @@ -558,20 +561,12 @@ sub save_single_bank_transaction { my $bank_transaction = $data{bank_transaction}; - # see pod - if (@{ $bank_transaction->linked_invoices } || $bank_transaction->invoice_amount != 0) { - return { - %data, - result => 'error', - message => $::locale->text("Bank transaction with id #1 has already been linked to one or more record and/or some amount is already assigned.", $bank_transaction->id), - }; - } my (@warnings); my $worker = sub { my $bt_id = $data{bank_transaction_id}; my $sign = $bank_transaction->amount < 0 ? -1 : 1; - my $amount_of_transaction = $sign * $bank_transaction->amount; + my $not_assigned_amount = $bank_transaction->not_assigned_amount; my $payment_received = $bank_transaction->amount > 0; my $payment_sent = $bank_transaction->amount < 0; @@ -620,7 +615,7 @@ sub save_single_bank_transaction { $n_invoices++ ; - if (!$amount_of_transaction && $invoice->open_amount) { + if (!$not_assigned_amount && $invoice->open_amount) { return { %data, result => 'error', @@ -634,82 +629,41 @@ sub save_single_bank_transaction { } else { $payment_type = 'without_skonto'; }; - - - # pay invoice or go to the next bank transaction if the amount is not sufficiently high - if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) { - my $open_amount = ($payment_type eq 'with_skonto_pt'?$invoice->amount_less_skonto:$invoice->open_amount); - # first calculate new bank transaction amount ... - if ($invoice->is_sales) { - $amount_of_transaction -= $sign * $open_amount; - $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $open_amount); - } else { - $amount_of_transaction += $sign * $open_amount; - $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $open_amount); - } - # ... and then pay the invoice - my @acc_ids = $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id, - trans_id => $invoice->id, - amount => $open_amount, - payment_type => $payment_type, - source => $source, - memo => $memo, - transdate => $bank_transaction->transdate->to_kivitendo); - # ... and record the origin via BankTransactionAccTrans - if (scalar(@acc_ids) != 2) { - return { - %data, - result => 'error', - message => $::locale->text("Unable to book transactions for bank purpose #1", $bank_transaction->purpose), - }; - } - foreach my $acc_trans_id (@acc_ids) { - my $id_type = $invoice->is_sales ? 'ar' : 'ap'; - my %props_acc = ( - acc_trans_id => $acc_trans_id, - bank_transaction_id => $bank_transaction->id, - $id_type => $invoice->id, - ); - SL::DB::BankTransactionAccTrans->new(%props_acc)->save; - } - - - } else { - # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary - - # $invoice->open_amount is negative for credit_notes - # $bank_transaction->amount is negative for outgoing transactions - # so $amount_of_transaction is negative but needs positive - # $invoice->open_amount may be negative for ap_transaction but may be positiv for negative ap_transaction - # if $invoice->open_amount is negative $bank_transaction->amount is positve - # if $invoice->open_amount is positive $bank_transaction->amount is negative - # but amount of transaction is for both positive - - $amount_of_transaction *= -1 if ($invoice->amount < 0); - - # if we have a skonto case - the last invoice needs skonto - $amount_of_transaction = $invoice->amount_less_skonto if ($payment_type eq 'with_skonto_pt'); - - - my $overpaid_amount = $amount_of_transaction - $invoice->open_amount; - $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id, - trans_id => $invoice->id, - amount => $amount_of_transaction, - payment_type => $payment_type, - source => $source, - memo => $memo, - transdate => $bank_transaction->transdate->to_kivitendo); - $bank_transaction->invoice_amount($bank_transaction->amount); - $amount_of_transaction = 0; - - if ($overpaid_amount >= 0.01) { - push @warnings, { - %data, - result => 'warning', - message => $::locale->text('Invoice #1 was overpaid by #2.', $invoice->invnumber, $::form->format_amount(\%::myconfig, $overpaid_amount, 2)), - }; - } - } + # pay invoice + # TODO rewrite this: really booked amount should be a return value of Payment.pm + # also this controller shouldnt care about how to calc skonto. we simply delegate the + # payment_type to the helper and get the corresponding bank_transaction values back + + my $open_amount = ($payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto : $invoice->open_amount); + my $amount_for_booking = abs(($open_amount < $not_assigned_amount) ? $open_amount : $not_assigned_amount); + $amount_for_booking *= $sign; + $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking); + + # ... and then pay the invoice + my @acc_ids = $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id, + trans_id => $invoice->id, + amount => ($open_amount < $not_assigned_amount) ? $open_amount : $not_assigned_amount, + payment_type => $payment_type, + source => $source, + memo => $memo, + transdate => $bank_transaction->transdate->to_kivitendo); + # ... and record the origin via BankTransactionAccTrans + if (scalar(@acc_ids) != 2) { + return { + %data, + result => 'error', + message => $::locale->text("Unable to book transactions for bank purpose #1", $bank_transaction->purpose), + }; + } + foreach my $acc_trans_id (@acc_ids) { + my $id_type = $invoice->is_sales ? 'ar' : 'ap'; + my %props_acc = ( + acc_trans_id => $acc_trans_id, + bank_transaction_id => $bank_transaction->id, + $id_type => $invoice->id, + ); + SL::DB::BankTransactionAccTrans->new(%props_acc)->save; + } # Record a record link from the bank transaction to the invoice my %props = ( from_table => 'bank_transactions', @@ -760,7 +714,73 @@ sub save_single_bank_transaction { return grep { $_ } ($error, @warnings); } +sub action_unlink_bank_transaction { + my ($self) = @_; + + croak("No bank transaction ids") unless scalar @{ $::form->{ids}} > 0; + + my $closedto = $::locale->parse_date_to_object($::instance_conf->get_closedto); + my $success_count; + foreach my $bt_id (@{ $::form->{ids}} ) { + + my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id); + croak("No valid bank transaction found") unless (ref($bank_transaction) eq 'SL::DB::BankTransaction'); + + # everything in one transaction + my $rez = $bank_transaction->db->with_transaction(sub { + # 1. remove all reconciliations (due to underlying trigger, this has to be the first step) + my $rec_links = SL::DB::Manager::ReconciliationLink->get_all(where => [ bank_transaction_id => $bt_id ]); + $_->delete for @{ $rec_links }; + + my %trans_ids; + foreach my $acc_trans_id_entry (@{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [bank_transaction_id => $bt_id ] )}) { + + my $acc_trans = SL::DB::Manager::AccTransaction->get_all(where => [acc_trans_id => $acc_trans_id_entry->acc_trans_id]); + # check closedto for acc trans entries + croak t8('Cannot unlink payment for a closed period!') if (ref $closedto && grep { $_->transdate < $closedto } @{ $acc_trans } ); + + # save trans_id and type + die "no type" unless ($acc_trans_id_entry->ar_id || $acc_trans_id_entry->ap_id || $acc_trans_id_entry->gl_id); + $trans_ids{$acc_trans_id_entry->ar_id} = 'ar' if $acc_trans_id_entry->ar_id; + $trans_ids{$acc_trans_id_entry->ap_id} = 'ap' if $acc_trans_id_entry->ap_id; + $trans_ids{$acc_trans_id_entry->gl_id} = 'gl' if $acc_trans_id_entry->gl_id; + + # 2. all good -> ready to delete acc_trans and bt_acc link + $acc_trans_id_entry->delete; + $_->delete for @{ $acc_trans }; + } + # 3. update arap.paid (may not be 0, yet) + while (my ($trans_id, $type) = each %trans_ids) { + next if $type eq 'gl'; + die ("invalid type") unless $type =~ m/^(ar|ap)$/; + + # recalc and set paid via database query + my $query = qq|UPDATE $type SET paid = + (SELECT COALESCE(abs(sum(amount)),0) FROM acc_trans + WHERE trans_id = ? + AND chart_link ilike '%paid%')|; + + die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id) == -1); + } + # 4. and delete all (if any) record links + my $rl = SL::DB::Manager::RecordLink->delete_all(where => [ from_id => $bt_id, from_table => 'bank_transactions' ]); + + # 5. finally reset this bank transaction + $bank_transaction->invoice_amount(0); + $bank_transaction->cleared(0); + $bank_transaction->save; + + 1; + + }) || die t8('error while unlinking payment #1 : ', $bank_transaction->purpose) . $bank_transaction->db->error . "\n"; + + $success_count++; + } + + flash('ok', t8('#1 bank transaction bookings undone.', $success_count)); + $self->action_list_all(); +} # # filters # @@ -803,10 +823,14 @@ sub prepare_report { my $report = SL::ReportGenerator->new(\%::myconfig, $::form); $self->{report} = $report; - 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); + my @columns = qw(ids 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); my @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code); my %column_defs = ( + ids => { raw_header_data => checkbox_tag("", id => "check_all", checkall => "[data-checkall=1]"), + 'align' => 'center', + raw_data => sub { if (@{ $_[0]->linked_invoices } && !(grep {ref ($_) eq 'SL::DB::GLTransaction' } @{ $_[0]->linked_invoices })) { + checkbox_tag("ids[]", value => $_[0]->id, "data-checkall" => 1); } } }, transdate => { sub => sub { $_[0]->transdate_as_date } }, valutadate => { sub => sub { $_[0]->valutadate_as_date } }, remote_name => { }, @@ -864,6 +888,7 @@ sub init_models { by => 'transdate', dir => 0, # 1 = ASC, 0 = DESC : default sort is newest at top }, + id => t8('ID'), transdate => t8('Transdate'), remote_name => t8('Remote name'), amount => t8('Amount'), @@ -906,7 +931,7 @@ sub load_gl_record_template_url { controller => 'gl.pl', action => 'load_record_template', id => $template->id, - 'form_defaults.amount_1' => abs($self->transaction->amount), # always positive + 'form_defaults.amount_1' => abs($self->transaction->not_assigned_amount), # always positive 'form_defaults.transdate' => $self->transaction->transdate_as_date, 'form_defaults.callback' => $self->callback, 'form_defaults.bt_id' => $self->transaction->id, @@ -933,9 +958,18 @@ sub setup_list_all_action_bar { for my $bar ($::request->layout->get('actionbar')) { $bar->add( - action => [ - t8('Filter'), - submit => [ '#filter_form', { action => 'BankTransaction/list_all' } ], + combobox => [ + action => [ t8('Actions') ], + action => [ + t8('Unlink bank transactions'), + submit => [ '#form', { action => 'BankTransaction/unlink_bank_transaction' } ], + checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ], + disabled => $::instance_conf->get_payments_changeable ? t8('Cannot safely unlink bank transactions, please set the posting configuration for payments to unchangeable.') : undef, + ], + ], + action => [ + t8('Filter'), + submit => [ '#filter_form', { action => 'BankTransaction/list_all' } ], accesskey => 'enter', ], ); @@ -965,12 +999,15 @@ tries to post its amount to a certain number of invoices (parameter C, an array ref of database IDs to purchase or sales invoice objects). +This method handles already partly assigned bank transactions. + This method cannot handle already partly assigned bank transactions, i.e. a bank transaction that has a invoice_amount <> 0 but not the fully transaction amount (invoice_amount == amount). If the amount of the bank transaction is higher than the sum of -the assigned invoices (1 .. n) the last invoice will be overpayed. +the assigned invoices (1 .. n) the bank transaction will only be +partly assigned. The whole function is wrapped in a database transaction. If an exception occurs the bank transaction is not posted at all. The same @@ -1007,6 +1044,18 @@ C =back +=item C + +Takes one or more bank transaction ID (as parameter C) and +tries to revert all payment bookings including already cleared bookings. + +This method won't undo payments that are in a closed period and assumes +that payments are not manually changed, i.e. only imported payments. + +GL-records will be deleted completely if a bank transaction was the source. + +TODO: we still rely on linked_records for the check boxes + =back =head1 AUTHOR