Projekt

Allgemein

Profil

Herunterladen (38,7 KB) Statistiken
| Zweig: | Markierung: | Revision:
6a12a968 Niclas Zimmermann
package SL::Controller::BankTransaction;

# idee- möglichkeit bankdaten zu übernehmen in stammdaten
# erst Kontenabgleich, um alle gl-Einträge wegzuhaben
use strict;

use parent qw(SL::Controller::Base);

use SL::Controller::Helper::GetModels;
use SL::Controller::Helper::ReportGenerator;
use SL::ReportGenerator;

use SL::DB::BankTransaction;
use SL::Helper::Flash;
use SL::Locale::String;
use SL::SEPA;
use SL::DB::Invoice;
use SL::DB::PurchaseInvoice;
use SL::DB::RecordLink;
use SL::JSON;
use SL::DB::Chart;
use SL::DB::AccTransaction;
use SL::DB::Tax;
use SL::DB::BankAccount;
32dc7476 Moritz Bunkus
use SL::DB::RecordTemplate;
88d162cc Martin Helmling
use SL::DB::SepaExportItem;
bc40bcab Moritz Bunkus
use SL::DBUtils qw(like);
15f58ff3 Geoffrey Richardson
use SL::Presenter;
0631432e Moritz Bunkus
use List::MoreUtils qw(any);
15f58ff3 Geoffrey Richardson
use List::Util qw(max);
6a12a968 Niclas Zimmermann
use Rose::Object::MakeMethods::Generic
(
32dc7476 Moritz Bunkus
scalar => [ qw(callback transaction) ],
66d468b0 Moritz Bunkus
'scalar --get_set_init' => [ qw(models problems) ],
6a12a968 Niclas Zimmermann
);

__PACKAGE__->run_before('check_auth');


#
# actions
#

sub action_search {
my ($self) = @_;

15f58ff3 Geoffrey Richardson
my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
6a12a968 Niclas Zimmermann
2003e056 Moritz Bunkus
$self->setup_search_action_bar;
6a12a968 Niclas Zimmermann
$self->render('bank_transactions/search',
BANK_ACCOUNTS => $bank_accounts);
}

sub action_list_all {
my ($self) = @_;

$self->make_filter_summary;
$self->prepare_report;

2003e056 Moritz Bunkus
$self->setup_list_all_action_bar;
15f58ff3 Geoffrey Richardson
$self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
6a12a968 Niclas Zimmermann
}

sub action_list {
my ($self) = @_;

if (!$::form->{filter}{bank_account}) {
flash('error', t8('No bank account chosen!'));
$self->action_search;
return;
}

my $sort_by = $::form->{sort_by} || 'transdate';
$sort_by = 'transdate' if $sort_by eq 'proposal';
$sort_by .= $::form->{sort_dir} ? ' DESC' : ' ASC';

15f58ff3 Geoffrey Richardson
my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
my $todate = $::locale->parse_date_to_object($::form->{filter}->{todate});
6a12a968 Niclas Zimmermann
$todate->add( days => 1 ) if $todate;

my @where = ();
push @where, (transdate => { ge => $fromdate }) if ($fromdate);
push @where, (transdate => { lt => $todate }) if ($todate);
15f58ff3 Geoffrey Richardson
my $bank_account = SL::DB::Manager::BankAccount->find_by( id => $::form->{filter}{bank_account} );
# bank_transactions no younger than starting date,
5d2c7ae2 Jan Büren
# including starting date (same search behaviour as fromdate)
15f58ff3 Geoffrey Richardson
# but OPEN invoices to be matched may be from before
if ( $bank_account->reconciliation_starting_date ) {
5d2c7ae2 Jan Büren
push @where, (transdate => { ge => $bank_account->reconciliation_starting_date });
15f58ff3 Geoffrey Richardson
};
6a12a968 Niclas Zimmermann
9e481f80 Moritz Bunkus
my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
with_objects => [ 'local_bank_account', 'currency' ],
sort_by => $sort_by,
limit => 10000,
where => [
amount => {ne => \'invoice_amount'},
local_bank_account_id => $::form->{filter}{bank_account},
@where
],
);
88d162cc Martin Helmling
$main::lxdebug->message(LXDebug->DEBUG2(),"count bt=".scalar(@{$bank_transactions}." bank_account=".$bank_account->id." chart=".$bank_account->chart_id));
6a12a968 Niclas Zimmermann
af131a46 Martin Helmling
# credit notes have a negative amount, treat differently
my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => [ or => [ amount => { gt => \'paid' },
and => [ type => 'credit_note',
amount => { lt => \'paid' }
],
],
],
with_objects => ['customer','payment_terms']);

