Sammelcommit Bankerweiterung und Skonto
authorG. Richardson <information@kivitendo-premium.de>
Tue, 28 Apr 2015 15:08:59 +0000 (17:08 +0200)
committerG. Richardson <information@kivitendo-premium.de>
Tue, 5 May 2015 07:53:41 +0000 (09:53 +0200)
Überarbeitung der Bankerweiterung vom Stand Niclas, und Einführung von
Bezahlung mit Skonto (alter payment Branch). Mehr Details siehe
changelog.

* changelog

* Ungültige Bankkonten ausblenden
* Punktesystem in Hash %points ausgelagert
* format_amount beim Erstellen von Kreditorenbuchungen behoben

* Debug-Modus in manchen Templates für zusätzliche Tabellenfelder, muß im Template angeschaltet werden: [% SET debug=1 %]

Rechnung zuweisen:
* Filterdatum bei Rechnungszuweisung repariert
* bank_transactions vor reconciliation_starting_date ignorieren
* Rechnungen mit offenem Betrag < 1 Cent ignorieren
* Reihenfolge der Bankfelder in Tabelle angepasst

Bankbuchungen
* Sortierreihenfolge ist standardmäßig Neueste zuerst

* aqbanking Binary in configuration hinterlegen

acc_tran in acc_trans umbenannt in MetaSetup/ReconciliationLink

Kontenabgleich - EB- und SB-Buchungen ignorieren

Payment Helper mit Skontomodus und Skontoautomatik

neuer DB Helper zum Bezahlen von Rechnungsobjekten: pay_invoice

Drei Bezahlarten:
* ohne Skonto
* mit Skonto laut Zahlungsbedingungen
* Differenz als Skonto

Neue Helpermethoden rund um Rechnungen für Einkauf und Verkauf.

Für das automatische Verbuchen von Skonto muß für jeden Steuertyp ein
Skontoautomatikkonto für Verkauf oder Einkauf konfiguriert (bei Steuerschlüssel
0 und 1 beides).

Skontomodi und pay_invoice für SEPA umgesetzt

Beim Auswählen von Rechnungen für den SEPA-Lauf kann nun auch Skonto
berücksichtigt werden.

Berichte Bankbewegungen - Export repariert

Bankkonten - Validierung beim Speichern temporär ausgeschaltet

CsvImport bank transactions - show name of bank in preview

Csv-Import Bank transaction - join remote names

like for purpose, join remote_name and remote_name_1 into one field

Punktesystem in Hash pflegen, und die Regeln, die matchen,
protokollieren, wird aber noch nirgends angezeigt.

Zahlungsauswahl bei el. Kontoauszug

im Tooltip auch Skonto-Information anzeigen
beim Auswählen Dropdown mit Bezahltyp anzeigen

pay_invoice aus Helper nutzt transdate_to_kivitendo

Bericht Bankbuchungen - Name des Bankkontos in eigener Spalte anzeigen

Übersetzung für Kontenabgleich - Bank und Buchung vertauscht

Kontenabgleich - bei Vorschlägen Rechnungsnummer verlinken

Kontenabgleich - "nicht abgeglichen" als Default

Vorschläge Kontenabgleich - Beschriftung und Template Default

Bankerweiterung CSV Import - Standardeinstellungen

Bankerweiterung - in Tooltip Skonto nur bei Bedarf anzeigen

BankAccounts Upgrade - bestehende Konten auf obsolete false setzen

displayable_name für BankAccount
kann im Bankkonto select_tag als title_key statt label_sub verwendet
werden.

SEPA - Umstellung auf Bankkonten-Controller

Bankerweiterung - BankAccount Dropdowns mit displayable_name
statt label_subs

Bankerweiterung - Übersetzung korrigiert

bank_accounts mit obsolete NOT NULL und DEFAULT FALSE intialisieren

SEPA payment - noch offene SEPA-Überweisungen mit berücksichtigen

Beim SEPA-Einzug und der SEPA-Überweisung wurden beim offenen Betrag die
noch nicht geschlossenen SEPA Aufträge mit berücksichtigt. Dies wird
jetzt auch bei der Skonto-Erweiterung berücksichtigt.
Dies ist v.A. dann wichtig, wenn man eine Rechnung in mehreren Schritten
per SEPA-Überweisung bezahlen möchte, oder vielleicht von mehreren
Bankkonten aus.
Beim SEPA-Einzug kommt der Fall wohl eher nicht vor.

Skontoerweiterung - Übersetzungen nachgepflegt

Bankerweiterung - Zahlungsverkehr Menü überarbeitet

