Projekt

Allgemein

Profil

Herunterladen (49,3 KB) Statistiken
| Zweig: | Markierung: | Revision:
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::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::GLTransaction;
use SL::DB::RecordTemplate;
use SL::DB::SepaExportItem;
use SL::DBUtils qw(like do_query);

use SL::Presenter::Tag qw(checkbox_tag html_tag);
use Carp;
use List::UtilsBy qw(partition_by);
use List::MoreUtils qw(any);
use List::Util qw(max);

use Rose::Object::MakeMethods::Generic
(
scalar => [ qw(callback transaction) ],
'scalar --get_set_init' => [ qw(models problems) ],
);

__PACKAGE__->run_before('check_auth');


#
# actions
#

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

my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );

$self->setup_search_action_bar;
$self->render('bank_transactions/search',
BANK_ACCOUNTS => $bank_accounts);
}

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

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

$self->setup_list_all_action_bar;
$self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
}

sub gather_bank_transactions_and_proposals {
my ($self, %params) = @_;

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

my @where = ();
push @where, (transdate => { ge => $params{fromdate} }) if $params{fromdate};
push @where, (transdate => { lt => $params{todate} }) if $params{todate};
# bank_transactions no younger than starting date,
# including starting date (same search behaviour as fromdate)
# but OPEN invoices to be matched may be from before
if ( $params{bank_account}->reconciliation_starting_date ) {
push @where, (transdate => { ge => $params{bank_account}->reconciliation_starting_date });
};

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'}, # '} make emacs happy
local_bank_account_id => $params{bank_account}->id,
cleared => 0,
@where
],
);
# credit notes have a negative amount, treat differently
my $all_open_ar_invoices = SL::DB::Manager::Invoice->get_all(where => [ or => [ amount => { gt => \'paid' }, # '} make emacs happy
and => [ type => 'credit_note',
amount => { lt => \'paid' } # '} make emacs happy
],
],
],
with_objects => ['customer','payment_terms']);

my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { ne => \'paid' }], # '}] make emacs happy
with_objects => ['vendor' ,'payment_terms']);
my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $params{bank_account}->chart_id ,
'sepa_export.executed' => 0,
'sepa_export.closed' => 0
],
with_objects => ['sepa_export']);

my @all_open_invoices;
# filter out invoices with less than 1 cent outstanding
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 };

my %sepa_exports;
my %sepa_export_items_by_id = partition_by { $_->ar_id || $_->ap_id } @$all_open_sepa_export_items;

# first collect sepa export items to open invoices
foreach my $open_invoice (@all_open_invoices){
$open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount,2);
$open_invoice->{skonto_type} = 'without_skonto';
foreach (@{ $sepa_export_items_by_id{ $open_invoice->id } || [] }) {
my $factor = ($_->ar_id == $open_invoice->id ? 1 : -1);
$open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2);

$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;
$sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor;
push @{ $sepa_exports{$_->sepa_export_id}->{invoices} }, $open_invoice;
}
}

# try to match each bank_transaction with each of the possible open invoices
# by awarding points
my @proposals;

foreach my $bt (@{ $bank_transactions }) {
## 5 Stellen hinter dem Komma auf 2 Stellen reduzieren
$bt->amount($bt->amount*1);
$bt->invoice_amount($bt->invoice_amount*1);

$bt->{proposals} = [];
$bt->{rule_matches} = [];

$bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};

if ( $bt->is_batch_transaction ) {
my $found=0;
foreach ( keys %sepa_exports) {
if ( abs(($sepa_exports{$_}->{amount} * 1) - ($bt->amount * 1)) < 0.01 ) {
## jupp
@{$bt->{proposals}} = @{$sepa_exports{$_}->{invoices}};
$bt->{sepa_export_ok} = 1;
$sepa_exports{$_}->{proposed}=1;
push(@proposals, $bt);
$found=1;
last;
}
}
next if $found;
# batch transaction has no remotename !!
}

# 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}

foreach my $open_invoice (@all_open_invoices) {
($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice,
sepa_export_items => $all_open_sepa_export_items,
);
$open_invoice->{realamount} = $::form->format_amount(\%::myconfig,
$open_invoice->amount * ($open_invoice->{is_ar} ? 1 : -1), 2);
}

my $agreement = 15;
my $min_agreement = 3; # suggestions must have at least this score

my $max_agreement = max map { $_->{agreement} } @all_open_invoices;

# 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 : '';

# store the rule_matches in a separate array, so they can be displayed in template
foreach ( @{ $bt->{proposals} } ) {
push(@{$bt->{rule_matches}}, $_->{rule_matches});
};
};
} # 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
# * agreement >= 5 TODO: make threshold configurable in configuration
# * there must be only one exact match
my $proposal_threshold = 5;
my @otherproposals = grep {
($_->{agreement} >= $proposal_threshold)
&& (1 == scalar @{ $_->{proposals} })
&& ($_->{proposals}->[0]->forex == 0) # nyi forex invoices for automatic booking
} @{ $bank_transactions };