fbcd5580 Martin Helmling
my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { ne => \'paid' }], with_objects => ['vendor' ,'payment_terms']);
88d162cc Martin Helmling
my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $bank_account->chart_id ,
'sepa_export.executed' => 0, 'sepa_export.closed' => 0 ], with_objects => ['sepa_export']);
$main::lxdebug->message(LXDebug->DEBUG2(),"count sepaexport=".scalar(@{$all_open_sepa_export_items}));
6a12a968 Niclas Zimmermann
my @all_open_invoices;
15f58ff3 Geoffrey Richardson
# filter out invoices with less than 1 cent outstanding
c55ef764 Martin Helmling
push @all_open_invoices, map { $_->{is_ar}=1 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
push @all_open_invoices, map { $_->{is_ar}=0 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
88d162cc Martin Helmling
$main::lxdebug->message(LXDebug->DEBUG2(),"bank_account=".$::form->{filter}{bank_account}." invoices: ".scalar(@{ $all_open_ar_invoices }).
fbcd5580 Martin Helmling
" + ".scalar(@{ $all_open_ap_invoices })." non fully paid=".scalar(@all_open_invoices)." transactions=".scalar(@{ $bank_transactions }));
88d162cc Martin Helmling
my @all_sepa_invoices;
my @all_non_sepa_invoices;
my %sepa_exports;
# first collect sepa export items to open invoices
foreach my $open_invoice (@all_open_invoices){
# my @items = grep { $_->ap_id == $open_invoice->id || $_->ar_id == $open_invoice->id } @{$all_open_sepa_export_items};
$open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount,2);
$open_invoice->{skonto_type} = 'without_skonto';
foreach ( @{$all_open_sepa_export_items}) {
if ( $_->ap_id == $open_invoice->id || $_->ar_id == $open_invoice->id ) {
5cd931eb Martin Helmling
my $factor = ($_->ar_id == $open_invoice->id?1:-1);
cadf8b8b Martin Helmling
$main::lxdebug->message(LXDebug->DEBUG2(),"exitem=".$_->id." for invoice ".$open_invoice->id." factor=".$factor);
$open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);
88d162cc Martin Helmling
$open_invoice->{sepa_export_item} = $_ ;
$open_invoice->{skonto_type} = $_->payment_type;
$sepa_exports{$_->sepa_export_id} ||= { count => 0, is_ar => 0, amount => 0, proposed => 0, invoices => [], item => $_ };
$sepa_exports{$_->sepa_export_id}->{count}++ ;
$sepa_exports{$_->sepa_export_id}->{is_ar}++ if $_->ar_id == $open_invoice->id;
c55ef764 Martin Helmling
$sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor;
9c3eecdf Moritz Bunkus
push @{ $sepa_exports{$_->sepa_export_id}->{invoices} }, $open_invoice;
88d162cc Martin Helmling
#$main::lxdebug->message(LXDebug->DEBUG2(),"amount for export id ".$_->sepa_export_id." = ".
# $sepa_exports{$_->sepa_export_id}->{amount}." count = ".
# $sepa_exports{$_->sepa_export_id}->{count}." is_ar = ".
# $sepa_exports{$_->sepa_export_id}->{is_ar} );
push @all_sepa_invoices , $open_invoice;
}
}
push @all_non_sepa_invoices , $open_invoice if ! $open_invoice->{sepa_export_item};
}
15f58ff3 Geoffrey Richardson
# try to match each bank_transaction with each of the possible open invoices
# by awarding points
88d162cc Martin Helmling
my @proposals;
6a12a968 Niclas Zimmermann
foreach my $bt (@{ $bank_transactions }) {
88d162cc Martin Helmling
## 5 Stellen hinter dem Komma auf 2 Stellen reduzieren
$bt->amount($bt->amount*1);
$bt->invoice_amount($bt->invoice_amount*1);
$main::lxdebug->message(LXDebug->DEBUG2(),"BT ".$bt->id." amount=".$bt->amount." invoice_amount=".$bt->invoice_amount." remote=". $bt->{remote_name});

2ee7cc2f Martin Helmling
$bt->{proposals} = [];
$bt->{rule_matches} = [];
6a12a968 Niclas Zimmermann
15f58ff3 Geoffrey Richardson
$bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};

88d162cc Martin Helmling
if ( $self->is_collective_transaction($bt) ) {
foreach ( keys %sepa_exports) {
c55ef764 Martin Helmling
#$main::lxdebug->message(LXDebug->DEBUG2(),"Exp ID=".$_." compare sum amount ".($sepa_exports{$_}->{amount} *1) ." == ".($bt->amount * 1));
728f5827 Moritz Bunkus
if ( $bt->transaction_code eq '191' && abs(($sepa_exports{$_}->{amount} * 1) - ($bt->amount * 1)) < 0.01 ) {
88d162cc Martin Helmling
## jupp
cadf8b8b Martin Helmling
@{$bt->{proposals}} = @{$sepa_exports{$_}->{invoices}};
c55ef764 Martin Helmling
$bt->{agreement} = 20;
cadf8b8b Martin Helmling
push(@{$bt->{rule_matches}},'sepa_export_item(20)');
88d162cc Martin Helmling
$sepa_exports{$_}->{proposed}=1;
#$main::lxdebug->message(LXDebug->DEBUG2(),"has ".scalar($bt->{proposals})." invoices");
push(@proposals, $bt);
next;
}
}
}
next unless $bt->{remote_name}; # bank has no name, usually fees, use create invoice to assign