CSV-Import der Bankbuchungen nach Menü "Zahlungsverkehr" verschoben
Reihenfolge geändert, entspricht der üblichen Abarbeitungsfolge:
Import -> Verbuchen -> Abgleich

SEPA Export - kein "Differenz als Skonto" vorschlagen

bei SEPA Export soll immer Geld fließen, wenn noch ein Betrag offen ist,
der nicht bezahlt werden soll, muß dies außerhalb des SEPA-Exports
verbucht werden.
Nur die Optionen "ohne Skonto" ohne "mit Skonto nach ZB" machen Sinn.

BankTransaction - diverse Änderungen

* Punktesystem erweitert
* Kontonummer und IBAN vergleichen
* zutreffende Punkt-Regeln in Template im Debug-Modus anzeigen, als
Mouseover Tooltip in der Punktespalte

Kontenabgleich verbessert

* bei schon verknüpften Belegen kleine Abweichungen
(Rundungsungenauigkeiten im Subcent-Bereich) tolerieren
* Sowohl auf IBAN als auf Kontonummer prüfen
* Kontenabgleichsstartdatum bei Gesamtsumme verwenden

Bankerweiterung: Bei Rechnung zuweisen Skontosumme anzeigen

Kontoauszug verbuchen: add_invoices um Parameter skonto erweitert

Wenn Zahlbetrag mit Skontobetrag übereinstimmt dann Dropdown mit
"mit Skonto nach ZB" vorausgewählt übergeben.
Kann an der Stelle im Template aber noch nicht prüfen, ob
Zahlungseingangsdatum innerhalb Skontofrist liegt.

Kontoauszug verbuchen - Payment Dropdown konditional

nur anzeigen, wenn überhaupt Skonto in Frage kommt

Es wird aber immer noch nicht auf Datum überprüft

bank_transactions: itime hinzugefügt

wird aber noch nirgends verwendet

Kontenabgleich - Gesamtsaldobeschriftungen waren vertauscht

BankTransaction-Controller: Paginaten beim CSV-/PDF-Export ausschalten
Analog zu Commit 06837707

Kontenabgleich - reconciliate nach reconcile umbenannt

Bankauszug verbuchen - leeren Konteninhaber nicht matchen

BankTransaction - Idee für Negativpunkt für Überzahlung

Kontoauszug verbuchen - korrekter Titel

Kontoauszug verbuchen - offenen Betrag berücksichtigen und anzeigen
und auf leere Regex-Ausdrücke prüfen

Payment Helper - Fließkommadifferenzen berücksichtigen

in pay_invoice für die Fälle "Differenz als Skonto" und "mit Skonto nach
ZB".

Moved BankTransaction matching from Controller to DB

There is now a new function in SL::DB::BankTransaction called
get_agreement_with_invoice that takes a Invoice or PurchaseInvoice
object as an argument and calculates the agreement.

Kontoauszug verbuchen - bei Zuweisung Zahlungsart berücksichtigen

Übernimmt man einen Vorschlag, wird nun per AJAX geprüft, ob die
Kombination aus $bt und $invoice mit Skonto verbucht wird oder nicht.
Es wird ein Optionsliste für eine select als HTML-Blog zurückgeliefert
und unter "Zugewiesene Rechnungen" eingefügt.

Wenn der Zahlungsbetrag genau dem Rechnungsbetrag abzgl. Skonto
entspricht, und die Zahlung innerhalb der Skontofrist erfolgt ist, wird
"mit Skonto nach ZB" vorausgewählt". Ist die Skontofrist vorbei wird
"ohne Skonto" vorausgewählt, dadurch bleibt der Skontobetrag offen,
sofern man nicht manuell auf "mit Skonto nach ZB" umstellt.

Gibt es für die Rechnung keine Skontooption so wird auch keine Dropdown
angezeigt.

Kontoauszug verbuchen - Übersetzung für "Add invoices" korrigiert

BankTransaction - Offene Subcent-Rechnungen rausfiltern
damit diese nicht mehr zum Zuweisen zur Verfügung stehen.

Payment Helper - Export get_payment_select_options_for_bank_transaction

EXPORT_OK angepasst und alle Methoden in SL::DB::Invoice und
SL::DB::PurchaseInvoice importieren

Doku von SL/DB/Helper/Payment.pm angepasst

Kontauszug verbuchen - max agreement refactored

Nicht mehr von einer hohen Zahl in einer Schleife herunterzählen,
sondern den Wert der höchsten Matches per map und max raussuchen und
direkt danach die Vorschläge greppen.