push @proposals, @otherproposals;

# sort bank transaction proposals by quality (score) of proposal
if ($params{sort_by} && $params{sort_by} eq 'proposal') {
my $dir = $params{sort_dir} ? 1 : -1;
$bank_transactions = [ sort { ($a->{agreement} <=> $b->{agreement}) * $dir } @{ $bank_transactions } ];
}

return ( $bank_transactions , \@proposals );
}

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

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

my $bank_account = SL::DB::BankAccount->load_cached($::form->{filter}->{bank_account});
my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
my $todate = $::locale->parse_date_to_object($::form->{filter}->{todate});
$todate->add( days => 1 ) if $todate;

my ($bank_transactions, $proposals) = $self->gather_bank_transactions_and_proposals(
bank_account => $bank_account,
fromdate => $fromdate,
todate => $todate,
sort_by => $::form->{sort_by},
sort_dir => $::form->{sort_dir},
);

$::request->layout->add_javascripts("kivi.BankTransaction.js");
$self->render('bank_transactions/list',
title => t8('Bank transactions MT940'),
BANK_TRANSACTIONS => $bank_transactions,
PROPOSALS => $proposals,
bank_account => $bank_account,
ui_tab => scalar(@{ $proposals }) > 0 ? 1 : 0,
);
}

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

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

$self->render('bank_transactions/assign_invoice',
{ layout => 0 },
title => t8('Assign invoice'),);
}

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

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

my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(iban => $self->transaction->{remote_account_number});
my $use_vendor_filter = $self->transaction->{remote_account_number} && $vendor_of_transaction;

my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
where => [ template_type => 'ap_transaction' ],
sort_by => [ qw(template_name) ],
with_objects => [ qw(employee vendor) ],
);
my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
query => [ template_type => 'gl_transaction',
chart_id => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
],
sort_by => [ qw(template_name) ],
with_objects => [ qw(employee record_template_items) ],
);

# pre filter templates_ap, if we have a vendor match (IBAN eq IBAN) - show and allow user to edit this via gui!
$templates_ap = [ grep { $_->vendor_id == $vendor_of_transaction->id } @{ $templates_ap } ] if $use_vendor_filter;

$self->callback($self->url_for(
action => 'list',
'filter.bank_account' => $::form->{filter}->{bank_account},
'filter.todate' => $::form->{filter}->{todate},
'filter.fromdate' => $::form->{filter}->{fromdate},
));

# if we have exactly one ap match, use this directly
if (1 == scalar @{ $templates_ap }) {
$self->redirect_to($self->load_ap_record_template_url($templates_ap->[0]));

} else {
my $dialog_html = $self->render(
'bank_transactions/create_invoice',
{ layout => 0, output => 0 },
title => t8('Create invoice'),
TEMPLATES_GL => $use_vendor_filter && @{ $templates_ap } ? undef : $templates_gl,
TEMPLATES_AP => $templates_ap,
vendor_name => $use_vendor_filter && @{ $templates_ap } ? $vendor_of_transaction->name : undef,
);
$self->js->run('kivi.BankTransaction.show_create_invoice_dialog', $dialog_html)->render;
}
}

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

croak("Need bt_id") unless $::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} );

croak("No valid invoice found") unless $invoice;

my $html = $self->render(
'bank_transactions/_payment_suggestion', { output => 0 },
bt_id => $::form->{bt_id},
invoice => $invoice,
);

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

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

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

my (@filter, @filter_ap);

# filter => gl and ap | filter_ap = ap (i.e. vendorname)
push @filter, ('template_name' => { ilike => '%' . $::form->{template} . '%' }) if $::form->{template};
push @filter, ('reference' => { ilike => '%' . $::form->{reference} . '%' }) if $::form->{reference};
push @filter_ap, ('vendor.name' => { ilike => '%' . $::form->{vendor} . '%' }) if $::form->{vendor};
push @filter_ap, @filter;
my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
query => [ template_type => 'gl_transaction',
chart_id => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
(and => \@filter) x !!@filter
],
with_objects => [ qw(employee record_template_items) ],
);

my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
where => [ template_type => 'ap_transaction', (and => \@filter_ap) x !!@filter_ap ],
with_objects => [ qw(employee vendor) ],
);
$::form->{filter} //= {};

$self->callback($self->url_for(
action => 'list',
'filter.bank_account' => $::form->{filter}->{bank_account},
'filter.todate' => $::form->{filter}->{todate},
'filter.fromdate' => $::form->{filter}->{fromdate},
));

my $output = $self->render(
'bank_transactions/_template_list',
{ output => 0 },
TEMPLATES_AP => $templates_ap,
TEMPLATES_GL => $templates_gl,
);

$self->render(\to_json({ html => $output