c55ef764 Martin Helmling
foreach ( @{$all_open_sepa_export_items}) {
last if scalar (@all_sepa_invoices) == 0;
foreach my $open_invoice (@all_sepa_invoices){
83ff5b5c Martin Helmling
$open_invoice->{agreement} = 0;
$open_invoice->{rule_matches} ='';
c55ef764 Martin Helmling
if ( $_->ap_id == $open_invoice->id || $_->ar_id == $open_invoice->id ) {
#$main::lxdebug->message(LXDebug->DEBUG2(),"exitem2=".$_->id." for invoice ".$open_invoice->id);
my $factor = ( $_->ar_id == $open_invoice->id?1:-1);
$_->amount($_->amount*1);
fbcd5580 Martin Helmling
#$main::lxdebug->message(LXDebug->DEBUG2(),"remote account '".$bt->{remote_account_number}."' bt_amount=".$bt->amount." factor=".$factor);
c55ef764 Martin Helmling
#$main::lxdebug->message(LXDebug->DEBUG2(),"compare with '".$_->vc_iban."' amount=".$_->amount);
fbcd5580 Martin Helmling
if ( $bt->{remote_account_number} eq $_->vc_iban && abs(abs($_->amount) - abs($bt->amount)) < 0.01 ) {
955f5491 Martin Helmling
my $iban;
$iban = $open_invoice->customer->iban if $open_invoice->is_sales;
$iban = $open_invoice->vendor->iban if ! $open_invoice->is_sales;
if($bt->{remote_account_number} eq $iban && abs(abs($open_invoice->amount) - abs($bt->amount)) < 0.01 ) {
($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
$open_invoice->{agreement} += 5;
$open_invoice->{rule_matches} .= 'sepa_export_item(5) ';
$main::lxdebug->message(LXDebug->DEBUG2(),"sepa invoice_id=".$open_invoice->id." agreement=".$open_invoice->{agreement}." rules matches=".$open_invoice->{rule_matches});
$open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);
}
c55ef764 Martin Helmling
}
88d162cc Martin Helmling
}
c55ef764 Martin Helmling
}
88d162cc Martin Helmling
}

15f58ff3 Geoffrey Richardson
# try to match the current $bt to each of the open_invoices, saving the
# results of get_agreement_with_invoice in $open_invoice->{agreement} and
# $open_invoice->{rule_matches}.

# The values are overwritten each time a new bt is checked, so at the end
# of each bt the likely results are filtered and those values are stored in
# the arrays $bt->{proposals} and $bt->{rule_matches}, and the agreement
# score is stored in $bt->{agreement}
6a12a968 Niclas Zimmermann
f0d5e4bb Jan Büren
foreach my $open_invoice (@all_non_sepa_invoices, @all_sepa_invoices) {
15f58ff3 Geoffrey Richardson
($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
87d5463d Jan Büren
$open_invoice->{realamount} = $::form->format_amount(\%::myconfig,
$open_invoice->amount * ($open_invoice->{is_ar} ? 1 : -1), 2);
}
6a12a968 Niclas Zimmermann
15f58ff3 Geoffrey Richardson
my $agreement = 15;
my $min_agreement = 3; # suggestions must have at least this score
6a12a968 Niclas Zimmermann
15f58ff3 Geoffrey Richardson
my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
6a12a968 Niclas Zimmermann
15f58ff3 Geoffrey Richardson
# add open_invoices with highest agreement into array $bt->{proposals}
if ( $max_agreement >= $min_agreement ) {
$bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
$bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
6a12a968 Niclas Zimmermann
15f58ff3 Geoffrey Richardson
# store the rule_matches in a separate array, so they can be displayed in template
foreach ( @{ $bt->{proposals} } ) {
push(@{$bt->{rule_matches}}, $_->{rule_matches});
};
};
6a12a968 Niclas Zimmermann
} # finished one bt
# finished all bt