Kontoauszug verbuchen - Refactoring von Rechnung zuweisen (html)

* das HTML wird jetzt nicht mehr per javascript, sondern in
SL/DB/Helper/Payment.pm mit Hilfe des Presenters (für die Tags)
als HTML Blob zusammengebaut

* der Rückgabewert von get_payment_select_options_for_bank_transaction
ist jetzt ein Array, mit dem direkt das select_tag erstellt werden kann.

* Die Daten werden jetzt an to_json als to_json( { 'html' => $html } ...
übergeben, und im AJAX code als data.html ausgelesen.

Reconciliation auf record umgestellt

Reconciliation - ajax as POST bei Vorschlägen, weil url zu lang werden
kann

61 files changed:
SL/AM.pm
SL/Controller/BankAccount.pm
SL/Controller/BankTransaction.pm
SL/Controller/CsvImport.pm
SL/Controller/CsvImport/BankTransaction.pm
SL/Controller/CsvImport/Base.pm
SL/Controller/Reconciliation.pm
SL/DB/BankAccount.pm
SL/DB/BankTransaction.pm
SL/DB/CsvImportProfile.pm
SL/DB/Helper/Paginated.pm
SL/DB/Helper/Payment.pm [new file with mode: 0644]
SL/DB/Invoice.pm
SL/DB/Manager/AccTransaction.pm [new file with mode: 0644]
SL/DB/Manager/BankAccount.pm [new file with mode: 0644]
SL/DB/MetaSetup/BankAccount.pm
SL/DB/MetaSetup/BankTransaction.pm
SL/DB/MetaSetup/ReconciliationLink.pm
SL/DB/MetaSetup/SepaExportItem.pm
SL/DB/MetaSetup/Tax.pm
SL/DB/Part.pm
SL/DB/PurchaseInvoice.pm
SL/Presenter/Invoice.pm
SL/SEPA.pm
bin/mozilla/sepa.pl
config/kivitendo.conf.default
doc/changelog
js/locale/de.js
locale/de/all
locale/en/all
menus/erp.ini
sql/Pg-upgrade2/automatic_reconciliation.sql
sql/Pg-upgrade2/bank_accounts_unique_chart_constraint.sql
sql/Pg-upgrade2/bank_transactions.sql
sql/Pg-upgrade2/bankaccounts_sortkey_and_obsolete.sql
sql/Pg-upgrade2/sepa_items_payment_type.sql [new file with mode: 0644]
sql/Pg-upgrade2/tax_skonto_automatic.sql [new file with mode: 0644]
t/db_helper/payment.t [new file with mode: 0644]
templates/webpages/am/edit_tax.html
templates/webpages/am/list_tax.html
templates/webpages/bank_transactions/_filter.html
templates/webpages/bank_transactions/add_list.html
templates/webpages/bank_transactions/assign_invoice.html
templates/webpages/bank_transactions/create_invoice.html
templates/webpages/bank_transactions/list.html
templates/webpages/bank_transactions/search.html
templates/webpages/bank_transactions/tabs/all.html
templates/webpages/bank_transactions/tabs/automatic.html
templates/webpages/csv_import/_form_banktransactions.html [new file with mode: 0644]
templates/webpages/csv_import/_form_mt940.html
templates/webpages/csv_import/form.html
templates/webpages/reconciliation/_linked_transactions.html
templates/webpages/reconciliation/assigning_table.html
templates/webpages/reconciliation/form.html
templates/webpages/reconciliation/proposals.html
templates/webpages/reconciliation/search.html
templates/webpages/reconciliation/tabs/automatic.html
templates/webpages/reconciliation/tabs/overview.html
templates/webpages/sepa/bank_transfer_add.html
templates/webpages/sepa/bank_transfer_create.html
templates/webpages/sepa/bank_transfer_edit.html

index 7306640..cbbfae0 100644 (file)
--- a/SL/AM.pm
+++ b/SL/AM.pm
@@ -45,6 +45,7 @@ use SL::DBUtils;
 use SL::DB::AuthUser;
 use SL::DB::Default;
 use SL::DB::Employee;
+use SL::DB::Chart;
 use SL::GenericTranslations;
 
 use strict;
@@ -1286,7 +1287,11 @@ sub taxes {
                    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|;
 
@@ -1328,6 +1333,17 @@ sub get_tax_accounts {
     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;
@@ -1350,7 +1366,9 @@ sub get_tax {
                    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 = ? |;
 
@@ -1414,15 +1432,17 @@ sub save_tax {
   $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 {
@@ -1434,10 +1454,12 @@ sub save_tax {
                   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);
index 15d5db3..dff4312 100644 (file)
@@ -1,4 +1,4 @@
- SL::Controller::BankAccount;
+package SL::Controller::BankAccount;
 
 use strict;
 
index afa06e9..918d6aa 100644 (file)
@@ -23,6 +23,8 @@ use SL::DB::AccTransaction;
 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
 (
@@ -39,22 +41,19 @@ __PACKAGE__->run_before('check_auth');
 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 {
@@ -70,13 +69,19 @@ 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},
@@ -88,160 +93,73 @@ sub action_list {
   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) = @_;
 
@@ -289,6 +207,33 @@ sub action_create_invoice {
       );
 }
 
+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) = @_;
 
@@ -352,27 +297,30 @@ sub action_ajax_add_list {
   }
 
   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',
@@ -404,7 +352,8 @@ sub action_ajax_accept_invoices {
 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);
@@ -423,8 +372,14 @@ sub action_save_invoices {
                        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));
@@ -432,7 +387,11 @@ sub action_save_invoices {
       }
       #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);
@@ -441,7 +400,11 @@ sub action_save_invoices {
           $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;
@@ -480,7 +443,7 @@ sub action_save_proposals {
     $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
@@ -520,12 +483,12 @@ sub make_filter_summary {
   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) {
@@ -543,8 +506,8 @@ sub prepare_report {
   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 } },
@@ -561,6 +524,7 @@ sub prepare_report {
     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                    => {},
   );
 
@@ -577,16 +541,15 @@ sub prepare_report {
   );
   $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 }),
   );
 }
@@ -599,7 +562,7 @@ sub init_models {
     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'),
@@ -613,6 +576,7 @@ sub init_models {
       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' ],
   );
index 3f65d0e..2bc2955 100644 (file)
@@ -224,7 +224,7 @@ sub check_auth {
 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});
 }
 
@@ -292,13 +292,13 @@ sub test_and_import_deferred {
 
   $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 |");
index ae49616..76e9020 100644 (file)
@@ -12,7 +12,7 @@ use parent qw(SL::Controller::CsvImport::Base);
 
 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 {
@@ -23,13 +23,14 @@ 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 };
@@ -38,14 +39,38 @@ sub check_objects {
 
     $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 {
@@ -53,21 +78,20 @@ 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 {
@@ -75,7 +99,7 @@ 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;
@@ -95,9 +119,13 @@ sub check_bank_account {
 
   }
 
-  # 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;
@@ -108,6 +136,7 @@ sub check_bank_account {
     }
 
     $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;
@@ -119,18 +148,29 @@ sub join_purposes {
   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;
index b896d99..3090182 100644 (file)
@@ -146,7 +146,7 @@ sub init_all_languages {
 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 {
index cd5707b..c6faef1 100644 (file)
@@ -10,9 +10,10 @@ use SL::Controller::Helper::ParseFilter;
 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) ],
@@ -28,12 +29,7 @@ __PACKAGE__->run_before('_bank_account');
 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 {
@@ -44,12 +40,7 @@ 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 {
@@ -74,11 +65,11 @@ sub action_filter_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 });
@@ -100,20 +91,20 @@ sub action_update_reconciliation_table {
   $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;
 }
@@ -140,11 +131,11 @@ sub action_delete_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 });
@@ -168,17 +159,17 @@ sub action_filter_proposals {
   $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;
@@ -229,6 +220,15 @@ sub _bank_account {
 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' ]);
@@ -243,6 +243,7 @@ sub _get_proposals {
     $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;
@@ -268,18 +269,27 @@ sub _get_proposals {
 
     #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];
@@ -339,17 +349,17 @@ sub _get_elements_and_validate {
   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) {
@@ -378,32 +388,41 @@ sub _filter_to_where {
   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} );
@@ -411,7 +430,7 @@ sub _filter_to_where {
 
   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} );
@@ -428,6 +447,9 @@ sub _get_linked_transactions {
   $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');
@@ -437,17 +459,17 @@ sub _get_linked_transactions {
   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 );
@@ -466,7 +488,7 @@ sub _get_linked_transactions {
     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 );
@@ -485,7 +507,7 @@ sub _get_linked_transactions {
     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;
@@ -495,10 +517,10 @@ sub _get_linked_transactions {
     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';
@@ -539,33 +561,46 @@ sub _get_balances {
   @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;
 
@@ -579,7 +614,7 @@ sub init_cleared {
 }
 
 sub init_BANK_ACCOUNTS {
-  SL::DB::Manager::BankAccount->get_all();
+  SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
 }
 
 1;
index 9ac755b..7bbd897 100644 (file)
@@ -24,8 +24,8 @@ sub validate {
     # 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} )) {
@@ -42,4 +42,10 @@ sub validate {
   return @errors;
 }
 
+sub displayable_name {
+  my ($self) = @_;
+
+  return join ' ', grep $_, $self->name, $self->bank, $self->iban;
+}
+
 1;
index cc6a1ee..d5d7c55 100644 (file)
@@ -9,12 +9,11 @@ use SL::DB::MetaSetup::BankTransaction;
 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;
@@ -44,10 +43,230 @@ sub linked_invoices {
     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
index 1902a51..9980ad5 100644 (file)
@@ -34,13 +34,6 @@ sub new_with_default {
 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%' });
@@ -59,10 +52,33 @@ sub set_defaults {
                          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;
 }
 
index 02e7728..e75de4c 100644 (file)
@@ -79,13 +79,13 @@ __END__
 
 =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
 
@@ -124,7 +124,7 @@ since they don't make sense with paginating.
 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
diff --git a/SL/DB/Helper/Payment.pm b/SL/DB/Helper/Payment.pm
new file mode 100644 (file)
index 0000000..baa2366
--- /dev/null
@@ -0,0 +1,995 @@
+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
index 3ecc957..3ed6cd3 100644 (file)
@@ -9,9 +9,9 @@ use Carp;
 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;
@@ -20,7 +20,6 @@ use SL::DB::Helper::PriceUpdater;
 use SL::DB::Helper::TransNumberGenerator;
 use SL::Locale::String qw(t8);
 use SL::DB::CustomVariable;
-use SL::DB::AccTransaction;
 
 __PACKAGE__->meta->add_relationship(
   invoiceitems => {
@@ -368,48 +367,6 @@ sub customervendor {
   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) = @_;
 
@@ -420,7 +377,6 @@ sub link {
   return $html;
 }
 
->>>>>>> Test: Bank-Commit zusammengefasst
 1;
 
 __END__
diff --git a/SL/DB/Manager/AccTransaction.pm b/SL/DB/Manager/AccTransaction.pm
new file mode 100644 (file)
index 0000000..2eae95a
--- /dev/null
@@ -0,0 +1,61 @@
+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
diff --git a/SL/DB/Manager/BankAccount.pm b/SL/DB/Manager/BankAccount.pm
new file mode 100644 (file)
index 0000000..6734999
--- /dev/null
@@ -0,0 +1,51 @@
+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
index d778138..8f1abea 100644 (file)
@@ -17,7 +17,7 @@ __PACKAGE__->meta->columns(
   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 },
index fefe1e5..a2dd295 100644 (file)
@@ -9,17 +9,17 @@ use base qw(SL::DB::Object);
 __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 },
@@ -27,6 +27,8 @@ __PACKAGE__->meta->columns(
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 
+__PACKAGE__->meta->allow_inline_column_values(1);
+
 __PACKAGE__->meta->foreign_keys(
   currency => {
     class       => 'SL::DB::Currency',
index 796d308..9b5c969 100644 (file)
@@ -18,7 +18,7 @@ __PACKAGE__->meta->columns(
 __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' },
   },
index 81d98b3..6f5051b 100644 (file)
@@ -20,9 +20,11 @@ __PACKAGE__->meta->columns(
   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 },
index dcc0d11..a3be400 100644 (file)
@@ -9,15 +9,17 @@ use base qw(SL::DB::Object);
 __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' ]);
@@ -29,6 +31,21 @@ __PACKAGE__->meta->foreign_keys(
     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;
index 2a74b74..57f0f31 100644 (file)
@@ -243,7 +243,7 @@ flavours called:
 
 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.
@@ -263,7 +263,7 @@ L</is_type> and others.
 =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.
 
@@ -336,12 +336,12 @@ The information retrieved by the function is cached.
 
 =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.
 
index 78034c7..ed0523b 100644 (file)
@@ -8,6 +8,7 @@ use SL::DB::MetaSetup::PurchaseInvoice;
 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.
@@ -82,48 +83,6 @@ sub abbreviation {
 
 };
 
-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) = @_;
 
index b18c064..151f500 100644 (file)
@@ -5,10 +5,28 @@ use strict;
 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) = @_;
 
@@ -85,10 +103,36 @@ transaction, purchase invoice and AP transaction Rose::DB objects
   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
index 63efe2c..cbea841 100644 (file)
@@ -4,7 +4,12 @@ use strict;
 
 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();
@@ -21,9 +26,21 @@ sub retrieve_open_invoices {
 
   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,
@@ -40,6 +57,8 @@ sub retrieve_open_invoices {
                   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
@@ -47,6 +66,21 @@ sub retrieve_open_invoices {
 
   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;
@@ -84,10 +118,12 @@ sub create_export {
   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 =
@@ -133,6 +169,17 @@ sub create_export {
                   $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';
 
@@ -392,6 +439,7 @@ sub post_payment {
   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.
@@ -400,23 +448,25 @@ sub post_payment {
 
     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);
index c9db071..f2170ae 100755 (executable)
@@ -4,7 +4,8 @@ use List::MoreUtils qw(any none uniq);
 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;
@@ -25,7 +26,7 @@ sub bank_transfer_add {
 
   $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.'));
@@ -47,8 +48,6 @@ sub bank_transfer_add {
   # 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 };
 
@@ -62,7 +61,6 @@ sub bank_transfer_add {
   print $form->parse_html_template('sepa/bank_transfer_add',
                                    { 'INVOICES'           => $invoices,
                                      'BANK_ACCOUNTS'      => $bank_accounts,
-                                     'bank_account_label' => $bank_account_label_sub,
                                      'vc'                 => $vc,
                                    });
 
@@ -79,21 +77,24 @@ sub bank_transfer_create {
 
   $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} } }, %{ $_ } },
@@ -101,6 +102,18 @@ sub bank_transfer_create {
     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.'));
   }
@@ -130,15 +143,12 @@ sub bank_transfer_create {
                                                    '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,
index 25a8d4c..0ff2aa1 100644 (file)
@@ -116,6 +116,10 @@ latex = pdflatex
 # 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
index 6e6789e..5c9aaab 100644 (file)
@@ -2,6 +2,45 @@
 # 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
index 0d84848..d5276b7 100644 (file)
@@ -64,6 +64,7 @@ namespace("kivi").setupLocale({
 "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.",
index c9bb440..97f3b5b 100755 (executable)
@@ -15,20 +15,17 @@ $self->{texts} = {
   ' 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---',
@@ -85,8 +82,8 @@ $self->{texts} = {
   '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',
@@ -187,7 +184,7 @@ $self->{texts} = {
   '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',
@@ -242,10 +239,11 @@ $self->{texts} = {
   '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.',
@@ -318,6 +316,8 @@ $self->{texts} = {
   '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',
@@ -340,13 +340,14 @@ $self->{texts} = {
   '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',
@@ -357,7 +358,7 @@ $self->{texts} = {
   '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',
@@ -450,7 +451,9 @@ $self->{texts} = {
   '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',
@@ -544,7 +547,6 @@ $self->{texts} = {
   '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',
@@ -616,7 +618,7 @@ $self->{texts} = {
   '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!',
@@ -899,6 +901,7 @@ $self->{texts} = {
   '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',
@@ -1122,7 +1125,7 @@ $self->{texts} = {
   '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',
@@ -1167,6 +1170,7 @@ $self->{texts} = {
   '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',
@@ -1319,9 +1323,9 @@ $self->{texts} = {
   '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',
@@ -1351,7 +1355,9 @@ $self->{texts} = {
   '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',
@@ -1388,6 +1394,7 @@ $self->{texts} = {
   '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',
@@ -1417,13 +1424,18 @@ $self->{texts} = {
   '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',
@@ -1528,9 +1540,7 @@ $self->{texts} = {
   '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:',
@@ -1543,7 +1553,7 @@ $self->{texts} = {
   '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',
@@ -1560,6 +1570,8 @@ $self->{texts} = {
   'MAILED'                      => 'Gesendet',
   'MD'                          => 'PT',
   'MIME type'                   => 'MIME-Typ',
+  'MT940'                       => 'MT940',
+  'MT940 import'                => 'MT940 Import',
   'Machine'                     => 'Maschine',
   'Main Preferences'            => 'Grundeinstellungen',
   'Main sorting'                => 'Hauptsortierung',
@@ -1634,11 +1646,9 @@ $self->{texts} = {
   '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&ouml;se Inland',
   'Net Income Statement'        => 'Einnahmenüberschußrechnung',
   'Net amount'                  => 'Nettobetrag',
   'Net amount (for verification)' => 'Nettobetrag (zur Überprüfung)',
@@ -1721,7 +1731,7 @@ $self->{texts} = {
   '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.',
@@ -1835,7 +1845,7 @@ $self->{texts} = {
   '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',
@@ -1892,6 +1902,7 @@ $self->{texts} = {
   '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.',
@@ -2067,6 +2078,7 @@ $self->{texts} = {
   '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',
@@ -2111,9 +2123,8 @@ $self->{texts} = {
   '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',
@@ -2137,7 +2148,6 @@ $self->{texts} = {
   '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',
@@ -2286,6 +2296,7 @@ $self->{texts} = {
   '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',
@@ -2415,11 +2426,15 @@ $self->{texts} = {
   '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.',
@@ -2839,7 +2854,7 @@ $self->{texts} = {
   '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&auml;lt momentan keine Eintr&auml;ge.',
@@ -2958,10 +2973,10 @@ $self->{texts} = {
   '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',
@@ -3064,7 +3079,7 @@ $self->{texts} = {
   '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',
@@ -3177,7 +3192,6 @@ $self->{texts} = {
   '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.',
@@ -3223,7 +3237,6 @@ $self->{texts} = {
   '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',
@@ -3256,6 +3269,8 @@ $self->{texts} = {
   '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',
@@ -3267,11 +3282,13 @@ $self->{texts} = {
   '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&uuml;r',
@@ -3324,6 +3341,7 @@ $self->{texts} = {
   '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',
@@ -3417,6 +3435,7 @@ $self->{texts} = {
   'uncleared'                   => 'Nicht abgeglichen',
   'unconfigured'                => 'unkonfiguriert',
   'uncorrect partnumber '       => 'Unbekannte Teilenummer ',
+  'until'                       => 'bis',
   'use program settings'        => 'benutze Programmeinstellungen',
   'use user config'             => 'Verwende Benutzereinstellung',
   'used'                        => 'Verbraucht',
@@ -3426,6 +3445,10 @@ $self->{texts} = {
   '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',
index 92b6863..d3f5498 100644 (file)
@@ -2620,6 +2620,8 @@ $self->{texts} = {
   'delete'                      => '',
   'delivered'                   => '',
   'deliverydate'                => '',
+  'difference as skonto'        => '',
+  'difference_as_skonto'        => 'remainder as skonto',
   'direct debit'                => '',
   'disposed'                    => '',
   'do not include'              => '',
@@ -2778,6 +2780,10 @@ $self->{texts} = {
   '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'                         => '',
index 37b4f80..05f2a29 100644 (file)
@@ -401,11 +401,6 @@ action=payment
 type=check
 vc=vendor
 
-[Cash--Reconciliation]
-ACCESS=cash
-module=rc.pl
-action=reconciliation
-
 [Cash--Bank collection via SEPA]
 ACCESS=cash
 module=sepa.pl
@@ -418,6 +413,23 @@ 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
@@ -429,6 +441,11 @@ 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
@@ -757,16 +774,6 @@ module=menu.pl
 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
index a911e07..924fceb 100644 (file)
@@ -1,6 +1,6 @@
 -- @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'),
index 59e0b0a..c5eb53f 100644 (file)
@@ -1,6 +1,6 @@
 -- @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);
index 4b57741..6e9f917 100644 (file)
@@ -1,6 +1,6 @@
 -- @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,
@@ -11,13 +11,12 @@ CREATE TABLE bank_transactions (
   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)
 );
index 49baff9..cdda464 100644 (file)
@@ -3,7 +3,8 @@
 -- @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;
diff --git a/sql/Pg-upgrade2/sepa_items_payment_type.sql b/sql/Pg-upgrade2/sepa_items_payment_type.sql
new file mode 100644 (file)
index 0000000..c4444b6
--- /dev/null
@@ -0,0 +1,10 @@
+-- @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);
diff --git a/sql/Pg-upgrade2/tax_skonto_automatic.sql b/sql/Pg-upgrade2/tax_skonto_automatic.sql
new file mode 100644 (file)
index 0000000..6f37854
--- /dev/null
@@ -0,0 +1,18 @@
+-- @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 );
diff --git a/t/db_helper/payment.t b/t/db_helper/payment.t
new file mode 100644 (file)
index 0000000..18da821
--- /dev/null
@@ -0,0 +1,1031 @@
+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;
index 88c7fdc..46c45d8 100644 (file)
     <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>
index 4dcfe04..836cca3 100644 (file)
@@ -9,6 +9,8 @@
    <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 %]
@@ -18,6 +20,8 @@
    <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>
index a72ff33..0aaab14 100644 (file)
@@ -12,7 +12,7 @@
  <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>
index 08eaf5b..ad48bfa 100644 (file)
@@ -8,6 +8,7 @@
     <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>
@@ -19,6 +20,7 @@
     <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>
index f8a6697..26229b8 100644 (file)
@@ -1,30 +1,32 @@
 [%- 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>
@@ -43,7 +45,7 @@
    </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>
index 09af08e..cfc9ce8 100644 (file)
@@ -72,7 +72,7 @@
 
    [% 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 %]&currency=[% 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) %]&currency=[% 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>
index d9d5238..0a38b2b 100644 (file)
@@ -6,7 +6,7 @@
 
 [%- 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 %]
@@ -69,7 +69,8 @@ function assign_invoice(bt_id) {
 }
 
 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];
@@ -77,11 +78,13 @@ function add_invoices(bt_id, prop_id, prop_invnumber) {
   }
   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) {
index adab511..3e234cd 100644 (file)
 
   <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>
index a392bb8..359ca5f 100644 (file)
@@ -1,11 +1,16 @@
 [%- 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>
@@ -55,7 +36,6 @@
         [% 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 %]&nbsp;[% '(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 %]>&larr;[% 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>
index 3ce007a..42f387f 100644 (file)
@@ -5,7 +5,7 @@
     <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) %]
diff --git a/templates/webpages/csv_import/_form_banktransactions.html b/templates/webpages/csv_import/_form_banktransactions.html
new file mode 100644 (file)
index 0000000..d6da117
--- /dev/null
@@ -0,0 +1,9 @@
+[% 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>
index 652bc98..cb63e7d 100644 (file)
@@ -1,9 +1,12 @@
+[% 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>
 
 
index 91b227d..745ef16 100644 (file)
  [%- 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>
index 29eaa08..c05be85 100644 (file)
@@ -3,6 +3,8 @@
 [%- 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>
@@ -21,7 +23,7 @@
 
             <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>
@@ -58,7 +60,7 @@
 
             <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>
index 2f6d4de..0da95a2 100644 (file)
@@ -35,5 +35,5 @@
     </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 %]
index 2c1a4db..39847ac 100644 (file)
@@ -34,7 +34,8 @@ html, body {
      <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>
@@ -95,9 +96,11 @@ html, body {
 <!--
 
 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']);
@@ -107,9 +110,11 @@ function load_proposals () {
 }
 
 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('');
index 10f9a13..df7bde4 100644 (file)
@@ -3,6 +3,8 @@
 [%- 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>
@@ -17,7 +19,7 @@
 
           <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>
index b87ed4a..3474b28 100644 (file)
@@ -11,7 +11,7 @@
  <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>
@@ -26,7 +26,7 @@
 
   <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>
index 6bff8c0..7c49346 100644 (file)
@@ -3,6 +3,8 @@
 [%- USE LxERP %]
 [%- USE L %]
 
+[% SET debug = 0 %]
+
 <table width=100% id="proposal_table">
   <thead>
     <tr class="listheading">
@@ -10,7 +12,7 @@
 
       <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>
@@ -25,7 +27,7 @@
   [% 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">
 <!--
@@ -45,12 +47,12 @@ function filter_table () {
   });
 }
 
-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();
 }
index 68539b3..013ba54 100644 (file)
@@ -3,15 +3,17 @@
 [%- 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('');
@@ -68,9 +72,11 @@ function filter_table () {
 }
 
 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']);
     }
@@ -92,7 +98,7 @@ function submit_with_action(action) {
     id : "action",
     name : "action",
     type : "hidden",
-    value : "Reconciliation/reconciliate"
+    value : "Reconciliation/reconcile"
   }).appendTo('#reconciliation_form');
   $("#reconciliation_form").submit();
 }
index d09a489..593a022 100644 (file)
@@ -1,4 +1,5 @@
 [%- USE T8 %]
+[%- USE L %]
 [% USE HTML %][% USE LxERP %]
 [% IF vc == 'vendor' %]
  [% SET is_vendor = 1 %]
@@ -9,7 +10,8 @@
  [% 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>
index cf3cfad..35b2fb9 100644 (file)
  [% 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>
@@ -82,6 +84,8 @@
      <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>
 
@@ -89,6 +93,9 @@
      <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>
index a0ccd97..37f1575 100644 (file)
@@ -32,6 +32,8 @@
       <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 %]
@@ -46,7 +48,7 @@
      <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 %]">&nbsp;</th>
+      <th class="listheading" colspan="[% IF vc == 'customer' %]6[% ELSE %]5[% END %]">&nbsp;</th>
       <th class="listheading">
         [% L.date_tag('set_all_execution_date', '', onchange='set_all_execution_date_fields(this);') %]
       </th>
@@ -75,6 +77,8 @@
       <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 %]