"BankTransaction.pm.html#L696" data-txt="696">
} elsif ($default_rate != $fx_rate) { # set record (banktransaction) exchangerate
|
|
$bank_transaction->exchangerate($fx_rate); # custom rate, will be displayed in ap, ir, is
|
|
} elsif (abs($default_rate - $fx_rate) < 0.001) {
|
|
# last valid state default rate is (nearly) the same as user input -> do nothing
|
|
} else { die "Invalid exchange rate state:" . $default_rate . " " . $fx_rate; }
|
|
} # end fx hook
|
|
|
|
# open amount is in default currency -> free_skonto is in default currency, no need to change
|
|
$open_amount = abs($open_amount);
|
|
$open_amount -= $free_skonto_amount if ($payment_type eq 'free_skonto');
|
|
my $not_assigned_amount = abs($bank_transaction->not_assigned_amount);
|
|
my $amount_for_booking = ($open_amount < $not_assigned_amount) ? $open_amount : $not_assigned_amount;
|
|
my $fx_fee_amount = $fx_book && ($open_amount < $not_assigned_amount) ? $not_assigned_amount - $open_amount : 0;
|
|
my $amount_for_payment = $amount_for_booking;
|
|
# add booking amount
|
|
# $amount_for_booking
|
|
|
|
# get the right direction for the payment bookings (all amounts < 0 are stornos, credit notes or negative ap)
|
|
$amount_for_payment *= -1 if $invoice->amount < 0;
|
|
$free_skonto_amount *= -1 if ($free_skonto_amount && $invoice->amount < 0);
|
|
# get the right direction for the bank transaction
|
|
# sign is simply the sign of amount in bank_transactions: positive for increase and negative for decrease
|
|
$amount_for_booking *= $sign;
|
|
|
|
# ... 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 => $amount_for_payment,
|
|
payment_type => $payment_type,
|
|
source => $source,
|
|
memo => $memo,
|
|
skonto_amount => $free_skonto_amount,
|
|
exchangerate => $fx_rate,
|
|
fx_book => $fx_book,
|
|
fx_fee_amount => $fx_fee_amount,
|
|
currency_id => $currency_id,
|
|
bt_id => $bt_id,
|
|
transdate => $bank_transaction->valutadate->to_kivitendo);
|
|
# First element is the booked amount for accno bank
|
|
my $bank_amount = shift @acc_ids;
|
|
|
|
if (!$invoice->forex) {
|
|
# die "Invalid state, calculated invoice_amount differs from expected invoice amount" unless (abs($bank_amount->{return_bank_amount}) - abs($amount_for_booking) < 0.001);
|
|
$bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking);
|
|
} else {
|
|
die "Invalid state, calculated invoice_amount differs from expected invoice amount: $amount_for_booking <> " . $bank_amount->{return_bank_amount}
|
|
unless $fx_book || (abs($bank_amount->{return_bank_amount}) - abs($amount_for_booking) < 0.005);
|
|
$bank_transaction->invoice_amount($bank_transaction->invoice_amount + $bank_amount->{return_bank_amount});
|
|
#$bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking);
|
|
}
|
|
# ... 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',
|
|
from_id => $bt_id,
|
|
to_table => $invoice->is_sales ? 'ar' : 'ap',
|
|
to_id => $invoice->id,
|
|
);
|
|
SL::DB::RecordLink->new(%props)->save;
|
|
|
|
# "close" a sepa_export_item if it exists
|
|
# code duplicated in action_save_proposals!
|
|
# currently only works, if there is only exactly one open sepa_export_item
|
|
if ( my $seis = $invoice->find_sepa_export_items({ executed => 0 }) ) {
|
|
if ( scalar @$seis == 1 ) {
|
|
# moved the execution and the check for sepa_export into a method,
|
|
# this isn't part of a transaction, though
|
|
$seis->[0]->set_executed if $invoice->id == $seis->[0]->arap_id;
|
|
}
|
|
}
|
|
|
|
}
|
|
$bank_transaction->save;
|
|
|
|
# 'undef' means 'no error' here.
|
|
return undef;
|
|
};
|
|
|
|
my $error;
|
|
my $rez = $data{bank_transaction}->db->with_transaction(sub {
|
|
eval {
|
|
$error = $worker->();
|
|
1;
|
|
|
|
} or do {
|
|
$error = {
|
|
%data,
|
|
result => 'error',
|
|
message => $@,
|
|
};
|
|
};
|
|
|
|
# Rollback Fehler nicht weiterreichen
|
|
# die if $error;
|
|
# aber einen rollback von hand
|
|
$::lxdebug->message(LXDebug->DEBUG2(),"finish worker with ". ($error ? $error->{result} : '-'));
|
|
$data{bank_transaction}->db->dbh->rollback if $error && $error->{result} eq 'error';
|
|
});
|
|
|
|
return grep { $_ } ($error, @warnings);
|
|
}
|
|
sub action_unlink_bank_transaction {
|
|
my ($self, %params) = @_;
|
|
|
|
croak("No bank transaction ids") unless scalar @{ $::form->{ids}} > 0;
|
|
|
|
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');
|
|
croak t8('Cannot unlink payment for a closed period!') if $bank_transaction->closed_period;
|
|
|
|
# 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]);
|
|
|
|
# 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)
|
|
# or in case of gl, delete whole entry
|
|
while (my ($trans_id, $type) = each %trans_ids) {
|
|
if ($type eq 'gl') {
|
|
SL::DB::Manager::GLTransaction->delete_all(where => [ id => $trans_id ]);
|
|
next;
|
|
}
|
|
die ("invalid type") unless $type =~ m/^(ar|ap)$/;
|
|
|
|
# recalc and set paid via database query
|
|
# add: fx_gain and fx_loss
|
|
my $query = qq|UPDATE $type SET paid =
|
|
(SELECT COALESCE(abs(sum(amount)),0) FROM acc_trans
|
|
WHERE trans_id = ?
|
|
AND (chart_link ilike '%paid%'
|
|
OR chart_id IN (SELECT fxgain_accno_id from defaults)
|
|
OR chart_id IN (SELECT fxloss_accno_id from defaults)
|
|
)
|
|
)
|
|
WHERE id = ?|;
|
|
|
|
die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id, $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->exchangerate(undef);
|
|
$bank_transaction->cleared(0);
|
|
$bank_transaction->save;
|
|
# 6. and add a log entry in history_erp
|
|
SL::DB::History->new(
|
|
trans_id => $bank_transaction->id,
|
|
snumbers => 'bank_transaction_unlink_' . $bank_transaction->id,
|
|
employee_id => SL::DB::Manager::Employee->current->id,
|
|
what_done => 'bank_transaction',
|
|
addition => 'UNLINKED',
|
|
)->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() unless $params{testcase};
|
|
}
|
|
#
|
|
# filters
|
|
#
|
|
|
|
sub check_auth {
|
|
$::auth->assert('bank_transaction');
|
|
}
|
|
|
|
#
|
|
# helpers
|
|
#
|
|
|
|
sub make_filter_summary {
|
|
my ($self) = @_;
|
|
|
|
my $filter = $::form->{filter} || {};
|
|
my @filter_strings;
|
|
|
|
my @filters = (
|
|
[ $filter->{"transdate:date::ge"}, $::locale->text('Transdate') . " " . $::locale->text('From Date') ],
|
|
[ $filter->{"transdate:date::le"}, $::locale->text('Transdate') . " " . $::locale->text('To Date') ],
|
|
[ $filter->{"valutadate:date::ge"}, $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
|
|
[ $filter->{"valutadate:date::le"}, $::locale->text('Valutadate') . " " . $::locale->text('To Date') ],
|
|
[ $filter->{"amount:number"}, $::locale->text('Amount') ],
|
|
[ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account') ],
|
|
[ $filter->{"remote_name:substr::ilike"}, $::locale->text('Remote name') ],
|
|
);
|
|
|
|
for (@filters) {
|
|
push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
|
|
}
|
|
|
|
$self->{filter_summary} = join ', ', @filter_strings;
|
|
}
|
|
|
|
sub prepare_report {
|
|
my ($self) = @_;
|
|
|
|
my $callback = $self->models->get_callback;
|
|
|
|
my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
|
|
$self->{report} = $report;
|
|
|
|
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 }) {
|
|
if ($_[0]->closed_period) {
|
|
html_tag('text', "X"); #, tooltip => t8('Bank Transaction is in a closed period.')),
|
|
} else {
|
|
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 => { },
|
|
remote_account_number => { },
|
|
remote_bank_code => { },
|
|
amount => { sub => sub { $_[0]->amount_as_number },
|
|
align => 'right' },
|
|
invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number },
|
|
align => 'right' },
|
|
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 } },
|
|
local_bank_code => { sub => sub { $_[0]->local_bank_account->bank_code } },
|
|
local_bank_name => { sub => sub { $_[0]->local_bank_account->name } },
|
|
id => {},
|
|
);
|
|
|
|
map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
|
|
|
|
$report->set_options(
|
|
std_column_visibility => 1,
|
|
controller_class => 'BankTransaction',
|
|
output_format => 'HTML',
|
|
top_info_text => $::locale->text('Bank transactions'),
|
|
title => $::locale->text('Bank transactions'),
|
|
allow_pdf_export => 1,
|
|
allow_csv_export => 1,
|
|
);
|
|
$report->set_columns(%column_defs);
|
|
$report->set_column_order(@columns);
|
|
$report->set_export_options(qw(list_all filter));
|
|
$report->set_options_from_form;
|
|
$self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
|
|
$self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
|
|
|
|
my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
|
|
|
|
$report->set_options(
|
|
raw_top_info_text => $self->render('bank_transactions/report_top', { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
|
|
raw_bottom_info_text => $self->render('bank_transactions/report_bottom', { output => 0 }),
|
|
);
|
|
}
|
|
|
|
sub init_problems { [] }
|
|
|
|
sub init_models {
|
|
my ($self) = @_;
|
|
|
|
SL::Controller::Helper::GetModels->new(
|
|
controller => $self,
|
|
sorted => {
|
|
_default => {
|
|
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'),
|
|
invoice_amount => t8('Assigned'),
|
|
invoices => t8('Linked invoices'),
|
|
valutadate => t8('Valutadate'),
|
|
remote_account_number => t8('Remote account number'),
|
|
remote_bank_code => t8('Remote bank code'),
|
|
currency => t8('Currency'),
|
|
purpose => t8('Purpose'),
|
|
local_account_number => t8('Local account number'),
|
|
local_bank_code => t8('Local bank code'),
|
|
local_bank_name => t8('Bank account'),
|
|
},
|
|
with_objects => [ 'local_bank_account', 'currency' ],
|
|
);
|
|
}
|
|
|
|
sub load_ap_record_template_url {
|
|
my ($self, $template) = @_;
|
|
|
|
return $self->url_for(
|
|
controller => 'ap.pl',
|
|
action => 'load_record_template',
|
|
id => $template->id,
|
|
'form_defaults.amount_1' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
|
|
'form_defaults.transdate' => $self->transaction->transdate_as_date,
|
|
'form_defaults.duedate' => $self->transaction->transdate_as_date,
|
|
'form_defaults.no_payment_bookings' => 1,
|
|
'form_defaults.paid_1_suggestion' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2),
|
|
'form_defaults.AP_paid_1_suggestion' => $self->transaction->local_bank_account->chart->accno,
|
|
'form_defaults.callback' => $self->callback,
|
|
'form_defaults.notes' => $self->convert_purpose_for_template($template, $self->transaction->purpose),
|
|
);
|
|
}
|
|
|
|
sub load_gl_record_template_url {
|
|
my ($self, $template) = @_;
|
|
|
|
return $self->url_for(
|
|
controller => 'gl.pl',
|
|
action => 'load_record_template',
|
|
id => $template->id,
|
|
'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,
|
|
'form_defaults.bt_chart_id' => $self->transaction->local_bank_account->chart->id,
|
|
'form_defaults.description' => $self->convert_purpose_for_template($template, $self->transaction->purpose),
|
|
);
|
|
}
|
|
|
|
sub convert_purpose_for_template {
|
|
my ($self, $template, $purpose) = @_;
|
|
|
|
# enter custom code here
|
|
|
|
return $purpose;
|
|
}
|
|
|
|
sub setup_search_action_bar {
|
|
my ($self, %params) = @_;
|
|
|
|
for my $bar ($::request->layout->get('actionbar')) {
|
|
$bar->add(
|
|
action => [
|
|
t8('Filter'),
|
|
submit => [ '#search_form', { action => 'BankTransaction/list' } ],
|
|
accesskey => 'enter',
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
sub setup_list_all_action_bar {
|
|
my ($self, %params) = @_;
|
|
|
|
for my $bar ($::request->layout->get('actionbar')) {
|
|
$bar->add(
|
|
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',
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
1;
|
|
__END__
|
|
|
|
=pod
|
|
|
|
=encoding utf8
|
|
|
|
=head1 NAME
|
|
|
|
SL::Controller::BankTransaction - Posting payments to invoices from
|
|
bank transactions imported earlier
|
|
|
|
=head1 FUNCTIONS
|
|
|
|
=over 4
|
|
|
|
=item C<save_single_bank_transaction %params>
|
|
|
|
Takes a bank transaction ID (as parameter C<bank_transaction_id> and
|
|
tries to post its amount to a certain number of invoices (parameter
|
|
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 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
|
|
is true if the code detects an error during the execution, e.g. a bank
|
|
transaction that's already been posted earlier. In both cases the
|
|
database transaction will be rolled back.
|
|
|
|
If warnings but not errors occur the database transaction is still
|
|
committed.
|
|
|
|
The return value is an error object or C<undef> if the function
|
|
succeeded. The calling function will collect all warnings and errors
|
|
and display them in a nicely formatted table if any occurred.
|
|
|
|
An error object is a hash reference containing the following members:
|
|
|
|
=over 2
|
|
|
|
=item * C<result> — can be either C<warning> or C<error>. Warnings are
|
|
displayed slightly different than errors.
|
|
|
|
=item * C<message> — a human-readable message included in the list of
|
|
errors meant as the description of why the problem happened
|
|
|
|
=item * C<bank_transaction_id>, C<invoice_ids> — the same parameters
|
|
that the function was called with
|
|
|
|
=item * C<bank_transaction> — the database object
|
|
(C<SL::DB::BankTransaction>) corresponding to C<bank_transaction_id>
|
|
|
|
=item * C<invoices> — an array ref of the database objects (either
|
|
C<SL::DB::Invoice> or C<SL::DB::PurchaseInvoice>) corresponding to
|
|
C<invoice_ids>
|
|
|
|
=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
|
|
|
|
=item C<convert_purpose_for_template>
|
|
|
|
This method can be used to parse, filter and convert the bank transaction's
|
|
purpose string before it will be assigned to the description field of a
|
|
gl transaction or to the notes field of an ap transaction.
|
|
You have to write your own custom code.
|
|
|
|
=back
|
|
|
|
=head1 AUTHOR
|
|
|
|
Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
|
|
Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>
|
|
|
|
=cut
|
Lade...