# separate filter for proposals (second tab, agreement >= 5 and exactly one match)
# to qualify as a proposal there has to be
15f58ff3 Geoffrey Richardson
# * agreement >= 5 TODO: make threshold configurable in configuration
# * there must be only one exact match
6a12a968 Niclas Zimmermann
# * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
15f58ff3 Geoffrey Richardson
my $proposal_threshold = 5;
88d162cc Martin Helmling
my @otherproposals = grep {
9e481f80 Moritz Bunkus
($_->{agreement} >= $proposal_threshold)
&& (1 == scalar @{ $_->{proposals} })
&& (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01
: abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01)
} @{ $bank_transactions };
6a12a968 Niclas Zimmermann
88d162cc Martin Helmling
push ( @proposals, @otherproposals);

15f58ff3 Geoffrey Richardson
# sort bank transaction proposals by quality (score) of proposal
6a12a968 Niclas Zimmermann
$bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 1;
$bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ] if $::form->{sort_by} eq 'proposal' and $::form->{sort_dir} == 0;

96c33451 Moritz Bunkus
$::request->layout->add_javascripts("kivi.BankTransaction.js");
6a12a968 Niclas Zimmermann
$self->render('bank_transactions/list',
15f58ff3 Geoffrey Richardson
title => t8('Bank transactions MT940'),
6a12a968 Niclas Zimmermann
BANK_TRANSACTIONS => $bank_transactions,
PROPOSALS => \@proposals,
88d162cc Martin Helmling
bank_account => $bank_account,
ui_tab => scalar(@proposals) > 0?1:0,
);
6a12a968 Niclas Zimmermann
}

sub action_assign_invoice {
my ($self) = @_;

$self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});

9e481f80 Moritz Bunkus
$self->render('bank_transactions/assign_invoice',
{ layout => 0 },
title => t8('Assign invoice'),);
6a12a968 Niclas Zimmermann
}

sub action_create_invoice {
my ($self) = @_;
my %myconfig = %main::myconfig;

32dc7476 Moritz Bunkus
$self->transaction(SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id}));
3b18f3f0 Sven Schöling
32dc7476 Moritz Bunkus
my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->transaction->{remote_account_number});
my $use_vendor_filter = $self->transaction->{remote_account_number} && $vendor_of_transaction;
6a12a968 Niclas Zimmermann
32dc7476 Moritz Bunkus
my $templates = SL::DB::Manager::RecordTemplate->get_all(
where => [ template_type => 'ap_transaction' ],
with_objects => [ qw(employee vendor) ],
);
6a12a968 Niclas Zimmermann
32dc7476 Moritz Bunkus
#Filter templates
$templates = [ grep { $_->vendor_id == $vendor_of_transaction->id } @{ $templates } ] if $use_vendor_filter;
6a12a968 Niclas Zimmermann
32dc7476 Moritz Bunkus
$self->callback($self->url_for(
action => 'list',
'filter.bank_account' => $::form->{filter}->{bank_account},
'filter.todate' => $::form->{filter}->{todate},
'filter.fromdate' => $::form->{filter}->{fromdate},
));
9e481f80 Moritz Bunkus
$self->render(
'bank_transactions/create_invoice',
{ layout => 0 },
title => t8('Create invoice'),
32dc7476 Moritz Bunkus
TEMPLATES => $templates,
9e481f80 Moritz Bunkus
vendor_id => $use_vendor_filter ? $vendor_of_transaction->id : undef,
vendor_name => $use_vendor_filter ? $vendor_of_transaction->name : undef,
);
6a12a968 Niclas Zimmermann
}

15f58ff3 Geoffrey Richardson
sub action_ajax_payment_suggestion {
my ($self) = @_;

# based on a BankTransaction ID and a Invoice or PurchaseInvoice ID passed via $::form,
# create an HTML blob to be used by the js function add_invoices in templates/webpages/bank_transactions/list.html
# and return encoded as JSON

9a2253e9 Geoffrey Richardson
my $bt = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
15f58ff3 Geoffrey Richardson
die unless $bt and $invoice;

my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});

my $html;
e9775242 Moritz Bunkus
$html = $self->render(
'bank_transactions/_payment_suggestion', { output => 0 },
bt_id => $::form->{bt_id},
prop_id => $::form->{prop_id},
invoice => $invoice,
SELECT_OPTIONS => \@select_options,
);

$self->render(\ SL::JSON::to_json( { 'html' => "$html" } ), { layout => 0, type => 'json', process => 0 });
15f58ff3 Geoffrey Richardson
};

32dc7476 Moritz Bunkus
sub action_filter_templates {
6a12a968 Niclas Zimmermann
my ($self) = @_;

9e481f80 Moritz Bunkus
$self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
6a12a968 Niclas Zimmermann
my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});

