use SL::DB::AuthUser;
use SL::DB::Default;
use SL::DB::Employee;
+use SL::DB::Chart;
use SL::GenericTranslations;
use strict;
t.taxdescription,
round(t.rate * 100, 2) AS rate,
(SELECT accno FROM chart WHERE id = chart_id) AS taxnumber,
- (SELECT description FROM chart WHERE id = chart_id) AS account_description
+ (SELECT description FROM chart WHERE id = chart_id) AS account_description,
+ (SELECT accno FROM chart WHERE id = skonto_sales_chart_id) AS skonto_chart_accno,
+ (SELECT description FROM chart WHERE id = skonto_sales_chart_id) AS skonto_chart_description,
+ (SELECT accno FROM chart WHERE id = skonto_purchase_chart_id) AS skonto_chart_purchase_accno,
+ (SELECT description FROM chart WHERE id = skonto_purchase_chart_id) AS skonto_chart_purchase_description
FROM tax t
ORDER BY taxkey, rate|;
push @{ $form->{ACCOUNTS} }, $ref;
}
+ $form->{AR_PAID} = SL::DB::Manager::Chart->get_all(where => [ link => { like => '%AR_paid%' } ], sort_by => 'accno ASC');
+ $form->{AP_PAID} = SL::DB::Manager::Chart->get_all(where => [ link => { like => '%AP_paid%' } ], sort_by => 'accno ASC');
+
+ $form->{skontochart_value_title_sub} = sub {
+ my $item = shift;
+ return [
+ $item->{id},
+ $item->{accno} .' '. $item->{description},
+ ];
+ };
+
$sth->finish;
$dbh->disconnect;
chart_id,
chart_categories,
(id IN (SELECT tax_id
- FROM acc_trans)) AS tax_already_used
+ FROM acc_trans)) AS tax_already_used,
+ skonto_sales_chart_id,
+ skonto_purchase_chart_id
FROM tax
WHERE id = ? |;
$chart_categories .= 'E' if $form->{expense};
$chart_categories .= 'C' if $form->{costs};
- my @values = ($form->{taxkey}, $form->{taxdescription}, $form->{rate}, conv_i($form->{chart_id}), conv_i($form->{chart_id}), $chart_categories);
+ my @values = ($form->{taxkey}, $form->{taxdescription}, $form->{rate}, conv_i($form->{chart_id}), conv_i($form->{chart_id}), conv_i($form->{skonto_sales_chart_id}), conv_i($form->{skonto_purchase_chart_id}), $chart_categories);
if ($form->{id} ne "") {
$query = qq|UPDATE tax SET
- taxkey = ?,
- taxdescription = ?,
- rate = ?,
- chart_id = ?,
- taxnumber = (SELECT accno FROM chart WHERE id= ? ),
- chart_categories = ?
+ taxkey = ?,
+ taxdescription = ?,
+ rate = ?,
+ chart_id = ?,
+ taxnumber = (SELECT accno FROM chart WHERE id = ? ),
+ skonto_sales_chart_id = ?,
+ skonto_purchase_chart_id = ?,
+ chart_categories = ?
WHERE id = ?|;
} else {
rate,
chart_id,
taxnumber,
+ skonto_sales_chart_id,
+ skonto_purchase_chart_id,
chart_categories,
id
)
- VALUES (?, ?, ?, ?, (SELECT accno FROM chart WHERE id = ?), ?, ?)|;
+ VALUES (?, ?, ?, ?, (SELECT accno FROM chart WHERE id = ?), ?, ?, ?, ?)|;
}
push(@values, $form->{id});
do_query($form, $dbh, $query, @values);
- SL::Controller::BankAccount;
+package SL::Controller::BankAccount;
use strict;
use SL::DB::Tax;
use SL::DB::Draft;
use SL::DB::BankAccount;
+use SL::Presenter;
+use List::Util qw(max);
use Rose::Object::MakeMethods::Generic
(
sub action_search {
my ($self) = @_;
- my $bank_accounts = SL::DB::Manager::BankAccount->get_all();
+ my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
$self->render('bank_transactions/search',
- label_sub => sub { t8('#1 - Account number #2, bank code #3, #4', $_[0]->name, $_[0]->account_number, $_[0]->bank_code, $_[0]->bank, )},
BANK_ACCOUNTS => $bank_accounts);
}
sub action_list_all {
my ($self) = @_;
- my $transactions = $self->models->get;
-
$self->make_filter_summary;
$self->prepare_report;
- $self->report_generator_list_objects(report => $self->{report}, objects => $transactions);
+ $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
}
sub action_list {
$sort_by = 'transdate' if $sort_by eq 'proposal';
$sort_by .= $::form->{sort_dir} ? ' DESC' : ' ASC';
- my $fromdate = $::locale->parse_date_to_object(\%::myconfig, $::form->{filter}->{fromdate});
- my $todate = $::locale->parse_date_to_object(\%::myconfig, $::form->{filter}->{todate});
+ 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 @where = ();
push @where, (transdate => { ge => $fromdate }) if ($fromdate);
push @where, (transdate => { lt => $todate }) if ($todate);
+ my $bank_account = SL::DB::Manager::BankAccount->find_by( id => $::form->{filter}{bank_account} );
+ # bank_transactions no younger than starting date,
+ # but OPEN invoices to be matched may be from before
+ if ( $bank_account->reconciliation_starting_date ) {
+ push @where, (transdate => { gt => $bank_account->reconciliation_starting_date });
+ };
my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => [ amount => {ne => \'invoice_amount'},
local_bank_account_id => $::form->{filter}{bank_account},
my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { gt => \'paid' }], with_objects => 'vendor');
my @all_open_invoices;
- push @all_open_invoices, @{ $all_open_ar_invoices };
- push @all_open_invoices, @{ $all_open_ap_invoices };
+ # filter out invoices with less than 1 cent outstanding
+ push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices };
+ push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
+
+ # try to match each bank_transaction with each of the possible open invoices
+ # by awarding points
foreach my $bt (@{ $bank_transactions }) {
next unless $bt->{remote_name}; # bank has no name, usually fees, use create invoice to assign
- foreach my $open_invoice (@all_open_invoices){
- $open_invoice->{agreement} = 0;
-
- #compare banking arrangements
- my ($bank_code, $account_number);
- $bank_code = $open_invoice->customer->bank_code if $open_invoice->is_sales;
- $account_number = $open_invoice->customer->account_number if $open_invoice->is_sales;
- $bank_code = $open_invoice->vendor->bank_code if ! $open_invoice->is_sales;
- $account_number = $open_invoice->vendor->account_number if ! $open_invoice->is_sales;
- ($bank_code eq $bt->remote_bank_code
- && $account_number eq $bt->remote_account_number) ? ($open_invoice->{agreement} += 2) : ();
-
- my $datediff = $bt->transdate->{utc_rd_days} - $open_invoice->transdate->{utc_rd_days};
- $open_invoice->{datediff} = $datediff;
-
- #compare amount
-# (abs($open_invoice->amount) == abs($bt->amount)) ? ($open_invoice->{agreement} += 2) : ();
-# do we need double abs here?
- (abs(abs($open_invoice->amount) - abs($bt->amount)) < 0.01) ? ($open_invoice->{agreement} += 4) : ();
-
- #search invoice number in purpose
- my $invnumber = $open_invoice->invnumber;
-# possible improvement: match has to have more than 1 character?
- $bt->purpose =~ /\b$invnumber\b/i ? ($open_invoice->{agreement} += 2) : ();
-
- #check sign
- if ( $open_invoice->is_sales && $bt->amount < 0 ) {
- $open_invoice->{agreement} -= 1;
- };
- if ( ! $open_invoice->is_sales && $bt->amount > 0 ) {
- $open_invoice->{agreement} -= 1;
- };
- #search customer/vendor number in purpose
- my $cvnumber;
- $cvnumber = $open_invoice->customer->customernumber if $open_invoice->is_sales;
- $cvnumber = $open_invoice->vendor->vendornumber if ! $open_invoice->is_sales;
- $bt->purpose =~ /\b$cvnumber\b/i ? ($open_invoice->{agreement}++) : ();
-
- #compare customer/vendor name and account holder
- my $cvname;
- $cvname = $open_invoice->customer->name if $open_invoice->is_sales;
- $cvname = $open_invoice->vendor->name if ! $open_invoice->is_sales;
- $bt->remote_name =~ /\b$cvname\b/i ? ($open_invoice->{agreement}++) : ();
-
- #Compare transdate of bank_transaction with transdate of invoice
- #Check if words in remote_name appear in cvname
- $open_invoice->{agreement} += &check_string($bt->remote_name,$cvname);
-
- $open_invoice->{agreement} -= 1 if $datediff < -5; # dies hebelt eventuell Vorkasse aus
- $open_invoice->{agreement} += 1 if $datediff < 30; # dies hebelt eventuell Vorkasse aus
-
- # only if we already have a good agreement, let date further change value of agreement.
- # this is so that if there are several open invoices which are all equal (rent jan, rent feb...) the one with the best date match is chose over the others
- # another way around this is to just pre-filter by periods instead of matching everything
- if ( $open_invoice->{agreement} > 5 ) {
- if ( $datediff == 0 ) {
- $open_invoice->{agreement} += 3;
- } elsif ( $datediff > 0 and $datediff <= 14 ) {
- $open_invoice->{agreement} += 2;
- } elsif ( $datediff >14 and $datediff < 35) {
- $open_invoice->{agreement} += 1;
- } elsif ( $datediff >34 and $datediff < 120) {
- $open_invoice->{agreement} += 1;
- } elsif ( $datediff < 0 ) {
- $open_invoice->{agreement} -= 1;
- } else {
- # e.g. datediff > 120
- };
- };
+ $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1};
+
+ # 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}
- #if ($open_invoice->transdate->{utc_rd_days} == $bt->transdate->{utc_rd_days}) {
- #$open_invoice->{agreement} += 4;
- #print FH "found matching date for invoice " . $open_invoice->invnumber . " ( " . $bt->transdate->{utc_rd_days} . " . \n";
- #} elsif (($open_invoice->transdate->{utc_rd_days} + 30) < $bt->transdate->{utc_rd_days}) {
- #$open_invoice->{agreement} -= 1;
- #} else {
- #$open_invoice->{agreement} -= 2;
- #print FH "found nomatch date -2 for invoice " . $open_invoice->invnumber . " ( " . $bt->transdate->{utc_rd_days} . " . \n";
- #};
- #print FH "agreement after date_agreement: " . $open_invoice->{agreement} . "\n";
+ foreach my $open_invoice (@all_open_invoices){
+ ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice);
+ };
+ $bt->{proposals} = [];
+ my $agreement = 15;
+ my $min_agreement = 3; # suggestions must have at least this score
- }
-# finished going through all open_invoices
+ my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
- # go through each bt
- # for each open_invoice try to match it to each open_invoice and store agreement in $open_invoice->{agreement} (which gets overwritten each time for each bt)
- # calculate
-#
+ # 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 : '';
- $bt->{proposals} = [];
- my $agreement = 11;
- # wird nie ausgeführt, bzw. nur ganz am Ende
-# oder einmal am Anfang?
-# es werden maximal 7 vorschläge gemacht?
- # 7 mal wird geprüft, ob etwas passt
- while (scalar @{ $bt->{proposals} } < 1 && $agreement-- > 0) {
- $bt->{proposals} = [ grep { $_->{agreement} > $agreement } @all_open_invoices ];
- #Kann wahrscheinlich weg:
-# map { $_->{style} = "green" } @{ $bt->{proposals} } if $agreement >= 5;
-# map { $_->{style} = "orange" } @{ $bt->{proposals} } if $agreement < 5 and $agreement >= 3;
-# map { $_->{style} = "red" } @{ $bt->{proposals} } if $agreement < 3;
- $bt->{agreement} = $agreement; # agreement value at cutoff, will correspond to several results if threshold is 7 and several are already above 7
- }
+ # 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
- # * there must be only one exact match
+ # * agreement >= 5 TODO: make threshold configurable in configuration
+ # * there must be only one exact match
# * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
-
- my @proposals = grep { $_->{agreement} >= 5
+ my $proposal_threshold = 5;
+ my @proposals = grep { $_->{agreement} >= $proposal_threshold
and 1 == scalar @{ $_->{proposals} }
and (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01 : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01) } @{ $bank_transactions };
- #Sort bank transactions by quality of proposal
+ # sort bank transaction proposals by quality (score) of proposal
$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;
$self->render('bank_transactions/list',
- title => t8('List of bank transactions'),
+ title => t8('Bank transactions MT940'),
BANK_TRANSACTIONS => $bank_transactions,
PROPOSALS => \@proposals,
- bank_account => SL::DB::Manager::BankAccount->find_by(id => $::form->{filter}{bank_account}) );
+ bank_account => $bank_account );
}
-sub check_string {
- my $bankstring = shift;
- my $namestring = shift;
- return 0 unless $bankstring and $namestring;
-
- my @bankwords = grep(/^\w+$/, split(/\b/,$bankstring));
-
- my $match = 0;
- foreach my $bankword ( @bankwords ) {
- # only try to match strings with more than 2 characters
- next unless length($bankword)>2;
- if ( $namestring =~ /\b$bankword\b/i ) {
- $match++;
- };
- };
- return $match;
-};
-
sub action_assign_invoice {
my ($self) = @_;
);
}
+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
+
+ my $bt = SL::DB::Manager::BankTransaction->find_by( id => $::form->{bt_id} );
+ my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} );
+ $invoice = SL::DB::Manager::PurchaseInvoice->find_By( id => $::form->{prop_id} ) unless $invoice;
+
+ die unless $bt and $invoice;
+
+ my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
+
+ my $html;
+ $html .= SL::Presenter->input_tag('invoice_ids.' . $::form->{bt_id} . '[]', $::form->{prop_id} , type => 'hidden');
+ $html .= SL::Presenter->escape( $invoice->invnumber );
+ $html .= SL::Presenter->select_tag('invoice_skontos.' . $::form->{bt_id} . '[]', \@select_options,
+ value_key => 'payment_type',
+ title_key => 'display' ) if @select_options;
+ $html .= '<a href=# onclick="delete_invoice(' . $::form->{bt_id} . ',' . $::form->{prop_id} . ');">x</a>';
+ $html = SL::Presenter->html_tag('div', $html, id => $::form->{bt_id} . '.' . $::form->{prop_id});
+
+ $self->render(\ SL::JSON::to_json( { 'html' => $html } ), { layout => 0, type => 'json', process => 0 });
+};
+
sub action_filter_drafts {
my ($self) = @_;
}
if ($::form->{transdatefrom}) {
- my $fromdate = $::locale->parse_date_to_object(\%::myconfig, $::form->{transdatefrom});
- push @where_sale, ('transdate' => { ge => $fromdate});
- push @where_purchase, ('transdate' => { ge => $fromdate});
+ 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});
+ };
}
if ($::form->{transdateto}) {
- my $todate = $::locale->parse_date_to_object(\%::myconfig, $::form->{transdateto});
- $todate->add(days => 1);
- push @where_sale, ('transdate' => { lt => $todate});
- push @where_purchase, ('transdate' => { lt => $todate});
+ 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});
+ };
}
my $all_open_ar_invoices = SL::DB::Manager::Invoice->get_all(where => \@where_sale, with_objects => 'customer');
my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => \@where_purchase, with_objects => 'vendor');
my @all_open_invoices;
- push @all_open_invoices, @{ $all_open_ar_invoices };
- push @all_open_invoices, @{ $all_open_ap_invoices };
+ # filter out subcent differences from ap invoices
+ push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices };
@all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices;
- #my $all_open_invoices = SL::DB::Manager::Invoice->get_all(where => \@where);
my $output = $self->render(
'bank_transactions/add_list',
sub action_save_invoices {
my ($self) = @_;
- my $invoice_hash = delete $::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
+ my $skonto_hash = delete $::form->{invoice_skontos} || {}; # array containing the payment type, could be empty
while ( my ($bt_id, $invoice_ids) = each(%$invoice_hash) ) {
my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
return 1; } @invoices if $bank_transaction->amount < 0;
foreach my $invoice (@invoices) {
+ my $payment_type;
+ if ( @$skonto_hash{"$bt_id"} ) {
+ $payment_type = shift( @$skonto_hash{"$bt_id"} );
+ } else {
+ $payment_type = 'without_skonto';
+ };
if ($amount_of_transaction == 0) {
- flash('warning', $::locale->text('There are invoices which could not be payed by bank transaction #1 (Account number: #2, bank code: #3)!',
+ flash('warning', $::locale->text('There are invoices which could not be paid by bank transaction #1 (Account number: #2, bank code: #3)!',
$bank_transaction->purpose,
$bank_transaction->remote_account_number,
$bank_transaction->remote_bank_code));
}
#pay invoice or go to the next bank transaction if the amount is not sufficiently high
if ($invoice->amount <= $amount_of_transaction) {
- $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id, trans_id => $invoice->id, amount => $invoice->amount, transdate => $bank_transaction->transdate);
+ $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
+ trans_id => $invoice->id,
+ amount => $invoice->amount,
+ payment_type => $payment_type,
+ transdate => $bank_transaction->transdate->to_kivitendo);
if ($invoice->is_sales) {
$amount_of_transaction -= $sign * $invoice->amount;
$bank_transaction->invoice_amount($bank_transaction->invoice_amount + $invoice->amount);
$bank_transaction->invoice_amount($bank_transaction->invoice_amount - $invoice->amount);
}
} else {
- $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id, trans_id => $invoice->id, amount => $amount_of_transaction, transdate => $bank_transaction->transdate);
+ $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
+ trans_id => $invoice->id,
+ amount => $amount_of_transaction,
+ payment_type => $payment_type,
+ transdate => $bank_transaction->transdate->to_kivitendo);
$bank_transaction->invoice_amount($bank_transaction->amount) if $invoice->is_sales;
$bank_transaction->invoice_amount($bank_transaction->amount) if !$invoice->is_sales;
$amount_of_transaction = 0;
$arap->pay_invoice(chart_id => $bt->local_bank_account->chart_id,
trans_id => $arap->id,
amount => $arap->amount,
- transdate => $bt->transdate);
+ transdate => $bt->transdate->to_kivitendo);
$arap->save;
#create record link
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->{"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->{"amount:number"}, $::locale->text('Amount') ],
+ [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account') ],
);
for (@filters) {
my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
$self->{report} = $report;
- my @columns = qw(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(transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code);
+ 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);
my %column_defs = (
transdate => { sub => sub { $_[0]->transdate_as_date } },
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 => {},
);
);
$report->set_columns(%column_defs);
$report->set_column_order(@columns);
- $report->set_export_options(qw(list filter));
+ $report->set_export_options(qw(list_all filter));
$report->set_options_from_form;
- $self->models->disable_pagination if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
+ $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();
- my $label_sub = sub { t8('#1 - Account number #2, bank code #3, #4', $_[0]->name, $_[0]->account_number, $_[0]->bank_code, $_[0]->bank )};
+ 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, label_sub => $label_sub),
+ 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 }),
);
}
sorted => {
_default => {
by => 'transdate',
- dir => 1,
+ dir => 0, # 1 = ASC, 0 = DESC : default sort is newest at top
},
transdate => t8('Transdate'),
remote_name => t8('Remote name'),
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 check_type {
my ($self) = @_;
- die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors addresses contacts projects orders);
+ die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors addresses contacts projects orders bank_transactions mt940);
$self->type($::form->{profile}->{type});
}
$self->profile_from_form;
-
if ( $::form->{file} && $::form->{FILENAME} =~ /\.940$/ ) {
my $mt940_file = SL::SessionFile->new($::form->{FILENAME}, mode => '>');
$mt940_file->fh->print($::form->{file});
$mt940_file->fh->close;
- my $aqbin = '/usr/bin/aqbanking-cli';
+ my $aqbin = $::lx_office_conf{applications}->{aqbanking};
+ die "Can't find aqbanking-cli, please check your configuration file.\n" unless -f $aqbin;
my $cmd = "$aqbin --cfgdir=\"users\" import --importer=\"swift\" --profile=\"SWIFT-MT940\" -f " . $mt940_file->file_name . " | $aqbin --cfgdir=\"users\" listtrans --exporter=\"csv\" --profile=\"AqMoney2\" ";
my $converted_mt940;
open(MT, "$cmd |");
use Rose::Object::MakeMethods::Generic
(
- 'scalar --get_set_init' => [ qw(table bank_accounts_by) ],
+ 'scalar --get_set_init' => [ qw(bank_accounts_by) ],
);
sub init_class {
sub init_bank_accounts_by {
my ($self) = @_;
- return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_bank_accounts } } ) } qw(id account_number) };
+ return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_bank_accounts } } ) } qw(id account_number iban) };
}
sub check_objects {
my ($self) = @_;
$self->controller->track_progress(phase => 'building data', progress => 0);
+ my $update_policy = $self->controller->profile->get('update_policy') || 'skip';
my $i;
my $num_data = scalar @{ $self->controller->data };
$self->check_bank_account($entry);
$self->check_currency($entry, take_default => 1);
-
$self->join_purposes($entry);
- #TODO: adde checks für die Variablen
+ $self->join_remote_names($entry);
+ $self->check_existing($entry) unless @{ $entry->{errors} };
} continue {
$i++;
}
- $self->add_cvar_raw_data_columns;
+ $self->add_info_columns({ header => $::locale->text('Bank account'), method => 'local_bank_name' });
+}
+
+sub check_existing {
+ my ($self, $entry) = @_;
+
+ my $object = $entry->{object};
+
+ # for each imported entry (line) we make a database call to find existing entries
+ # we don't use the init_by hash because we have to check several fields
+ # this means that we can't detect duplicates in the import file
+
+ if ( $object->amount ) {
+ # check for same
+ # * purpose
+ # * transdate
+ # * remote_account_number (may be empty for records of our own bank)
+ # * amount
+ my $num;
+ if ( $num = SL::DB::Manager::BankTransaction->get_all_count(query =>[ remote_account_number => $object->remote_account_number, transdate => $object->transdate, purpose => $object->purpose, amount => $object->amount] ) ) {
+ push(@{$entry->{errors}}, $::locale->text('Skipping due to existing bank transaction in database'));
+ };
+ } else {
+ push(@{$entry->{errors}}, $::locale->text('Skipping because transfer amount is empty.'));
+ };
}
sub setup_displayable_columns {
$self->SUPER::setup_displayable_columns;
- $self->add_displayable_columns({ name => 'transaction_id', description => $::locale->text('Transaction ID') },
- { name => 'local_bank_code', description => $::locale->text('Own bank code') },
- { name => 'local_account_number', description => $::locale->text('Own bank account number') },
- { name => 'local_bank_account_id', description => $::locale->text('ID of own bank account') },
- { name => 'remote_bank_code', description => $::locale->text('Bank code of the goal/source') },
- { name => 'remote_account_number', description => $::locale->text('Account number of the goal/source') },
- { name => 'transdate', description => $::locale->text('Date of transaction') },
- { name => 'valutadate', description => $::locale->text('Valuta') },
- { name => 'amount', description => $::locale->text('Amount') },
- { name => 'currency', description => $::locale->text('Currency') },
- { name => 'currency_id', description => $::locale->text('Currency (database ID)') },
- { name => 'remote_name', description => $::locale->text('Name of the goal/source') },
- { name => 'remote_name_1', description => $::locale->text('Name of the goal/source') },
- { name => 'purpose', description => $::locale->text('Purpose') },
- );
+ # TODO: don't show fields cleared, invoice_amount and transaction_id in the help text, as these should not be imported
+ $self->add_displayable_columns({ name => 'local_bank_code', description => $::locale->text('Own bank code') },
+ { name => 'local_account_number', description => $::locale->text('Own bank account number or IBAN') },
+ { name => 'local_bank_account_id', description => $::locale->text('ID of own bank account') },
+ { name => 'remote_bank_code', description => $::locale->text('Bank code of the goal/source') },
+ { name => 'remote_account_number', description => $::locale->text('Account number of the goal/source') },
+ { name => 'transdate', description => $::locale->text('Date of transaction') },
+ { name => 'valutadate', description => $::locale->text('Valuta date') },
+ { name => 'amount', description => $::locale->text('Amount') },
+ { name => 'currency', description => $::locale->text('Currency') },
+ { name => 'currency_id', description => $::locale->text('Currency (database ID)') },
+ { name => 'remote_name', description => $::locale->text('Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")') },
+ { name => 'purpose', description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+ );
}
sub check_bank_account {
my $object = $entry->{object};
- # Check whether or not local_bank_account ID is valid.
+ # Check whether or not local_bank_account ID exists and is valid.
if ($object->local_bank_account_id && !$self->bank_accounts_by->{id}->{ $object->local_bank_account_id }) {
push @{ $entry->{errors} }, $::locale->text('Error: Invalid local bank account');
return 0;
}
- # Map account information to ID if given.
+ # Map account information to ID via local_account_number if no local_bank_account_id was given
+ # local_account_number checks for match of account number or IBAN
if (!$object->local_bank_account_id && $entry->{raw_data}->{local_account_number}) {
my $bank_account = $self->bank_accounts_by->{account_number}->{ $entry->{raw_data}->{local_account_number} };
+ if (!$bank_account) {
+ $bank_account = $self->bank_accounts_by->{iban}->{ $entry->{raw_data}->{local_account_number} };
+ };
if (!$bank_account) {
push @{ $entry->{errors} }, $::locale->text('Error: Invalid local bank account');
return 0;
}
$object->local_bank_account_id($bank_account->id);
+ $entry->{info_data}->{local_bank_name} = $bank_account->name;
}
return $object->local_bank_account_id ? 1 : 0;
my $object = $entry->{object};
my $purpose = join('', $entry->{raw_data}->{purpose},
- $entry->{raw_data}->{purpose1},
- $entry->{raw_data}->{purpose2},
- $entry->{raw_data}->{purpose3},
- $entry->{raw_data}->{purpose4},
- $entry->{raw_data}->{purpose5},
- $entry->{raw_data}->{purpose6},
- $entry->{raw_data}->{purpose7},
- $entry->{raw_data}->{purpose8},
- $entry->{raw_data}->{purpose9},
- $entry->{raw_data}->{purpose10},
- $entry->{raw_data}->{purpose11} );
+ $entry->{raw_data}->{purpose1},
+ $entry->{raw_data}->{purpose2},
+ $entry->{raw_data}->{purpose3},
+ $entry->{raw_data}->{purpose4},
+ $entry->{raw_data}->{purpose5},
+ $entry->{raw_data}->{purpose6},
+ $entry->{raw_data}->{purpose7},
+ $entry->{raw_data}->{purpose8},
+ $entry->{raw_data}->{purpose9},
+ $entry->{raw_data}->{purpose10},
+ $entry->{raw_data}->{purpose11} );
$object->purpose($purpose);
+
+}
+
+sub join_remote_names {
+ my ($self, $entry) = @_;
+
+ my $object = $entry->{object};
+
+ my $remote_name = join('', $entry->{raw_data}->{remote_name},
+ $entry->{raw_data}->{remote_name_1} );
+ $object->remote_name($remote_name);
}
1;
sub init_all_bank_accounts {
my ($self) = @_;
- return SL::DB::Manager::BankAccount->get_all;
+ return SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
}
sub init_payment_terms_by {
use SL::Helper::Flash;
use SL::DB::BankTransaction;
-use SL::DB::BankAccount;
+use SL::DB::Manager::BankAccount;
use SL::DB::AccTransaction;
use SL::DB::ReconciliationLink;
+use List::Util qw(sum);
use Rose::Object::MakeMethods::Generic (
'scalar --get_set_init' => [ qw(cleared BANK_ACCOUNTS) ],
sub action_search {
my ($self) = @_;
- $self->render('reconciliation/search',
- label_sub => sub { t8('#1 - Account number #2, bank code #3, #4',
- $_[0]->name,
- $_[0]->bank,
- $_[0]->account_number,
- $_[0]->bank_code) });
+ $self->render('reconciliation/search');
}
sub action_reconciliation {
$self->_get_balances;
$self->render('reconciliation/form',
- title => t8('Reconciliation'),
- label_sub => sub { t8('#1 - Account number #2, bank code #3, #4',
- $_[0]->name,
- $_[0]->bank,
- $_[0]->account_number,
- $_[0]->bank_code) });
+ title => t8('Reconciliation'));
}
sub action_load_overview {
$self->_get_balances;
my $output = $self->render('reconciliation/_linked_transactions', { output => 0 });
- my %result = ( html => $output,
- absolut_bt_balance => $::form->format_amount(\%::myconfig, $self->{absolut_bt_balance}, 2),
+ my %result = ( html => $output,
+ absolut_bt_balance => $::form->format_amount(\%::myconfig, $self->{absolut_bt_balance}, 2),
absolut_bb_balance => $::form->format_amount(\%::myconfig, -1 * $self->{absolut_bb_balance}, 2),
- bt_balance => $::form->format_amount(\%::myconfig, $self->{bt_balance}, 2),
- bb_balance => $::form->format_amount(\%::myconfig, -1 * $self->{bb_balance}, 2)
+ bt_balance => $::form->format_amount(\%::myconfig, $self->{bt_balance}, 2),
+ bb_balance => $::form->format_amount(\%::myconfig, -1 * $self->{bb_balance}, 2)
);
$self->render(\to_json(\%result), { type => 'json', process => 0 });
$self->render(\to_json(\%result), { type => 'json', process => 0 });
}
-sub action_reconciliate {
+sub action_reconcile {
my ($self) = @_;
#Check elements
my @errors = $self->_get_elements_and_validate;
if (@errors) {
- unshift(@errors, (t8('Could not reconciliate chosen elements!')));
+ unshift(@errors, (t8('Could not reconcile chosen elements!')));
flash('error', @errors);
$self->action_reconciliation;
return;
}
- $self->_reconciliate;
+ $self->_reconcile;
$self->action_reconciliation;
}
$self->_get_balances;
my $output = $self->render('reconciliation/_linked_transactions', { output => 0 });
- my %result = ( html => $output,
- absolut_bt_balance => $::form->format_amount(\%::myconfig, $self->{absolut_bt_balance}, 2),
- absolut_bb_balance => $::form->format_amount(\%::myconfig, -1 * $self->{absolut_bb_balance}, 2),
- bt_balance => $::form->format_amount(\%::myconfig, $self->{bt_balance}, 2),
- bb_balance => $::form->format_amount(\%::myconfig, -1 * $self->{bb_balance}, 2)
+ my %result = ( html => $output,
+ absolut_bt_balance => $::form->format_amount(\%::myconfig, $self ->{absolut_bt_balance}, 2),
+ absolut_bb_balance => $::form->format_amount(\%::myconfig, -1 * $self ->{absolut_bb_balance}, 2),
+ bt_balance => $::form->format_amount(\%::myconfig, $self ->{bt_balance}, 2),
+ bb_balance => $::form->format_amount(\%::myconfig, -1 * $self ->{bb_balance}, 2)
);
$self->render(\to_json(\%result), { type => 'json', process => 0 });
$self->_get_proposals;
my $output = $self->render('reconciliation/proposals', { output => 0 });
- my %result = ( html => $output,
- absolut_bt_balance => $::form->format_amount(\%::myconfig, $self->{absolut_bt_balance}, 2),
- absolut_bb_balance => $::form->format_amount(\%::myconfig, -1 * $self->{absolut_bb_balance}, 2),
- bt_balance => $::form->format_amount(\%::myconfig, $self->{bt_balance}, 2),
- bb_balance => $::form->format_amount(\%::myconfig, -1 * $self->{bb_balance}, 2)
+ my %result = ( html => $output,
+ absolut_bt_balance => $::form->format_amount(\%::myconfig, $self ->{absolut_bt_balance}, 2),
+ absolut_bb_balance => $::form->format_amount(\%::myconfig, -1 * $self ->{absolut_bb_balance}, 2),
+ bt_balance => $::form->format_amount(\%::myconfig, $self ->{bt_balance}, 2),
+ bb_balance => $::form->format_amount(\%::myconfig, -1 * $self ->{bb_balance}, 2)
);
$self->render(\to_json(\%result), { type => 'json', process => 0 });
}
-sub action_reconciliate_proposals {
+sub action_reconcile_proposals {
my ($self) = @_;
my $counter = 0;
sub _get_proposals {
my ($self) = @_;
+ # reconciliation suggestion is based on:
+ # * record_link exists (was paid by bank transaction)
+ # or acc_trans entry exists where
+ # * amount is exactly the same
+ # * date is the same
+ # * IBAN or account number have to match exactly (cv details, no spaces)
+ # * not a gl storno
+ # * there is exactly one match for all conditions
+
$self->_filter_to_where;
my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => [ @{ $self->{bt_where} }, cleared => '0' ]);
$proposal->{BT} = $bt;
$proposal->{BB} = [];
+ # first of all check if any of the bank_transactions are already linked (i.e. were paid via bank transactions)
my $linked_records = SL::DB::Manager::RecordLink->get_all(where => [ from_table => 'bank_transactions', from_id => $bt->id ]);
foreach my $linked_record (@{ $linked_records }) {
my $invoice;
#add proposal if something in acc_trans was found
#otherwise try to find another entry in acc_trans and add it
- if (scalar @{ $proposal->{BB} } and !$check_sum) {
+ # for linked_records we allow a slight difference / imprecision, for acc_trans search we don't
+ if (scalar @{ $proposal->{BB} } and abs($check_sum) <= 0.01 ) {
push @proposals, $proposal;
} elsif (!scalar @{ $proposal->{BB} }) {
+ # use account_number and iban for matching remote account number
+ # don't suggest gl stornos (ar and ap stornos shouldn't have any payments)
+
+ my @account_number_match = (
+ ( 'ar.customer.iban' => $bt->remote_account_number ),
+ ( 'ar.customer.account_number' => $bt->remote_account_number ),
+ ( 'ap.vendor.iban' => $bt->remote_account_number ),
+ ( 'ap.vendor.account_number' => $bt->remote_account_number ),
+ ( 'gl.storno' => '0' ),
+ );
+
my $acc_transactions = SL::DB::Manager::AccTransaction->get_all(where => [ @{ $self->{bb_where} },
amount => -1 * $bt->amount,
cleared => '0',
- or => [
- and => [ 'ar.customer.account_number' => $bt->remote_account_number,
- 'ar.customer.bank_code' => $bt->remote_bank_code, ],
- and => [ 'ap.vendor.account_number' => $bt->remote_account_number,
- 'ap.vendor.bank_code' => $bt->remote_bank_code, ],
- 'gl.storno' => '0' ]],
+ 'transdate' => $bt->transdate,
+ or => [ @account_number_match ]
+ ],
with_objects => [ 'ar', 'ap', 'ar.customer', 'ap.vendor', 'gl' ]);
if (scalar @{ $acc_transactions } == 1) {
push @{ $proposal->{BB} }, @{ $acc_transactions }[0];
return @errors;
}
-sub _reconciliate {
+sub _reconcile {
my ($self) = @_;
- #1. Step: Set AccTrans and BankTransactions to 'cleared'
+ # 1. step: set AccTrans and BankTransactions to 'cleared'
foreach my $element (@{ $self->{ELEMENTS} }) {
$element->cleared('1');
$element->invoice_amount($element->amount) if $element->isa('SL::DB::BankTransaction');
$element->save;
}
- #2. Step: Insert entry in reconciliation_links
+ # 2. step: insert entry in reconciliation_links
my $rec_group = SL::DB::Manager::ReconciliationLink->get_new_rec_group();
#There is either a 1:n relation or a n:1 relation
if (scalar @{ $::form->{bt_ids} } == 1) {
my %filter = @{ $parse_filter{query} };
my (@rl_where, @bt_where, @bb_where);
- @rl_where = ('bank_transaction.local_bank_account_id' => $filter{local_bank_account_id});
+ @rl_where = ('bank_transaction.local_bank_account_id' => $filter{local_bank_account_id});
@bt_where = (local_bank_account_id => $filter{local_bank_account_id});
@bb_where = (chart_id => $self->{bank_account}->chart_id);
if ($filter{fromdate} and $filter{todate}) {
- push @rl_where, (or => [ and => [ 'acc_tran.transdate' => $filter{fromdate},
- 'acc_tran.transdate' => $filter{todate} ],
+ push @rl_where, (or => [ and => [ 'acc_trans.transdate' => $filter{fromdate},
+ 'acc_trans.transdate' => $filter{todate} ],
and => [ 'bank_transaction.transdate' => $filter{fromdate},
- 'bank_transaction.transdate' => $filter{todate} ] ] );
+ 'bank_transaction.transdate' => $filter{todate} ] ] );
- push @bt_where, (transdate => $filter{todate} );
- push @bt_where, (transdate => $filter{fromdate} );
- push @bb_where, (transdate => $filter{todate} );
- push @bb_where, (transdate => $filter{fromdate} );
+ push @bt_where, (transdate => $filter{todate} );
+ push @bt_where, (transdate => $filter{fromdate} );
+ push @bb_where, (transdate => $filter{todate} );
+ push @bb_where, (transdate => $filter{fromdate} );
+ }
+
+ if ( $self->{bank_account}->reconciliation_starting_date ) {
+ push @bt_where, (transdate => { gt => $self->{bank_account}->reconciliation_starting_date });
+ push @bb_where, (transdate => { gt => $self->{bank_account}->reconciliation_starting_date });
}
+ # don't try to reconcile opening and closing balance transactions
+ push @bb_where, ('acc_trans.ob_transaction' => 0);
+ push @bb_where, ('acc_trans.cb_transaction' => 0);
+
if ($filter{fromdate} and not $filter{todate}) {
- push @rl_where, (or => [ 'acc_tran.transdate' => $filter{fromdate},
+ push @rl_where, (or => [ 'acc_trans.transdate' => $filter{fromdate},
'bank_transaction.transdate' => $filter{fromdate} ] );
push @bt_where, (transdate => $filter{fromdate} );
push @bb_where, (transdate => $filter{fromdate} );
}
if ($filter{todate} and not $filter{fromdate}) {
- push @rl_where, ( or => [ 'acc_tran.transdate' => $filter{todate} ,
+ push @rl_where, ( or => [ 'acc_trans.transdate' => $filter{todate} ,
'bank_transaction.transdate' => $filter{todate} ] );
push @bt_where, (transdate => $filter{todate} );
push @bb_where, (transdate => $filter{todate} );
if ($filter{cleared}) {
$filter{cleared} = $filter{cleared} eq 'FALSE' ? '0' : '1';
- push @rl_where, ('acc_tran.cleared' => $filter{cleared} );
+ push @rl_where, ('acc_trans.cleared' => $filter{cleared} );
push @bt_where, (cleared => $filter{cleared} );
push @bb_where, (cleared => $filter{cleared} );
$self->_filter_to_where;
my (@where, @bt_where, @bb_where);
+ # don't try to reconcile opening and closing balances
+ # instead use an offset in configuration
+
@where = (@{ $self->{rl_where} });
@bt_where = (@{ $self->{bt_where} }, cleared => '0');
@bb_where = (@{ $self->{bb_where} }, cleared => '0');
my $reconciliation_groups = SL::DB::Manager::ReconciliationLink->get_all(distinct => 1,
select => ['rec_group'],
where => \@where,
- with_objects => ['bank_transaction', 'acc_tran']);
+ with_objects => ['bank_transaction', 'acc_trans']);
- my $fromdate = $::locale->parse_date_to_object(\%::myconfig, $::form->{filter}->{fromdate_date__ge});
- my $todate = $::locale->parse_date_to_object(\%::myconfig, $::form->{filter}->{todate_date__le});
+ my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate_date__ge});
+ my $todate = $::locale->parse_date_to_object($::form->{filter}->{todate_date__le});
foreach my $rec_group (@{ $reconciliation_groups }) {
- my $linked_transactions = SL::DB::Manager::ReconciliationLink->get_all(where => [rec_group => $rec_group->rec_group], with_objects => ['bank_transaction', 'acc_tran']);
+ my $linked_transactions = SL::DB::Manager::ReconciliationLink->get_all(where => [rec_group => $rec_group->rec_group], with_objects => ['bank_transaction', 'acc_trans']);
my $line;
my $first_transaction = shift @{ $linked_transactions };
my $first_bt = $first_transaction->bank_transaction;
- my $first_bb = $first_transaction->acc_tran;
+ my $first_bb = $first_transaction->acc_trans;
if (defined $fromdate) {
$first_bt->{class} = 'out_of_balance' if ( $first_bt->transdate lt $fromdate );
my ($previous_bt_id, $previous_acc_trans_id) = ($first_transaction->bank_transaction_id, $first_transaction->acc_trans_id);
foreach my $linked_transaction (@{ $linked_transactions }) {
my $bank_transaction = $linked_transaction->bank_transaction;
- my $acc_transaction = $linked_transaction->acc_tran;
+ my $acc_transaction = $linked_transaction->acc_trans;
if (defined $fromdate) {
$bank_transaction->{class} = 'out_of_balance' if ( $bank_transaction->transdate lt $fromdate );
$acc_transaction->{class} = 'out_of_balance' if ( $acc_transaction->transdate lt $fromdate );
push @rows, $line;
}
- #add non-cleared bank transactions
+ # add non-cleared bank transactions
my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => \@bt_where);
foreach my $bt (@{ $bank_transactions }) {
my $line;
push @rows, $line;
}
- #add non-cleared bookings on bank
+ # add non-cleared bookings on bank
my $bookings_on_bank = SL::DB::Manager::AccTransaction->get_all(where => \@bb_where);
foreach my $bb (@{ $bookings_on_bank }) {
- if ($::form->{filter}->{show_stornos} or !$bb->get_transaction->storno) {
+ if ($::form->{filter}->{show_stornos} or !$bb->record->storno) {
my $line;
$line->{BB} = [ $bb ];
$line->{type} = 'BB';
@bt_where = @{ $self->{bt_where} };
@bb_where = @{ $self->{bb_where} };
- my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => \@bt_where );
- my $payments = SL::DB::Manager::AccTransaction ->get_all(where => \@bb_where );
-
- #for absolute balance get all bookings till todate
- my $todate = $::locale->parse_date_to_object(\%::myconfig, $::form->{filter}->{todate_date__le});
-
my @all_bt_where = (local_bank_account_id => $self->{bank_account}->id);
my @all_bb_where = (chart_id => $self->{bank_account}->chart_id);
+ my ($bt_balance, $bb_balance) = (0,0);
+ my ($absolut_bt_balance, $absolut_bb_balance) = (0,0);
+
+ if ( $self->{bank_account}->reconciliation_starting_date ) {
+ $bt_balance = $self->{bank_account}->reconciliation_starting_balance;
+ $bb_balance = $self->{bank_account}->reconciliation_starting_balance * -1;
+ $absolut_bt_balance = $self->{bank_account}->reconciliation_starting_balance;
+ $absolut_bb_balance = $self->{bank_account}->reconciliation_starting_balance * -1;
+
+ push @all_bt_where, ( transdate => { gt => $self->{bank_account}->reconciliation_starting_date });
+ push @all_bb_where, ( transdate => { gt => $self->{bank_account}->reconciliation_starting_date });
+ }
+
+ my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => \@bt_where );
+ my $payments = SL::DB::Manager::AccTransaction ->get_all(where => \@bb_where );
+
+ # for absolute balance get all bookings until todate
+ my $todate = $::locale->parse_date_to_object($::form->{filter}->{todate_date__le});
+ my $fromdate = $::locale->parse_date_to_object($::form->{filter}->{fromdate_date__le});
+
if ($todate) {
push @all_bt_where, (transdate => { le => $todate });
push @all_bb_where, (transdate => { le => $todate });
}
my $all_bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => \@all_bt_where);
- my $all_payments = SL::DB::Manager::AccTransaction ->get_all(where => \@all_bb_where);
+ my $all_payments = SL::DB::Manager::AccTransaction ->get_all(where => \@all_bb_where);
- my ($bt_balance, $bb_balance) = (0,0);
- my ($absolut_bt_balance, $absolut_bb_balance) = (0,0);
+ $bt_balance += sum map { $_->amount } @{ $bank_transactions };
+ $bb_balance += sum map { $_->amount if ($::form->{filter}->{show_stornos} or !$_->record->storno) } @{ $payments };
+
+ $absolut_bt_balance += sum map { $_->amount } @{ $all_bank_transactions };
+ $absolut_bb_balance += sum map { $_->amount } @{ $all_payments };
- map { $bt_balance += $_->amount } @{ $bank_transactions };
- map { $bb_balance += $_->amount if ($::form->{filter}->{show_stornos} or !$_->get_transaction->storno) } @{ $payments };
- map { $absolut_bt_balance += $_->amount } @{ $all_bank_transactions };
- map { $absolut_bb_balance += $_->amount } @{ $all_payments };
- $self->{bt_balance} = $bt_balance || 0;
- $self->{bb_balance} = $bb_balance || 0;
+ $self->{bt_balance} = $bt_balance || 0;
+ $self->{bb_balance} = $bb_balance || 0;
$self->{absolut_bt_balance} = $absolut_bt_balance || 0;
$self->{absolut_bb_balance} = $absolut_bb_balance || 0;
}
sub init_BANK_ACCOUNTS {
- SL::DB::Manager::BankAccount->get_all();
+ SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
}
1;
# chart_id)
my $chart_id = $self->chart_id;
- my $chart = SL::DB::Chart->new( id => $chart_id );
- if ( $chart->load(speculative => 1) ) {
+ my $chart = SL::DB::Manager::Chart->find_by( id => $chart_id );
+ if ( $chart ) {
my $linked_bank = SL::DB::Manager::BankAccount->find_by( chart_id => $chart_id );
if ( $linked_bank ) {
if ( not $self->{id} or ( $self->{id} && $linked_bank->id != $self->{id} )) {
return @errors;
}
+sub displayable_name {
+ my ($self) = @_;
+
+ return join ' ', grep $_, $self->name, $self->bank, $self->iban;
+}
+
1;
use SL::DB::Manager::BankTransaction;
use SL::DB::Helper::LinkedRecords;
-__PACKAGE__->meta->initialize;
+require SL::DB::Invoice;
+require SL::DB::PurchaseInvoice;
-use SL::DB::Invoice;
-use SL::DB::PurchaseInvoice;
+__PACKAGE__->meta->initialize;
-use Data::Dumper;
# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
#__PACKAGE__->meta->make_manager_class;
push @linked_invoices, SL::DB::Manager::PurchaseInvoice->find_by(id => $record_link->to_id)->invnumber if $record_link->to_table eq 'ap';
}
-# $main::lxdebug->message(0, "linked invoices sind: " . Dumper(@linked_invoices));
-# $main::lxdebug->message(0, "record_links sind: " . Dumper($record_links));
-
return [ @linked_invoices ];
}
+sub get_agreement_with_invoice {
+ my ($self, $invoice) = @_;
+
+ die "first argument is not an invoice object"
+ unless ref($invoice) eq 'SL::DB::Invoice' or ref($invoice) eq 'SL::DB::PurchaseInvoice';
+
+ my %points = (
+ cust_vend_name_in_purpose => 1,
+ cust_vend_number_in_purpose => 1,
+ datebonus0 => 3,
+ datebonus14 => 2,
+ datebonus35 => 1,
+ datebonus120 => 0,
+ datebonus_negative => -1,
+ depositor_matches => 2,
+ exact_amount => 4,
+ exact_open_amount => 4,
+ invnumber_in_purpose => 2,
+ # overpayment => -1, # either other invoice is more likely, or several invoices paid at once
+ payment_before_invoice => -2,
+ payment_within_30_days => 1,
+ remote_account_number => 3,
+ skonto_exact_amount => 5,
+ wrong_sign => -1,
+ );
+
+ my ($agreement,$rule_matches);
+
+ # compare banking arrangements
+ my ($iban, $bank_code, $account_number);
+ $bank_code = $invoice->customer->bank_code if $invoice->is_sales;
+ $account_number = $invoice->customer->account_number if $invoice->is_sales;
+ $iban = $invoice->customer->iban if $invoice->is_sales;
+ $bank_code = $invoice->vendor->bank_code if ! $invoice->is_sales;
+ $iban = $invoice->vendor->iban if ! $invoice->is_sales;
+ $account_number = $invoice->vendor->account_number if ! $invoice->is_sales;
+ if ( $bank_code eq $self->remote_bank_code && $account_number eq $self->remote_account_number ) {
+ $agreement += $points{remote_account_number};
+ $rule_matches .= 'remote_account_number(' . $points{'remote_account_number'} . ') ';
+ };
+ if ( $iban eq $self->remote_account_number ) {
+ $agreement += $points{remote_account_number};
+ $rule_matches .= 'remote_account_number(' . $points{'remote_account_number'} . ') ';
+ };
+
+ my $datediff = $self->transdate->{utc_rd_days} - $invoice->transdate->{utc_rd_days};
+ $invoice->{datediff} = $datediff;
+
+ # compare amount
+ if (abs(abs($invoice->amount) - abs($self->amount)) < 0.01) {
+ $agreement += $points{exact_amount};
+ $rule_matches .= 'exact_amount(' . $points{'exact_amount'} . ') ';
+ };
+
+ # compare open amount, preventing double points when open amount = invoice amount
+ if ( $invoice->amount != $invoice->open_amount && abs(abs($invoice->open_amount) - abs($self->amount)) < 0.01) {
+ $agreement += $points{exact_open_amount};
+ $rule_matches .= 'exact_open_amount(' . $points{'exact_open_amount'} . ') ';
+ };
+
+ if ( $invoice->skonto_date && abs(abs($invoice->amount_less_skonto) - abs($self->amount)) < 0.01) {
+ $agreement += $points{skonto_exact_amount};
+ $rule_matches .= 'skonto_exact_amount(' . $points{'skonto_exact_amount'} . ') ';
+ };
+
+ #search invoice number in purpose
+ my $invnumber = $invoice->invnumber;
+ # invnumbernhas to have at least 3 characters
+ if ( length($invnumber) > 2 && $self->purpose =~ /\b$invnumber\b/i ) {
+ $agreement += $points{invnumber_in_purpose};
+ $rule_matches .= 'invnumber_in_purpose(' . $points{'invnumber_in_purpose'} . ') ';
+ };
+
+ #check sign
+ if ( $invoice->is_sales && $self->amount < 0 ) {
+ $agreement += $points{wrong_sign};
+ $rule_matches .= 'wrong_sign(' . $points{'wrong_sign'} . ') ';
+ };
+ if ( ! $invoice->is_sales && $self->amount > 0 ) {
+ $agreement += $points{wrong_sign};
+ $rule_matches .= 'wrong_sign(' . $points{'wrong_sign'} . ') ';
+ };
+
+ # search customer/vendor number in purpose
+ my $cvnumber;
+ $cvnumber = $invoice->customer->customernumber if $invoice->is_sales;
+ $cvnumber = $invoice->vendor->vendornumber if ! $invoice->is_sales;
+ if ( $cvnumber && $self->purpose =~ /\b$cvnumber\b/i ) {
+ $agreement += $points{cust_vend_number_in_purpose};
+ $rule_matches .= 'cust_vend_number_in_purpose(' . $points{'cust_vend_number_in_purpose'} . ') ';
+ }
+
+ # search for customer/vendor name in purpose (may contain GMBH, CO KG, ...)
+ my $cvname;
+ $cvname = $invoice->customer->name if $invoice->is_sales;
+ $cvname = $invoice->vendor->name if ! $invoice->is_sales;
+ if ( $cvname && $self->purpose =~ /\b$cvname\b/i ) {
+ $agreement += $points{cust_vend_name_in_purpose};
+ $rule_matches .= 'cust_vend_name_in_purpose(' . $points{'cust_vend_name_in_purpose'} . ') ';
+ };
+
+ # compare depositorname, don't try to match empty depositors
+ my $depositorname;
+ $depositorname = $invoice->customer->depositor if $invoice->is_sales;
+ $depositorname = $invoice->vendor->depositor if ! $invoice->is_sales;
+ if ( $depositorname && $self->remote_name =~ /$depositorname/ ) {
+ $agreement += $points{depositor_matches};
+ $rule_matches .= 'depositor_matches(' . $points{'depositor_matches'} . ') ';
+ };
+
+ #Check if words in remote_name appear in cvname
+ my $check_string_points = _check_string($self->remote_name,$cvname);
+ if ( $check_string_points ) {
+ $agreement += $check_string_points;
+ $rule_matches .= 'remote_name(' . $check_string_points . ') ';
+ };
+
+ # transdate prefilter: compare transdate of bank_transaction with transdate of invoice
+ if ( $datediff < -5 ) { # this might conflict with advance payments
+ $agreement += $points{payment_before_invoice};
+ $rule_matches .= 'payment_before_invoice(' . $points{'payment_before_invoice'} . ') ';
+ };
+ if ( $datediff < 30 ) {
+ $agreement += $points{payment_within_30_days};
+ $rule_matches .= 'payment_within_30_days(' . $points{'payment_within_30_days'} . ') ';
+ };
+
+ # only if we already have a good agreement, let date further change value of agreement.
+ # this is so that if there are several plausible open invoices which are all equal
+ # (rent jan, rent feb...) the one with the best date match is chosen over
+ # the others
+
+ # another way around this is to just pre-filter by periods instead of matching everything
+ if ( $agreement > 5 ) {
+ if ( $datediff == 0 ) {
+ $agreement += $points{datebonus0};
+ $rule_matches .= 'datebonus0(' . $points{'datebonus0'} . ') ';
+ } elsif ( $datediff > 0 and $datediff <= 14 ) {
+ $agreement += $points{datebonus14};
+ $rule_matches .= 'datebonus14(' . $points{'datebonus14'} . ') ';
+ } elsif ( $datediff >14 and $datediff < 35) {
+ $agreement += $points{datebonus35};
+ $rule_matches .= 'datebonus35(' . $points{'datebonus35'} . ') ';
+ } elsif ( $datediff >34 and $datediff < 120) {
+ $agreement += $points{datebonus120};
+ $rule_matches .= 'datebonus120(' . $points{'datebonus120'} . ') ';
+ } elsif ( $datediff < 0 ) {
+ $agreement += $points{datebonus_negative};
+ $rule_matches .= 'datebonus_negative(' . $points{'datebonus_negative'} . ') ';
+ } else {
+ # e.g. datediff > 120
+ };
+ };
+
+ return ($agreement,$rule_matches);
+};
+
+sub _check_string {
+ my $bankstring = shift;
+ my $namestring = shift;
+ return 0 unless $bankstring and $namestring;
+
+ my @bankwords = grep(/^\w+$/, split(/\b/,$bankstring));
+
+ my $match = 0;
+ foreach my $bankword ( @bankwords ) {
+ # only try to match strings with more than 2 characters
+ next unless length($bankword)>2;
+ if ( $namestring =~ /\b$bankword\b/i ) {
+ $match++;
+ };
+ };
+ return $match;
+};
+
1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+SL::DB::BankTransaction
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<get_agreement_with_invoice $invoice>
+
+Using a point system this function checks whether the bank transaction matches
+an invoices, using a variety of tests, such as
+
+=over 2
+
+=item * amount
+
+=item * amount_less_skonto
+
+=item * payment date
+
+=item * invoice number in purpose
+
+=item * customer or vendor name in purpose
+
+=item * account number matches account number of customer or vendor
+
+=back
+
+The total number of points, and the rules that matched, are returned.
+
+Example:
+ my $bt = SL::DB::Manager::BankTransaction->find_by(id => 522);
+ my $invoice = SL::DB::Manager::Invoice->find_by(invnumber => '198');
+ my ($agreement,rule_matches) = $bt->get_agreement_with_invoice($invoice);
+
+=back
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitendo-premium.de<gt>
+
+=cut
sub set_defaults {
my ($self) = @_;
- $self->_set_defaults(sep_char => ',',
- quote_char => '"',
- escape_char => '"',
- charset => 'CP850',
- numberformat => $::myconfig{numberformat},
- duplicates => 'no_check',
- );
if ($self->type eq 'parts') {
my $bugru = SL::DB::Manager::Buchungsgruppe->find_by(description => { like => 'Standard%19%' });
item_column => $::locale->text('OrderItem'),
max_amount_diff => 0.02,
);
+ } elsif ($self->type eq 'mt940') {
+ $self->_set_defaults(charset => 'UTF8',
+ sep_char => ';',
+ numberformat => '1000.00',
+ update_policy => 'skip',
+ );
+ } elsif ($self->type eq 'bank_transactions') {
+ $self->_set_defaults(charset => 'UTF8',
+ update_policy => 'skip',
+ );
} else {
$self->_set_defaults(table => 'customer');
}
+ # TODO: move the defaults into their own controller
+ # defaults can only be set once, so use these values as default if they
+ # haven't already been set above for one of the special import types
+ # If the values have been set above they won't be overwritten here:
+
+ $self->_set_defaults(sep_char => ',',
+ quote_char => '"',
+ escape_char => '"',
+ charset => 'CP850',
+ numberformat => $::myconfig{numberformat},
+ duplicates => 'no_check',
+ );
+
return $self;
}
=head1 NAME
-SL::Helper::Paginated - Manager mixin for paginating results.
+SL::DB::Helper::Paginated - Manager mixin for paginating results.
=head1 SYNOPSIS
In the manager:
- use SL::Helper::Paginated;
+ use SL::DB::Helper::Paginated;
__PACKAGE__->default_objects_per_page(10); # optional, defaults to 20
C<page> should contain a value between 1 and the maximum pages. Will be
sanitized.
-The parameter C<per_page> is optional. If not given the default value of the
+The parameter C<per_page> is optional, otherwise the default value of the
Manager will be used.
=back
--- /dev/null
+package SL::DB::Helper::Payment;
+
+use strict;
+
+use parent qw(Exporter);
+our @EXPORT = qw(pay_invoice);
+our @EXPORT_OK = qw(skonto_date skonto_charts amount_less_skonto within_skonto_period percent_skonto reference_account reference_amount transactions open_amount open_percent remaining_skonto_days skonto_amount check_skonto_configuration valid_skonto_amount get_payment_suggestions validate_payment_type open_sepa_transfer_amount get_payment_select_options_for_bank_transaction);
+our %EXPORT_TAGS = (
+ "ALL" => [@EXPORT, @EXPORT_OK],
+);
+
+require SL::DB::Chart;
+use Data::Dumper;
+use DateTime;
+use SL::DATEV qw(:CONSTANTS);
+use SL::Locale::String qw(t8);
+use List::Util qw(sum);
+use Carp;
+
+#
+# Public functions not exported by default
+#
+
+sub pay_invoice {
+ my ($self, %params) = @_;
+
+ require SL::DB::Tax;
+
+ my $is_sales = ref($self) eq 'SL::DB::Invoice';
+ my $mult = $is_sales ? 1 : -1; # multiplier for getting the right sign depending on ar/ap
+
+ my $paid_amount = 0; # the amount that will be later added to $self->paid
+
+ # default values if not set
+ $params{payment_type} = 'without_skonto' unless $params{payment_type};
+ validate_payment_type($params{payment_type});
+
+ # check for required parameters
+ Common::check_params(\%params, qw(chart_id transdate));
+
+ my $transdate_obj = $::locale->parse_date_to_object($params{transdate});
+ croak t8('Illegal date') unless ref $transdate_obj;
+
+ # check for closed period
+ my $closedto = $::locale->parse_date_to_object($::instance_conf->get_closedto);
+ if ( ref $closedto && $transdate_obj < $closedto ) {
+ croak t8('Cannot post payment for a closed period!');
+ };
+
+ # check for maximum number of future days
+ if ( $::instance_conf->get_max_future_booking_interval > 0 ) {
+ croak t8('Cannot post transaction above the maximum future booking date!') if $transdate_obj > DateTime->now->add( days => $::instance_conf->get_max_future_booking_interval );
+ };
+
+ # input checks:
+ if ( $params{'payment_type'} eq 'without_skonto' ) {
+ croak "invalid amount for payment_type 'without_skonto': $params{'amount'}\n" unless abs($params{'amount'}) > 0;
+ };
+
+ # options with_skonto_pt and difference_as_skonto don't require the parameter
+ # amount, but if amount is passed, make sure it matches the expected value
+ if ( $params{'payment_type'} eq 'difference_as_skonto' ) {
+ croak "amount $params{amount} doesn't match open amount " . $self->open_amount . ", diff = " . ($params{amount}-$self->open_amount) if $params{amount} && abs($self->open_amount - $params{amount} ) > 0.0000001;
+ } elsif ( $params{'payment_type'} eq 'with_skonto_pt' ) {
+ croak "amount $params{amount} doesn't match amount less skonto: " . $self->open_amount . "\n" if $params{amount} && abs($self->amount_less_skonto - $params{amount} ) > 0.0000001;
+ croak "payment type with_skonto_pt can't be used if payments have already been made" if $self->paid != 0;
+ };
+
+ # absolute skonto amount for invoice, use as reference sum to see if the
+ # calculated skontos add up
+ # only needed for payment_term "with_skonto_pt"
+
+ my $skonto_amount_check = $self->skonto_amount; # variable should be zero after calculating all skonto
+ my $total_open_amount = $self->open_amount;
+
+ # account where money is paid to/from: bank account or cash
+ my $account_bank = SL::DB::Manager::Chart->find_by(id => $params{chart_id});
+ croak "can't find bank account" unless ref $account_bank;
+
+ my $reference_account = $self->reference_account;
+ croak "can't find reference account (link = AR/AP) for invoice" unless ref $reference_account;
+
+ my $memo = $params{'memo'} || '';
+ my $source = $params{'source'} || '';
+
+ my $rounded_params_amount = _round( $params{amount} );
+
+ my $db = $self->db;
+ $db->do_transaction(sub {
+ my $new_acc_trans;
+
+ # all three payment type create 1 AR/AP booking (the paid part)
+ # difference_as_skonto creates n skonto bookings (1 for each tax type)
+ # with_skonto_pt creates 1 bank booking and n skonto bookings (1 for each tax type)
+ # without_skonto creates 1 bank booking
+
+ # as long as there is no automatic tax, payments are always booked with
+ # taxkey 0
+
+ unless ( $params{payment_type} eq 'difference_as_skonto' ) {
+ # cases with_skonto_pt and without_skonto
+
+ # for case with_skonto_pt we need to know the corrected amount at this
+ # stage if we are going to use $params{amount}
+
+ my $pay_amount = $rounded_params_amount;
+ $pay_amount = $self->amount_less_skonto if $params{payment_type} eq 'with_skonto_pt';
+
+ # bank account and AR/AP
+ $paid_amount += $pay_amount;
+
+ # total amount against bank, do we already know this by now?
+ $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $self->id,
+ chart_id => $account_bank->id,
+ chart_link => $account_bank->link,
+ amount => (-1 * $pay_amount) * $mult,
+ transdate => $transdate_obj,
+ source => $source,
+ memo => $memo,
+ taxkey => 0,
+ tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+ $new_acc_trans->save;
+ };
+
+ if ( $params{payment_type} eq 'difference_as_skonto' or $params{payment_type} eq 'with_skonto_pt' ) {
+
+ my $total_skonto_amount;
+ if ( $params{payment_type} eq 'with_skonto_pt' ) {
+ $total_skonto_amount = $self->skonto_amount;
+ } elsif ( $params{payment_type} eq 'difference_as_skonto' ) {
+ $total_skonto_amount = $self->open_amount;
+ };
+
+ my @skonto_bookings = $self->skonto_charts($total_skonto_amount);
+
+ # error checking:
+ if ( $params{payment_type} eq 'difference_as_skonto' ) {
+ my $calculated_skonto_sum = sum map { $_->{skonto_amount} } @skonto_bookings;
+ croak "calculated skonto for difference_as_skonto = $calculated_skonto_sum doesn't add up open amount: " . $self->open_amount unless _round($calculated_skonto_sum) == _round($self->open_amount);
+ };
+
+ my $reference_amount = $total_skonto_amount;
+
+ # create an acc_trans entry for each result of $self->skonto_charts
+ foreach my $skonto_booking ( @skonto_bookings ) {
+ next unless $skonto_booking->{'chart_id'};
+ next unless $skonto_booking->{'skonto_amount'} != 0;
+ my $amount = -1 * $skonto_booking->{skonto_amount};
+ $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $self->id,
+ chart_id => $skonto_booking->{'chart_id'},
+ chart_link => SL::DB::Manager::Chart->find_by(id => $skonto_booking->{'chart_id'})->{'link'},
+ amount => $amount * $mult,
+ transdate => $transdate_obj,
+ source => $params{source},
+ taxkey => 0,
+ tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+ $new_acc_trans->save;
+
+ $reference_amount -= abs($amount);
+ $paid_amount += -1 * $amount;
+ $skonto_amount_check -= $skonto_booking->{'skonto_amount'};
+ };
+ die "difference_as_skonto calculated incorrectly, sum of calculated payments doesn't add up to open amount $total_open_amount, reference_amount = $reference_amount\n" unless _round($reference_amount) == 0;
+
+ };
+
+ my $arap_amount = 0;
+
+ if ( $params{payment_type} eq 'difference_as_skonto' ) {
+ $arap_amount = $total_open_amount;
+ } elsif ( $params{payment_type} eq 'without_skonto' ) {
+ $arap_amount = $rounded_params_amount;
+ } elsif ( $params{payment_type} eq 'with_skonto_pt' ) {
+ # this should be amount + sum(amount+skonto), but while we only allow
+ # with_skonto_pt for completely unpaid invoices we just use the value
+ # from the invoice
+ $arap_amount = $total_open_amount;
+ };
+
+ # regardless of payment_type there is always only exactly one arap booking
+ # TODO: compare $arap_amount to running total
+ my $arap_booking= SL::DB::AccTransaction->new(trans_id => $self->id,
+ chart_id => $reference_account->id,
+ chart_link => $reference_account->link,
+ amount => $arap_amount * $mult,
+ transdate => $transdate_obj,
+ source => '', #$params{source},
+ taxkey => 0,
+ tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+ $arap_booking->save;
+
+ $self->paid($self->paid+$paid_amount) if $paid_amount;
+ $self->datepaid($transdate_obj);
+ $self->save;
+
+ my $datev_check = 0;
+ if ( $is_sales ) {
+ if ( ( $self->invoice && $::instance_conf->get_datev_check_on_sales_invoice ) ||
+ ( !$self->invoice && $::instance_conf->get_datev_check_on_ar_transaction )) {
+ $datev_check = 1;
+ };
+ } else {
+ if ( ( $self->invoice && $::instance_conf->get_datev_check_on_purchase_invoice ) ||
+ ( !$self->invoice && $::instance_conf->get_datev_check_on_ap_transaction )) {
+ $datev_check = 1;
+ };
+ };
+
+ if ( $datev_check ) {
+
+ my $datev = SL::DATEV->new(
+ exporttype => DATEV_ET_BUCHUNGEN,
+ format => DATEV_FORMAT_KNE,
+ dbh => $db->dbh,
+ trans_id => $self->{id},
+ );
+
+ $datev->clean_temporary_directories;
+ $datev->export;
+
+ if ($datev->errors) {
+ # this exception should be caught by do_transaction, which handles the rollback
+ die join "\n", $::locale->text('DATEV check returned errors:'), $datev->errors;
+ }
+ };
+
+ }) || die t8('error while paying invoice #1 : ', $self->invnumber) . $db->error . "\n";
+
+ return 1;
+};
+
+sub skonto_date {
+
+ my $self = shift;
+
+ my $is_sales = ref($self) eq 'SL::DB::Invoice';
+
+ my $skonto_date;
+
+ if ( $is_sales ) {
+ return undef unless ref $self->payment_terms;
+ return undef unless $self->payment_terms->terms_skonto > 0;
+ $skonto_date = DateTime->from_object(object => $self->transdate)->add(days => $self->payment_terms->terms_skonto);
+ } else {
+ return undef unless ref $self->vendor->payment_terms;
+ return undef unless $self->vendor->payment_terms->terms_skonto > 0;
+ $skonto_date = DateTime->from_object(object => $self->transdate)->add(days => $self->vendor->payment_terms->terms_skonto);
+ };
+
+ return $skonto_date;
+};
+
+sub reference_account {
+ my $self = shift;
+
+ my $is_sales = ref($self) eq 'SL::DB::Invoice';
+
+ require SL::DB::Manager::AccTransaction;
+
+ my $link_filter = $is_sales ? 'AR' : 'AP';
+
+ my $acc_trans = SL::DB::Manager::AccTransaction->find_by(
+ trans_id => $self->id,
+ SL::DB::Manager::AccTransaction->chart_link_filter("$link_filter")
+ );
+
+ return undef unless ref $acc_trans;
+
+ my $reference_account = SL::DB::Manager::Chart->find_by(id => $acc_trans->chart_id);
+
+ return $reference_account;
+};
+
+sub reference_amount {
+ my $self = shift;
+
+ my $is_sales = ref($self) eq 'SL::DB::Invoice';
+
+ require SL::DB::Manager::AccTransaction;
+
+ my $link_filter = $is_sales ? 'AR' : 'AP';
+
+ my $acc_trans = SL::DB::Manager::AccTransaction->find_by(
+ trans_id => $self->id,
+ SL::DB::Manager::AccTransaction->chart_link_filter("$link_filter")
+ );
+
+ return undef unless ref $acc_trans;
+
+ # this should be the same as $self->amount
+ return $acc_trans->amount;
+};
+
+
+sub open_amount {
+ my $self = shift;
+
+ # in the future maybe calculate this from acc_trans
+
+ # if the difference is 0.01 Cent this may end up as 0.009999999999998
+ # numerically, so round this value when checking for cent threshold >= 0.01
+
+ return $self->amount - $self->paid;
+};
+
+sub open_percent {
+ my $self = shift;
+
+ return 0 if $self->amount == 0;
+ my $open_percent;
+ if ( $self->open_amount < 0 ) {
+ # overpaid, currently treated identically
+ $open_percent = $self->open_amount * 100 / $self->amount;
+ } else {
+ $open_percent = $self->open_amount * 100 / $self->amount;
+ };
+
+ return _round($open_percent) || 0;
+};
+
+sub skonto_amount {
+ my $self = shift;
+
+ return $self->amount - $self->amount_less_skonto;
+};
+
+sub remaining_skonto_days {
+ my $self = shift;
+
+ return undef unless ref $self->skonto_date;
+
+ my $dur = DateTime::Duration->new($self->skonto_date - DateTime->today);
+ return $dur->delta_days();
+
+};
+
+sub percent_skonto {
+ my $self = shift;
+
+ my $is_sales = ref($self) eq 'SL::DB::Invoice';
+
+ my $percent_skonto = 0;
+
+ if ( $is_sales ) {
+ return undef unless ref $self->payment_terms;
+ return undef unless $self->payment_terms->percent_skonto > 0;
+ $percent_skonto = $self->payment_terms->percent_skonto;
+ } else {
+ return undef unless ref $self->vendor->payment_terms;
+ return undef unless $self->vendor->payment_terms->terms_skonto > 0;
+ $percent_skonto = $self->vendor->payment_terms->percent_skonto;
+ };
+
+ return $percent_skonto;
+};
+
+sub amount_less_skonto {
+ # amount that has to be paid if skonto applies, always return positive rounded values
+ # the result is rounded so we can directly compare it with the user input
+ my $self = shift;
+
+ my $is_sales = ref($self) eq 'SL::DB::Invoice';
+
+ my $percent_skonto = $self->percent_skonto;
+
+ return _round($self->amount - ( $self->amount * $percent_skonto) );
+
+};
+
+sub check_skonto_configuration {
+ my $self = shift;
+
+ my $is_sales = ref($self) eq 'SL::DB::Invoice';
+
+ my $skonto_configured = 1; # default is assume skonto works
+
+ my $transactions = $self->transactions;
+ foreach my $transaction (@{ $transactions }) {
+ # find all transactions with an AR_amount or AP_amount link
+ my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->{taxkey}]);
+ croak "no tax for taxkey " . $transaction->{taxkey} unless ref $tax;
+
+ $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->{chart_link}) };
+ if ( $is_sales && $transaction->{chartlinks}->{AR_amount} ) {
+ $skonto_configured = 0 unless $tax->skonto_sales_chart_id;
+ } elsif ( !$is_sales && $transaction->{chartlinks}->{AP_amount}) {
+ $skonto_configured = 0 unless $tax->skonto_purchase_chart_id;
+ };
+ };
+
+ return $skonto_configured;
+};
+
+sub open_sepa_transfer_amount {
+ my $self = shift;
+
+ my ($vc, $key, $type);
+ if ( ref($self) eq 'SL::DB::Invoice' ) {
+ $vc = 'customer';
+ $key = 'ap_id';
+ $type = 'ar';
+ } else {
+ $vc = 'vendor';
+ $key = 'ap_id';
+ $type = 'ap';
+ };
+
+ my $sql = qq|SELECT SUM(sei.amount) AS amount FROM sepa_export_items sei | .
+ qq| LEFT JOIN sepa_export se ON (sei.sepa_export_id = se.id) | .
+ qq| WHERE $key = ? AND NOT se.closed AND (se.vc = '$vc') |;
+
+ my ($open_sepa_amount) = $self->db->dbh->selectrow_array($sql, undef, $self->id);
+
+ return $open_sepa_amount || 0;
+
+};
+
+
+sub skonto_charts {
+ my $self = shift;
+
+ # TODO: use param for amount, may also want to calculate skonto_amounts by
+ # passing percentage in the future
+
+ my $amount = shift || $self->skonto_amount;
+
+ croak "no amount passed to skonto_charts" unless abs(_round($amount)) >= 0.01;
+
+ # TODO: check whether there are negative values in invoice / acc_trans ... credited items
+
+ # don't check whether skonto applies, because user may want to override this
+ # return undef unless $self->percent_skonto; # for is_sales
+ # return undef unless $self->vendor->payment_terms->percent_skonto; # for purchase
+
+ my $is_sales = ref($self) eq 'SL::DB::Invoice';
+
+ my $mult = $is_sales ? 1 : -1; # multiplier for getting the right sign
+
+ my @skonto_charts; # resulting array with all income/expense accounts that have to be corrected
+
+ # calculate effective skonto (percentage) in difference_as_skonto mode
+ # only works if there are no negative acc_trans values
+ my $effective_skonto_rate = $amount ? $amount / $self->amount : 0;
+
+ # checks:
+ my $total_skonto_amount = 0;
+ my $total_rounding_error = 0;
+
+ my $reference_ARAP_amount = 0;
+
+ my $transactions = $self->transactions;
+ foreach my $transaction (@{ $transactions }) {
+ # find all transactions with an AR_amount or AP_amount link
+ $transaction->{chartlinks} = { map { $_ => 1 } split(m/:/, $transaction->{chart_link}) };
+ # second condition is that we can determine an automatic Skonto account for each AR_amount entry
+
+ if ( ( $is_sales && $transaction->{chartlinks}->{AR_amount} ) or ( !$is_sales && $transaction->{chartlinks}->{AP_amount}) ) {
+ # $reference_ARAP_amount += $transaction->{amount} * $mult;
+
+ # quick hack that works around problem of non-unique tax keys in SKR04
+ my $tax = SL::DB::Manager::Tax->get_first( where => [taxkey => $transaction->{taxkey}]);
+ croak "no tax for taxkey " . $transaction->{taxkey} unless ref $tax;
+
+ if ( $is_sales ) {
+ die t8('no skonto_chart configured for taxkey #1 : #2 : #3', $transaction->{taxkey} , $tax->taxdescription , $tax->rate*100) unless ref $tax->skonto_sales_chart;
+ } else {
+ die t8('no skonto_chart configured for taxkey #1 : #2 : #3', $transaction->{taxkey} , $tax->taxdescription , $tax->rate*100) unless ref $tax->skonto_purchase_chart;
+ };
+
+ my $skonto_amount_unrounded;
+
+ my $skonto_percent_abs = $self->amount ? abs($transaction->amount * (1 + $tax->rate) * 100 / $self->amount) : 0;
+
+ my $transaction_amount = abs($transaction->{amount} * (1 + $tax->rate));
+ my $transaction_skonto_percent = abs($transaction_amount/$self->amount); # abs($transaction->{amount} * (1 + $tax->rate));
+
+
+ $skonto_amount_unrounded = abs($amount * $transaction_skonto_percent);
+ my $skonto_amount_rounded = _round($skonto_amount_unrounded);
+ my $rounding_error = $skonto_amount_unrounded - $skonto_amount_rounded;
+ my $rounded_rounding_error = _round($rounding_error);
+
+ $total_rounding_error += $rounding_error;
+ $total_skonto_amount += $skonto_amount_rounded;
+
+ my $rec = {
+ # skonto_percent_abs: relative part of amount + tax to the total invoice amount
+ 'skonto_percent_abs' => $skonto_percent_abs,
+ 'chart_id' => $is_sales ? $tax->skonto_sales_chart->id : $tax->skonto_purchase_chart->id,
+ 'skonto_amount' => $skonto_amount_rounded,
+ # 'rounding_error' => $rounding_error,
+ # 'rounded_rounding_error' => $rounded_rounding_error,
+ };
+
+ push @skonto_charts, $rec;
+ };
+ };
+
+ # if the rounded sum of all rounding_errors reaches 0.01 this sum is
+ # subtracted from the largest skonto_amount
+ my $rounded_total_rounding_error = abs(_round($total_rounding_error));
+
+ if ( $rounded_total_rounding_error > 0 ) {
+ my $highest_amount_pos = 0;
+ my $highest_amount = 0;
+ my $i = -1;
+ foreach my $ref ( @skonto_charts ) {
+ $i++;
+ if ( $ref->{skonto_amount} > $highest_amount ) {
+ $highest_amount = $ref->{skonto_amount};
+ $highest_amount_pos = $i;
+ };
+ };
+ $skonto_charts[$i]->{skonto_amount} -= $rounded_total_rounding_error;
+ };
+
+ return @skonto_charts;
+};
+
+
+sub within_skonto_period {
+ my $self = shift;
+ my $dateref = shift || DateTime->now->truncate( to => 'day' );
+
+ return undef unless ref $dateref eq 'DateTime';
+ return 0 unless $self->skonto_date;
+
+ # return 1 if requested date (or today) is inside skonto period
+ # this will also return 1 if date is before the invoice date
+ return $dateref <= $self->skonto_date;
+};
+
+sub valid_skonto_amount {
+ my $self = shift;
+ my $amount = shift || 0;
+ my $max_skonto_percent = 0.10;
+
+ return 0 unless $amount > 0;
+
+ # does this work for other currencies?
+ return ($self->amount*$max_skonto_percent) > $amount;
+};
+
+sub get_payment_select_options_for_bank_transaction {
+ my ($self, $bt_id, %params) = @_;
+
+ my $bt = SL::DB::Manager::BankTransaction->find_by( id => $bt_id );
+ die unless $bt;
+
+ my $open_amount = $self->open_amount;
+
+ my @options;
+ if ( $open_amount && # invoice amount not 0
+ abs(abs($self->amount_less_skonto) - abs($bt->amount)) < 0.01 &&
+ $self->check_skonto_configuration) {
+ if ( $self->within_skonto_period($bt->transdate) ) {
+ push(@options, { payment_type => 'without_skonto', display => t8('without skonto') });
+ push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt'), selected => 1 });
+ } else {
+ push(@options, { payment_type => 'without_skonto', display => t8('without skonto') }, selected => 1 );
+ push(@options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt')});
+ };
+ };
+
+ return @options;
+
+};
+
+
+sub get_payment_suggestions {
+
+ my ($self, %params) = @_;
+
+ my $open_amount = $self->open_amount;
+ $open_amount -= $self->open_sepa_transfer_amount if $params{sepa};
+
+ $self->{invoice_amount_suggestion} = $open_amount;
+ undef $self->{payment_select_options};
+ push(@{$self->{payment_select_options}} , { payment_type => 'without_skonto', display => t8('without skonto') });
+ if ( $self->within_skonto_period ) {
+ # If there have been no payments yet suggest amount_less_skonto, otherwise the open amount
+ if ( $open_amount && # invoice amount not 0
+ $open_amount == $self->amount && # no payments yet, or sum of payments and sepa export amounts is zero
+ $self->check_skonto_configuration) {
+ $self->{invoice_amount_suggestion} = $self->amount_less_skonto;
+ push(@{$self->{payment_select_options}} , { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt') , selected => 1 });
+ } else {
+ if ( ( $self->valid_skonto_amount($self->open_amount) || $self->valid_skonto_amount($open_amount) ) and not $params{sepa} ) {
+ $self->{invoice_amount_suggestion} = $open_amount;
+ # only suggest difference_as_skonto if open_amount exactly matches skonto_amount
+ # AND we aren't in SEPA mode
+ my $selected = 0;
+ $selected = 1 if _round($open_amount) == _round($self->skonto_amount);
+ push(@{$self->{payment_select_options}} , { payment_type => 'difference_as_skonto', display => t8('difference as skonto') , selected => $selected });
+ };
+ };
+ } else {
+ # invoice was configured with skonto, but skonto date has passed, or no skonto available
+ $self->{invoice_amount_suggestion} = $open_amount;
+ # difference_as_skonto doesn't make any sense for SEPA transfer, as this doesn't cause any actual payment
+ if ( $self->valid_skonto_amount($self->open_amount) && not $params{sepa} ) {
+ push(@{$self->{payment_select_options}} , { payment_type => 'difference_as_skonto', display => t8('difference as skonto') , selected => 0 });
+ };
+ };
+ return 1;
+};
+
+sub transactions {
+ my ($self) = @_;
+
+ return unless $self->id;
+
+ require SL::DB::AccTransaction;
+ SL::DB::Manager::AccTransaction->get_all(query => [ trans_id => $self->id ]);
+}
+
+sub validate_payment_type {
+ my $payment_type = shift;
+
+ my %allowed_payment_types = map { $_ => 1 } qw(without_skonto with_skonto_pt difference_as_skonto);
+ croak "illegal payment type: $payment_type, must be one of: " . join(' ', keys %allowed_payment_types) unless $allowed_payment_types{ $payment_type };
+
+ return 1;
+}
+
+sub _round {
+ my $value = shift;
+ my $num_dec = 2;
+ return $::form->round_amount($value, 2);
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+SL::DB::Helper::Payment Mixin providing helper methods for paying C<Invoice>
+ and C<PurchaseInvoice> objects and using skonto
+
+=head1 SYNOPSIS
+
+In addition to actually causing a payment via pay_invoice this helper contains
+many methods that help in determining information about the status of the
+invoice, such as the remaining open amount, whether skonto applies, until which
+date skonto applies, the skonto amount and relative percentages, what to do
+with skonto, ...
+
+To prevent duplicate code this was all added in this mixin rather than directly
+in SL::DB::Invoice and SL::DB::PurchaseInvoice.
+
+=over 4
+
+=item C<pay_invoice %params>
+
+Create a payment booking for an existing invoice object (type ar/ap/is/ir) via
+a configured bank account.
+
+This function deals with all the acc_trans entries and also updates paid and datepaid.
+
+Example:
+
+ my $ap = SL::DB::Manager::PurchaseInvoice->find_by( invnumber => '1');
+ my $bank = SL::DB::Manager::BankAccount->find_by( name => 'Bank');
+ $ap->pay_invoice(chart_id => $bank->chart_id,
+ amount => $ap->open_amount,
+ transdate => DateTime->now->to_kivitendo,
+ memo => 'foobar;
+ source => 'barfoo;
+ payment_type => 'without_skonto', # default if not specified
+ );
+
+or with skonto:
+ $ap->pay_invoice(chart_id => $bank->chart_id,
+ amount => $ap->amount, # doesn't need to be specified
+ transdate => DateTime->now->to_kivitendo,
+ memo => 'foobar;
+ source => 'barfoo;
+ payment_type => 'with_skonto',
+ );
+
+Allowed payment types are:
+ without_skonto with_skonto_pt difference_as_skonto
+
+The option C<payment_type> allows for a basic skonto mechanism.
+
+C<without_skonto> is the default mode, "amount" is paid to the account in
+chart_id. This can also be used for partial payments and corrections via
+negative amounts.
+
+C<with_skonto_pt> can't be used for partial payments. When used on unpaid
+invoices the whole amount is paid, with the skonto part automatically being
+booked according to the skonto chart configured in the tax settings for each
+tax key. If an amount is passed it is ignored and the actual configured skonto
+amount is used.
+
+C<difference_as_skonto> can only be used after partial payments have been made,
+the whole specified amount is booked according to the skonto charts configured
+in the tax settings for each tax key.
+
+So passing amount doesn't have any effect for the cases C<with_skonto_pt> and
+C<difference_as_skonto>, as all necessary values are taken from the stored
+invoice.
+
+The skonto modes automatically calculate the relative amounts for a mix of
+taxes, e.g. items with 7% and 19% in one invoice. There is a helper method
+skonto_charts, which calculates the relative percentages according to the
+amounts in acc_trans (which are grouped by tax).
+
+There is currently no way of excluding certain items in an invoice from having
+skonto applied to them. If this feature was added to parts the calculation
+method of relative skonto would have to be completely rewritten using the
+invoice items rather than acc_trans.
+
+The skonto modes also still don't automatically correct the tax, this still has
+to be done manually. Therefore all payments generated by pay_invoice have
+taxkey 0.
+
+There is currently no way to directly pay an invoice via this method if the
+effective skonto differs from the skonto according to the payment terms
+configured for the invoice/vendor.
+
+In this case one has to pay in two steps: first the actual paid amount via
+"without skonto", and then the remainder via "difference_as_skonto". The user
+has to there actively decide whether to accept the differing skonto.
+
+Because of the way skonto_charts works the calculation doesn't work if there
+are negative values in acc_trans. E.g. one invoice with a positive value for
+19% tax and a negative value for the acc_trans line with 7%
+
+Skonto doesn't/shouldn't apply if the invoice contains credited items.
+
+=item C<reference_account>
+
+Returns a chart object which is the chart of the invoice with link AR or AP.
+
+Example (1200 is the AR account for SKR04):
+ my $invoice = invoice(invnumber => '144');
+ $invoice->reference_account->accno
+ # 1200
+
+=item C<percent_skonto>
+
+Returns the configured skonto percentage of the payment terms of an invoice,
+e.g. 0.02 for 2%. Payment terms come from invoice settings for ar, from vendor
+settings for ap.
+
+=item C<amount_less_skonto>
+
+If the invoice has a payment term (via ar for sales, via vendor for purchase),
+calculate the amount to be paid in the case of skonto. This doesn't check,
+whether skonto applies (i.e. skonto doesn't wasn't exceeded), it just subtracts
+the configured percentage (e.g. 2%) from the total amount.
+
+The returned value is rounded to two decimals.
+
+=item C<skonto_date>
+
+The date up to which skonto may be taken. This is calculated from the invoice
+date + the number of days configured in the payment terms.
+
+This method can also be used to determine whether skonto applies for the
+invoice, as it returns undef if there is no payment term or skonto days is set
+to 0.
+
+=item C<within_skonto_period [DATE]>
+
+Returns 0 or 1.
+
+Checks whether the invoice has payment terms configured, and whether the date
+is within the skonto max date. If no date is passed the current date is used.
+
+You can also pass a dateref object as a parameter to check whether skonto
+applies for that date rather than the current date.
+
+=item C<valid_skonto_amount>
+
+Takes an amount as an argument and checks whether the amount is less than 10%
+of the total amount of the invoice. The value of 10% is currently hardcoded in
+the method. This method is currently used to check whether to offer the payment
+option "difference as skonto".
+
+Example:
+ if ( $invoice->valid_skonto_amount($invoice->open_amount) ) {
+ # ... do something
+ }
+
+=item C<skonto_charts [$amount]>
+
+Returns a list of chart_ids and some calculated numbers that can be used for
+paying the invoice with skonto. This function will automatically calculate the
+relative skonto amounts even if the invoice contains several types of taxes
+(e.g. 7% and 19%).
+
+Example usage:
+ my $invoice = SL::DB::Manager::Invoice->find_by(invnumber => '211');
+ my @skonto_charts = $invoice->skonto_charts;
+
+or with the total skonto amount as an argument:
+ my @skonto_charts = $invoice->skonto_charts($invoice->open_amount);
+
+The following values are generated for each chart:
+
+=over 2
+
+=item C<chart_id>
+
+The chart id of the skonto amount to be booked.
+
+=item C<skonto_amount>
+
+The total amount to be paid to the account
+
+=item C<skonto_percent>
+
+The relative percentage of that skonto chart. This can be useful if the actual
+ekonto that is paid deviates from the granted skonto, e.g. customer effectively
+pays 2.6% skonto instead of 2%, and we accept this. Then we can still calculate
+the relative skonto amounts for different taxes based on the absolute
+percentages. Used for case C<difference_as_skonto>.
+
+=item C<skonto_percent_abs>
+
+The absolute percentage of that skonto chart in relation to the total amount.
+Used to calculate skonto_amount for case C<with_skonto_pt>.
+
+=back
+
+If the invoice contains several types of taxes then skonto_charts can be used
+to calculate the relative amounts.
+
+Example in console of an invoice with 100 Euro at 7% and 100 Euro at 19% with
+tax not included:
+
+ my $invoice = invoice(invnumber => '144');
+ $invoice->amount
+ 226.00000
+ $invoice->payment_terms->percent_skonto
+ 0.02
+ $invoice->skonto_charts
+ pp $invoice->skonto_charts
+ # $VAR1 = {
+ # 'chart_id' => 128,
+ # 'skonto_amount' => '2.14',
+ # 'skonto_percent' => '47.3451327433627'
+ # };
+ # $VAR2 = {
+ # 'chart_id' => 130,
+ # 'skonto_amount' => '2.38',
+ # 'skonto_percent' => '52.654867256637'
+ # };
+
+C<skonto_charts> always returns positive values (abs) for C<skonto_amount> and
+C<skonto_percent>.
+
+C<skonto_charts> generates one entry for each acc_trans entry. ar and ap
+bookings only have one acc_trans entry for each taxkey (e.g. 7% and 19%). This
+is because all the items are grouped according to the Buchungsgruppen mechanism
+and the totals are written to acc_trans. For is and ir it is possible to have
+several acc_trans entries with the same tax. In this case skonto_charts
+generates a skonto booking for each acc_trans income/expense entry.
+
+In the future this function may also be used to calculate the corrections for
+the income tax.
+
+=item C<open_amount>
+
+Unrounded total open amount of invoice (amount - paid).
+Doesn't take into account pending SEPA transfers.
+
+=item C<open_percent>
+
+Percentage of the invoice that is still unpaid, e.g. 100,00 if no payments have
+been made yet, 0,00 if fully paid.
+
+=item C<remaining_skonto_days>
+
+How many days skonto can still be taken, calculated from current day. Returns 0
+if current day is the max skonto date, and negative number if skonto date has
+already passed.
+
+Returns undef if skonto is not configured for that invoice.
+
+=item C<get_payment_suggestions %params>
+
+Creates data intended for an L.select_tag dropdown that can be used in a
+template. Depending on the rules it will choose from the options
+without_skonto, with_skonto_pt and difference_as_skonto, and select the most
+likely one.
+
+If the parameter "sepa" is passed, the SEPA export payments that haven't been
+executed yet are considered when determining the open amount of the invoice.
+
+The current rules are:
+
+=over 2
+
+=item * without_skonto is always an option
+
+=item * with_skonto_pt is only offered if there haven't been any payments yet and the current date is within the skonto period.
+
+=item * difference_as_skonto is only offered if there have already been payments made and the open amount is smaller than 10% of the total amount.
+
+with_skonto_pt will only be offered, if all the AR_amount/AP_amount have a
+taxkey with a configured skonto chart
+
+=back
+
+It will also fill $self->{invoice_amount_suggestion} with either the open
+amount, or if with_skonto_pt is selected, with amount_less_skonto, so the
+template can fill the input with the likely amount.
+
+Example in console:
+ my $ar = invoice( invnumber => '257');
+ $ar->get_payment_suggestions;
+ print $ar->{invoice_amount_suggestion} . "\n";
+ # 97.23
+ pp $ar->{payment_select_options}
+ # $VAR1 = [
+ # {
+ # 'display' => 'ohne Skonto',
+ # 'payment_type' => 'without_skonto'
+ # },
+ # {
+ # 'display' => 'mit Skonto nach ZB',
+ # 'payment_type' => 'with_skonto_pt',
+ # 'selected' => 1
+ # }
+ # ];
+
+The resulting array $ar->{payment_select_options} can be used in a template
+select_tag using value_key and title_key:
+
+[% L.select_tag('payment_type_' _ loop.count, invoice.payment_select_options, value_key => 'payment_type', title_key => 'display', id => 'payment_type_' _ loop.count) %]
+
+It would probably make sense to have different rules for the pre-selected items
+for sales and purchase, and to also make these rules configurable in the
+defaults. E.g. when creating a SEPA bank transfer for vendor invoices a company
+might always want to pay quickly making use of skonto, while another company
+might always want to pay as late as possible.
+
+=item C<transactions>
+
+Returns all acc_trans Objects of an ar/ap object.
+
+Example in console to print account numbers and booked amounts of an invoice:
+ my $invoice = invoice(invnumber => '144');
+ foreach my $acc_trans ( @{ $invoice->transactions } ) {
+ print $acc_trans->chart->accno . " : " . $acc_trans->amount_as_number . "\n"
+ };
+ # 1200 : 226,00000
+ # 1800 : -226,00000
+ # 4300 : 100,00000
+ # 3801 : 7,00000
+ # 3806 : 19,00000
+ # 4400 : 100,00000
+ # 1200 : -226,00000
+
+=item C<get_payment_select_options_for_bank_transaction $banktransaction_id %params>
+
+Make suggestion for a skonto payment type by returning an HTML blob of the options
+of a HTML drop-down select with the most likely option preselected.
+
+This is a helper function for BankTransaction/ajax_payment_suggestion.
+
+We are working with an existing payment, so difference_as_skonto never makes sense.
+
+If skonto is possible (skonto_date exists), add two possibilities:
+without_skonto and with_skonto_pt if payment date is within skonto_date,
+preselect with_skonto_pt, otherwise preselect without skonto.
+
+=back
+
+=head1 TODO AND CAVEATS
+
+=over 4
+
+=item *
+
+when looking at open amount, maybe consider that there may already be queued
+amounts in SEPA Export
+
+=item *
+
+Can only handle default currency.
+
+=back
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitendo-premium.de<gt>
+
+=cut
use List::Util qw(first);
use Rose::DB::Object::Helpers ();
-
use SL::DB::MetaSetup::Invoice;
use SL::DB::Manager::Invoice;
+use SL::DB::Helper::Payment qw(:ALL);
use SL::DB::Helper::AttrHTML;
use SL::DB::Helper::FlattenToForm;
use SL::DB::Helper::LinkedRecords;
use SL::DB::Helper::TransNumberGenerator;
use SL::Locale::String qw(t8);
use SL::DB::CustomVariable;
-use SL::DB::AccTransaction;
__PACKAGE__->meta->add_relationship(
invoiceitems => {
goto &customer;
}
-sub pay_invoice {
- my ($self, %params) = @_;
-
- #Mark invoice as paid
- $self->paid($self->paid+$params{amount});
- $self->save;
-
- Common::check_params(\%params, qw(chart_id trans_id amount transdate));
-
- #account of bank account or cash
- my $account_bank = SL::DB::Manager::Chart->find_by(id => $params{chart_id});
-
- #Search the contra account
- my $acc_trans = SL::DB::Manager::AccTransaction->find_by(trans_id => $params{trans_id},
- or => [ chart_link => { like => "%:AR" },
- chart_link => { like => "AR:%" },
- chart_link => "AR" ]);
- my $contra_account = SL::DB::Manager::Chart->find_by(id => $acc_trans->chart_id);
-
- #Two new transfers in acc_trans (for bank account and for contra account)
- my $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $params{trans_id},
- chart_id => $account_bank->id,
- chart_link => $account_bank->link,
- amount => (-1 * $params{amount}),
- transdate => $params{transdate},
- source => $params{source},
- memo => '',
- taxkey => 0,
- tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
- $new_acc_trans->save;
- $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $params{trans_id},
- chart_id => $contra_account->id,
- chart_link => $contra_account->link,
- amount => $params{amount},
- transdate => $params{transdate},
- source => $params{source},
- memo => '',
- taxkey => 0,
- tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
- $new_acc_trans->save;
-}
-
sub link {
my ($self) = @_;
return $html;
}
->>>>>>> Test: Bank-Commit zusammengefasst
1;
__END__
--- /dev/null
+package SL::DB::Manager::AccTransaction;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Sorted;
+use SL::DBUtils;
+
+sub object_class { 'SL::DB::AccTransaction' }
+
+__PACKAGE__->make_manager_methods;
+
+sub chart_link_filter {
+ my ($class, $link) = @_;
+
+ return (or => [ chart_link => $link,
+ chart_link => { like => "${link}:\%" },
+ chart_link => { like => "\%:${link}" },
+ chart_link => { like => "\%:${link}:\%" } ]);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Manager::AccTransaction - Manager class for the model for the C<acc_trans> table
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<chart_link_filter $link>
+
+Returns a query builder filter that matches acc_trans lines whose 'C<chart_link>'
+field contains C<$chart_link>. Matching is done so that the exact value of
+C<$chart_link> matches but not if C<$chart_link> is only a substring of a
+match. Therefore C<$chart_link = 'AR'> will match the column content 'C<AR>'
+or 'C<AR_paid:AR>' but not 'C<AR_amount>'.
+
+The code and functionality was copied from the function link_filter in
+SL::DB::Manager::Chart.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitendo-premium.de<gt>
+
+=cut
--- /dev/null
+package SL::DB::Manager::BankAccount;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::BankAccount' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+ return ( default => [ 'sortkey', 1 ],
+ columns => { SIMPLE => 'ALL' } );
+}
+
+sub get_default {
+ return $_[0]->get_first(where => [ obsolete => 0 ], sort_by => 'sortkey');
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Manager::BankAccount - RDBO manager for the C<bank_accounts> table
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<get_default>
+
+Returns an RDBO instance corresponding to the default bank account. The default
+bank account is defined as the bank account with the highest sort order (usually 1) that
+is not set to obsolete.
+
+Example:
+
+ my $default_bank_account_id = SL::DB::Manager::BankAccount->get_default->id;
+
+=back
+
+=cut
iban => { type => 'varchar', length => 100 },
id => { type => 'integer', not_null => 1, sequence => 'id' },
name => { type => 'text' },
- obsolete => { type => 'boolean' },
+ obsolete => { type => 'boolean', default => 'false', not_null => 1 },
reconciliation_starting_balance => { type => 'numeric', precision => 15, scale => 5 },
reconciliation_starting_date => { type => 'date' },
sortkey => { type => 'integer', not_null => 1 },
__PACKAGE__->meta->table('bank_transactions');
__PACKAGE__->meta->columns(
- amount => { type => 'numeric', not_null => 1, precision => 5, scale => 15 },
+ amount => { type => 'numeric', not_null => 1, precision => 15, scale => 5 },
cleared => { type => 'boolean', default => 'false', not_null => 1 },
- currency_id => { type => 'integer' },
+ currency_id => { type => 'integer', not_null => 1 },
id => { type => 'serial', not_null => 1 },
- invoice_amount => { type => 'numeric', default => '0', precision => 5, scale => 15 },
+ invoice_amount => { type => 'numeric', default => '0', precision => 15, scale => 5 },
+ itime => { type => 'timestamp', default => 'now()' },
local_bank_account_id => { type => 'integer', not_null => 1 },
purpose => { type => 'text' },
remote_account_number => { type => 'text' },
remote_bank_code => { type => 'text' },
remote_name => { type => 'text' },
- remote_name_1 => { type => 'text' },
transaction_id => { type => 'integer' },
transdate => { type => 'date', not_null => 1 },
valutadate => { type => 'date', not_null => 1 },
__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+__PACKAGE__->meta->allow_inline_column_values(1);
+
__PACKAGE__->meta->foreign_keys(
currency => {
class => 'SL::DB::Currency',
__PACKAGE__->meta->primary_key_columns([ 'id' ]);
__PACKAGE__->meta->foreign_keys(
- acc_tran => {
+ acc_trans => {
class => 'SL::DB::AccTransaction',
key_columns => { acc_trans_id => 'acc_trans_id' },
},
our_bic => { type => 'varchar', length => 100 },
our_depositor => { type => 'text' },
our_iban => { type => 'varchar', length => 100 },
+ payment_type => { type => 'text', default => 'without_skonto' },
reference => { type => 'varchar', length => 35 },
requested_execution_date => { type => 'date' },
sepa_export_id => { type => 'integer', not_null => 1 },
+ skonto_amount => { type => 'numeric', precision => 25, scale => 5 },
vc_bic => { type => 'varchar', length => 100 },
vc_depositor => { type => 'text' },
vc_iban => { type => 'varchar', length => 100 },
__PACKAGE__->meta->table('tax');
__PACKAGE__->meta->columns(
- chart_categories => { type => 'text', not_null => 1 },
- chart_id => { type => 'integer' },
- id => { type => 'integer', not_null => 1, sequence => 'id' },
- itime => { type => 'timestamp', default => 'now()' },
- mtime => { type => 'timestamp' },
- rate => { type => 'numeric', default => '0', not_null => 1, precision => 15, scale => 5 },
- taxdescription => { type => 'text', not_null => 1 },
- taxkey => { type => 'integer', not_null => 1 },
- taxnumber => { type => 'text' },
+ chart_categories => { type => 'text', not_null => 1 },
+ chart_id => { type => 'integer' },
+ id => { type => 'integer', not_null => 1, sequence => 'id' },
+ itime => { type => 'timestamp', default => 'now()' },
+ mtime => { type => 'timestamp' },
+ rate => { type => 'numeric', default => '0', not_null => 1, precision => 15, scale => 5 },
+ skonto_purchase_chart_id => { type => 'integer' },
+ skonto_sales_chart_id => { type => 'integer' },
+ taxdescription => { type => 'text', not_null => 1 },
+ taxkey => { type => 'integer', not_null => 1 },
+ taxnumber => { type => 'text' },
);
__PACKAGE__->meta->primary_key_columns([ 'id' ]);
class => 'SL::DB::Chart',
key_columns => { chart_id => 'id' },
},
+
+ skonto_purchase_chart => {
+ class => 'SL::DB::Chart',
+ key_columns => { skonto_purchase_chart_id => 'id' },
+ },
+
+ skonto_sales_chart => {
+ class => 'SL::DB::Chart',
+ key_columns => { skonto_sales_chart_id => 'id' },
+ },
+
+ skonto_sales_chart_obj => {
+ class => 'SL::DB::Chart',
+ key_columns => { skonto_sales_chart_id => 'id' },
+ },
);
1;
These types are sadly represented by data inside the class and cannot be
migrated into a flag. To work around this, each C<Part> object knows what type
-it currently is. Since the type ist data driven, there ist no explicit setting
+it currently is. Since the type is data driven, there ist no explicit setting
method for it, but you can construct them explicitly with C<new_part>,
C<new_service>, and C<new_assembly>. A Buchungsgruppe should be supplied in this
case, but it will use the default Buchungsgruppe if you don't.
=item C<new_assembly %PARAMS>
Will set the appropriate data fields so that the resulting instance will be of
-tthe requested type. Since part of the distinction are accounting targets,
+the requested type. Since accounting targets are part of the distinction,
providing a C<Buchungsgruppe> is recommended. If none is given the constructor
will load a default one and set the accounting targets from it.
=item C<orphaned>
-Checks if this articke is used in orders, invoices, delivery orders or
+Checks if this article is used in orders, invoices, delivery orders or
assemblies.
=item C<buchungsgruppe BUCHUNGSGRUPPE>
-Used to set the accounting informations from a L<SL:DB::Buchungsgruppe> object.
+Used to set the accounting information from a L<SL:DB::Buchungsgruppe> object.
Please note, that this is a write only accessor, the original Buchungsgruppe can
not be retrieved from an article once set.
use SL::DB::Manager::PurchaseInvoice;
use SL::DB::Helper::AttrHTML;
use SL::DB::Helper::LinkedRecords;
+use SL::DB::Helper::Payment qw(:ALL);
use SL::Locale::String qw(t8);
# The calculator hasn't been adjusted for purchase invoices yet.
};
-sub pay_invoice {
- my ($self, %params) = @_;
-
- #Mark invoice as paid
- $self->paid($self->paid+$params{amount});
- $self->save;
-
- Common::check_params(\%params, qw(chart_id trans_id amount transdate));
-
- #account of bank account or cash
- my $account_bank = SL::DB::Manager::Chart->find_by(id => $params{chart_id});
-
- #Search the contra account
- my $acc_trans = SL::DB::Manager::AccTransaction->find_by(trans_id => $params{trans_id},
- or => [ chart_link => { like => "%:AP" },
- chart_link => { like => "AP:%" },
- chart_link => "AP" ]);
- my $contra_account = SL::DB::Manager::Chart->find_by(id => $acc_trans->chart_id);
-
- #Two new transfers in acc_trans (for bank account and for contra account)
- my $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $params{trans_id},
- chart_id => $account_bank->id,
- chart_link => $account_bank->link,
- amount => $params{amount},
- transdate => $params{transdate},
- source => $params{source},
- memo => '',
- taxkey => 0,
- tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
- $new_acc_trans->save;
- $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $params{trans_id},
- chart_id => $contra_account->id,
- chart_link => $contra_account->link,
- amount => (-1 * $params{amount}),
- transdate => $params{transdate},
- source => $params{source},
- memo => '',
- taxkey => 0,
- tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
- $new_acc_trans->save;
-}
-
sub link {
my ($self) = @_;
use parent qw(Exporter);
use Exporter qw(import);
-our @EXPORT = qw(sales_invoice ar_transaction purchase_invoice ap_transaction gl_transaction);
+our @EXPORT = qw(invoice sales_invoice ar_transaction purchase_invoice ap_transaction gl_transaction);
use Carp;
+sub invoice {
+ my ($self, $invoice, %params) = @_;
+
+ if ( $invoice->is_sales ) {
+ if ( $invoice->invoice ) {
+ return _is_ir_record($self, $invoice, 'is', %params);
+ } else {
+ return _is_ir_record($self, $invoice, 'ar', %params);
+ }
+ } else {
+ if ( $invoice->invoice ) {
+ return _is_ir_record($self, $invoice, 'ir', %params);
+ } else {
+ return _is_ir_record($self, $invoice, 'ap', %params);
+ }
+ };
+};
+
sub sales_invoice {
my ($self, $invoice, %params) = @_;
my $object = SL::DB::Manager::PurchaseInvoice->get_first(where => [ or => [ invoice => undef, invoice => 0 ]]);
my $html = SL::Presenter->get->ar_transaction($object, display => 'inline');
+ # use with any of the above ar/ap/is/ir types:
+ my $html = SL::Presenter->get->invoice($object, display => 'inline');
+
=head1 FUNCTIONS
=over 4
+=item C<invoice $object, %params>
+
+Returns a rendered version (actually an instance of
+L<SL::Presenter::EscapedText>) of an ar/ap/is/ir object C<$object> . Determines
+which type (sales or purchase, invoice or not) the object is.
+
+C<%params> can include:
+
+=over 2
+
+=item * display
+
+Either C<inline> (the default) or C<table-cell>. At the moment both
+representations are identical and produce the invoice number linked
+to the corresponding 'edit' action.
+
+=item * no_link
+
+If falsish (the default) then the invoice number will be linked to the
+"edit invoice" dialog from the sales menu.
+
+=back
+
=item C<sales_invoice $object, %params>
Returns a rendered version (actually an instance of
use POSIX qw(strftime);
+use Data::Dumper;
use SL::DBUtils;
+use SL::DB::Invoice;
+use SL::DB::PurchaseInvoice;
+use SL::Locale::String qw(t8);
+use DateTime;
sub retrieve_open_invoices {
$main::lxdebug->enter_sub();
my $mandate = $params{vc} eq 'customer' ? " AND COALESCE(vc.mandator_id, '') <> '' AND vc.mandate_date_of_signature IS NOT NULL " : '';
+ # in query: for customers, use payment terms from invoice, for vendors use
+ # payment terms from vendor settings
+ # currently there is no option in vendor invoices for setting payment terms,
+ # so the vendor settings are always used
+
+ my $payment_term_type = $params{vc} eq 'customer' ? "${arap}" : 'vc';
+
+ # open_amount is not the current open amount according to bookkeeping, but
+ # the open amount minus the SEPA transfer amounts that haven't been closed yet
my $query =
qq|
- SELECT ${arap}.id, ${arap}.invnumber, ${arap}.${vc}_id as vc_id, ${arap}.amount AS invoice_amount, ${arap}.invoice,
+ SELECT ${arap}.id, ${arap}.invnumber, ${arap}.transdate, ${arap}.${vc}_id as vc_id, ${arap}.amount AS invoice_amount, ${arap}.invoice,
+ (${arap}.transdate + pt.terms_skonto) as skonto_date, (pt.percent_skonto * 100) as percent_skonto,
+ (${arap}.amount - (${arap}.amount * pt.percent_skonto)) as amount_less_skonto,
+ (${arap}.amount * pt.percent_skonto) as skonto_amount,
vc.name AS vcname, vc.language_id, ${arap}.duedate as duedate, ${arap}.direct_debit,
COALESCE(vc.iban, '') <> '' AND COALESCE(vc.bic, '') <> '' ${mandate} AS vc_bank_info_ok,
GROUP BY sei.ap_id)
AS open_transfers ON (${arap}.id = open_transfers.ap_id)
+ LEFT JOIN payment_terms pt ON (${payment_term_type}.payment_id = pt.id)
+
WHERE ${arap}.amount > (COALESCE(open_transfers.amount, 0) + ${arap}.paid)
ORDER BY lower(vc.name) ASC, lower(${arap}.invnumber) ASC
my $results = selectall_hashref_query($form, $dbh, $query);
+ # add some more data to $results:
+ # create drop-down data for payment types and suggest amount to be paid according
+ # to open amount or skonto
+
+ foreach my $result ( @$results ) {
+ my $invoice = $vc eq 'customer' ? SL::DB::Manager::Invoice->find_by( id => $result->{id} )
+ : SL::DB::Manager::PurchaseInvoice->find_by( id => $result->{id} );
+
+ $invoice->get_payment_suggestions(sepa => 1); # consider amounts of open entries in sepa_export_items
+ $result->{skonto_amount} = $invoice->skonto_amount;
+ $result->{within_skonto_period} = $invoice->within_skonto_period;
+ $result->{invoice_amount_suggestion} = $invoice->{invoice_amount_suggestion};
+ $result->{payment_select_options} = $invoice->{payment_select_options};
+ };
+
$main::lxdebug->leave_sub();
return $results;
my $q_insert =
qq|INSERT INTO sepa_export_items (id, sepa_export_id, ${arap}_id, chart_id,
amount, requested_execution_date, reference, end_to_end_id,
- our_iban, our_bic, vc_iban, vc_bic ${c_mandate})
+ our_iban, our_bic, vc_iban, vc_bic,
+ skonto_amount, payment_type ${c_mandate})
VALUES (?, ?, ?, ?,
?, ?, ?, ?,
- ?, ?, ?, ? ${p_mandate})|;
+ ?, ?, ?, ?,
+ ?, ? ${p_mandate})|;
my $h_insert = prepare_query($form, $dbh, $q_insert);
my $q_reference =
$transfer->{amount}, conv_date($transfer->{requested_execution_date}),
$transfer->{reference}, $end_to_end_id,
map { my $pfx = $_; map { $transfer->{"${pfx}_${_}"} } qw(iban bic) } qw(our vc));
+ # save value of skonto_amount and payment_type
+ if ( $transfer->{payment_type} eq 'without_skonto' ) {
+ push(@values, 0);
+ } elsif ($transfer->{payment_type} eq 'difference_as_skonto' ) {
+ push(@values, $transfer->{amount});
+ } elsif ($transfer->{payment_type} eq 'with_skonto_pt' ) {
+ push(@values, $transfer->{skonto_amount});
+ } else {
+ die "illegal payment_type: " . $transfer->{payment_type} . "\n";
+ };
+ push(@values, $transfer->{payment_type});
push @values, $transfer->{vc_mandator_id}, conv_date($transfer->{vc_mandate_date_of_signature}) if $params{vc} eq 'customer';
map { unshift @{ $_ }, prepare_query($form, $dbh, $_->[0]) } values %handles;
foreach my $item (@items) {
+
my $item_id = conv_i($item->{id});
# Retrieve the item data belonging to the ID.
next if (!$orig_item);
- # Retrieve the invoice's AR/AP chart ID.
- do_statement($form, @{ $handles{get_arap} }, $orig_item->{"${arap}_id"});
- my ($arap_chart_id) = $handles{get_arap}->[0]->fetchrow_array();
-
- # Record the payment in acc_trans offsetting AR/AP.
- do_statement($form, @{ $handles{add_acc_trans} }, $orig_item->{"${arap}_id"}, $arap_chart_id, -1 * $mult * $orig_item->{amount}, $item->{execution_date}, '', $arap_chart_id);
- do_statement($form, @{ $handles{add_acc_trans} }, $orig_item->{"${arap}_id"}, $orig_item->{chart_id}, $mult * $orig_item->{amount}, $item->{execution_date}, $orig_item->{reference},
- $orig_item->{chart_id});
-
- # Update the invoice to reflect the new paid amount.
- do_statement($form, @{ $handles{update_arap} }, $orig_item->{amount}, $orig_item->{"${arap}_id"});
-
- # Update datepaid of invoice. set_datepaid (which has some extra logic)
- # finds the date from acc_trans, where the payment has already been
- # recorded above, so we don't need to explicitly pass
- # $item->{execution_date}
- IO->set_datepaid(table => "$arap", id => $orig_item->{"${arap}_id"}, dbh => $dbh);
+ # fetch item_id via Rose (same id as orig_item)
+ my $sepa_export_item = SL::DB::Manager::SepaExportItem->find_by( id => $item_id);
+
+ my $invoice;
+
+ if ( $sepa_export_item->ar_id ) {
+ $invoice = SL::DB::Manager::Invoice->find_by( id => $sepa_export_item->ar_id);
+ } elsif ( $sepa_export_item->ap_id ) {
+ $invoice = SL::DB::Manager::PurchaseInvoice->find_by( id => $sepa_export_item->ap_id);
+ } else {
+ die "sepa_export_item needs either ar_id or ap_id\n";
+ };
+
+ $invoice->pay_invoice(amount => $sepa_export_item->amount,
+ payment_type => $sepa_export_item->payment_type,
+ chart_id => $sepa_export_item->chart_id,
+ source => $sepa_export_item->reference,
+ transdate => $item->{execution_date}, # value from user form
+ );
# Update the item to reflect that it has been posted.
do_statement($form, @{ $handles{finish_item} }, $item->{execution_date}, $item_id);
use List::Util qw(sum first);
use POSIX qw(strftime);
-use SL::BankAccount;
+use Data::Dumper;
+use SL::DB::BankAccount;
use SL::Chart;
use SL::CT;
use SL::Form;
$form->{title} = $vc eq 'customer' ? $::locale->text('Prepare bank collection via SEPA XML') : $locale->text('Prepare bank transfer via SEPA XML');
- my $bank_accounts = SL::BankAccount->list();
+ my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
if (!scalar @{ $bank_accounts }) {
$form->error($locale->text('You have not added bank accounts yet.'));
# from us automatically and we don't have to send money manually.
$_->{checked} = ($vc eq 'customer' ? $_->{direct_debit} : !$_->{direct_debit}) for @{ $invoices };
- my $bank_account_label_sub = sub { $locale->text('#1 - Account number #2, bank code #3, #4', $_[0]->{name}, $_[0]->{account_number}, $_[0]->{bank_code}, $_[0]->{bank} ) };
-
my $translation_list = GenericTranslations->list(translation_type => 'sepa_remittance_info_pfx');
my %translations = map { ( ($_->{language_id} || 'default') => $_->{translation} ) } @{ $translation_list };
print $form->parse_html_template('sepa/bank_transfer_add',
{ 'INVOICES' => $invoices,
'BANK_ACCOUNTS' => $bank_accounts,
- 'bank_account_label' => $bank_account_label_sub,
'vc' => $vc,
});
$form->{title} = $vc eq 'customer' ? $::locale->text('Create bank collection via SEPA XML') : $locale->text('Create bank transfer via SEPA XML');
- my $bank_accounts = SL::BankAccount->list();
-
+ my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
if (!scalar @{ $bank_accounts }) {
$form->error($locale->text('You have not added bank accounts yet.'));
}
- my $bank_account = first { $form->{bank_account}->{id} == $_->{id} } @{ $bank_accounts };
+ my $bank_account = SL::DB::Manager::BankAccount->find_by( id => $form->{bank_account} );
- if (!$bank_account) {
+ unless ( $bank_account ) {
$form->error($locale->text('The selected bank account does not exist anymore.'));
}
my $arap_id = $vc eq 'customer' ? 'ar_id' : 'ap_id';
my $invoices = SL::SEPA->retrieve_open_invoices(vc => $vc);
+ # load all open invoices (again), but grep out the ones that were selected with checkboxes beforehand ($_->selected). At this stage we again have all the invoice information, including dropdown with payment_type options
+ # all the information from retrieve_open_invoices is then ADDED to what was passed via @{ $form->{bank_transfers} }
+ # parse amount from the entry in the form, but take skonto_amount from PT again
+ # the map inserts the values of invoice_map directly into the array of hashes
my %invoices_map = map { $_->{id} => $_ } @{ $invoices };
my @bank_transfers =
map +{ %{ $invoices_map{ $_->{$arap_id} } }, %{ $_ } },
map { $_->{amount} = $form->parse_amount($myconfig, $_->{amount}); $_ }
@{ $form->{bank_transfers} || [] };
+ # override default payment_type selection and set it to the one chosen by the user
+ # in the previous step, so that we don't need the logic in the template
+ foreach my $bt (@bank_transfers) {
+ foreach my $type ( @{$bt->{payment_select_options}} ) {
+ if ( $type->{payment_type} eq $bt->{payment_type} ) {
+ $type->{selected} = 1;
+ } else {
+ $type->{selected} = 0;
+ };
+ };
+ };
+
if (!scalar @bank_transfers) {
$form->error($locale->text('You have selected none of the invoices.'));
}
'id' => \@vc_ids);
my @vc_bank_info = sort { lc $a->{name} cmp lc $b->{name} } values %{ $vc_bank_info };
- my $bank_account_label_sub = sub { $locale->text('#1 - Account number #2, bank code #3, #4', $_[0]->{name}, $_[0]->{account_number}, $_[0]->{bank_code}, $_[0]->{bank} ) };
-
$form->header();
print $form->parse_html_template('sepa/bank_transfer_create',
{ 'BANK_TRANSFERS' => \@bank_transfers,
'BANK_ACCOUNTS' => $bank_accounts,
'VC_BANK_INFO' => \@vc_bank_info,
'bank_account' => $bank_account,
- 'bank_account_label' => $bank_account_label_sub,
'error_message' => $error_message,
'vc' => $vc,
'total_trans' => $total_trans,
# binary.
python_uno = python
+# Location of the aqbanking binary to use when converting MT940 files
+# into the kivitendo import format
+aqbanking = /usr/bin/aqbanking-cli
+
[environment]
# Add the following paths to the PATH environment variable.
path = /usr/local/bin:/usr/X11R6/bin:/usr/X11/bin
# Veränderungen von kivitendo #
###############################
+2015-0x-xx - Release 3.2.2-unstable
+
+
+Größere neue Features:
+
+Bankerweiterung und Skontobehandlung
+
+ Bei der Bankerweiterung kann man
+ * Kontoauszüge importieren (für MT940 wird aqbanking-cli benötigt)
+ * anhand der Kontoauszüge Zahlungen verbuchen
+ * die FiBu-Buchungen auf die Bankkonten mit den importieren Auszügen
+ abgleichen
+
+__Es wurde ein neues Recht "Bankbewegungen" eingeführt.
+
+ Beim Verbuchen der Zahlungen werden Rechnungsvorschläge gemacht, die anhand
+ eines internen Punktesystems bewertet werden.
+
+ Es wurde eine Skontobehandlung bei der Zahlung der Rechnungen implementiert,
+ und zwar nach der Bruttomethode. D.h. es wird der skontierte Betrag auf
+ erhaltene oder gewährte Skonti gebucht, allerdings gibt es hier keine
+ Steuerautomatik, d.h. man muß am Monatsende die Salden noch manuell umbuchen.
+
+ Die zu buchenden Skontokonten müssen unter System->Steuern konfiguriert
+ werden.
+
+ Die Skontobehandlung wurde beim Verbuchen der Skontobelege und beim
+ SEPA-Einzug bzw der SEPA-Überweisung implementiert.
+ Beim Bezahlen von Rechnungen kann man auswählen ob man die Zahlung
+ * ohne Skonto
+ * mit Skonto laut Zahlungsbedingungen
+ * die Differenz als Skonto
+ buchen möchte. Es wird je nach Zahlungsbetrag und Zahlungsdatum ein sinnvoller
+ Vorschlag gemacht.
+
+
+Kleinere neue Features und Detailverbesserungen:
+
+
2015-04-10 - Release 3.2.1
Dies ist ein Unstable Bugfix-Release für die 3.2. D.h. es wurden ein paar
"Select template to paste":"Einzufügende Vorlage auswählen",
"Text block actions":"Textblockaktionen",
"Text block picture actions":"Aktionen für Textblockbilder",
+"The IBAN is missing.":"Die IBAN fehlt.",
"The description is missing.":"Die Beschreibung fehlt.",
"The name is missing.":"Der Name fehlt.",
"The name must only consist of letters, numbers and underscores and start with a letter.":"Der Name darf nur aus Buchstaben (keine Umlaute), Ziffern und Unterstrichen bestehen und muss mit einem Buchstaben beginnen.",
' Part Number missing!' => ' Artikelnummer fehlt!',
' missing!' => ' fehlt!',
'#1 (custom variable)' => '#1 (benutzerdefinierte Variable)',
- '#1 - Account number #2, bank code #3, #4' => '#1 - Kontonummber #2, BLZ #3, #4',
'#1 MD' => '#1 PT',
'#1 additional part(s)' => '#1 zusätzliche(r) Artikel',
'#1 h' => '#1 h',
'#1 of #2 importable objects were imported.' => '#1 von #2 importierbaren Objekten wurden importiert.',
'#1 prices were updated.' => '#1 Preise wurden aktualisiert.',
+ '#1 proposal(s) saved.' => '#1 Vorschläge gespeichert.',
'#1 section(s)' => '#1 Abschnitt(e)',
'#1 text block(s) back' => '#1 Textlock/-blöcke vorne',
'#1 text block(s) front' => '#1 Textblock/-blöcke hinten',
'%' => '%',
'(recommended) Insert the used currencies in the system. You can simply change the name of the currencies by editing the textfields above. Do not use a name of a currency that is already in use.' => '(empfohlen) Fügen Sie die verwaisten Währungen in Ihr System ein. Sie können den Namen der Währung einfach ändern, indem Sie die Felder oben bearbeiten. Benutzen Sie keine Namen von Währungen, die Sie bereits benutzen.',
- '#1 proposal(s) saved.' => '#1 Vorschläge gespeichert.',
- '#1, Account number #2, bank code #3' => '#1, Kontonummer #2, BLZ #3',
- '(Purchase)' => '',
'*/' => '*/',
', if set' => ', falls gesetzt',
'---please select---' => '---bitte auswählen---',
'AUTOMATICALLY MATCH BINS' => 'LAGERPLÄTZE AUTOMATISCH ZUWEISEN',
'Abort' => 'Abbrechen',
'Abrechnungsnummer' => 'Abrechnungsnummer',
- 'Absolute BB Balance' => 'Gesamtsaldo laut Kontoauszug',
- 'Absolute BT Balance' => 'Gesamtsaldo laut Bankbuchungen',
+ 'Absolute BB Balance' => 'Gesamtsaldo laut Bankbuchungen',
+ 'Absolute BT Balance' => 'Gesamtsaldo laut Kontoauszug',
'Abteilung' => 'Abteilung',
'Acceptance Statuses' => 'Abnahmestatus',
'Acc Transaction' => 'Hauptbuch',
'Add bank account' => 'Bankkonto erfassen',
'Add custom variable' => 'Benutzerdefinierte Variable erfassen',
'Add function block' => 'Funktionsblock hinzufügen',
- 'Add invoices' => 'Rechnung hinzufügen',
+ 'Add invoices' => 'Rechnungen hinzufügen',
'Add link: select records to link with' => 'Verknüpfungen hinzufügen: zu verknüpfende Belege auswählen',
'Add linked record' => 'Verknüpften Beleg hinzufügen',
'Add links' => 'Verknüpfungen hinzufügen',
'Amended Advance Turnover Tax Return (Nr. 10)' => 'Ist dies eine berichtigte Anmeldung? (Nr. 10/Zeile 15 Steuererklärung)',
'Amount' => 'Betrag',
'Amount (for verification)' => 'Betrag (zur Überprüfung)',
- 'Amount BB' => 'Betrag Bank',
- 'Amount BT' => 'Betrag Buchungen',
+ 'Amount BB' => 'Betrag Buchungen',
+ 'Amount BT' => 'Betrag Bank',
'Amount Due' => 'Betrag fällig',
'Amount and net amount are calculated by kivitendo. "verify_amount" and "verify_netamount" can be used for sanity checks.' => 'Betrag und Nettobetrag werden von kivitendo berechnet. "verify_amount" und "verify_netamount" können für Plausibilitätsprüfungen angegeben werden.',
+ 'Amount less skonto' => 'Betrag abzgl. Skonto',
'Amount payable' => 'Noch zu bezahlender Betrag',
'Amount payable less discount' => 'Noch zu bezahlender Betrag abzüglich Skonto',
'An exception occurred during execution.' => 'Während der Ausführung trat eine Ausnahme auf.',
'Auto Send?' => 'Auto. Versand?',
'Automatic deletion of leading, trailing and excessive (repetitive) spaces in customer or vendor names' => 'Automatisches Löschen von voran-/nachgestellten und aufeinanderfolgenden Leerzeichen im Kunden- oder Lieferantennamen',
'Automatic deletion of leading, trailing and excessive (repetitive) spaces in part description and part notes. Affects the CSV import as well.' => 'Automatisches Löschen von voran-/nachgestellten und aufeinanderfolgenden Leerzeichen in Artikelbeschreibungen und -bemerkungen. Betrifft auch den CSV-Import.',
+ 'Automatic skonto chart purchase' => 'Skontoautomatik Einkauf',
+ 'Automatic skonto chart sales' => 'Skontoautomatik Verkauf',
'Automatically created invoice for fee and interest for dunning %s' => 'Automatisch erzeugte Rechnung für Gebühren und Zinsen zu Mahnung %s',
'Available' => 'Verfügbar',
'Available Prices' => 'Mögliche Preise',
'Balances' => 'Salden',
'Balancing' => 'Bilanzierung',
'Bank' => 'Bank',
+ 'Bank Account can\'t be found' => 'Bankkkonto kann nicht gefunden werden',
'Bank Code' => 'BLZ',
'Bank Code (long)' => 'Bankleitzahl (BLZ)',
'Bank Code Number' => 'Bankleitzahl',
'Bank Connection Tax Office' => 'Bankverbindung des Finanzamts',
'Bank Connections' => 'Bankverbindungen',
+ 'Bank Import' => 'Kontoauszug importieren',
'Bank Transaction' => 'Bankkonto',
- 'Bank Transactions' => 'Bankbewegungen',
'Bank account' => 'Bankkonto',
'Bank accounts' => 'Bankkonten',
'Bank code' => 'Bankleitzahl',
'Bank collections via SEPA' => 'Bankeinzüge via SEPA',
'Bank transaction' => 'Bankbuchung',
'Bank transactions' => 'Bankbewegungen',
- 'Bank transactions MT940' => 'Elektr. Kontoauszug',
+ 'Bank transactions MT940' => 'Kontoauszug verbuchen',
'Bank transfer amount' => 'Überweisungssumme',
'Bank transfer payment list for export #1' => 'Überweisungszahlungsliste für SEPA-Export #1',
'Bank transfer via SEPA' => 'Überweisung via SEPA',
'CRM termin' => 'Termine',
'CRM user' => 'Admin Benutzer',
'CSS style for pictures' => 'CSS Style für Bilder',
+ 'CSV' => 'CSV',
'CSV export -- options' => 'CSV-Export -- Optionen',
+ 'CSV import: MT940' => 'CSV Import: MT940',
'CSV import: bank transactions' => 'CSV Import: Bankbewegungen',
'CSV import: contacts' => 'CSV-Import: Ansprechpersonen',
'CSV import: customers and vendors' => 'CSV-Import: Kunden und Lieferanten',
'Choose Vendor' => 'Händler wählen',
'Choose a Tax Number' => 'Bitte eine Steuernummer angeben',
'Choose bank account for reconciliation' => 'Wählen Sie das Bankkonto für den Kontenabgleich',
- 'Choose chart' => 'Konto auswählen',
'City' => 'Stadt',
'Clear fields' => 'Felder leeren',
'Cleared Balance' => 'abgeschlossen',
'Could not load this customer' => 'Konnte diesen Kunden nicht laden',
'Could not load this vendor' => 'Konnte diesen Lieferanten nicht laden',
'Could not print dunning.' => 'Die Mahnungen konnten nicht gedruckt werden.',
- 'Could not reconciliate chosen elements!' => 'Die gewählten Elemente konnten nicht ausgeglichen werden!',
+ 'Could not reconcile chosen elements!' => 'Die gewählten Elemente konnten nicht ausgeglichen werden!',
'Could not spawn ghostscript.' => 'Die Anwendung "ghostscript" konnte nicht gestartet werden.',
'Could not spawn the printer command.' => 'Die Druckanwendung konnte nicht gestartet werden.',
'Could not update prices!' => 'Preise konnten nicht aktualisiert werden!',
'Details (one letter abbreviation)' => 'D',
'Dial command missing in kivitendo configuration\'s [cti] section' => 'Wählbefehl fehlt im Abschnitt [cti] der kivitendo-Konfiguration',
'Difference' => 'Differenz',
+ 'Difference as skonto' => 'Differenz als Skonto',
'Dimensions' => 'Abmessungen',
'Directory' => 'Verzeichnis',
'Disabled Price Sources' => 'Deaktivierte Preisquellen',
'Error: Invalid delivery terms' => 'Fehler: Lieferbedingungen ungültig',
'Error: Invalid department' => 'Fehler: Abteilung ungültig',
'Error: Invalid language' => 'Fehler: Sprache ungültig',
- 'Error: Invalid local bank account' => '',
+ 'Error: Invalid local bank account' => 'Fehler: ungültiges Bankkonto',
'Error: Invalid order for this order item' => 'Fehler: Auftrag für diese Position ungültig',
'Error: Invalid part' => 'Fehler: Artikel ungültig',
'Error: Invalid part type' => 'Fehler: Artikeltyp ungültig',
'Execution status' => 'Ausführungsstatus',
'Execution type' => 'Ausführungsart',
'Existing Datasets' => 'Existierende Datenbanken',
+ 'Existing bank transactions' => 'Existierende Bankbuchungen',
'Existing contacts (with column \'cp_id\')' => 'Existierende Ansprechpersonen (mit Spalte \'cp_id\')',
'Existing customers/vendors with same customer/vendor number' => 'Existierende Kunden/Lieferanten mit derselben Kunden-/Lieferantennummer',
'Existing file on server' => 'Auf dem Server existierende Datei',
'I' => 'I',
'IBAN' => 'IBAN',
'ID' => 'Buchungsnummer',
- 'ID of own bank account' => '',
+ 'ID of own bank account' => 'Datenbank-ID des Bankkontos',
'ID-Nummer' => 'ID-Nummer (intern)',
- 'ID/Acc_ID' => '',
+ 'ID/Acc_ID' => 'ID/Acc_ID',
'II' => 'II',
'III' => 'III',
'IV' => 'IV',
'If you want to delete such a dataset you have to edit the client(s) that are using the dataset in question and have them use another dataset.' => 'Wenn Sie eine solche Datenbank löschen möchten, dann müssen Sie zuerst den/die Mandanten auf eine andere Datenbank umstellen, die die zu löschende Datenbank benutzen.',
'If you want to set up the authentication database yourself then log in to the administration panel. kivitendo will then create the database and tables for you.' => 'Wenn Sie die Authentifizierungs-Datenbank selber einrichten wollen, so melden Sie sich im Administrationsbereich an. kivitendo wird dann die Datenbank und die erforderlichen Tabellen für Sie anlegen.',
'If your old bins match exactly Bins in the Warehouse CLICK on <b>AUTOMATICALLY MATCH BINS</b>.' => 'Falls die alte Lagerplatz-Beschreibung in Stammdaten genau mit einem Lagerplatz in einem vorhandenem Lager übereinstimmt, KLICK auf <b>LAGERPLÄTZE AUTOMATISCH ZUWEISEN</b>',
+ 'Illegal amount' => 'Ungültiger Betrag',
'Illegal characters have been removed from the following fields: #1' => 'Ungültige Zeichen wurden aus den folgenden Feldern entfernt: #1',
+ 'Illegal date' => 'Ungültiges Datum',
'Image' => 'Grafik',
'Import' => 'Import',
'Import CSV' => 'CSV-Import',
'Information' => 'Information',
'Initial version.' => 'Initiale Version.',
'Insert' => 'Einfügen',
+ 'Insert new' => 'Hinzufügen',
'Insert with new customer/vendor number' => 'Mit neuer Kunden-/Lieferantennummer anlegen',
'Insert with new database ID' => 'Neu anlegen mit neuer Datenbank-ID',
'Insert with new part number' => 'Mit neuer Artikelnummer einfügen',
'Invnumber' => 'Rechnungsnummer',
'Invnumber missing!' => 'Rechnungsnummer fehlt!',
'Invoice' => 'Rechnung',
+ 'Invoice #1: paid #2 to bank #3, rest for skonto.' => 'Rechnung #1: #2 an Konto #3, Rest als Skonto',
+ 'Invoice #1: paid #2 to bank #3.' => 'Rechnung #1: #2 an Konto #3',
+ 'Invoice #1: paid #2 to skonto.' => 'Rechnung #1: #2 als Skonto bezahlt',
'Invoice (one letter abbreviation)' => 'R',
'Invoice Date' => 'Rechnungsdatum',
'Invoice Date missing!' => 'Rechnungsdatum fehlt!',
'Invoice Duedate' => 'Fälligkeitsdatum',
'Invoice Number' => 'Rechnungsnummer',
'Invoice Number missing!' => 'Rechnungsnummer fehlt!',
+ 'Invoice can\'t be found' => '',
'Invoice deleted!' => 'Rechnung gelöscht!',
+ 'Invoice filter' => 'Rechnungsfilter',
'Invoice for fees' => 'Rechnung über Gebühren',
'Invoice has already been storno\'d!' => 'Diese Rechnung wurde bereits storniert.',
'Invoice number' => 'Rechnungsnummer',
'List Users, Clients and User Groups' => 'Benutzer, Mandanten und Benutzergruppen anzeigen',
'List current background jobs' => 'Aktuelle Hintergrund-Jobs anzeigen',
'List export' => 'Export anzeigen',
- 'List of bank accounts' => 'Liste der Bankkonten',
'List of bank collections' => 'Bankeinzugsliste',
- 'List of bank transactions' => 'Liste der Bankbewegungen',
'List of bank transfers' => 'Überweisungsliste',
'List of custom variables' => 'Liste der benutzerdefinierten Variablen',
'List of database upgrades to be applied:' => 'Liste der noch einzuspielenden Datenbankupgrades:',
'Local Bank Code' => 'Lokale Bankleitzahl',
'Local Tax Office Preferences' => 'Angaben zum Finanzamt',
'Local account number' => 'Lokale Kontonummer',
- 'Local bank account' => '',
+ 'Local bank account' => 'Lokales Bankkonto',
'Local bank code' => 'Lokale Bankleitzahl',
'Lock System' => 'System sperren',
'Lock and unlock installation' => 'Installation sperren/entsperren',
'MAILED' => 'Gesendet',
'MD' => 'PT',
'MIME type' => 'MIME-Typ',
+ 'MT940' => 'MT940',
+ 'MT940 import' => 'MT940 Import',
'Machine' => 'Maschine',
'Main Preferences' => 'Grundeinstellungen',
'Main sorting' => 'Hauptsortierung',
'Name and Street' => 'Name und Straße',
'Name does not make sense without any bsooqr options' => 'Option "Name in gewählten Belegen" wird ignoriert.',
'Name in Selected Records' => 'Name in gewählten Belegen',
+ 'Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")' => 'Name des Ziel- oder Quellkontos (wenn die Spalten remote_name und remote_name_1 existieren werden diese zu Feld "remote_name" zusammengefügt)',
'Negative reductions are possible to model price increases.' => 'Negative Abschläge sind möglich um Aufschläge zu modellieren.',
'Neither sections nor function blocks have been created yet.' => 'Es wurden bisher weder Abschnitte noch Funktionsblöcke angelegt.',
- 'Name of the goal/source' => 'Name des Ziel- oder Quellkontos',
- 'National Expenses' => 'Aufwand Inland',
- 'National Revenues' => 'Erlöse Inland',
'Net Income Statement' => 'Einnahmenüberschußrechnung',
'Net amount' => 'Nettobetrag',
'Net amount (for verification)' => 'Nettobetrag (zur Überprüfung)',
'No text blocks have been created for this position.' => 'Für diese Position wurden noch keine Textblöcke angelegt.',
'No text has been entered yet.' => 'Es wurde noch kein Text eingegeben.',
'No title yet' => 'Bisher ohne Titel',
- 'No transaction on chart bank chosen!' => '',
+ 'No transaction on chart bank chosen!' => 'Keine Buchung auf Bankkonto gewählt.',
'No transaction selected!' => 'Keine Transaktion ausgewählt',
'No transactions yet.' => 'Bisher keine Buchungen.',
'No transfers were executed in this export.' => 'In diesem SEPA-Export wurden keine Überweisungen ausgeführt.',
'Override' => 'Override',
'Override invoice language' => 'Diese Sprache verwenden',
'Overview' => 'Übersicht',
- 'Own bank account number' => 'Eigene Kontonummer',
+ 'Own bank account number or IBAN' => 'Eigene Kontonummer oder IBAN',
'Own bank code' => 'Eigene Bankleitzahl',
'Owner of account' => 'Kontoinhaber',
'PAYMENT POSTED' => 'Rechung gebucht',
'Payment terms' => 'Zahlungsbedingungen',
'Payment terms (database ID)' => 'Zahlungsbedingungen (Datenbank-ID)',
'Payment terms (name)' => 'Zahlungsbedingungen (Name)',
+ 'Payment type' => 'Zahlungsart',
'Payments' => 'Zahlungsausgänge',
'Payments Changeable' => 'Änderbarkeit von Zahlungen',
'Per. Inv.' => 'Wied. Rech.',
'Purchase price total' => 'EK-Betrag',
'Purchasing & Sales' => 'Einkauf & Verkauf',
'Purpose' => 'Verwendungszweck',
+ 'Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")' => 'Verwendungszweck (wenn die Spalten purpose, purpose1, purpose2 ... existieren werden diese zum Feld "purpose" zusammengefügt)',
'Purpose/Reference' => 'Verwendungszweck und Referenz',
'Qty' => 'Menge',
'Qty according to delivery order' => 'Menge laut Lieferschein',
'Receipt, payment, reconciliation' => 'Zahlungseingang, Zahlungsausgang, Kontenabgleich',
'Receipts' => 'Zahlungseingänge',
'Receivables' => 'Forderungen',
- 'Rechnung/Buchung' => 'Invoice/transaction',
'Rechnungsnummer' => 'Rechnungsnummer',
- 'Reconciliate' => 'Abgleichen',
+ 'Reconcile' => 'Abgleichen',
'Reconciliation' => 'Kontenabgleich',
'Reconciliation with bank' => 'Kontenabgleich mit Bank',
'Record Vendor Invoice' => 'Einkaufsrechnung erfassen',
'Remote account number' => 'Fremde Kontonummer',
'Remote bank code' => 'Fremde Bankleitzahl',
'Remote name' => 'Fremder Kontoinhaber',
- 'Remote name 1' => 'Fremder Kontoname 1',
'Removal' => 'Entnahme',
'Removal from Warehouse' => 'Lagerentnahme',
'Removal from warehouse' => 'Entnahme aus Lager',
'Save settings as' => 'Einstellungen speichern unter',
'Saving failed. Error message from the database: #1' => 'Speichern schlug fehl. Fehlermeldung der Datenbank: #1',
'Saving the file \'%s\' failed. OS error message: %s' => 'Das Speichern der Datei \'%s\' schlug fehl. Fehlermeldung des Betriebssystems: %s',
+ 'Score' => 'Punkte',
'Screen' => 'Bildschirm',
'Search' => 'Suchen',
'Search AP Aging' => 'Offene Verbindlichkeiten',
'Sketch' => 'Skizze',
'Skip' => 'Überspringen',
'Skip entry' => 'Eintrag überspringen',
+ 'Skipping because transfer amount is empty.' => 'Übersprungen wegen leeren Betrags.',
+ 'Skipping due to existing bank transaction in database' => 'Wegen schon existierender Bankbewegung in Datenbank übersprungen',
'Skipping due to existing entry in database' => 'Wegen existierendem Eintrag mit selber Nummer übersprungen',
'Skipping due to existing entry in database with different type' => 'Wegen existierendem Eintrag von unterschiedlichem Artikeltyp übersprungen',
'Skipping, for assemblies are not importable (yet)' => 'Übersprungen, da Erzeugnisse (noch) nicht importiert werden können',
'Skonto' => 'Skonto',
'Skonto Terms' => 'Zahlungsziel Skonto',
+ 'Skonto amount' => 'Skontobetrag',
+ 'Skonto information' => 'Skonto Information',
'So far you could use one partnumber for severel parts, for example a service and an article.' => 'Bisher war es möglich eine Artikelnummer für mehrere Artikel zu verwenden, zum Beispiel eine Artikelnummer für eine Dienstleistung, eine Ware und ein Erzeugnis.',
'Sold' => 'Verkauft',
'Soldtotal does not make sense without any bsooqr options' => 'Option "Menge in gewählten Belegen" ohne gewählte Belege wird ignoriert.',
'There are entries in tax where taxkey is NULL.' => 'In der Datenbank sind Steuern ohne Steuerschlüssel vorhanden (in der Tabelle tax Spalte taxkey).',
'There are invalid taxnumbers in use.' => 'Es werden ungültige Steuerautomatik-Konten benutzt.',
'There are invalid transactions in your database.' => 'Sie haben ungültige Buchungen in Ihrer Datenbank.',
- 'There are invoices which could not be payed by bank transaction #1 (Account number: #2, bank code: #3)!' => 'Einige Rechnungen konnten nicht durch die Bankbewegung #1 (Kontonummer: #2, Bankleitzahl: #3) bezahlt werden!',
+ 'There are invoices which could not be paid by bank transaction #1 (Account number: #2, bank code: #3)!' => 'Einige Rechnungen konnten nicht durch die Bankbewegung #1 (Kontonummer: #2, Bankleitzahl: #3) bezahlt werden!',
'There are no entries in the background job history.' => 'Es gibt keine Einträge im Hintergrund-Job-Verlauf.',
'There are no items in stock.' => 'Dieser Artikel ist nicht eingelagert.',
'There are no items on your TODO list at the moment.' => 'Ihre Aufgabenliste enthält momentan keine Einträge.',
'Transactions without reference:' => 'Buchungen ohne Referenz:',
'Transactions, AR transactions, AP transactions' => 'Dialogbuchen, Debitorenrechnungen, Kreditorenrechnungen',
'Transdate' => 'Belegdatum',
+ 'Transdate from' => 'Kontoauszugsdatum von',
'Transdate is #1' => 'Belegdatum ist #1',
'Transdate is after #1' => 'Belegdatum ist nach #1',
'Transdate is before #1' => 'Belegdatum ist vor #1',
- 'Transdate from' => 'Kontoauszugsdatum von',
'Transdate to' => 'Buchungsdatum bis',
'Transfer' => 'Umlagern',
'Transfer Quantity' => 'Umlagermenge',
'Valid/Obsolete' => 'Gültig/ungültig',
'Value' => 'Wert',
'Value of transferred goods' => 'Verkaufswert der ausgelagerten Waren',
- 'Valuta' => 'Valuta',
+ 'Valuta date' => 'Valutadatum',
'Valutadate' => 'Valutadatum',
'Valutadate from' => 'Valutadatum von',
'Valutadate to' => 'Valutadatum bis',
'You have to define a unit as a multiple of a smaller unit.' => 'Sie müssen Einheiten als ein Vielfaches einer kleineren Einheit eingeben.',
'You have to enter a company name in the client configuration.' => 'Sie müssen in der Mandantenkonfiguration einen Firmennamen angeben.',
'You have to enter the SEPA creditor ID in the client configuration.' => 'Sie müssen in der Mandantenkonfiguration eine SEPA-Kreditoren-Identifikation angeben.',
- 'You have to fill in at least a name, an account number, the bank code, the IBAN and the BIC.' => 'Sie müssen zumindest einen Namen, die Kontonummer, die Bankleitzahl, die IBAN und den BIC angeben.',
'You have to grant users access to one or more clients.' => 'Benutzern muss dann Zugriff auf einzelne Mandanten gewährt werden.',
'You have to specify a department.' => 'Sie müssen eine Abteilung wählen.',
'You have to specify an execution date for each antry.' => 'Sie müssen für jeden zu buchenden Eintrag ein Ausführungsdatum angeben.',
'balance' => 'Betriebsvermögensvergleich/Bilanzierung',
'bank_collection_payment_list_#1' => 'bankeinzugszahlungsliste_#1',
'bank_transfer_payment_list_#1' => 'ueberweisungszahlungsliste_#1',
- 'bankaccounts' => 'Bankkonten',
'banktransfers' => 'ueberweisungen',
'bestbefore #1' => 'Mindesthaltbarkeit #1',
'bin_list' => 'Lagerliste',
'delete' => 'Löschen',
'delivered' => 'geliefert',
'deliverydate' => 'Lieferdatum',
+ 'difference as skonto' => 'Differenz als Skonto',
+ 'difference_as_skonto' => 'Differenz als Skonto',
'direct debit' => 'Lastschrifteinzug',
'disposed' => 'Entsorgung',
'do not include' => 'Nicht aufnehmen',
'ea' => 'St.',
'emailed to' => 'gemailt an',
'empty' => 'leer',
+ 'error while paying invoice #1 : ' => 'Fehler beim Bezahlen von Rechnung #1 : ',
'every third month' => 'vierteljährlich',
'every time' => 'immer',
'executed' => 'ausgeführt',
'failed' => 'fehlgeschlagen',
'female' => 'weiblich',
+ 'finalised' => '',
'flat-rate position' => 'Pauschalposition',
'follow_up_list' => 'wiedervorlageliste',
'for' => 'für',
'no article assigned yet' => 'noch kein Artikel zugewiesen',
'no bestbefore' => 'keine Mindesthaltbarkeit',
'no chargenumber' => 'keine Chargennummer',
+ 'no skonto_chart configured for taxkey #1 : #2 : #3' => 'Kein Skontokonto für Steuerschlüssel #1 : #2 : #3',
'not configured' => 'nicht konfiguriert',
'not delivered' => 'nicht geliefert',
'not executed' => 'nicht ausgeführt',
'uncleared' => 'Nicht abgeglichen',
'unconfigured' => 'unkonfiguriert',
'uncorrect partnumber ' => 'Unbekannte Teilenummer ',
+ 'until' => 'bis',
'use program settings' => 'benutze Programmeinstellungen',
'use user config' => 'Verwende Benutzereinstellung',
'used' => 'Verbraucht',
'vendor_list' => 'lieferantenliste',
'warehouse_journal_list' => 'lagerbuchungsliste',
'warehouse_report_list' => 'lagerbestandsliste',
+ 'with skonto acc. to pt' => 'mit Skonto nach ZB',
+ 'with_skonto_pt' => 'mit Skonto nach ZB',
+ 'without skonto' => 'ohne Skonto',
+ 'without_skonto' => 'ohne Skonto',
'working copy' => 'Arbeitskopie',
'wrongformat' => 'Falsches Format',
'yearly' => 'jährlich',
'delete' => '',
'delivered' => '',
'deliverydate' => '',
+ 'difference as skonto' => '',
+ 'difference_as_skonto' => 'remainder as skonto',
'direct debit' => '',
'disposed' => '',
'do not include' => '',
'vendor_list' => '',
'warehouse_journal_list' => '',
'warehouse_report_list' => '',
+ 'with skonto acc. to pt' => ''
+ 'with_skonto_pt' => 'with skonto payment terms',
+ 'without skonto' => '',
+ 'without_skonto' => 'without skonto',
'wrongformat' => '',
'yearly' => '',
'yes' => '',
type=check
vc=vendor
-[Cash--Reconciliation]
-ACCESS=cash
-module=rc.pl
-action=reconciliation
-
[Cash--Bank collection via SEPA]
ACCESS=cash
module=sepa.pl
action=bank_transfer_add
vc=vendor
+[Cash--Bank Import]
+module=menu.pl
+action=acc_menu
+submenu=1
+
+[Cash--Bank Import--CSV]
+ACCESS=bank_transaction
+module=controller.pl
+action=CsvImport/new
+profile.type=bank_transactions
+
+[Cash--Bank Import--MT940]
+ACCESS=bank_transaction
+module=controller.pl
+action=CsvImport/new
+profile.type=mt940
+
[Cash--Bank transactions MT940]
ACCESS=bank_transaction
module=controller.pl
action=Reconciliation/search
next_sub=Reconciliation/reconciliation
+[Cash--Reconciliation]
+ACCESS=cash
+module=rc.pl
+action=reconciliation
+
[Cash--Reports]
module=menu.pl
action=acc_menu
action=acc_menu
submenu=1
-[System--Import CSV--Bank Transactions]
-module=controller.pl
-action=CsvImport/new
-profile.type=bank_transactions
-
-[System--Import CSV--MT940]
-module=controller.pl
-action=CsvImport/new
-profile.type=mt940
-
[System--Import CSV--Customers and vendors]
module=controller.pl
action=CsvImport/new
-- @tag: automatic_reconciliation
-- @description: Erstellt Tabelle reconiliation_links für den automatischen Kontenabgleich.
--- @depends: release_3_0_0 bank_transactions
+-- @depends: release_3_2_0 bank_transactions
CREATE TABLE reconciliation_links (
id integer NOT NULL DEFAULT nextval('id'),
-- @tag: bank_accounts_unique_chart_constraint
-- @description: Bankkonto - Constraint für eindeutiges Konto
--- @depends: release_3_2_0
+-- @depends: release_3_2_0 bank_accounts
-- @encoding: utf-8
ALTER TABLE bank_accounts ADD CONSTRAINT chart_id_unique UNIQUE (chart_id);
-- @tag: bank_transactions
-- @description: Erstellen der Tabelle bank_transactions.
--- @depends: release_3_0_0 currencies
+-- @depends: release_3_2_0 currencies
CREATE TABLE bank_transactions (
id SERIAL PRIMARY KEY,
valutadate DATE NOT NULL,
amount numeric(15,5) NOT NULL,
remote_name TEXT,
- remote_name_1 TEXT,
purpose TEXT,
invoice_amount numeric(15,5) DEFAULT 0,
local_bank_account_id INTEGER NOT NULL,
currency_id INTEGER NOT NULL,
cleared BOOLEAN NOT NULL DEFAULT FALSE,
-
+ itime TIMESTAMP DEFAULT now(),
FOREIGN KEY (currency_id) REFERENCES currencies (id),
FOREIGN KEY (local_bank_account_id) REFERENCES bank_accounts (id)
);
-- @depends: release_3_2_0
-- @encoding: utf-8
-ALTER TABLE bank_accounts ADD COLUMN obsolete BOOLEAN;
+-- default false needed so that get_all_sorted( query => [ obsolete => 0 ] ) works
+ALTER TABLE bank_accounts ADD COLUMN obsolete BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE bank_accounts ADD COLUMN sortkey INTEGER;
CREATE SEQUENCE tmp_counter;
--- /dev/null
+-- @tag: sepa_items_payment_type
+-- @description: Zahlungsart und Skontobetrag in SEPA-Auftrag speichern
+-- @depends: release_3_2_0
+-- @ignore: 0
+
+ALTER TABLE sepa_export_items ADD COLUMN payment_type TEXT;
+UPDATE sepa_export_items SET payment_type = 'without_skonto' WHERE payment_type IS NULL;
+ALTER TABLE sepa_export_items ALTER COLUMN payment_type SET DEFAULT 'without_skonto';
+
+ALTER TABLE sepa_export_items ADD COLUMN skonto_amount NUMERIC(25,5);
--- /dev/null
+-- @tag: tax_skonto_automatic
+-- @description: Skontoautomatikkonten für Steuern mit minimaler Vorbelegung
+-- @depends: release_3_2_0
+-- @ignore: 0
+
+ALTER TABLE tax ADD COLUMN skonto_sales_chart_id integer;
+ALTER TABLE tax ADD FOREIGN KEY (skonto_sales_chart_id) REFERENCES chart (id);
+ALTER TABLE tax ADD COLUMN skonto_purchase_chart_id integer;
+ALTER TABLE tax ADD FOREIGN KEY (skonto_purchase_chart_id) REFERENCES chart (id);
+
+UPDATE tax SET skonto_purchase_chart_id = (SELECT id FROM chart WHERE description LIKE '%Erhaltene Skonti %19%' limit 1) WHERE rate = '0.19' AND ( taxkey >= 7 AND taxkey <= 9 );
+UPDATE tax SET skonto_purchase_chart_id = (SELECT id FROM chart WHERE description LIKE '%Erhaltene Skonti 7%' limit 1) WHERE rate = '0.07' AND ( taxkey >= 7 AND taxkey <= 9 );
+UPDATE tax SET skonto_purchase_chart_id = (SELECT id FROM chart WHERE description LIKE '%Erhaltene Skonti %16%' limit 1) WHERE rate = '0.16' AND ( taxkey = 7 );
+UPDATE tax SET skonto_sales_chart_id = (SELECT id FROM chart WHERE description LIKE '%Gewährte Skonti 7%' limit 1) WHERE rate = '0.07' AND ( taxkey >= 2 AND taxkey <= 5 );
+UPDATE tax SET skonto_sales_chart_id = (SELECT id FROM chart WHERE description LIKE '%Gewährte Skonti %19%' limit 1) WHERE rate = '0.19' AND ( taxkey >= 2 AND taxkey <= 5 );
+UPDATE tax SET skonto_sales_chart_id = (SELECT id FROM chart WHERE description LIKE '%Gewährte Skonti %16%' limit 1) WHERE rate = '0.16' AND ( taxkey = 5 );
+UPDATE tax SET skonto_sales_chart_id = (SELECT id FROM chart WHERE description LIKE 'Gewährte Skonti' limit 1) WHERE rate = '0' AND ( taxkey = 0 or taxkey = 1 );
+UPDATE tax SET skonto_purchase_chart_id = (SELECT id FROM chart WHERE description LIKE 'Erhaltene Skonti' limit 1) WHERE rate = '0' AND ( taxkey = 0 or taxkey = 1 );
--- /dev/null
+use Test::More;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Carp;
+use Support::TestSetup;
+use Test::Exception;
+use List::Util qw(sum);
+
+use SL::DB::Buchungsgruppe;
+use SL::DB::Currency;
+use SL::DB::Customer;
+use SL::DB::Vendor;
+use SL::DB::Employee;
+use SL::DB::Invoice;
+use SL::DB::Part;
+use SL::DB::Unit;
+use SL::DB::TaxZone;
+use SL::DB::BankAccount;
+use SL::DB::PaymentTerm;
+
+my ($customer, $vendor, $currency_id, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $tax, $tax7, $taxzone, $payment_terms, $bank_account);
+
+my $ALWAYS_RESET = 1;
+
+my $reset_state_counter = 0;
+
+my $purchase_invoice_counter = 0; # used for generating purchase invnumber
+
+sub clear_up {
+ SL::DB::Manager::InvoiceItem->delete_all(all => 1);
+ SL::DB::Manager::Invoice->delete_all(all => 1);
+ SL::DB::Manager::PurchaseInvoice->delete_all(all => 1);
+ SL::DB::Manager::Part->delete_all(all => 1);
+ SL::DB::Manager::Customer->delete_all(all => 1);
+ SL::DB::Manager::Vendor->delete_all(all => 1);
+ SL::DB::Manager::BankAccount->delete_all(all => 1);
+ SL::DB::Manager::PaymentTerm->delete_all(all => 1);
+};
+
+sub reset_state {
+ my %params = @_;
+
+ return if $reset_state_counter;
+
+ $params{$_} ||= {} for qw(buchungsgruppe unit customer part tax vendor);
+
+ clear_up();
+
+
+ $buchungsgruppe = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%', %{ $params{buchungsgruppe} }) || croak "No accounting group";
+ $buchungsgruppe7 = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%') || croak "No accounting group for 7\%";
+ $unit = SL::DB::Manager::Unit->find_by(name => 'kg', %{ $params{unit} }) || croak "No unit";
+ $employee = SL::DB::Manager::Employee->current || croak "No employee";
+ $tax = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{tax} }) || croak "No tax";
+ $tax7 = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07) || croak "No tax for 7\%";
+ $taxzone = SL::DB::Manager::TaxZone->find_by( description => 'Inland') || croak "No taxzone";
+
+ $currency_id = $::instance_conf->get_currency_id;
+
+ $customer = SL::DB::Customer->new(
+ name => 'Test Customer',
+ currency_id => $currency_id,
+ taxzone_id => $taxzone->id,
+ %{ $params{customer} }
+ )->save;
+
+ $bank_account = SL::DB::BankAccount->new(
+ account_number => '123',
+ bank_code => '123',
+ iban => '123',
+ bic => '123',
+ bank => '123',
+ chart_id => SL::DB::Manager::Chart->find_by( description => 'Bank' )->id,
+ name => SL::DB::Manager::Chart->find_by( description => 'Bank' )->description,
+ )->save;
+
+ $payment_terms = SL::DB::PaymentTerm->new(
+ description => 'payment',
+ description_long => 'payment',
+ terms_netto => '30',
+ terms_skonto => '5',
+ percent_skonto => '0.05'
+ )->save;
+
+ $vendor = SL::DB::Vendor->new(
+ name => 'Test Vendor',
+ currency_id => $currency_id,
+ taxzone_id => $taxzone->id,
+ payment_id => $payment_terms->id,
+ %{ $params{vendor} }
+ )->save;
+
+
+ @parts = ();
+ push @parts, SL::DB::Part->new(
+ partnumber => 'T4254',
+ description => 'Fourty-two fifty-four',
+ lastcost => 1.93,
+ sellprice => 2.34,
+ buchungsgruppen_id => $buchungsgruppe->id,
+ unit => $unit->name,
+ %{ $params{part1} }
+ )->save;
+
+ push @parts, SL::DB::Part->new(
+ partnumber => 'T0815',
+ description => 'Zero EIGHT fifteeN @ 7%',
+ lastcost => 5.473,
+ sellprice => 9.714,
+ buchungsgruppen_id => $buchungsgruppe7->id,
+ unit => $unit->name,
+ %{ $params{part2} }
+ )->save;
+ push @parts, SL::DB::Part->new(
+ partnumber => '19%',
+ description => 'Testware 19%',
+ lastcost => 0,
+ sellprice => 50,
+ buchungsgruppen_id => $buchungsgruppe->id,
+ unit => $unit->name,
+ %{ $params{part3} }
+ )->save;
+ push @parts, SL::DB::Part->new(
+ partnumber => '7%',
+ description => 'Testware 7%',
+ lastcost => 0,
+ sellprice => 50,
+ buchungsgruppen_id => $buchungsgruppe7->id,
+ unit => $unit->name,
+ %{ $params{part4} }
+ )->save;
+
+ $reset_state_counter++;
+}
+
+sub new_invoice {
+ my %params = @_;
+
+ return SL::DB::Invoice->new(
+ customer_id => $customer->id,
+ currency_id => $currency_id,
+ employee_id => $employee->id,
+ salesman_id => $employee->id,
+ gldate => DateTime->today_local->to_kivitendo,
+ taxzone_id => $taxzone->id,
+ transdate => DateTime->today_local->to_kivitendo,
+ invoice => 1,
+ type => 'invoice',
+ %params,
+ );
+
+}
+
+sub new_purchase_invoice {
+ # my %params = @_;
+ # manually create a Kreditorenbuchung from scratch, ap + acc_trans bookings, as no helper exists yet, like $invoice->post.
+ # arap-Booking must come last in the acc_trans order
+ $purchase_invoice_counter++;
+
+ my $purchase_invoice = SL::DB::PurchaseInvoice->new(
+ vendor_id => $vendor->id,
+ invnumber => 'newap ' . $purchase_invoice_counter ,
+ currency_id => $currency_id,
+ employee_id => $employee->id,
+ gldate => DateTime->today_local->to_kivitendo,
+ taxzone_id => $taxzone->id,
+ transdate => DateTime->today_local->to_kivitendo,
+ invoice => 0,
+ type => 'invoice',
+ taxincluded => 0,
+ amount => '226',
+ netamount => '200',
+ paid => '0',
+ # %params,
+ )->save;
+
+ my $today = DateTime->today_local->to_kivitendo;
+ my $expense_chart = SL::DB::Manager::Chart->find_by(accno => '3400');
+ my $expense_chart_booking= SL::DB::AccTransaction->new(
+ trans_id => $purchase_invoice->id,
+ chart_id => $expense_chart->id,
+ chart_link => $expense_chart->link,
+ amount => '-100',
+ transdate => $today,
+ source => '',
+ taxkey => 9,
+ tax_id => SL::DB::Manager::Tax->find_by(taxkey => 9)->id);
+ $expense_chart_booking->save;
+
+ my $tax_chart = SL::DB::Manager::Chart->find_by(accno => '1576');
+ my $tax_chart_booking= SL::DB::AccTransaction->new(
+ trans_id => $purchase_invoice->id,
+ chart_id => $tax_chart->id,
+ chart_link => $tax_chart->link,
+ amount => '-19',
+ transdate => $today,
+ source => '',
+ taxkey => 0,
+ tax_id => SL::DB::Manager::Tax->find_by(taxkey => 9)->id);
+ $tax_chart_booking->save;
+ $expense_chart = SL::DB::Manager::Chart->find_by(accno => '3300');
+ $expense_chart_booking= SL::DB::AccTransaction->new(
+ trans_id => $purchase_invoice->id,
+ chart_id => $expense_chart->id,
+ chart_link => $expense_chart->link,
+ amount => '-100',
+ transdate => $today,
+ source => '',
+ taxkey => 8,
+ tax_id => SL::DB::Manager::Tax->find_by(taxkey => 8)->id);
+ $expense_chart_booking->save;
+
+
+ $tax_chart = SL::DB::Manager::Chart->find_by(accno => '1571');
+ $tax_chart_booking= SL::DB::AccTransaction->new(
+ trans_id => $purchase_invoice->id,
+ chart_id => $tax_chart->id,
+ chart_link => $tax_chart->link,
+ amount => '-7',
+ transdate => $today,
+ source => '',
+ taxkey => 0,
+ tax_id => SL::DB::Manager::Tax->find_by(taxkey => 8)->id);
+ $tax_chart_booking->save;
+ my $arap_chart = SL::DB::Manager::Chart->find_by(accno => '1600');
+ my $arap_booking= SL::DB::AccTransaction->new(trans_id => $purchase_invoice->id,
+ chart_id => $arap_chart->id,
+ chart_link => $arap_chart->link,
+ amount => '226',
+ transdate => $today,
+ source => '',
+ taxkey => 0,
+ tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
+ $arap_booking->save;
+
+ return $purchase_invoice;
+}
+
+sub new_item {
+ my (%params) = @_;
+
+ my $part = delete($params{part}) || $parts[0];
+
+ return SL::DB::InvoiceItem->new(
+ parts_id => $part->id,
+ lastcost => $part->lastcost,
+ sellprice => $part->sellprice,
+ description => $part->description,
+ unit => $part->unit,
+ %params,
+ );
+}
+
+sub number_of_payments {
+ my $transactions = shift;
+
+ my $number_of_payments;
+ my $paid_amount;
+ foreach my $transaction ( @$transactions ) {
+ if ( $transaction->chart_link =~ /(AR_paid|AP_paid)/ ) {
+ $paid_amount += $transaction->amount ;
+ $number_of_payments++;
+ };
+ };
+ return ($number_of_payments, $paid_amount);
+};
+
+sub total_amount {
+ my $transactions = shift;
+
+ my $total = sum map { $_->amount } @$transactions;
+
+ return $::form->round_amount($total, 5);
+
+};
+
+
+# test 1
+sub test_default_invoice_one_item_19_without_skonto() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item = new_item(qty => 2.5);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ my $purchase_invoice = new_purchase_invoice();
+
+
+ # default values
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{amount} = '6.96';
+ $params{payment_type} = 'without_skonto';
+
+ $invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, one item, 19% tax, without_skonto';
+
+ is($invoice->netamount, 5.85, "${title}: netamount");
+ is($invoice->amount, 6.96, "${title}: amount");
+ is($paid_amount, -6.96, "${title}: paid amount");
+ is($number_of_payments, 1, "${title}: 1 AR_paid booking");
+ is($invoice->paid, 6.96, "${title}: paid");
+ is($total, 0, "${title}: even balance");
+
+}
+
+sub test_default_invoice_one_item_19_without_skonto_overpaid() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item = new_item(qty => 2.5);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ my $purchase_invoice = new_purchase_invoice();
+
+
+ # default values
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{amount} = '16.96';
+ $params{payment_type} = 'without_skonto';
+ $invoice->pay_invoice( %params );
+
+ $params{amount} = '-10.00';
+ $invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, one item, 19% tax, without_skonto';
+
+ is($invoice->netamount, 5.85, "${title}: netamount");
+ is($invoice->amount, 6.96, "${title}: amount");
+ is($paid_amount, -6.96, "${title}: paid amount");
+ is($number_of_payments, 2, "${title}: 1 AR_paid booking");
+ is($invoice->paid, 6.96, "${title}: paid");
+ is($total, 0, "${title}: even balance");
+
+}
+
+
+# test 2
+sub test_default_invoice_two_items_19_7_tax_with_skonto() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item1 = new_item(qty => 2.5);
+ my $item2 = new_item(qty => 1.2, part => $parts[1]);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ # default values
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{payment_type} = 'with_skonto_pt';
+ $params{amount} = $invoice->amount_less_skonto;
+
+ $invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax with_skonto_pt';
+
+ is($invoice->netamount, 5.85 + 11.66, "${title}: netamount");
+ is($invoice->amount, 6.96 + 12.48, "${title}: amount");
+ is($paid_amount, -19.44, "${title}: paid amount");
+ is($invoice->paid, 19.44, "${title}: paid");
+ is($number_of_payments, 3, "${title}: 3 AR_paid bookings");
+ is($total, 0, "${title}: even balance");
+}
+
+sub test_default_invoice_two_items_19_7_tax_with_skonto_tax_included() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item1 = new_item(qty => 2.5);
+ my $item2 = new_item(qty => 1.2, part => $parts[1]);
+ my $invoice = new_invoice(
+ taxincluded => 1,
+ invoiceitems => [ $item1, $item2 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ # default values
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{payment_type} = 'with_skonto_pt';
+ $params{amount} = $invoice->amount_less_skonto;
+
+ $invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax with_skonto_pt';
+
+ is($invoice->netamount, 15.82, "${title}: netamount");
+ is($invoice->amount, 17.51, "${title}: amount");
+ is($paid_amount, -17.51, "${title}: paid amount");
+ is($invoice->paid, 17.51, "${title}: paid");
+ is($number_of_payments, 3, "${title}: 3 AR_paid bookings");
+ is($total, 0, "${title}: even balance");
+}
+
+# test 3 : two items, without skonto
+sub test_default_invoice_two_items_19_7_without_skonto() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item1 = new_item(qty => 2.5);
+ my $item2 = new_item(qty => 1.2, part => $parts[1]);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ # default values
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{amount} = '19.44'; # pass full amount
+ $params{payment_type} = 'without_skonto';
+
+ $invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax without skonto';
+
+ is($invoice->netamount, 5.85 + 11.66, "${title}: netamount");
+ is($invoice->amount, 6.96 + 12.48, "${title}: amount");
+ is($paid_amount, -19.44, "${title}: paid amount");
+ is($invoice->paid, 19.44, "${title}: paid");
+ is($number_of_payments, 1, "${title}: 1 AR_paid bookings");
+ is($total, 0, "${title}: even balance");
+}
+
+# test 4
+sub test_default_invoice_two_items_19_7_without_skonto_incomplete_payment() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item1 = new_item(qty => 2.5);
+ my $item2 = new_item(qty => 1.2, part => $parts[1]);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ $invoice->pay_invoice( amount => '9.44',
+ payment_type => 'without_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo,
+ );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax without skonto incomplete payment';
+
+ is($invoice->netamount, 5.85 + 11.66, "${title}: netamount");
+ is($invoice->amount, 6.96 + 12.48, "${title}: amount");
+ is($paid_amount, -9.44, "${title}: paid amount");
+ is($invoice->paid, 9.44, "${title}: paid");
+ is($number_of_payments, 1, "${title}: 1 AR_paid bookings");
+ is($total, 0, "${title}: even balance");
+}
+
+# test 5
+sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item1 = new_item(qty => 2.5);
+ my $item2 = new_item(qty => 1.2, part => $parts[1]);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ $invoice->pay_invoice( amount => '9.44',
+ payment_type => 'without_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+ $invoice->pay_invoice( amount => '10.00',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax not included';
+
+ is($invoice->netamount, 5.85 + 11.66, "${title}: netamount");
+ is($invoice->amount, 6.96 + 12.48, "${title}: amount");
+ is($paid_amount, -19.44, "${title}: paid amount");
+ is($invoice->paid, 19.44, "${title}: paid");
+ is($number_of_payments, 2, "${title}: 2 AR_paid bookings");
+ is($total, 0, "${title}: even balance");
+
+}
+
+# test 6
+sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item1 = new_item(qty => 2.5);
+ my $item2 = new_item(qty => 1.2, part => $parts[1]);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ $invoice->pay_invoice( amount => '9.44',
+ payment_type => 'without_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+ $invoice->pay_invoice( amount => '8.73',
+ payment_type => 'without_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+ $invoice->pay_invoice( amount => $invoice->open_amount,
+ payment_type => 'difference_as_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax not included';
+
+ is($invoice->netamount, 5.85 + 11.66, "${title}: netamount");
+ is($invoice->amount, 6.96 + 12.48, "${title}: amount");
+ is($paid_amount, -19.44, "${title}: paid amount");
+ is($invoice->paid, 19.44, "${title}: paid");
+ is($number_of_payments, 4, "${title}: 4 AR_paid bookings");
+ is($total, 0, "${title}: even balance");
+
+}
+
+sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto_1cent() {
+ reset_state() if $ALWAYS_RESET;
+
+ # if there is only one cent left there can only be one skonto booking, the
+ # error handling should choose the highest amount, which is the 7% account
+ # (11.66) rather than the 19% account (5.85). The actual tax amount is
+ # higher for the 19% case, though (1.11 compared to 0.82)
+
+ my $item1 = new_item(qty => 2.5);
+ my $item2 = new_item(qty => 1.2, part => $parts[1]);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ $invoice->pay_invoice( amount => '19.42',
+ payment_type => 'without_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+ $invoice->pay_invoice( amount => $invoice->open_amount,
+ payment_type => 'difference_as_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax not included';
+
+ is($invoice->netamount, 5.85 + 11.66, "${title}: netamount");
+ is($invoice->amount, 6.96 + 12.48, "${title}: amount");
+ is($paid_amount, -19.44, "${title}: paid amount");
+ is($invoice->paid, 19.44, "${title}: paid");
+ is($number_of_payments, 3, "${title}: 2 AR_paid bookings");
+ is($total, 0, "${title}: even balance");
+
+}
+
+sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto_2cent() {
+ reset_state() if $ALWAYS_RESET;
+
+ # if there are two cents left there will be two skonto bookings, 1 cent each
+ my $item1 = new_item(qty => 2.5);
+ my $item2 = new_item(qty => 1.2, part => $parts[1]);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ $invoice->pay_invoice( amount => '19.42',
+ payment_type => 'without_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+ $invoice->pay_invoice( amount => $invoice->open_amount,
+ payment_type => 'difference_as_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax not included';
+
+ is($invoice->netamount, 5.85 + 11.66, "${title}: netamount");
+ is($invoice->amount, 6.96 + 12.48, "${title}: amount");
+ is($paid_amount, -19.44, "${title}: paid amount");
+ is($invoice->paid, 19.44, "${title}: paid");
+ is($number_of_payments, 3, "${title}: 3 AR_paid bookings");
+ is($total, 0, "${title}: even balance");
+
+}
+
+sub test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item = new_item(qty => 2.5);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ # default values
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{amount} = '2.32';
+ $params{payment_type} = 'without_skonto';
+ $invoice->pay_invoice( %params );
+
+ $params{amount} = '3.81';
+ $params{payment_type} = 'without_skonto';
+ $invoice->pay_invoice( %params );
+
+ $params{amount} = $invoice->open_amount; # set amount, otherwise previous 3.81 is used
+ $params{payment_type} = 'difference_as_skonto';
+ $invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, one item, 19% tax, without_skonto';
+
+ is($invoice->netamount, 5.85, "${title}: netamount");
+ is($invoice->amount, 6.96, "${title}: amount");
+ is($paid_amount, -6.96, "${title}: paid amount");
+ is($number_of_payments, 3, "${title}: 3 AR_paid booking");
+ is($invoice->paid, 6.96, "${title}: paid");
+ is($total, 0, "${title}: even balance");
+
+}
+
+sub test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto_1cent() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item = new_item(qty => 2.5);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ # default values
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{amount} = '6.95';
+ $params{payment_type} = 'without_skonto';
+ $invoice->pay_invoice( %params );
+
+ $params{amount} = $invoice->open_amount; # set amount, otherwise previous value 6.95 is used
+ $params{payment_type} = 'difference_as_skonto';
+ $invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, one item, 19% tax, without_skonto';
+
+ is($invoice->netamount, 5.85, "${title}: netamount");
+ is($invoice->amount, 6.96, "${title}: amount");
+ is($paid_amount, -6.96, "${title}: paid amount");
+ is($number_of_payments, 2, "${title}: 3 AR_paid booking");
+ is($invoice->paid, 6.96, "${title}: paid");
+ is($total, 0, "${title}: even balance");
+
+}
+
+# test 3 : two items, without skonto
+sub test_default_purchase_invoice_two_charts_19_7_without_skonto() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $purchase_invoice = new_purchase_invoice();
+
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{amount} = '226'; # pass full amount
+ $params{payment_type} = 'without_skonto';
+
+ $purchase_invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($purchase_invoice->transactions);
+ my $total = total_amount($purchase_invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax without skonto';
+
+ is($paid_amount, 226, "${title}: paid amount");
+ is($number_of_payments, 1, "${title}: 1 AP_paid bookings");
+ is($total, 0, "${title}: even balance");
+
+}
+
+sub test_default_purchase_invoice_two_charts_19_7_with_skonto() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $purchase_invoice = new_purchase_invoice();
+
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ # $params{amount} = '226'; # pass full amount
+ $params{payment_type} = 'with_skonto_pt';
+
+ $purchase_invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($purchase_invoice->transactions);
+ my $total = total_amount($purchase_invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax without skonto';
+
+ is($paid_amount, 226, "${title}: paid amount");
+ is($number_of_payments, 3, "${title}: 1 AP_paid bookings");
+ is($total, 0, "${title}: even balance");
+
+}
+
+sub test_default_purchase_invoice_two_charts_19_7_tax_partial_unrounded_payment_without_skonto() {
+ # check whether unrounded amounts passed via $params{amount} are rounded for without_skonto case
+ reset_state() if $ALWAYS_RESET;
+ my $purchase_invoice = new_purchase_invoice();
+ $purchase_invoice->pay_invoice(
+ amount => ( $purchase_invoice->amount / 3 * 2),
+ payment_type => 'without_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+ my ($number_of_payments, $paid_amount) = number_of_payments($purchase_invoice->transactions);
+ my $total = total_amount($purchase_invoice->transactions);
+
+ my $title = 'default purchase_invoice, two charts, 19/7% tax multiple payments with final difference as skonto';
+
+ is($paid_amount, 150.67, "${title}: paid amount");
+ is($number_of_payments, 1, "${title}: 1 AP_paid bookings");
+ is($total, 0, "${title}: even balance");
+};
+
+
+sub test_default_purchase_invoice_two_charts_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $purchase_invoice = new_purchase_invoice();
+
+ # pay 2/3 and 1/5, leaves 3.83% to be used as Skonto
+ $purchase_invoice->pay_invoice(
+ amount => ( $purchase_invoice->amount / 3 * 2),
+ payment_type => 'without_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+ $purchase_invoice->pay_invoice(
+ amount => ( $purchase_invoice->amount / 5 ),
+ payment_type => 'without_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+ $purchase_invoice->pay_invoice(
+ payment_type => 'difference_as_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($purchase_invoice->transactions);
+ my $total = total_amount($purchase_invoice->transactions);
+
+ my $title = 'default purchase_invoice, two charts, 19/7% tax multiple payments with final difference as skonto';
+
+ is($paid_amount, 226, "${title}: paid amount");
+ is($number_of_payments, 4, "${title}: 1 AP_paid bookings");
+ is($total, 0, "${title}: even balance");
+
+}
+
+# test
+sub test_default_invoice_two_items_19_7_tax_with_skonto_50_50() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item1 = new_item(qty => 1, part => $parts[2]);
+ my $item2 = new_item(qty => 1, part => $parts[3]);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ # default values
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{amount} = $invoice->amount_less_skonto;
+ $params{payment_type} = 'with_skonto_pt';
+
+ $invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, two items, 19/7% tax with_skonto_pt 50/50';
+
+ is($invoice->netamount, 100, "${title}: netamount");
+ is($invoice->amount, 113, "${title}: amount");
+ is($paid_amount, -113, "${title}: paid amount");
+ is($invoice->paid, 113, "${title}: paid");
+ is($number_of_payments, 3, "${title}: 3 AR_paid bookings");
+ is($total, 0, "${title}: even balance");
+}
+
+# test
+sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item1 = new_item(qty => 0.5, part => $parts[2]);
+ my $item2 = new_item(qty => 0.5, part => $parts[3]);
+ my $item3 = new_item(qty => 0.5, part => $parts[2]);
+ my $item4 = new_item(qty => 0.5, part => $parts[3]);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2, $item3, $item4 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ # default values
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{amount} = $invoice->amount_less_skonto;
+ $params{payment_type} = 'with_skonto_pt';
+
+ $invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, four items, 19/7% tax with_skonto_pt 4x25';
+
+ is($invoice->netamount , 100 , "${title}: netamount");
+ is($invoice->amount , 113 , "${title}: amount");
+ is($paid_amount , -113 , "${title}: paid amount");
+ is($invoice->paid , 113 , "${title}: paid");
+ is($number_of_payments , 3 , "${title}: 3 AR_paid bookings");
+ is($total , 0 , "${title}: even balance");
+}
+
+sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_tax_included() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item1 = new_item(qty => 0.5, part => $parts[2]);
+ my $item2 = new_item(qty => 0.5, part => $parts[3]);
+ my $item3 = new_item(qty => 0.5, part => $parts[2]);
+ my $item4 = new_item(qty => 0.5, part => $parts[3]);
+ my $invoice = new_invoice(
+ taxincluded => 1,
+ invoiceitems => [ $item1, $item2, $item3, $item4 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ # default values
+ my %params = ( chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ $params{amount} = $invoice->amount_less_skonto;
+ $params{payment_type} = 'with_skonto_pt';
+
+ $invoice->pay_invoice( %params );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, four items, 19/7% tax with_skonto_pt 4x25';
+
+ is($invoice->netamount, 88.75, "${title}: netamount");
+ is($invoice->amount, 100, "${title}: amount");
+ is($paid_amount, -100, "${title}: paid amount");
+ is($invoice->paid, 100, "${title}: paid");
+ is($number_of_payments, 3, "${title}: 3 AR_paid bookings");
+# currently this test fails because the code writing the invoice is buggy, the calculation of skonto is correct
+ is($total, 0, "${title}: even balance: this will fail due to rounding error in invoice post, not the skonto");
+}
+
+sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_multiple() {
+ reset_state() if $ALWAYS_RESET;
+
+ my $item1 = new_item(qty => 0.5, part => $parts[2]);
+ my $item2 = new_item(qty => 0.5, part => $parts[3]);
+ my $item3 = new_item(qty => 0.5, part => $parts[2]);
+ my $item4 = new_item(qty => 0.5, part => $parts[3]);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2, $item3, $item4 ],
+ payment_id => $payment_terms->id,
+ );
+ $invoice->post;
+
+ $invoice->pay_invoice( amount => '90',
+ payment_type => 'without_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+ $invoice->pay_invoice( payment_type => 'difference_as_skonto',
+ chart_id => $bank_account->chart_id,
+ transdate => DateTime->today_local->to_kivitendo
+ );
+
+ my ($number_of_payments, $paid_amount) = number_of_payments($invoice->transactions);
+ my $total = total_amount($invoice->transactions);
+
+ my $title = 'default invoice, four items, 19/7% tax with_skonto_pt 4x25';
+
+ is($invoice->netamount, 100, "${title}: netamount");
+ is($invoice->amount, 113, "${title}: amount");
+ is($paid_amount, -113, "${title}: paid amount");
+ is($invoice->paid, 113, "${title}: paid");
+ is($number_of_payments, 3, "${title}: 3 AR_paid bookings");
+ is($total, 0, "${title}: even balance: this will fail due to rounding error in invoice post, not the skonto");
+}
+
+Support::TestSetup::login();
+ # die;
+
+# test cases: without_skonto
+ test_default_invoice_one_item_19_without_skonto();
+ test_default_invoice_two_items_19_7_tax_with_skonto();
+ test_default_invoice_two_items_19_7_without_skonto();
+ test_default_invoice_two_items_19_7_without_skonto_incomplete_payment();
+ test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments();
+ test_default_purchase_invoice_two_charts_19_7_without_skonto();
+ test_default_purchase_invoice_two_charts_19_7_tax_partial_unrounded_payment_without_skonto();
+ test_default_invoice_one_item_19_without_skonto_overpaid();
+
+# test cases: difference_as_skonto
+ test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto();
+ test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto_1cent();
+ test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto_2cent();
+ test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto();
+ test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto_1cent();
+ test_default_purchase_invoice_two_charts_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto();
+
+# test cases: with_skonto_pt
+ test_default_invoice_two_items_19_7_tax_with_skonto_50_50();
+ test_default_invoice_four_items_19_7_tax_with_skonto_4x_25();
+ test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_multiple();
+ test_default_purchase_invoice_two_charts_19_7_with_skonto();
+ test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_tax_included();
+ test_default_invoice_two_items_19_7_tax_with_skonto_tax_included();
+
+# remove all created data at end of test
+clear_up();
+
+done_testing();
+
+1;
<td>[% 'tax_chartaccno' | $T8 %]</td>
<td><select name="chart_id"><option value="">[% 'None' | $T8 %]</option>[% FOREACH row = ACCOUNTS %]<option value="[% HTML.escape(row.id) %]" [% IF row.selected %]selected[% END %]>[% HTML.escape(row.taxaccount) %]</option>[% END %]</select></td>
</tr>
-
+ <tr>
+ <td>[% 'Automatic skonto chart sales' | $T8 %]</td>
+ <td> [% L.select_tag('skonto_sales_chart_id', AR_PAID, value_title_sub = \skontochart_value_title_sub, with_empty = 1, default = skonto_sales_chart_id) %]</td>
+ </tr>
+ <tr>
+ <td>[% 'Automatic skonto chart purchase' | $T8 %]</td>
+ <td> [% L.select_tag('skonto_purchase_chart_id', AP_PAID, value_title_sub = \skontochart_value_title_sub, with_empty = 1, default = skonto_purchase_chart_id) %]</td>
+ </tr>
+ <tr>
<td>[% 'Account categories' | $T8 %]</td>
<td><table>
<colgroup>
<th class="listheading">[% 'tax_rate' | $T8 %]</th>
<th class="listheading">[% 'taxnumber' | $T8 %]</th>
<th class="listheading">[% 'account_description' | $T8 %]</th>
+ <th class="listheading">[% 'Automatic skonto chart sales' | $T8 %]</th>
+ <th class="listheading">[% 'Automatic skonto chart purchase' | $T8 %]</th>
</tr>
[% SET row_odd = '1' %][% FOREACH row = TAX %]
<td align="right">[% HTML.escape(row.rate) %] %</td>
<td align="right">[% HTML.escape(row.taxnumber) %]</td>
<td>[% HTML.escape(row.account_description) %]</td>
+ <td>[% HTML.escape(row.skonto_chart_accno) %] [% HTML.escape(row.skonto_chart_description) %]</td>
+ <td>[% HTML.escape(row.skonto_chart_purchase_accno) %] [% HTML.escape(row.skonto_chart_purchase_description) %]</td>
</tr>
[% END %]
</table>
<table id='filter_table'>
<tr>
<th align="right">[% 'Bank account' | $T8 %]</th>
- <td>[% L.select_tag('filter.local_bank_account_id', BANK_ACCOUNTS, default=filter.local_bank_account_id, title_sub=\label_sub, with_empty=1, style='width:250px') %]</td>
+ <td>[% L.select_tag('filter.local_bank_account_id', BANK_ACCOUNTS, default=filter.local_bank_account_id, title_key='displayable_name', with_empty=1, style='width:500px') %]</td>
</tr>
<tr>
<th>[%- LxERP.t8("Invoice number") %]</th>
<th>[%- LxERP.t8("Amount") %]</th>
<th>[%- LxERP.t8("Open amount") %]</th>
+ <th>[%- LxERP.t8("Amount less skonto") %]</th>
<th>[%- LxERP.t8("Transdate") %]</th>
<th>[%- LxERP.t8("Customer/Vendor number") %]</th>
<th>[%- LxERP.t8("Customer/Vendor name") %]</th>
<td>[%- invoice.invnumber %]</td>
<td align="right">[%- LxERP.format_amount(invoice.amount, 2) %]</td>
<td align="right">[%- LxERP.format_amount(invoice.amount - invoice.paid, 2) %]</td>
+ <td align="right">[%- LxERP.format_amount(invoice.amount_less_skonto, 2) %]</td>
<td align="right">[%- invoice.transdate_as_date %]</td>
<td>[%- invoice.vendor.vendornumber %][%- invoice.customer.customernumber %]</td>
<td>[%- invoice.vendor.name %][%- invoice.customer.name %]</td>
[%- USE HTML %][%- USE L %][%- USE LxERP %][%- USE T8 %]
+[% SET debug = 0 %]
+
<form method="post" action="javascript:filter_invoices();">
- <b>Transaction</b>
+ <b>[%- LxERP.t8("Bank transaction") %]:</b>
<table>
<tr class="listheading">
- <td>[%- LxERP.t8("ID") %]:</td>
+ [% IF debug %]<td>[%- LxERP.t8("ID") %]:</td>[% END %]
+ <td>[%- LxERP.t8("Transdate") %]:</td>
<td>[%- LxERP.t8("Amount") %]:</td>
- <td>[%- LxERP.t8("Remote bank code") %]:</td>
- <td>[%- LxERP.t8("Remote account number") %]:</td>
<td>[%- LxERP.t8("Remote name") %]:</td>
<td>[%- LxERP.t8("Purpose") %]:</td>
- <td>[%- LxERP.t8("Transdate") %]:</td>
+ <td>[%- LxERP.t8("Remote account number") %]:</td>
+ <td>[%- LxERP.t8("Remote bank code") %]:</td>
</tr>
<tr class="listrow">
- <td>[% SELF.transaction.id %]</td>
+ [% IF debug %]<td>[% SELF.transaction.id %]</td>[% END %]
+ <td>[% SELF.transaction.transdate_as_date %]</td>
<td>[% LxERP.format_amount(SELF.transaction.amount, 2) %]</td>
- <td>[% SELF.transaction.remote_bank_code %]</td>
- <td>[% SELF.transaction.remote_account_number %]</td>
<td>[% SELF.transaction.remote_name %]</td>
<td>[% SELF.transaction.purpose %]</td>
- <td>[% SELF.transaction.transdate_as_date %]</td>
+ <td>[% SELF.transaction.remote_account_number %]</td>
+ <td>[% SELF.transaction.remote_bank_code %]</td>
</tr>
</table>
- <b>Filter</b>
+ <b>[%- LxERP.t8("Invoice filter") %]:</b>
<table>
<tr>
<th align="right">[%- LxERP.t8("Invoice number") %]</th>
</tr>
<tr>
- <th align="right">[%- LxERP.t8("Transdate from") %]</th>
+ <th align="right">[%- LxERP.t8("Invdate from") %]</th>
<td>[% L.date_tag('transdatefrom') %]</td>
<th align="right">[%- LxERP.t8("to (date)") %]</th>
[% FOREACH draft = DRAFTS %]
<tr class="listrow[% loop.count % 2 %]">
- <td><a href="[% draft.module %].pl?action=load_draft&id=[% HTML.url(draft.id) %]&amount_1=[% -1 * SELF.transaction.amount_as_number %]&transdate=[% HTML.url(SELF.transaction.transdate_as_date) %]&duedate=[% HTML.url(SELF.transaction.transdate_as_date) %]&datepaid_1=[% HTML.url(SELF.transaction.transdate_as_date) %]&paid_1=[% -1 * SELF.transaction.amount_as_number %]¤cy=[% HTML.url(SELF.transaction.currency.name) %]&AP_paid_1=[% HTML.url(SELF.transaction.local_bank_account.chart.accno) %]&remove_draft=0&callback=[% HTML.url(callback) %]">[% HTML.escape(draft.description) %]</a></td>
+ <td><a href="[% draft.module %].pl?action=load_draft&id=[% HTML.url(draft.id) %]&amount_1=[% LxERP.format_amount(-1 * SELF.transaction.amount, 2) %]&transdate=[% HTML.url(SELF.transaction.transdate_as_date) %]&duedate=[% HTML.url(SELF.transaction.transdate_as_date) %]&datepaid_1=[% HTML.url(SELF.transaction.transdate_as_date) %]&paid_1=[% LxERP.format_amount(-1 * SELF.transaction.amount, 2) %]¤cy=[% HTML.url(SELF.transaction.currency.name) %]&AP_paid_1=[% HTML.url(SELF.transaction.local_bank_account.chart.accno) %]&remove_draft=0&callback=[% HTML.url(callback) %]">[% HTML.escape(draft.description) %]</a></td>
<td>[% HTML.escape(draft.vendor) %]</td>
<td>[% HTML.escape(draft.employee.name) %]</td>
<td>[% HTML.escape(draft.itime_as_date) %]</td>
[%- INCLUDE 'common/flash.html' %]
-<p>[% 'Account number' | $T8 %] [% bank_account.account_number %], [% 'Bank code' | $T8 %] [% bank_account.bank_code %], [% 'Bank' | $T8 %] [% bank_account.bank %]</p>
+<p>[% HTML.escape(bank_account.name) %] [% HTML.escape(bank_account.iban) %], [% 'Bank code' | $T8 %] [% HTML.escape(bank_account.bank_code) %], [% 'Bank' | $T8 %] [% HTML.escape(bank_account.bank) %]</p>
<p>
[% IF FORM.filter.fromdate %] [% 'From' | $T8 %] [% FORM.filter.fromdate %] [% END %]
[% IF FORM.filter.todate %] [% 'to (date)' | $T8 %] [% FORM.filter.todate %][% END %]
}
function add_invoices(bt_id, prop_id, prop_invnumber) {
- //prop_id is a proposed invoice_id
+ // prop_id is a proposed invoice_id
+ // remove the added invoice from all the other suggestions
var number_of_elements = document.getElementsByName(prop_id).length;
for( var i = 0; i < number_of_elements; i++ ) {
var node = document.getElementsByName(prop_id)[0];
}
UnTip();
var invoices = document.getElementById('assigned_invoices_' + bt_id);
- var div_element = '<div id="' + bt_id + '.' + prop_id + '">';
- var hidden_element = '<input type="hidden" name="invoice_ids.' + bt_id + '[]" value="' + prop_id + '">' + prop_invnumber;
- var link_element = '<a href=# onclick="delete_invoice(' + bt_id + ',' + prop_id + ');">x</a>';
- var new_html = div_element + hidden_element + link_element + '</div>';
- invoices.innerHTML += new_html;
+
+ $.ajax({
+ url: 'controller.pl?action=BankTransaction/ajax_payment_suggestion&bt_id=' + bt_id + '&prop_id=' + prop_id,
+ success: function(data) {
+ invoices.innerHTML += data.html;
+ }
+ });
}
function delete_invoice(bt_id, prop_id) {
<p>
<table>
-<!--
- <tr>
- <th align="right">[% 'Valutadate from' | $T8 %]</th>
- <td>[% L.date_tag('filter.valutadate:date::ge', filter.valutadate_date__ge) %]</td>
- </tr>
-
- <tr>
- <th align="right">[% 'Valutadate to' | $T8 %]</th>
- <td>[% L.date_tag('filter.valutadate:date::le', filter.valutadate_date__le) %]</td>
- </tr>
-
- <tr>
- <th align="right">[% 'Remote name' | $T8 %]</th>
- <td>[% L.input_tag('filter.remote_name:substr::ilike', filter.remote_name_substr__ilike, size=60, class='initial_focus') %]</td>
- </tr>
-
- <tr>
- <th align="right">[% 'Remote account number' | $T8 %]</th>
- <td>[% L.input_tag('filter.remote_account_number:substr::ilike', filter.remote_account_number_substr__ilike, size=60, class='initial_focus') %]</td>
- </tr>
-
- <tr>
- <th align="right">[% 'Remote bank code' | $T8 %]</th>
- <td>[% L.input_tag('filter.remote_bank_code:substr::ilike', filter.remote_bank_code_substr__ilike, size=60, class='initial_focus') %]</td>
- </tr>
-
- <tr>
- <th align="right">[% 'Amount' | $T8 %]</th>
- <td>[% L.input_tag('filter.amount:number', filter.amount_number, size = 20) %]</td>
- </tr>
-
- <tr>
- <th align="right">[% 'Purpose' | $T8 %]</th>
- <td>[% L.input_tag('filter.purpose:substr::ilike', filter.purpose_substr__ilike, size=60, class='initial_focus') %]</td>
- </tr>
- -->
<tr>
<th align="right">[% 'Bank account' | $T8 %]</th>
- <td>[% L.select_tag('filter.bank_account', BANK_ACCOUNTS, default=bank_acount, title_sub=\label_sub, with_empty=0, style='width:450px') %]</td>
+ <td>[% L.select_tag('filter.bank_account', BANK_ACCOUNTS, default=bank_account, title_key='displayable_name', with_empty=0, style='width:450px') %]</td>
</tr>
<tr>
[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+[% SET debug=1 %]
+
<table id="bt_list">
<thead>
<tr class="listheading">
<th></th>
<th></th>
<th>[% 'Assigned invoices' | $T8 %]</th>
+ [% IF debug %]
+ <th>[% 'Score' | $T8 %]</th>
+ [% END %]
<th>[% IF FORM.sort_by == 'proposal'%]
<a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=proposal&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
[% 'Proposal' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
[% 'Proposal' | $T8 %]</a>
[% END %]
</th>
- <th>[% IF FORM.sort_by == 'remote_bank_code'%]
- <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_bank_code&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
- [% 'Remote bank code' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
- [% ELSE %]
- <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_bank_code&sort_dir=0" class="sort_link">
- [% 'Remote bank code' | $T8 %]</a>
- [% END %]
- </th>
- <th>[% IF FORM.sort_by == 'remote_account_number'%]
- <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_account_number&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
- [% 'Remote account number' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
- [% ELSE %]
- <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_account_number&sort_dir=0" class="sort_link">
- [% 'Remote account number' | $T8 %]</a>
- [% END %]
- </th>
<th>[% IF FORM.sort_by == 'transdate'%]
<a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=transdate&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
[% 'Transdate' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
[% 'Transdate' | $T8 %]</a>
[% END %]
</th>
- <th>[% IF FORM.sort_by == 'valutadate'%]
- <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=valutadate&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
- [% 'Valutadate' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
- [% ELSE %]
- <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=valutadate&sort_dir=0" class="sort_link">
- [% 'Valutadate' | $T8 %]</a>
- [% END %]
- </th>
<th>[% IF FORM.sort_by == 'amount'%]
<a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=amount&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
[% 'Amount' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
[% END %]
</th>
<th>[% 'Assigned' | $T8 %]</th>
- <th>[% 'Currency' | $T8 %]</th>
<th>[% IF FORM.sort_by == 'remote_name'%]
<a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_name&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
[% 'Remote name' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
[% 'Remote name' | $T8 %]</a>
[% END %]
</th>
- <th>[% 'Remote name 1' | $T8 %]</th>
<th>[% 'Purpose' | $T8 %]</th>
+ <th>[% IF FORM.sort_by == 'remote_account_number'%]
+ <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_account_number&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+ [% 'Remote account number' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+ [% ELSE %]
+ <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_account_number&sort_dir=0" class="sort_link">
+ [% 'Remote account number' | $T8 %]</a>
+ [% END %]
+ </th>
+ <th>[% IF FORM.sort_by == 'remote_bank_code'%]
+ <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_bank_code&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+ [% 'Remote bank code' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+ [% ELSE %]
+ <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_bank_code&sort_dir=0" class="sort_link">
+ [% 'Remote bank code' | $T8 %]</a>
+ [% END %]
+ </th>
+ <th>[% IF FORM.sort_by == 'valutadate'%]
+ <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=valutadate&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+ [% 'Valutadate' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+ [% ELSE %]
+ <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=valutadate&sort_dir=0" class="sort_link">
+ [% 'Valutadate' | $T8 %]</a>
+ [% END %]
+ </th>
+ <th>[% 'Currency' | $T8 %]</th>
</tr>
</thead>
<tr class="listrow" id="bt_id_[% bt.id %]">
<td><a href=# onclick="assign_invoice('[% bt.id %]'); return false;">[% 'Assign invoice' | $T8 %]</a></td>
<td><a href=# onclick="create_invoice('[% bt.id %]'); return false;">[% 'Create invoice' | $T8 %]</a></td>
- <td id="assigned_invoices_[% bt.id %]"></td>
+ <td id="assigned_invoices_[% bt.id %]" nowrap></td>
+ [% IF debug %]
+ <td onmouseover="Tip('[% FOREACH match = bt.rule_matches %] [% match %]<br> [% END %]')" onmouseout="UnTip()">[% bt.agreement %]</td>
+ [% END %]
<td>
[% FOREACH prop = bt.proposals %]
<div name='[% prop.id %]'> <a href=# onclick="add_invoices('[% bt.id %]', '[% prop.id %]', '[% HTML.escape(prop.invnumber) %]');"
- onmouseover="Tip('<table><tr><th></th><td>[% 'Suggested invoice' | $T8 %][% IF !prop.is_sales %] [% '(Purchase)' | $T8 %][% END %]</td><td>[% 'Bank transaction' | $T8 %]</td></tr><tr><th>[% 'Amount' | $T8 %]</th><td>[% LxERP.format_amount(prop.amount, 2) %]</td><td>[% LxERP.format_amount(bt.amount, 2) %]</td></tr><tr><th>[% 'Customer/Vendor' | $T8 %]</th><td>[% HTML.escape(prop.customer.name) %][% HTML.escape(prop.vendor.name) %]</td><td>[% HTML.escape(bt.remote_name) %]</td></tr><tr><th>[% 'Customer/Vendor Number' | $T8 %]</th><td>[% HTML.escape(prop.customer.customernumber) %][% HTML.escape(prop.vendor.vendornumber) %]</td><td></td></tr><tr><th>[% 'Invoice Date' | $T8 %]</th><td>[% HTML.escape(prop.transdate_as_date) %]</td><td>[% HTML.escape(bt.transdate_as_date) %] ([% HTML.escape(bt.transdate.utc_rd_days - prop.transdate.utc_rd_days) %])</td></tr><tr><th>[% 'Invoice Number' | $T8 %]</th><td>[% HTML.escape(prop.invnumber) %]</td><td>[% HTML.escape(bt.purpose) %]</td></tr></table>')" onmouseout="UnTip()"
+ onmouseover="Tip('<table><tr><th></th><th>[% 'Suggested invoice' | $T8 %][% IF !prop.is_sales %] ([% 'AP' | $T8 %])[% END %]</th><th>[% 'Bank transaction' | $T8 %]</th></tr><tr><th>[% 'Amount' | $T8 %]</th><td>[% LxERP.format_amount(prop.amount, 2) %] ([% 'open' | $T8 %]: [% LxERP.format_amount(prop.open_amount, 2) %])</td><td>[% LxERP.format_amount(bt.amount, 2) %]</td></tr>[% IF prop.skonto_date %]<tr><th>[% 'Payment terms' | $T8 %]</th><td>[% LxERP.format_amount(prop.amount_less_skonto, 2) %] [% 'until' | $T8 %] [% HTML.escape(prop.skonto_date.to_kivitendo) %] ([% prop.percent_skonto * 100 %] %)</td><td></td></tr>[% END %]<tr><th>[% 'Customer/Vendor' | $T8 %]</th><td>[% HTML.escape(prop.customer.displayable_name) %][% HTML.escape(prop.vendor.displayable_name) %]</td><td>[% HTML.escape(bt.remote_name) %]</td></tr><tr><th>[% 'Invoice Date' | $T8 %]</th><td>[% HTML.escape(prop.transdate_as_date) %]</td><td>[% HTML.escape(bt.transdate_as_date) %] ([% HTML.escape(bt.transdate.utc_rd_days - prop.transdate.utc_rd_days) %])</td></tr><tr><th>[% 'Invoice Number' | $T8 %]</th><td>[% HTML.escape(prop.invnumber) %]</td><td>[% HTML.escape(bt.purpose) %]</td></tr></table>')" onmouseout="UnTip()"
class=[% IF bt.agreement >= 5 %]"green"[% ELSIF bt.agreement < 5 and bt.agreement >= 3 %]"orange"[% ELSE %]"red"[% END %]>←[% HTML.escape(prop.invnumber)%]</a></div>
[% END %]
</td>
- <td>[% HTML.escape(bt.remote_bank_code) %]</td>
- <td>[% HTML.escape(bt.remote_account_number) %]</td>
<td align=right>[% bt.transdate_as_date %]</td>
- <td align=right>[% bt.valutadate_as_date %]</td>
<td align=right>[% bt.amount_as_number %]</td>
<td align=right>[% bt.invoice_amount_as_number %]</td>
- <td align=center>[% HTML.escape(bt.currency.name) %]</td>
<td>[% HTML.escape(bt.remote_name) %]</td>
- <td>[% HTML.escape(bt.remote_name_1) %]</td>
<td>[% HTML.escape(bt.purpose) %]</td>
+ <td>[% HTML.escape(bt.remote_account_number) %]</td>
+ <td>[% HTML.escape(bt.remote_bank_code) %]</td>
+ <td align=right>[% bt.valutadate_as_date %]</td>
+ <td align=center>[% HTML.escape(bt.currency.name) %]</td>
</tr>
[%- END %]
</tbody>
<tr class="listheading">
<th>[% L.checkbox_tag('check_all') %]</th>
- <th>[% 'Typ' | $T8 %]</th>
+ <th>[% 'Type' | $T8 %]</th>
<th>[% 'ID' | $T8 %]</th>
<th>[% 'Transdate' | $T8 %]</th>
<th>[% 'Amount' | $T8 %]</th>
[% FOREACH proposed_invoice = proposal.proposals %]
<tr>
- <td>[% 'Rechnung/Buchung' | $T8 %]</td>
+ <td>[% 'Invoice' | $T8 %]</td>
<td>[% proposed_invoice.id %]</td>
<td>[% proposed_invoice.transdate_as_date %]</td>
<td>[% proposed_invoice.amount_as_number %]</td>
- <td>[% HTML.escape(proposed_invoice.invnumber) %]</td>
+ <td>[% proposed_invoice.link %]</td>
<td>[% HTML.escape(proposed_invoice.customer.name) %][% HTML.escape(proposed_invoice.vendor.name) %]</td>
</tr>
[% L.hidden_tag("proposed_invoice_" _ proposal.id, proposed_invoice.id) %]
--- /dev/null
+[% USE LxERP %]
+[% USE L %]
+<tr>
+ <th align="right">[%- LxERP.t8("Existing bank transactions") %]:</th>
+ <td colspan="10">
+ [% opts = [ [ 'skip', LxERP.t8('Skip entry') ] , [ 'insert_new', LxERP.t8('Insert new') ] ] %]
+ [% L.select_tag('settings.update_policy', opts, default = SELF.profile.get('update_policy'), style = 'width: 300px') %]
+ </td>
+</tr>
+[% USE LxERP %]
+[% USE L %]
-<script>
-$(function() {
- $("input[name=sep_char][value='semicolon']").prop('checked', true);
- $('#settings_numberformat option')[3].selected = true;
-});
-</script>
+<tr>
+ <th align="right">[%- LxERP.t8("Existing bank transactions") %]:</th>
+ <td colspan="10">
+ [% opts = [ [ 'skip', LxERP.t8('Skip entry') ] , [ 'insert_new', LxERP.t8('Insert new') ] ] %]
+ [% L.select_tag('settings.update_policy', opts, default = SELF.profile.get('update_policy'), style = 'width: 300px') %]
+ </td>
+</tr>
[%- INCLUDE 'csv_import/_form_orders.html' %]
[%- ELSIF SELF.type == 'mt940' %]
[%- INCLUDE 'csv_import/_form_mt940.html' %]
+[%- ELSIF SELF.type == 'bank_transactions' %]
+ [%- INCLUDE 'csv_import/_form_banktransactions.html' %]
[%- END %]
<tr>
[%- USE L %]
[%- USE LxERP %]
+[% SET debug = 0 %]
+
[% IF !SELF.LINKED_TRANSACTIONS.size %]
<tbody class="listrow">
<td colspan="11"><p class="message_hint">[% 'No data was found.' | $T8 %]</p></td>
<td><img width="16px" height="16px" src="image/bank-building.jpg"></td>
<td>[% 'Bank Transaction' | $T8 %]</td>
- <td>[% HTML.escape(bt.id) %]</td>
+ [% IF debug %]<td>[% HTML.escape(bt.id) %]</td>[% END %]
<td align="right" class="[% HTML.escape(bt.class) %]">[% HTML.escape(bt.transdate_as_date) %]</td>
<td align="right" class="[% HTML.escape(bt.class) %]">[% HTML.escape(bt.amount_as_number) %]</td>
<td></td>
<tr>
<td><div class="icon16 general-ledger--reports--journal"></div></td>
<td>[% 'Acc Transaction' | $T8 %]</td>
- <td>[% HTML.escape(bb.acc_trans_id) %]</td>
+ [% IF debug %]<td>[% HTML.escape(bb.acc_trans_id) %]</td>[% END %]
<td align="right" class="[% HTML.escape(bb.class) %]">[% HTML.escape(bb.transdate_as_date) %]</td>
<td></td>
<td align="right" class="[% HTML.escape(bb.class) %]">[% LxERP.format_amount(-1 * bb.amount, 2) %]</td>
- <td>[% HTML.escape(bb.get_transaction.customer.name) %][% HTML.escape(bb.get_transaction.vendor.name) %][% HTML.escape(bb.get_transaction.description) %]</td>
- <td>[% bb.get_transaction.link %]</td>
+ <td>[% HTML.escape(bb.record.customer.name) %][% HTML.escape(bb.record.vendor.name) %][% HTML.escape(bb.record.description) %]</td>
+ <td>[% bb.record.link %] [% HTML.escape(bb.source) %] [% HTML.escape(bb.memo) %]</td>
<td></td>
<td></td>
<td>[% HTML.escape(bb.source) %]</td>
<td><img width="16px" height="16px" src="image/bank-building.jpg"></td>
<td>[% 'Bank Transaction' | $T8 %]</td>
- <td>[% HTML.escape(bt.id) %]</td>
+ [% IF debug %]<td>[% HTML.escape(bt.id) %]</td>[% END %]
<td align="right">[% HTML.escape(bt.transdate_as_date) %]</td>
<td align="right">[% HTML.escape(bt.amount_as_number) %]</td>
<td></td>
<td><div class="icon16 general-ledger--reports--journal"></div></td>
<td>[% 'Acc Transaction' | $T8 %]</td>
- <td>[% HTML.escape(bb.acc_trans_id) %]</td>
+ [% IF debug %]<td>[% HTML.escape(bb.acc_trans_id) %]</td>[% END %]
<td align="right">[% HTML.escape(bb.transdate_as_date) %]</td>
<td></td>
<td align="right">[% LxERP.format_amount(-1 * bb.amount, 2) %]</td>
- <td>[% HTML.escape(bb.get_transaction.customer.name) %][% HTML.escape(bb.get_transaction.vendor.name) %][% HTML.escape(bb.get_transaction.description) %]</td>
- <td>[% bb.get_transaction.link %]</td>
+ <td>[% HTML.escape(bb.record.customer.name) %][% HTML.escape(bb.record.vendor.name) %][% HTML.escape(bb.record.description) %]</td>
+ <td>[% bb.record.link %] [% HTML.escape(bb.source) %] [% HTML.escape(bb.memo) %]</td>
<td></td>
<td></td>
<td>[% HTML.escape(bb.source) %]</td>
</tr>
</tbody>
</table>
- [% IF show_button %][% L.button_tag("submit_with_action('reconciliate')", LxERP.t8("Reconciliate")) %][% END %]
+ [% IF show_button %][% L.button_tag("submit_with_action('reconcile')", LxERP.t8("Reconcile")) %][% END %]
[% END %]
<td>[% L.select_tag('filter.local_bank_account_id:number',
SELF.BANK_ACCOUNTS,
default=FORM.filter.local_bank_account_id_number,
- title_sub=\label_sub, with_empty=0,
+ title_key='displayable_name',
+ with_empty=0,
style='width:450px',
onchange='filter_table();') %]</td>
</tr>
<!--
function load_proposals () {
- var url="controller.pl?action=Reconciliation/load_proposals&" + $('#reconciliation_form') . serialize();
+ var url="controller.pl?action=Reconciliation/load_proposals";
$.ajax({
url: url,
+ type: "POST",
+ data: $('#reconciliation_form').serialize(),
success: function(new_data) {
$('#overview').html('');
$('#automatic').html(new_data['html']);
}
function load_overview () {
- var url="controller.pl?action=Reconciliation/load_overview&" + $('#reconciliation_form') . serialize();
+ var url="controller.pl?action=Reconciliation/load_overview";
$.ajax({
url: url,
+ type: "GET",
+ data: $('#reconciliation_form').serialize(),
success: function(new_data) {
$('#overview').html(new_data['html']);
$('#automatic').html('');
[%- USE L %]
[%- USE LxERP %]
+[% SET debug = 0 %]
+
[% IF !SELF.PROPOSALS.size %]
<tbody class="listrow">
<td colspan="11"><p class="message_hint">[% 'No data was found.' | $T8 %]</p></td>
<td><img width="16px" height="16px" src="image/bank-building.jpg"></td>
<td>[% 'Bank Transaction' | $T8 %]</td>
- <td>[% HTML.escape(proposal.BT.id) %]</td>
+ [% IF debug %] <td>[% HTML.escape(proposal.BT.id) %]</td>[% END %]
<td align="right">[% HTML.escape(proposal.BT.transdate_as_date) %]</td>
<td align="right">[% HTML.escape(proposal.BT.amount_as_number) %]</td>
<td></td>
[% FOREACH bb = proposal.BB %]
<tr>
<td><div class="icon16 general-ledger--reports--journal"></div></td>
- <td>[% 'Acc Transaction' | $T8 %]</td>
- <td>[% HTML.escape(bb.acc_trans_id) %]</td>
+ <td>[% 'Invoice' | $T8 %]</td>
+ [% IF debug %] <td>[% HTML.escape(bb.acc_trans_id) %]</td>[% END %]
<td align="right">[% HTML.escape(bb.transdate_as_date) %]</td>
<td></td>
<td align="right">[% LxERP.format_amount(-1 * bb.amount, 2) %]</td>
- <td>[% HTML.escape(bb.get_transaction.customer.name) %][% HTML.escape(bb.get_transaction.vendor.name) %][% HTML.escape(bb.get_transaction.description) %]</td>
- <td>[% bb.get_transaction.link %]</td>
+ <td>[% HTML.escape(bb.record.customer.name) %][% HTML.escape(bb.record.vendor.name) %][% HTML.escape(bb.record.description) %]</td>
+ <td>[% bb.record.link %]</td>
<td></td>
<td></td>
<td>[% HTML.escape(bb.source) %]</td>
<table>
<tr>
<th align="right">[% 'Bank account' | $T8 %]</th>
- <td>[% L.select_tag('filter.local_bank_account_id:number', SELF.BANK_ACCOUNTS, default=bank_account, title_sub=\label_sub, with_empty=0, style='width:450px') %]</td>
+ <td>[% L.select_tag('filter.local_bank_account_id:number', SELF.BANK_ACCOUNTS, title_key='displayable_name', with_empty=0, style='width:450px') %]</td>
</tr>
<tr>
<tr>
<th align="right">[% 'Cleared/uncleared only' | $T8 %]</th>
- <td>[% L.select_tag('filter.cleared:eq_ignore_empty', SELF.cleared, value_key = 'value', title_key = 'title') %]</td>
+ <td>[% L.select_tag('filter.cleared:eq_ignore_empty', SELF.cleared, value_key = 'value', title_key = 'title', default = 'FALSE' ) %]</td>
</tr>
<tr>
[%- USE LxERP %]
[%- USE L %]
+[% SET debug = 0 %]
+
<table width=100% id="proposal_table">
<thead>
<tr class="listheading">
<th></th>
<th>[% 'Type' | $T8 %]</th>
- <th>[% 'ID/Acc_ID' | $T8 %]</th>
+ [% IF debug %]<th>[% 'ID/Acc_ID' | $T8 %]</th>[% END %]
<th>[% 'Transdate' | $T8 %]</th>
<th>[% 'Amount BT' | $T8 %]</th>
<th>[% 'Amount BB' | $T8 %]</th>
[% PROCESS "reconciliation/proposals.html" %]
<table>
-[% L.button_tag("reconciliate_proposals()", LxERP.t8("Reconciliate")) %]
+[% L.button_tag("reconcile_proposals()", LxERP.t8("Reconcile")) %]
<script type="text/javascript">
<!--
});
}
-function reconciliate_proposals() {
+function reconcile_proposals() {
$('<input>').attr({
id : "action",
name : "action",
type : "hidden",
- value : "Reconciliation/reconciliate_proposals"
+ value : "Reconciliation/reconcile_proposals"
}).appendTo('#reconciliation_form');
$("#reconciliation_form").submit();
}
[%- USE LxERP %]
[%- USE L %]
- <div style="height:60%;overflow:auto;">
- <table width=100% id="link_table">
+[% SET debug = 0 %]
+
+ <div style="height:500px; overflow:auto;">
+ <table width=99% id="link_table">
<thead>
<tr class="listheading">
<th></th>
<th></th>
<th>[% 'Type' | $T8 %]</th>
- <th>[% 'ID/Acc_ID' | $T8 %]</th>
+ [% IF debug %]<th>[% 'ID/Acc_ID' | $T8 %]</th>[% END %]
<th>[% 'Transdate' | $T8 %]</th>
<th>[% 'Amount BT' | $T8 %]</th>
<th>[% 'Amount BB' | $T8 %]</th>
<!--
function filter_table () {
- var url="controller.pl?action=Reconciliation/filter_overview&" + $('#reconciliation_form') . serialize();
+ var url="controller.pl?action=Reconciliation/filter_overview";
$.ajax({
url: url,
+ type: "POST",
+ data: $('#reconciliation_form').serialize(),
success: function(new_data) {
$("tbody[class^='listrow']").remove();
$("#assigned_elements").html('');
}
function update_reconciliation_table () {
- var url="controller.pl?action=Reconciliation/update_reconciliation_table&" + $('#reconciliation_form') . serialize();
+ var url="controller.pl?action=Reconciliation/update_reconciliation_table";
$.ajax({
url: url,
+ type: "POST",
+ data: $('#reconciliation_form').serialize(),
success: function(new_data) {
$('#assigned_elements').html(new_data['html']);
}
id : "action",
name : "action",
type : "hidden",
- value : "Reconciliation/reconciliate"
+ value : "Reconciliation/reconcile"
}).appendTo('#reconciliation_form');
$("#reconciliation_form").submit();
}
[%- USE T8 %]
+[%- USE L %]
[% USE HTML %][% USE LxERP %]
[% IF vc == 'vendor' %]
[% SET is_vendor = 1 %]
[% SET arap = 'ar' %]
[% SET iris = 'is' %]
[%- END %]
-<h1>[% title %]</h1>
+
+ <p><div class="listtop">[% title %]</div></p>
<form action="sepa.pl" method="post">
<p>
[% 'Please select the destination bank account for the collections:' | $T8 %]
[%- END %]
<br>
- [%- INCLUDE generic/multibox.html
- name = 'bank_account.id',
- DATA = BANK_ACCOUNTS,
- id_key = 'id',
- label_sub = 'bank_account_label',
- -%]
+ [% L.select_tag('bank_account',
+ BANK_ACCOUNTS,
+ title_key='displayable_name',
+ with_empty=0,
+ style='width:450px',
+ ) %]
</p>
<p>
<th class="listheading">[% 'Invoice' | $T8 %]</th>
<th class="listheading" align="right">[% 'Amount' | $T8 %]</th>
<th class="listheading" align="right">[% 'Open amount' | $T8 %]</th>
+ <th class="listheading" align="right">[% 'Invoice Date' | $T8 %]</th>
<th class="listheading" align="right">[% 'Due Date' | $T8 %]</th>
<th class="listheading">[% 'Purpose' | $T8 %]</th>
<th class="listheading" align="right">[% 'Bank transfer amount' | $T8 %]</th>
+ <th class="listheading" align="right">[% 'Payment type' | $T8 %]</th>
+ <th class="listheading" align="right">[% 'Skonto information' | $T8 %]</th>
</tr>
[%- FOREACH invoice = INVOICES %]
<input type="hidden" name="bank_transfers[+].[% arap %]_id" value="[% HTML.escape(invoice.id) %]">
+ <input type="hidden" id="amount_less_skonto_[% loop.count %]" name="amount_less_skonto_[% loop.count %]" value="[% LxERP.format_amount(invoice.amount_less_skonto, 2) %]">
+ <input type="hidden" id="invoice_open_amount_[% loop.count %]" name="invoice_open_amount_[% loop.count %]" value="[% LxERP.format_amount(invoice.open_amount - invoice.open_sepa_transfer_amount, 2) %]">
+ <input type="hidden" id="skonto_amount_[% loop.count %]" name="skonto_amount_[% loop.count %]" value="[% LxERP.format_amount(invoice.skonto_amount, 2) %]">
+
<tr class="listrow[% loop.count % 2 %]">
<td align="center">
</a>
</td>
- <td align="right">[% LxERP.format_amount(invoice.invoice_amount, -2) %]</td>
- <td align="right">[% LxERP.format_amount(invoice.open_amount, -2) %]</td>
+ <td align="right">[% LxERP.format_amount(invoice.invoice_amount-invoice.open_sepa_transfer_amount, 2) %]</td>
+ <td align="right">[% LxERP.format_amount(invoice.open_amount-invoice.open_sepa_transfer_amount, 2) %]</td>
+ <td align="right">[% invoice.transdate %]</td>
<td align="right">[% invoice.duedate %]</td>
<td>
[%- SET reference = invoice.reference_prefix _ invoice.invnumber %]
- <input name="bank_transfers[].reference" value="[% HTML.escape(reference.substr(0, 140)) %]" maxlength="140" size="60">
+ <input name="bank_transfers[].reference" value="[% HTML.escape(reference.substr(0, 140)) %]" maxlength="140" size="20">
</td>
<td align="right">
- <input name="bank_transfers[].amount" value="[% LxERP.format_amount(invoice.invoice_amount, 2) %]" style="text-align: right" size="12">
+ <input id=[% loop.count %] name="bank_transfers[].amount" id="amount_[% loop.count %]" value="[% LxERP.format_amount(invoice.invoice_amount_suggestion, 2) %]" style="text-align: right" size="12">
+ </td>
+ <td>
+ [% L.select_tag('bank_transfers[].payment_type', invoice.payment_select_options, value_key => 'payment_type', title_key => 'display', id => 'payment_type_' _ loop.count, class => 'type_target' ) %]
</td>
+ <td align="left" [%- IF invoice.within_skonto_period %]style="background-color: LightGreen"[%- END %]>[%- IF invoice.skonto_amount %] [% LxERP.format_amount(invoice.percent_skonto, 2) %] % = [% LxERP.format_amount(invoice.skonto_amount, 2) %] € bis [% invoice.skonto_date %] [%- END %]</td>
</tr>
[%- END %]
</table>
$("#select_all").checkall('INPUT[name="bank_transfers[].selected"]');
});
-->
+
+$( ".type_target" ).change(function() {
+ type_id = $(this).attr('id');
+ var id = type_id.match(/\d*$/);
+ // alert("found id " + id);
+ if ( $(this).val() == "without_skonto" ) {
+ $('#' + id).val( $('#invoice_open_amount_' + id).val() );
+ } else if ( $(this).val() == "difference_as_skonto" ) {
+ $('#' + id).val( $('#invoice_open_amount_' + id).val() );
+ } else if ( $(this).val() == "with_skonto_pt" ) {
+ $('#' + id).val( $('#amount_less_skonto_' + id).val() );
+ }
+});
+
</script>
[% SET arap = 'ar' %]
[% SET iris = 'is' %]
[%- END %]
-<h1>[% title %]</h1>
[%- IF error_message %]
<p><div class="message_error">[% error_message %]</div></p>
[%- END %]
+ <p><div class="listtop">[% title %]</div></p>
+
<form action="sepa.pl" method="post">
<p>1.
[%- IF is_vendor %]
[% 'Please select the destination bank account for the collections:' | $T8 %]
[%- END %]
<br>
- [%- INCLUDE generic/multibox.html
- name = 'bank_account.id',
- DATA = BANK_ACCOUNTS,
- id_key = 'id',
- label_sub = 'bank_account_label',
- -%]
+ [% L.select_tag('bank_account',
+ BANK_ACCOUNTS,
+ title_key='displayable_name',
+ default=bank_account.id,
+ with_empty=0,
+ style='width:450px',
+ ) %]
</p>
<p>
<th class="listheading" align="right">[% 'Open amount' | $T8 %]</th>
<th class="listheading">[% 'Purpose' | $T8 %]</th>
<th class="listheading" align="right">[%- IF is_vendor %][% 'Bank transfer amount' | $T8 %][%- ELSE %][%- LxERP.t8('Bank collection amount') %][%- END %]</th>
+ <th class="listheading" align="right">[% LxERP.t8('Payment type') %]</th>
+ <th class="listheading" align="right">[% LxERP.t8('Skonto information') %]</th>
<th class="listheading">[% 'Execution date' | $T8 %]</th>
</tr>
<input type="hidden" name="bank_transfers[+].[% arap %]_id" value="[% HTML.escape(bank_transfer.id) %]">
<input type="hidden" name="bank_transfers[].vc_id" value="[% HTML.escape(bank_transfer.vc_id) %]">
<input type="hidden" name="bank_transfers[].selected" value="1">
+ <input type="hidden" id="amount_less_skonto_[% loop.count %]" name="amount_less_skonto_[% loop.count %]" value="[% LxERP.format_amount(bank_transfer.amount_less_skonto, 2) %]">
+ <input type="hidden" id="skonto_amount_[% loop.count %]" name="skonto_amount_[% loop.count %]" value="[% LxERP.format_amount(bank_transfer.skonto_amount, 2) %]">
+ <input type="hidden" id="invoice_open_amount_[% loop.count %]" name="invoice_open_amount_[% loop.count %]" value="[% LxERP.format_amount(bank_transfer.open_amount, 2) %]">
<tr class="listrow[% loop.count % 2 %]">
<td>
<td align="right">[% LxERP.format_amount(bank_transfer.invoice_amount, -2) %]</td>
<td align="right">[% LxERP.format_amount(bank_transfer.open_amount, -2) %]</td>
<td>
- <input name="bank_transfers[].reference" value="[% HTML.escape(bank_transfer.reference.substr(0, 140)) %]" size="60" maxlength="140">
+ <input name="bank_transfers[].reference" value="[% HTML.escape(bank_transfer.reference.substr(0, 140)) %]" size="40" maxlength="140">
+ </td>
+ <td align="right"><input id=[% loop.count %] name="bank_transfers[].amount" value="[% LxERP.format_amount(bank_transfer.amount, -2) %]" style="text-align: right" size="12"></td>
+ <td>
+ [% L.select_tag('bank_transfers[].payment_type', bank_transfer.payment_select_options, value_key => 'payment_type', title_key => 'display', id => 'payment_type_' _ loop.count, class => 'type_target' ) %]
</td>
- <td align="right"><input name="bank_transfers[].amount" value="[% LxERP.format_amount(bank_transfer.amount, -2) %]" style="text-align: right" size="12"></td>
+ <td align="left" [%- IF bank_transfer.within_skonto_period %]style="background-color: LightGreen"[%- END %]>[%- IF bank_transfer.skonto_amount %] [% LxERP.format_amount(bank_transfer.percent_skonto, 2) %] % = [% LxERP.format_amount(bank_transfer.skonto_amount, 2) %] € [% 'until' | $T8 %] [% bank_transfer.skonto_date %] [% END %]</td>
<td nowrap>
[% L.date_tag('bank_transfers[].requested_execution_date', bank_transfer.requested_execution_date) %]
</td>
<input type="hidden" name="vc" value="[%- HTML.escape(vc) %]">
<input type="hidden" name="confirmation" value="1">
</form>
+
+ <script type="text/javascript">
+
+ // function toggle(id) {
+ // $('#skonto_' + id).change(function() {
+ // if($('#skonto_' + id).prop("checked")) {
+ // $('#' + id).val( $('#amount_less_skonto_' + id).val() );
+ // } else {
+ // $('#' + id).val( $('#invoice_open_amount_' + id).val() );
+ // }
+ // });
+ // };
+
+$( ".type_target" ).change(function() {
+ type_id = $(this).attr('id');
+ var id = type_id.match(/\d*$/);
+ // alert("found id " + id);
+ if ( $(this).val() == "without_skonto" ) {
+ $('#' + id).val( $('#invoice_open_amount_' + id).val() );
+ } else if ( $(this).val() == "difference_as_skonto" ) {
+ $('#' + id).val( $('#invoice_open_amount_' + id).val() );
+ } else if ( $(this).val() == "with_skonto_pt" ) {
+ $('#' + id).val( $('#amount_less_skonto_' + id).val() );
+ }
+});
+
+</script>
<th class="listheading" colspan="2">[% 'Source bank account' | $T8 %]</th>
[%- END %]
<th class="listheading" align="right">[% 'Amount' | $T8 %]</th>
+ <th class="listheading" align="right">[% 'Skonto amount' | $T8 %]</th>
+ <th class="listheading" align="right">[% 'Payment type' | $T8 %]</th>
[% IF vc == 'customer' %]
<th class="listheading" align="right">[% 'Mandator ID' | $T8 %]</th>
[%- END %]
<th class="listheading">[% 'IBAN' | $T8 %]</th>
<th class="listheading">[% 'BIC' | $T8 %]</th>
[%- IF show_post_payments_button %]
- <th class="listheading" colspan="[% IF vc == 'customer' %]4[% ELSE %]3[% END %]"> </th>
+ <th class="listheading" colspan="[% IF vc == 'customer' %]6[% ELSE %]5[% END %]"> </th>
<th class="listheading">
[% L.date_tag('set_all_execution_date', '', onchange='set_all_execution_date_fields(this);') %]
</th>
<td>[% HTML.escape(item.vc_iban) %]</td>
<td>[% HTML.escape(item.vc_bic) %]</td>
<td align="right">[% HTML.escape(LxERP.format_amount(item.amount, 2)) %]</td>
+ <td align="right">[% HTML.escape(LxERP.format_amount(item.skonto_amount, 2)) %]</td>
+ <td align="right">[% item.payment_type | $T8 %]</td>
[% IF vc == 'customer' %]
<td>[% HTML.escape(item.mandator_id) %]</td>
[%- END %]