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;
+use SL::DB::BankTransactionAccTrans;
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);
# for testing with t/bank/banktransaction.t :
if ( $::form->{dont_render_for_test} ) {
- return $bank_transactions;
+ return ( $bank_transactions , \@proposals );
}
$::request->layout->add_javascripts("kivi.BankTransaction.js");
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;
$n_invoices++ ;
- if (!$amount_of_transaction && $invoice->open_amount) {
+ if (!$not_assigned_amount && $invoice->open_amount) {
return {
%data,
result => 'error',
} 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
- $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);
- } else {
- # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary
-
- # this catches credit_notes and negative sales invoices
- if ( $invoice->is_sales && $invoice->amount < 0 ) {
- # $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
- $amount_of_transaction *= -1;
-
- } elsif (!$invoice->is_sales && $invoice->invoice_type =~ m/ap_transaction|purchase_invoice/) {
- # $invoice->open_amount may be negative for ap_transaction but may be positiv for negativ 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->open_amount == - $amount_of_transaction;
- }
-
- 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 = (
+ my %props = (
from_table => 'bank_transactions',
from_id => $bt_id,
to_table => $invoice->is_sales ? 'ar' : 'ap',
to_id => $invoice->id,
);
-
- SL::DB::RecordLink->new(@props)->save;
+ SL::DB::RecordLink->new(%props)->save;
# "close" a sepa_export_item if it exists
# code duplicated in action_save_proposals!
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
#
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 => { },
align => 'right' },
invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number },
align => 'right' },
- invoices => { sub => sub { $_[0]->linked_invoices } },
+ invoices => { sub => sub { my @invnumbers; for my $obj (@{ $_[0]->linked_invoices }) {
+ next unless $obj; push @invnumbers, $obj->invnumber } return \@invnumbers } },
currency => { sub => sub { $_[0]->currency->name } },
purpose => { },
local_account_number => { sub => sub { $_[0]->local_bank_account->account_number } },
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'),
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,
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',
],
);
C<invoice_ids>, 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
=back
+=item C<action_unlink_bank_transaction>
+
+Takes one or more bank transaction ID (as parameter C<form::ids>) 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