32dc7476 Moritz Bunkus
my @filter;
push @filter, ('vendor.id' => $::form->{vendor_id}) if $::form->{vendor_id};
push @filter, ('vendor.name' => { ilike => '%' . $::form->{vendor} . '%' }) if $::form->{vendor};
9e481f80 Moritz Bunkus
32dc7476 Moritz Bunkus
my $templates = SL::DB::Manager::RecordTemplate->get_all(
where => [ template_type => 'ap_transaction', (or => \@filter) x !!@filter ],
with_objects => [ qw(employee vendor) ],
);
6a12a968 Niclas Zimmermann
32dc7476 Moritz Bunkus
$::form->{filter} //= {};
6a12a968 Niclas Zimmermann
32dc7476 Moritz Bunkus
$self->callback($self->url_for(
action => 'list',
'filter.bank_account' => $::form->{filter}->{bank_account},
'filter.todate' => $::form->{filter}->{todate},
'filter.fromdate' => $::form->{filter}->{fromdate},
));
6a12a968 Niclas Zimmermann
my $output = $self->render(
32dc7476 Moritz Bunkus
'bank_transactions/_template_list',
9e481f80 Moritz Bunkus
{ output => 0 },
32dc7476 Moritz Bunkus
TEMPLATES => $templates,
9e481f80 Moritz Bunkus
);
6a12a968 Niclas Zimmermann
32dc7476 Moritz Bunkus
$self->render(\to_json({ html => $output }), { type => 'json', process => 0 });
6a12a968 Niclas Zimmermann
}

sub action_ajax_add_list {
my ($self) = @_;

my @where_sale = (amount => { ne => \'paid' });
my @where_purchase = (amount => { ne => \'paid' });

if ($::form->{invnumber}) {
bc40bcab Moritz Bunkus
push @where_sale, (invnumber => { ilike => like($::form->{invnumber})});
push @where_purchase, (invnumber => { ilike => like($::form->{invnumber})});
6a12a968 Niclas Zimmermann
}

if ($::form->{amount}) {
push @where_sale, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
}

if ($::form->{vcnumber}) {
bc40bcab Moritz Bunkus
push @where_sale, ('customer.customernumber' => { ilike => like($::form->{vcnumber})});
push @where_purchase, ('vendor.vendornumber' => { ilike => like($::form->{vcnumber})});
6a12a968 Niclas Zimmermann
}

if ($::form->{vcname}) {
bc40bcab Moritz Bunkus
push @where_sale, ('customer.name' => { ilike => like($::form->{vcname})});
push @where_purchase, ('vendor.name' => { ilike => like($::form->{vcname})});
6a12a968 Niclas Zimmermann
}

if ($::form->{transdatefrom}) {
15f58ff3 Geoffrey Richardson
my $fromdate = $::locale->parse_date_to_object($::form->{transdatefrom});
if ( ref($fromdate) eq 'DateTime' ) {
push @where_sale, ('transdate' => { ge => $fromdate});
push @where_purchase, ('transdate' => { ge => $fromdate});
};
6a12a968 Niclas Zimmermann
}

if ($::form->{transdateto}) {
15f58ff3 Geoffrey Richardson
my $todate = $::locale->parse_date_to_object($::form->{transdateto});
if ( ref($todate) eq 'DateTime' ) {
$todate->add(days => 1);
push @where_sale, ('transdate' => { lt => $todate});
push @where_purchase, ('transdate' => { lt => $todate});
};
6a12a968 Niclas Zimmermann
}

9e481f80 Moritz Bunkus
my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => \@where_sale, with_objects => 'customer');
6a12a968 Niclas Zimmermann
my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');

18140010 Geoffrey Richardson
my @all_open_invoices = @{ $all_open_ar_invoices };
# add ap invoices, filtering out subcent open amounts
15f58ff3 Geoffrey Richardson
push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
6a12a968 Niclas Zimmermann
@all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;

my $output = $self->render(
9e481f80 Moritz Bunkus
'bank_transactions/add_list',
{ output => 0 },
INVOICES => \@all_open_invoices,
);
6a12a968 Niclas Zimmermann
my %result = ( count => 0, html => $output );

$self->render(\to_json(\%result), { type => 'json', process => 0 });
}

sub action_ajax_accept_invoices {
my ($self) = @_;

my @selected_invoices;
foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
9a2253e9 Geoffrey Richardson
my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
6a12a968 Niclas Zimmermann
push @selected_invoices, $invoice_object;
}

9e481f80 Moritz Bunkus
$self->render(
'bank_transactions/invoices',
{ layout => 0 },
INVOICES => \@selected_invoices,
bt_id => $::form->{bt_id},
);
6a12a968 Niclas Zimmermann
}

88d162cc Martin Helmling
sub save_invoices {
6a12a968 Niclas Zimmermann
my ($self) = @_;

88d162cc Martin Helmling
return 0 if !$::form->{invoice_ids};

my %invoice_hash = %{ delete $::form->{invoice_ids} }; # each key (the bt line with a bt_id) contains an array of invoice_ids
75063bf3 Geoffrey Richardson
# e.g. three partial payments with bt_ids 54, 55 and 56 for invoice with id 74:
# $invoice_hash = {
# '55' => [
# '74'
# ],
# '54' => [
# '74'
# ],
# '56' => [
# '74'
# ]
# };
#
# or if the payment with bt_id 44 is used to pay invoices with ids 50, 51 and 52
# $invoice_hash = {
# '44' => [ '50', '51', 52' ]
# };

66d468b0 Moritz Bunkus
$::form->{invoice_skontos} ||= {}; # hash of arrays containing the payment types, could be empty
6a12a968 Niclas Zimmermann
cd887de4 Geoffrey Richardson
# a bank_transaction may be assigned to several invoices, i.e. a customer
# might pay several open invoices with one transaction

66d468b0 Moritz Bunkus
$self->problems([]);

88d162cc Martin Helmling
my $count = 0;

if ( $::form->{proposal_ids} ) {
foreach (@{ $::form->{proposal_ids} }) {
my $bank_transaction_id = $_;
my $invoice_ids = $invoice_hash{$_};
push @{ $self->problems }, $self->save_single_bank_transaction(
bank_transaction_id => $bank_transaction_id,
invoice_ids => $invoice_ids,
c4d3f82d Moritz Bunkus
sources => ($::form->{sources} // {})->{$_},
memos => ($::form->{memos} // {})->{$_},
88d162cc Martin Helmling
);
$count += scalar( @{$invoice_ids} );
}
} else {
while ( my ($bank_transaction_id, $invoice_ids) = each(%invoice_hash) ) {
push @{ $self->problems }, $self->save_single_bank_transaction(
bank_transaction_id => $bank_transaction_id,
invoice_ids => $invoice_ids,
e9775242 Moritz Bunkus
sources => [ map { $::form->{"sources_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
memos => [ map { $::form->{"memos_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ],
88d162cc Martin Helmling
);
$count += scalar( @{$invoice_ids} );
}
66d468b0 Moritz Bunkus
}
55e3ae55 Martin Helmling
foreach (@{ $self->problems }) {
$count-- if $_->{result} eq 'error';
}
88d162cc Martin Helmling
return $count;
}

sub action_save_invoices {
my ($self) = @_;
my $count = $self->save_invoices();

flash('ok', t8('#1 invoice(s) saved.', $count));
66d468b0 Moritz Bunkus
$self->action_list();
}

88d162cc Martin Helmling
sub action_save_proposals {
my ($self) = @_;
c4d3f82d Moritz Bunkus
88d162cc Martin Helmling
if ( $::form->{proposal_ids} ) {
my $propcount = scalar(@{ $::form->{proposal_ids} });
if ( $propcount > 0 ) {
my $count = $self->save_invoices();

flash('ok', t8('#1 proposal(s) with #2 invoice(s) saved.', $propcount, $count));
}
}
$self->action_list();

}

sub is_collective_transaction {
my ($self, $bt) = @_;
728f5827 Moritz Bunkus
return $bt->transaction_code eq "191";
88d162cc Martin Helmling
}

66d468b0 Moritz Bunkus
sub save_single_bank_transaction {
my ($self, %params) = @_;

my %data = (
%params,
bank_transaction => SL::DB::Manager::BankTransaction->find_by(id => $params{bank_transaction_id}),
invoices => [],
);

if (!$data{bank_transaction}) {
return {
%data,
result => 'error',
message => $::locale->text('The ID #1 is not a valid database ID.', $data{bank_transaction_id}),
};
}

my (@warnings);

my $worker = sub {
my $bt_id = $data{bank_transaction_id};
my $bank_transaction = $data{bank_transaction};
75063bf3 Geoffrey Richardson
my $sign = $bank_transaction->amount < 0 ? -1 : 1;
6a12a968 Niclas Zimmermann
my $amount_of_transaction = $sign * $bank_transaction->amount;
0631432e Moritz Bunkus
my $payment_received = $bank_transaction->amount > 0;
my $payment_sent = $bank_transaction->amount < 0;
6a12a968 Niclas Zimmermann
fbcd5580 Martin Helmling
66d468b0 Moritz Bunkus
foreach my $invoice_id (@{ $params{invoice_ids} }) {
my $invoice = SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
if (!$invoice) {
return {
%data,
result => 'error',
message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id),
};
}
0631432e Moritz Bunkus
push @{ $data{invoices} }, $invoice;
6a12a968 Niclas Zimmermann
}
66d468b0 Moritz Bunkus
0631432e Moritz Bunkus
if ( $payment_received
&& any { ( $_->is_sales && ($_->amount < 0))
|| (!$_->is_sales && ($_->amount > 0))
} @{ $data{invoices} }) {
return {
%data,
result => 'error',
message => $::locale->text("Received payments can only be posted for sales invoices and purchase credit notes."),
};
}
66d468b0 Moritz Bunkus
0631432e Moritz Bunkus
if ( $payment_sent
&& any { ( $_->is_sales && ($_->amount > 0))
b8741ec3 Martin Helmling
|| (!$_->is_sales && ($_->amount < 0) && ($_->invoice_type eq 'purchase_invoice'))
0631432e Moritz Bunkus
} @{ $data{invoices} }) {
return {
%data,
result => 'error',
message => $::locale->text("Sent payments can only be posted for purchase invoices and sales credit notes."),
};
}
6a12a968 Niclas Zimmermann
0631432e Moritz Bunkus
my $max_invoices = scalar(@{ $data{invoices} });
92e2fb59 Martin Helmling
my $n_invoices = 0;

0631432e Moritz Bunkus
foreach my $invoice (@{ $data{invoices} }) {
c4d3f82d Moritz Bunkus
my $source = ($data{sources} // [])->[$n_invoices];
my $memo = ($data{memos} // [])->[$n_invoices];
cd887de4 Geoffrey Richardson
92e2fb59 Martin Helmling
$n_invoices++ ;
0c93bf20 Moritz Bunkus
cd887de4 Geoffrey Richardson
# Check if bank_transaction already has a link to the invoice, may only be linked once per invoice
# This might be caused by the user reloading a page and resending the form
66d468b0 Moritz Bunkus
if (_existing_record_link($bank_transaction, $invoice)) {
return {
%data,
result => 'error',
message => $::locale->text("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name),
};
}

0c93bf20 Moritz Bunkus
if (!$amount_of_transaction && $invoice->open_amount) {
return {
66d468b0 Moritz Bunkus
%data,
0c93bf20 Moritz Bunkus
result => 'error',
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."),
66d468b0 Moritz Bunkus
};
}
cd887de4 Geoffrey Richardson
15f58ff3 Geoffrey Richardson
my $payment_type;
66d468b0 Moritz Bunkus
if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
$payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
15f58ff3 Geoffrey Richardson
} else {
$payment_type = 'without_skonto';
};
66d468b0 Moritz Bunkus
fbcd5580 Martin Helmling
cd887de4 Geoffrey Richardson
# pay invoice or go to the next bank transaction if the amount is not sufficiently high
92e2fb59 Martin Helmling
if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) {
88d162cc Martin Helmling
my $open_amount = ($payment_type eq 'with_skonto_pt'?$invoice->amount_less_skonto:$invoice->open_amount);
29ecf3e8 Jan Büren
# first calculate new bank transaction amount ...
6a12a968 Niclas Zimmermann
if ($invoice->is_sales) {
88d162cc Martin Helmling
$amount_of_transaction -= $sign * $open_amount;
$bank_transaction->invoice_amount($bank_transaction->invoice_amount + $open_amount);
6a12a968 Niclas Zimmermann
} else {
88d162cc Martin Helmling
$amount_of_transaction += $sign * $open_amount;
$bank_transaction->invoice_amount($bank_transaction->invoice_amount - $open_amount);
6a12a968 Niclas Zimmermann
}
29ecf3e8 Jan Büren
# ... and then pay the invoice
$invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
trans_id => $invoice->id,
88d162cc Martin Helmling
amount => $open_amount,
29ecf3e8 Jan Büren
payment_type => $payment_type,
c4d3f82d Moritz Bunkus
source => $source,
memo => $memo,
29ecf3e8 Jan Büren
transdate => $bank_transaction->transdate->to_kivitendo);
b6f37661 Martin Helmling
} else {
# use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary

2c5a1cef Jan Büren
# this catches credit_notes and negative sales invoices
if ( $invoice->is_sales && $invoice->amount < 0 ) {
b6f37661 Martin Helmling
# $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 eq 'ap_transaction' ) {
# $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;
}
9a527d73 Jan Büren
20392548 Moritz Bunkus
my $overpaid_amount = $amount_of_transaction - $invoice->open_amount;
15f58ff3 Geoffrey Richardson
$invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
trans_id => $invoice->id,
amount => $amount_of_transaction,
payment_type => $payment_type,
c4d3f82d Moritz Bunkus
source => $source,
memo => $memo,
15f58ff3 Geoffrey Richardson
transdate => $bank_transaction->transdate->to_kivitendo);
98a59d3e Jan Büren
$bank_transaction->invoice_amount($bank_transaction->amount);
6a12a968 Niclas Zimmermann
$amount_of_transaction = 0;
20392548 Moritz Bunkus
8b14060f Moritz Bunkus
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)),
};
}
6a12a968 Niclas Zimmermann
}
cd887de4 Geoffrey Richardson
# Record a record link from the bank transaction to the invoice
6a12a968 Niclas Zimmermann
my @props = (
9e481f80 Moritz Bunkus
from_table => 'bank_transactions',
from_id => $bt_id,
to_table => $invoice->is_sales ? 'ar' : 'ap',
to_id => $invoice->id,
);
6a12a968 Niclas Zimmermann
cd887de4 Geoffrey Richardson
SL::DB::RecordLink->new(@props)->save;
f36da7b6 Geoffrey Richardson
# "close" a sepa_export_item if it exists
397b133c Geoffrey Richardson
# code duplicated in action_save_proposals!
f36da7b6 Geoffrey Richardson
# 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;
9e481f80 Moritz Bunkus
}
}
f36da7b6 Geoffrey Richardson
6a12a968 Niclas Zimmermann
}
$bank_transaction->save;

66d468b0 Moritz Bunkus
# '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 => $@,
};
};

542befb1 Martin Helmling
# Rollback Fehler nicht weiterreichen
# die if $error;
66d468b0 Moritz Bunkus
});

return grep { $_ } ($error, @warnings);
6a12a968 Niclas Zimmermann
}

#
# filters
#

sub check_auth {
$::auth->assert('bank_transaction');
}

#
# helpers
#

sub make_filter_summary {
my ($self) = @_;

my $filter = $::form->{filter} || {};
my @filter_strings;

my @filters = (
9e481f80 Moritz Bunkus
[ $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') ],
6a12a968 Niclas Zimmermann
);

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;

15f58ff3 Geoffrey Richardson
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 @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code);
6a12a968 Niclas Zimmermann
my %column_defs = (
9e481f80 Moritz Bunkus
transdate => { sub => sub { $_[0]->transdate_as_date } },
valutadate => { sub => sub { $_[0]->valutadate_as_date } },
6a12a968 Niclas Zimmermann
remote_name => { },
remote_account_number => { },
remote_bank_code => { },
9e481f80 Moritz Bunkus
amount => { sub => sub { $_[0]->amount_as_number },
6a12a968 Niclas Zimmermann
align => 'right' },
9e481f80 Moritz Bunkus
invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number },
6a12a968 Niclas Zimmermann
align => 'right' },
9e481f80 Moritz Bunkus
invoices => { sub => sub { $_[0]->linked_invoices } },
currency => { sub => sub { $_[0]->currency->name } },
6a12a968 Niclas Zimmermann
purpose => { },
9e481f80 Moritz Bunkus
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 } },
6a12a968 Niclas Zimmermann
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);
15f58ff3 Geoffrey Richardson
$report->set_export_options(qw(list_all filter));
6a12a968 Niclas Zimmermann
$report->set_options_from_form;
15f58ff3 Geoffrey Richardson
$self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
6a12a968 Niclas Zimmermann
$self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);

15f58ff3 Geoffrey Richardson
my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
6a12a968 Niclas Zimmermann
$report->set_options(
15f58ff3 Geoffrey Richardson
raw_top_info_text => $self->render('bank_transactions/report_top', { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
6a12a968 Niclas Zimmermann
raw_bottom_info_text => $self->render('bank_transactions/report_bottom', { output => 0 }),
);
}

cd887de4 Geoffrey Richardson
sub _existing_record_link {
my ($bt, $invoice) = @_;

# check whether a record link from banktransaction $bt already exists to
# invoice $invoice, returns 1 if that is the case

die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );

my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ] );

return @$linked_records ? 1 : 0;
};

66d468b0 Moritz Bunkus
sub init_problems { [] }
cd887de4 Geoffrey Richardson
6a12a968 Niclas Zimmermann
sub init_models {
my ($self) = @_;

SL::Controller::Helper::GetModels->new(
controller => $self,
9e481f80 Moritz Bunkus
sorted => {
6a12a968 Niclas Zimmermann
_default => {
9e481f80 Moritz Bunkus
by => 'transdate',
dir => 0, # 1 = ASC, 0 = DESC : default sort is newest at top
6a12a968 Niclas Zimmermann
},
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'),
15f58ff3 Geoffrey Richardson
local_bank_name => t8('Bank account'),
6a12a968 Niclas Zimmermann
},
with_objects => [ 'local_bank_account', 'currency' ],
);
}

32dc7476 Moritz Bunkus
sub load_ap_record_template_url {
my ($self, $template) = @_;

return $self->url_for(
da6ff55e Jan Büren
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,
32dc7476 Moritz Bunkus
);
}

2003e056 Moritz Bunkus
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(
action => [
t8('Filter'),
submit => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
accesskey => 'enter',
],
);
}
}

6a12a968 Niclas Zimmermann
1;
bbdb5edd Moritz Bunkus
__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).

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

=back

=head1 AUTHOR

Niclas Zimmermann E<lt>niclas@kivitendo-premium.deE<gt>,
Geoffrey Richardson E<lt>information@richardson-bueren.deE<gt>

=cut