Merge branch 'bankerweiterung_und_skonto'
authorG. Richardson <information@kivitendo-premium.de>
Tue, 5 May 2015 07:56:42 +0000 (09:56 +0200)
committerG. Richardson <information@kivitendo-premium.de>
Tue, 5 May 2015 07:56:42 +0000 (09:56 +0200)
Conflicts:
doc/changelog
locale/de/all

106 files changed:
SL/AM.pm
SL/AP.pm
SL/AR.pm
SL/Auth.pm
SL/BankAccount.pm [deleted file]
SL/Controller/BankAccount.pm [new file with mode: 0644]
SL/Controller/BankTransaction.pm [new file with mode: 0644]
SL/Controller/CsvImport.pm
SL/Controller/CsvImport/BankTransaction.pm [new file with mode: 0644]
SL/Controller/CsvImport/Base.pm
SL/Controller/Helper/GetModels/Sorted.pm
SL/Controller/Project.pm
SL/Controller/Reconciliation.pm [new file with mode: 0644]
SL/DATEV.pm
SL/DB/AccTransaction.pm
SL/DB/BankAccount.pm
SL/DB/BankTransaction.pm [new file with mode: 0644]
SL/DB/CsvImportProfile.pm
SL/DB/GLTransaction.pm
SL/DB/Helper/ALL.pm
SL/DB/Helper/Mappings.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/Manager/BankTransaction.pm [new file with mode: 0644]
SL/DB/Manager/Invoice.pm
SL/DB/Manager/OrderItem.pm
SL/DB/Manager/ReconciliationLink.pm [new file with mode: 0644]
SL/DB/MetaSetup/BankAccount.pm
SL/DB/MetaSetup/BankTransaction.pm [new file with mode: 0644]
SL/DB/MetaSetup/ReconciliationLink.pm [new file with mode: 0644]
SL/DB/MetaSetup/SepaExportItem.pm
SL/DB/MetaSetup/Tax.pm
SL/DB/PurchaseInvoice.pm
SL/DB/ReconciliationLink.pm [new file with mode: 0644]
SL/GL.pm
SL/Helper/Flash.pm
SL/IR.pm
SL/IS.pm
SL/Presenter.pm
SL/Presenter/BankAccount.pm [new file with mode: 0644]
SL/Presenter/Invoice.pm
SL/Presenter/Record.pm
SL/RC.pm
SL/SEPA.pm
VERSION
bin/mozilla/bankaccounts.pl [deleted file]
bin/mozilla/drafts.pl
bin/mozilla/sepa.pl
config/kivitendo.conf.default
css/kivitendo/main.css
doc/changelog
image/bank-building.jpg [new file with mode: 0644]
js/locale/de.js
js/wz_tooltip.js
locale/de/all
locale/en/all
menus/erp.ini
sql/Pg-upgrade2/automatic_reconciliation.sql [new file with mode: 0644]
sql/Pg-upgrade2/bank_accounts_unique_chart_constraint.sql [new file with mode: 0644]
sql/Pg-upgrade2/bank_transactions.sql [new file with mode: 0644]
sql/Pg-upgrade2/bankaccounts_reconciliation.sql [new file with mode: 0644]
sql/Pg-upgrade2/bankaccounts_sortkey_and_obsolete.sql [new file with mode: 0644]
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/background_job/create_periodic_invoices.t
t/controllers/financial_overview/sales_orders.t
t/db_helper/payment.t [new file with mode: 0644]
t/db_helper/price_tax_calculator.t
t/db_helper/record_links.t
templates/webpages/am/edit_tax.html
templates/webpages/am/list_tax.html
templates/webpages/bank_transactions/_filter.html [new file with mode: 0644]
templates/webpages/bank_transactions/add_list.html [new file with mode: 0644]
templates/webpages/bank_transactions/assign_invoice.html [new file with mode: 0644]
templates/webpages/bank_transactions/create_invoice.html [new file with mode: 0644]
templates/webpages/bank_transactions/filter_drafts.html [new file with mode: 0644]
templates/webpages/bank_transactions/invoices.html [new file with mode: 0644]
templates/webpages/bank_transactions/list.html [new file with mode: 0644]
templates/webpages/bank_transactions/report_bottom.html [new file with mode: 0644]
templates/webpages/bank_transactions/report_top.html [new file with mode: 0644]
templates/webpages/bank_transactions/search.html [new file with mode: 0644]
templates/webpages/bank_transactions/tabs/all.html [new file with mode: 0644]
templates/webpages/bank_transactions/tabs/automatic.html [new file with mode: 0644]
templates/webpages/bankaccounts/bank_account_display_form.html [deleted file]
templates/webpages/bankaccounts/bank_account_list_bottom.html [deleted file]
templates/webpages/bankaccounts/form.html [new file with mode: 0644]
templates/webpages/bankaccounts/list.html [new file with mode: 0644]
templates/webpages/common/flash.html
templates/webpages/csv_import/_form_banktransactions.html [new file with mode: 0644]
templates/webpages/csv_import/_form_mt940.html [new file with mode: 0644]
templates/webpages/csv_import/form.html
templates/webpages/ic/form_header.html
templates/webpages/reconciliation/_linked_transactions.html [new file with mode: 0644]
templates/webpages/reconciliation/assigning_table.html [new file with mode: 0644]
templates/webpages/reconciliation/form.html [new file with mode: 0644]
templates/webpages/reconciliation/proposals.html [new file with mode: 0644]
templates/webpages/reconciliation/search.html [new file with mode: 0644]
templates/webpages/reconciliation/tabs/automatic.html [new file with mode: 0644]
templates/webpages/reconciliation/tabs/overview.html [new file with mode: 0644]
templates/webpages/reconciliation/tabs/set_cleared.html [new file with mode: 0644]
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 8de1c3c..d8cd5d7 100644 (file)
--- a/SL/AP.pm
+++ b/SL/AP.pm
@@ -362,8 +362,6 @@ sub post_transaction {
       exporttype => DATEV_ET_BUCHUNGEN,
       format     => DATEV_FORMAT_KNE,
       dbh        => $dbh,
-      from       => $transdate,
-      to         => $transdate,
       trans_id   => $form->{id},
     );
 
index 74bfeee..81be410 100644 (file)
--- a/SL/AR.pm
+++ b/SL/AR.pm
@@ -302,8 +302,6 @@ sub post_transaction {
       exporttype => DATEV_ET_BUCHUNGEN,
       format     => DATEV_FORMAT_KNE,
       dbh        => $dbh,
-      from       => $transdate,
-      to         => $transdate,
       trans_id   => $form->{id},
     );
 
index 2647ea8..5629fd7 100644 (file)
@@ -963,6 +963,7 @@ sub all_rights_full {
     ["general_ledger",                 $locale->text("Transactions, AR transactions, AP transactions")],
     ["datev_export",                   $locale->text("DATEV Export")],
     ["cash",                           $locale->text("Receipt, payment, reconciliation")],
+    ["bank_transaction",               $locale->text("Bank transactions")],
     ["--reports",                      $locale->text('Reports')],
     ["report",                         $locale->text('All reports')],
     ["advance_turnover_tax_return",    $locale->text('Advance turnover tax return')],
diff --git a/SL/BankAccount.pm b/SL/BankAccount.pm
deleted file mode 100644 (file)
index 3178b2a..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-package SL::BankAccount;
-
-use strict;
-
-use SL::DBUtils;
-
-sub save {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my %params   = @_;
-
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
-
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-
-  if (!$params{id}) {
-    ($params{id}) = selectfirst_array_query($form, $dbh, qq|SELECT nextval('id')|);
-    do_query($form, $dbh, qq|INSERT INTO bank_accounts (id, chart_id)
-                             VALUES (?, (SELECT id FROM chart LIMIT 1))|, conv_i($params{id}));
-  }
-
-  my $query =
-    qq|UPDATE bank_accounts
-       SET name= ?, account_number = ?, bank_code = ?, bank = ?, iban = ?, bic = ?, chart_id = ?
-       WHERE id = ?|;
-  my @values = (@params{qw(name account_number bank_code bank iban bic)}, conv_i($params{chart_id}), conv_i($params{id}));
-
-  do_query($form, $dbh, $query, @values);
-
-  $dbh->commit() unless ($params{dbh});
-
-  $main::lxdebug->leave_sub();
-
-  return $params{id};
-}
-
-sub retrieve {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my %params   = @_;
-
-  Common::check_params(\%params, qw(id));
-
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
-
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-
-  my $query    = qq|SELECT * FROM bank_accounts WHERE id = ?|;
-  my $account  = selectfirst_hashref_query($form, $dbh, $query, conv_i($params{id}));
-
-  $main::lxdebug->leave_sub();
-
-  return $account;
-}
-
-sub delete {
-  $::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my %params   = @_;
-
-  Common::check_params(\%params, qw(id));
-
-  my $dbh = $params{dbh} || $::form->get_standard_dbh(%::myconfig);
-
-  my $query = '
-    DELETE
-    FROM bank_accounts
-    WHERE id = ?';
-
-  do_query($::form, $dbh, $query, conv_i($params{id}));
-
-  $dbh->commit();
-
-  $::lxdebug->leave_sub();
-}
-
-sub list {
-  $main::lxdebug->enter_sub();
-
-  my $self     = shift;
-  my %params   = @_;
-
-  my $myconfig = \%main::myconfig;
-  my $form     = $main::form;
-
-  my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
-
-  my %sort_columns = (
-    'name'              => [ 'ba.name', ],
-    'account_number'    => [ 'ba.account_number', ],
-    'bank_code'         => [ 'ba.bank_code', 'ba.account_number', ],
-    'bank'              => [ 'ba.bank',      'ba.account_number', ],
-    'iban'              => [ 'ba.iban',      'ba.account_number', ],
-    'bic'               => [ 'ba.bic',       'ba.account_number', ],
-    'chart_accno'       => [ 'c.accno', ],
-    'chart_description' => [ 'c.description', ],
-    );
-
-  my %sort_spec = create_sort_spec('defs' => \%sort_columns, 'default' => 'bank', 'column' => $params{sortorder}, 'dir' => $params{sortdir});
-
-  my $query =
-    qq|SELECT ba.id, ba.name, ba.account_number, ba.bank_code, ba.bank, ba.iban, ba.bic, ba.chart_id,
-         c.accno AS chart_accno, c.description AS chart_description
-       FROM bank_accounts ba
-       LEFT JOIN chart c ON (ba.chart_id = c.id)
-       ORDER BY $sort_spec{sql}|;
-
-  my $results = selectall_hashref_query($form, $dbh, $query);
-
-  $main::lxdebug->leave_sub();
-
-  return $results;
-}
-
-
-1;
diff --git a/SL/Controller/BankAccount.pm b/SL/Controller/BankAccount.pm
new file mode 100644 (file)
index 0000000..dff4312
--- /dev/null
@@ -0,0 +1,122 @@
+package SL::Controller::BankAccount;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::Helper::Flash;
+use SL::Locale::String;
+use SL::DB::Default;
+use SL::DB::Manager::BankAccount;
+use SL::DB::Manager::BankTransaction;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar                  => [ qw(bank_account) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('load_bank_account', only => [ qw(edit update delete) ]);
+
+#
+# actions
+#
+
+sub action_list {
+  my ($self) = @_;
+
+  $self->render('bankaccounts/list',
+                title           => t8('Bank accounts'),
+                BANKACCOUNTS    => SL::DB::Manager::BankAccount->get_all_sorted,
+               );
+}
+
+sub action_new {
+  my ($self) = @_;
+
+  $self->{bank_account} = SL::DB::BankAccount->new;
+  $self->render('bankaccounts/form',
+                 title => t8('Add bank account'));
+}
+
+sub action_edit {
+  my ($self) = @_;
+
+  $self->render('bankaccounts/form', title => t8('Edit bank account'));
+}
+
+sub action_create {
+  my ($self) = @_;
+
+  $self->{bank_account} = SL::DB::BankAccount->new;
+  $self->create_or_update;
+}
+
+sub action_update {
+  my ($self) = @_;
+  $self->create_or_update;
+}
+
+sub action_delete {
+  my ($self) = @_;
+
+  if ( $self->{bank_account}->{number_of_bank_transactions} > 0 ) {
+    flash_later('error', $::locale->text('The bank account has been used and cannot be deleted.'));
+  } elsif ( eval { $self->{bank_account}->delete; 1; } ) {
+    flash_later('info',  $::locale->text('The bank account has been deleted.'));
+  } else {
+    flash_later('error', $::locale->text('The bank account has been used and cannot be deleted.'));
+  };
+  $self->redirect_to(action => 'list');
+
+}
+
+sub action_reorder {
+  my ($self) = @_;
+
+  SL::DB::BankAccount->reorder_list(@{ $::form->{account_id} || [] });
+  $self->render(\'', { type => 'json' });
+}
+
+#
+# filters
+#
+
+sub check_auth {
+  $::auth->assert('config');
+}
+
+sub load_bank_account {
+  my ($self) = @_;
+
+  $self->{bank_account} = SL::DB::BankAccount->new(id => $::form->{id})->load;
+  $self->{bank_account}->{number_of_bank_transactions} = SL::DB::Manager::BankTransaction->get_all_count( query => [ local_bank_account_id => $self->{bank_account}->{id} ] );
+}
+
+#
+# helpers
+#
+
+sub create_or_update {
+  my ($self) = @_;
+  my $is_new = !$self->{bank_account}->id;
+
+  my $params = delete($::form->{bank_account}) || { };
+
+  $self->{bank_account}->assign_attributes(%{ $params });
+
+  my @errors = $self->{bank_account}->validate;
+
+  if (@errors) {
+    flash('error', @errors);
+    $self->render('bankaccounts/form',
+                   title => $is_new ? t8('Add bank account') : t8('Edit bank account'));
+    return;
+  }
+
+  $self->{bank_account}->save;
+
+  flash_later('info', $is_new ? t8('The bank account has been created.') : t8('The bank account has been saved.'));
+  $self->redirect_to(action => 'list');
+}
+
+1;
diff --git a/SL/Controller/BankTransaction.pm b/SL/Controller/BankTransaction.pm
new file mode 100644 (file)
index 0000000..918d6aa
--- /dev/null
@@ -0,0 +1,585 @@
+package SL::Controller::BankTransaction;
+
+# idee- möglichkeit bankdaten zu übernehmen in stammdaten
+# erst Kontenabgleich, um alle gl-Einträge wegzuhaben
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Helper::ReportGenerator;
+use SL::ReportGenerator;
+
+use SL::DB::BankTransaction;
+use SL::Helper::Flash;
+use SL::Locale::String;
+use SL::SEPA;
+use SL::DB::Invoice;
+use SL::DB::PurchaseInvoice;
+use SL::DB::RecordLink;
+use SL::JSON;
+use SL::DB::Chart;
+use SL::DB::AccTransaction;
+use SL::DB::Tax;
+use SL::DB::Draft;
+use SL::DB::BankAccount;
+use SL::Presenter;
+use List::Util qw(max);
+
+use Rose::Object::MakeMethods::Generic
+(
+ 'scalar --get_set_init' => [ qw(models) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+
+
+#
+# actions
+#
+
+sub action_search {
+  my ($self) = @_;
+
+  my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
+
+  $self->render('bank_transactions/search',
+                 BANK_ACCOUNTS => $bank_accounts);
+}
+
+sub action_list_all {
+  my ($self) = @_;
+
+  $self->make_filter_summary;
+  $self->prepare_report;
+
+  $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
+}
+
+sub action_list {
+  my ($self) = @_;
+
+  if (!$::form->{filter}{bank_account}) {
+    flash('error', t8('No bank account chosen!'));
+    $self->action_search;
+    return;
+  }
+
+  my $sort_by = $::form->{sort_by} || 'transdate';
+  $sort_by = 'transdate' if $sort_by eq 'proposal';
+  $sort_by .= $::form->{sort_dir} ? ' DESC' : ' ASC';
+
+  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},
+                                                                               @where ],
+                                                                    with_objects => [ 'local_bank_account', 'currency' ],
+                                                                    sort_by => $sort_by, limit => 10000);
+
+  my $all_open_ar_invoices = SL::DB::Manager::Invoice->get_all(where => [amount => { gt => \'paid' }], with_objects => 'customer');
+  my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { gt => \'paid' }], with_objects => 'vendor');
+
+  my @all_open_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
+
+    $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}
+
+    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
+
+    my $max_agreement = max map { $_->{agreement} } @all_open_invoices;
+
+    # add open_invoices with highest agreement into array $bt->{proposals}
+    if ( $max_agreement >= $min_agreement ) {
+      $bt->{proposals} = [ grep { $_->{agreement} == $max_agreement } @all_open_invoices ];
+      $bt->{agreement} = $max_agreement; #scalar @{ $bt->{proposals} } ? $agreement + 1 : '';
+
+      # store the rule_matches in a separate array, so they can be displayed in template
+      foreach ( @{ $bt->{proposals} } ) {
+        push(@{$bt->{rule_matches}}, $_->{rule_matches});
+      };
+    };
+  }  # finished one bt
+  # finished all bt
+
+  # separate filter for proposals (second tab, agreement >= 5 and exactly one match)
+  # to qualify as a proposal there has to be
+  # * agreement >= 5  TODO: make threshold configurable in configuration
+  # * there must be only one exact match
+  # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?)
+  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 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('Bank transactions MT940'),
+                BANK_TRANSACTIONS => $bank_transactions,
+                PROPOSALS         => \@proposals,
+                bank_account      => $bank_account );
+}
+
+sub action_assign_invoice {
+  my ($self) = @_;
+
+  $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
+
+  $self->render('bank_transactions/assign_invoice', { layout  => 0 },
+                title      => t8('Assign invoice'),);
+}
+
+sub action_create_invoice {
+  my ($self) = @_;
+  my %myconfig = %main::myconfig;
+
+  $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
+  my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
+
+  my $drafts = SL::DB::Manager::Draft->get_all(where => [ module => 'ap'] , with_objects => 'employee');
+
+  my @filtered_drafts;
+
+  foreach my $draft ( @{ $drafts } ) {
+    my $draft_as_object = YAML::Load($draft->form);
+    my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
+    $draft->{vendor} = $vendor->name;
+    $draft->{vendor_id} = $vendor->id;
+    push @filtered_drafts, $draft;
+  }
+
+  #Filter drafts
+  @filtered_drafts = grep { $_->{vendor_id} == $vendor_of_transaction->id } @filtered_drafts if $vendor_of_transaction;
+
+  my $all_vendors = SL::DB::Manager::Vendor->get_all();
+
+  $self->render('bank_transactions/create_invoice', { layout  => 0 },
+      title      => t8('Create invoice'),
+      DRAFTS     => \@filtered_drafts,
+      vendor_id  => $vendor_of_transaction ? $vendor_of_transaction->id : undef,
+      vendor_name => $vendor_of_transaction ? $vendor_of_transaction->name : undef,
+      ALL_VENDORS => $all_vendors,
+      limit      => $myconfig{vclimit},
+      callback   => $self->url_for(action                => 'list',
+                                   'filter.bank_account' => $::form->{filter}->{bank_account},
+                                   'filter.todate'       => $::form->{filter}->{todate},
+                                   'filter.fromdate'     => $::form->{filter}->{fromdate}),
+      );
+}
+
+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) = @_;
+
+  $self->{transaction} = SL::DB::Manager::BankTransaction->find_by(id => $::form->{bt_id});
+  my $vendor_of_transaction = SL::DB::Manager::Vendor->find_by(account_number => $self->{transaction}->{remote_account_number});
+
+  my $drafts = SL::DB::Manager::Draft->get_all(with_objects => 'employee');
+
+  my @filtered_drafts;
+
+  foreach my $draft ( @{ $drafts } ) {
+    my $draft_as_object = YAML::Load($draft->form);
+    my $vendor = SL::DB::Manager::Vendor->find_by(id => $draft_as_object->{vendor_id});
+    $draft->{vendor} = $vendor->name;
+    $draft->{vendor_id} = $vendor->id;
+    push @filtered_drafts, $draft;
+  }
+
+  my $vendor_name = $::form->{vendor};
+  my $vendor_id = $::form->{vendor_id};
+
+  #Filter drafts
+  @filtered_drafts = grep { $_->{vendor_id} == $vendor_id } @filtered_drafts if $vendor_id;
+  @filtered_drafts = grep { $_->{vendor} =~ /$vendor_name/i } @filtered_drafts if $vendor_name;
+
+  my $output  = $self->render(
+      'bank_transactions/filter_drafts',
+      { output      => 0 },
+      DRAFTS => \@filtered_drafts,
+      );
+
+  my %result = ( count => 0, html => $output );
+
+  $self->render(\to_json(\%result), { type => 'json', process => 0 });
+}
+
+sub action_ajax_add_list {
+  my ($self) = @_;
+
+  my @where_sale     = (amount => { ne => \'paid' });
+  my @where_purchase = (amount => { ne => \'paid' });
+
+  if ($::form->{invnumber}) {
+    push @where_sale,     (invnumber => { ilike => '%' . $::form->{invnumber} . '%'});
+    push @where_purchase, (invnumber => { ilike => '%' . $::form->{invnumber} . '%'});
+  }
+
+  if ($::form->{amount}) {
+    push @where_sale,     (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
+    push @where_purchase, (amount => $::form->parse_amount(\%::myconfig, $::form->{amount}));
+  }
+
+  if ($::form->{vcnumber}) {
+    push @where_sale,     ('customer.customernumber' => { ilike => '%' . $::form->{vcnumber} . '%'});
+    push @where_purchase, ('vendor.vendornumber'     => { ilike => '%' . $::form->{vcnumber} . '%'});
+  }
+
+  if ($::form->{vcname}) {
+    push @where_sale,     ('customer.name' => { ilike => '%' . $::form->{vcname} . '%'});
+    push @where_purchase, ('vendor.name'   => { ilike => '%' . $::form->{vcname} . '%'});
+  }
+
+  if ($::form->{transdatefrom}) {
+    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($::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;
+  # 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 $output  = $self->render(
+      'bank_transactions/add_list',
+      { output      => 0 },
+      INVOICES => \@all_open_invoices,
+      );
+
+  my %result = ( count => 0, html => $output );
+
+  $self->render(\to_json(\%result), { type => 'json', process => 0 });
+}
+
+sub action_ajax_accept_invoices {
+  my ($self) = @_;
+
+  my @selected_invoices;
+  foreach my $invoice_id (@{ $::form->{invoice_id} || [] }) {
+    my $invoice_object = SL::DB::Manager::Invoice->find_by(id => $invoice_id);
+    $invoice_object ||= SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id);
+
+    push @selected_invoices, $invoice_object;
+  }
+
+  $self->render('bank_transactions/invoices', { layout => 0 },
+                INVOICES => \@selected_invoices,
+                bt_id    => $::form->{bt_id} );
+}
+
+sub action_save_invoices {
+  my ($self) = @_;
+
+  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);
+    my $sign = $bank_transaction->amount < 0 ? -1 : 1;
+    my $amount_of_transaction = $sign * $bank_transaction->amount;
+
+    my @invoices;
+    foreach my $invoice_id (@{ $invoice_ids }) {
+      push @invoices, (SL::DB::Manager::Invoice->find_by(id => $invoice_id) || SL::DB::Manager::PurchaseInvoice->find_by(id => $invoice_id));
+    }
+    @invoices = sort { return 1 if ($a->is_sales and $a->amount > 0);
+                          return 1 if (!$a->is_sales and $a->amount < 0);
+                          return -1; } @invoices                if $bank_transaction->amount > 0;
+    @invoices = sort { return -1 if ($a->is_sales and $a->amount > 0);
+                       return -1 if (!$a->is_sales and $a->amount < 0);
+                       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 paid by bank transaction #1 (Account number: #2, bank code: #3)!',
+                                            $bank_transaction->purpose,
+                                            $bank_transaction->remote_account_number,
+                                            $bank_transaction->remote_bank_code));
+        last;
+      }
+      #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,
+                              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);
+        } else {
+          $amount_of_transaction += $sign * $invoice->amount if (!$invoice->is_sales);
+          $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,
+                              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;
+      }
+
+      #Record a link from the bank transaction to the invoice
+      my @props = (
+          from_table => 'bank_transactions',
+          from_id    => $bt_id,
+          to_table   => $invoice->is_sales ? 'ar' : 'ap',
+          to_id      => $invoice->id,
+          );
+
+      my $existing = SL::DB::Manager::RecordLink->get_all(where => \@props, limit => 1)->[0];
+
+      SL::DB::RecordLink->new(@props)->save if !$existing;
+    }
+    $bank_transaction->save;
+  }
+
+  $self->action_list();
+}
+
+sub action_save_proposals {
+  my ($self) = @_;
+
+  foreach my $bt_id (@{ $::form->{proposal_ids} }) {
+    #mark bt as booked
+    my $bt = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
+    $bt->invoice_amount($bt->amount);
+    $bt->save;
+
+    #pay invoice
+    my $arap = SL::DB::Manager::Invoice->find_by(id => $::form->{"proposed_invoice_$bt_id"});
+    $arap    = SL::DB::Manager::PurchaseInvoice->find_by(id => $::form->{"proposed_invoice_$bt_id"}) if not defined $arap;
+    $arap->pay_invoice(chart_id  => $bt->local_bank_account->chart_id,
+                       trans_id  => $arap->id,
+                       amount    => $arap->amount,
+                       transdate => $bt->transdate->to_kivitendo);
+    $arap->save;
+
+    #create record link
+    my @props = (
+        from_table => 'bank_transactions',
+        from_id    => $bt_id,
+        to_table   => $arap->is_sales ? 'ar' : 'ap',
+        to_id      => $arap->id,
+        );
+
+    my $existing = SL::DB::Manager::RecordLink->get_all(where => \@props, limit => 1)->[0];
+
+    SL::DB::RecordLink->new(@props)->save if !$existing;
+  }
+
+  flash('ok', t8('#1 proposal(s) saved.', scalar @{ $::form->{proposal_ids} }));
+
+  $self->action_list();
+}
+
+#
+# filters
+#
+
+sub check_auth {
+  $::auth->assert('bank_transaction');
+}
+
+#
+# helpers
+#
+
+sub make_filter_summary {
+  my ($self) = @_;
+
+  my $filter = $::form->{filter} || {};
+  my @filter_strings;
+
+  my @filters = (
+    [ $filter->{"transdate:date::ge"},  $::locale->text('Transdate')  . " " . $::locale->text('From Date') ],
+    [ $filter->{"transdate:date::le"},  $::locale->text('Transdate')  . " " . $::locale->text('To Date')   ],
+    [ $filter->{"valutadate:date::ge"}, $::locale->text('Valutadate') . " " . $::locale->text('From Date') ],
+    [ $filter->{"valutadate:date::le"}, $::locale->text('Valutadate') . " " . $::locale->text('To Date')   ],
+    [ $filter->{"amount:number"},       $::locale->text('Amount')                                          ],
+    [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account')                          ],
+  );
+
+  for (@filters) {
+    push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
+  }
+
+  $self->{filter_summary} = join ', ', @filter_strings;
+}
+
+sub prepare_report {
+  my ($self)      = @_;
+
+  my $callback    = $self->models->get_callback;
+
+  my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
+  $self->{report} = $report;
+
+  my @columns     = qw(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 } },
+    valutadate            => { sub => sub { $_[0]->valutadate_as_date } },
+    remote_name           => { },
+    remote_account_number => { },
+    remote_bank_code      => { },
+    amount                => { sub => sub { $_[0]->amount_as_number },
+                               align => 'right' },
+    invoice_amount        => { sub => sub { $_[0]->invoice_amount_as_number },
+                               align => 'right' },
+    invoices              => { sub => sub { $_[0]->linked_invoices } },
+    currency              => { sub => sub { $_[0]->currency->name } },
+    purpose               => { },
+    local_account_number  => { sub => sub { $_[0]->local_bank_account->account_number } },
+    local_bank_code       => { sub => sub { $_[0]->local_bank_account->bank_code } },
+    local_bank_name       => { sub => sub { $_[0]->local_bank_account->name } },
+    id                    => {},
+  );
+
+  map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
+
+  $report->set_options(
+    std_column_visibility => 1,
+    controller_class      => 'BankTransaction',
+    output_format         => 'HTML',
+    top_info_text         => $::locale->text('Bank transactions'),
+    title                 => $::locale->text('Bank transactions'),
+    allow_pdf_export      => 1,
+    allow_csv_export      => 1,
+  );
+  $report->set_columns(%column_defs);
+  $report->set_column_order(@columns);
+  $report->set_export_options(qw(list_all filter));
+  $report->set_options_from_form;
+  $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
+  $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
+
+  my $bank_accounts = SL::DB::Manager::BankAccount->get_all_sorted();
+
+  $report->set_options(
+    raw_top_info_text     => $self->render('bank_transactions/report_top',    { output => 0 }, BANK_ACCOUNTS => $bank_accounts),
+    raw_bottom_info_text  => $self->render('bank_transactions/report_bottom', { output => 0 }),
+  );
+}
+
+sub init_models {
+  my ($self) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller => $self,
+    sorted => {
+      _default => {
+        by    => 'transdate',
+        dir   => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
+      },
+      transdate             => t8('Transdate'),
+      remote_name           => t8('Remote name'),
+      amount                => t8('Amount'),
+      invoice_amount        => t8('Assigned'),
+      invoices              => t8('Linked invoices'),
+      valutadate            => t8('Valutadate'),
+      remote_account_number => t8('Remote account number'),
+      remote_bank_code      => t8('Remote bank code'),
+      currency              => t8('Currency'),
+      purpose               => t8('Purpose'),
+      local_account_number  => t8('Local account number'),
+      local_bank_code       => t8('Local bank code'),
+      local_bank_name       => t8('Bank account'),
+    },
+    with_objects => [ 'local_bank_account', 'currency' ],
+  );
+}
+
+1;
index 1992210..2bc2955 100644 (file)
@@ -18,6 +18,7 @@ use SL::Controller::CsvImport::Shipto;
 use SL::Controller::CsvImport::Project;
 use SL::Controller::CsvImport::Order;
 use SL::JSON;
+use SL::Controller::CsvImport::BankTransaction;
 use SL::BackgroundJob::CsvImport;
 use SL::System::TaskServer;
 
@@ -223,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});
 }
 
@@ -268,6 +269,8 @@ sub render_inputs {
             : $self->type eq 'inventories'       ? $::locale->text('CSV import: inventories')
             : $self->type eq 'projects'          ? $::locale->text('CSV import: projects')
             : $self->type eq 'orders'            ? $::locale->text('CSV import: orders')
+            : $self->type eq 'bank_transactions' ? $::locale->text('CSV import: bank transactions')
+            : $self->type eq 'mt940'             ? $::locale->text('CSV import: MT940')
             : die;
 
   if ($self->{type} eq 'customers_vendors' or $self->{type} eq 'orders'  ) {
@@ -289,10 +292,28 @@ sub test_and_import_deferred {
 
   $self->profile_from_form;
 
-  if ($::form->{file}) {
+  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 = $::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 |");
+    $converted_mt940 .=  '"transaction_id";"local_bank_code";"local_account_number";"remote_bank_code";"remote_account_number";"transdate";"valutadate";"amount";"currency";"remote_name";"remote_name_1";"purpose";"purpose1";"purpose2";"purpose3";"purpose4";"purpose5";"purpose6";"purpose7";"purpose8";"purpose9";"purpose10";"purpose11"' . "\n";
+    my $headerline = <MT>;  # discard original header line
+    while (<MT>) {
+      $converted_mt940 .= $_;
+    };
     my $file = SL::SessionFile->new($self->csv_file_name, mode => '>');
-    $file->fh->print($::form->{file});
+    $file->fh->print($converted_mt940);
     $file->fh->close;
+  } elsif ($::form->{file}) {
+      my $file = SL::SessionFile->new($self->csv_file_name, mode => '>');
+      $file->fh->print($::form->{file});
+      $file->fh->close;
   }
 
   my $file = SL::SessionFile->new($self->csv_file_name, mode => '<', encoding => $self->profile->get('charset'));
@@ -618,6 +639,8 @@ sub init_worker {
        : $self->{type} eq 'inventories'       ? SL::Controller::CsvImport::Inventory->new(@args)
        : $self->{type} eq 'projects'          ? SL::Controller::CsvImport::Project->new(@args)
        : $self->{type} eq 'orders'            ? SL::Controller::CsvImport::Order->new(@args)
+       : $self->{type} eq 'bank_transactions' ? SL::Controller::CsvImport::BankTransaction->new(@args)
+       : $self->{type} eq 'mt940'             ? SL::Controller::CsvImport::BankTransaction->new(@args)
        :                                        die "Program logic error";
 }
 
diff --git a/SL/Controller/CsvImport/BankTransaction.pm b/SL/Controller/CsvImport/BankTransaction.pm
new file mode 100644 (file)
index 0000000..76e9020
--- /dev/null
@@ -0,0 +1,176 @@
+package SL::Controller::CsvImport::BankTransaction;
+
+use strict;
+
+use SL::Helper::Csv;
+use SL::Controller::CsvImport::Helper::Consistency;
+use SL::DB::BankTransaction;
+
+use Data::Dumper;
+
+use parent qw(SL::Controller::CsvImport::Base);
+
+use Rose::Object::MakeMethods::Generic
+(
+ 'scalar --get_set_init' => [ qw(bank_accounts_by) ],
+);
+
+sub init_class {
+  my ($self) = @_;
+  $self->class('SL::DB::BankTransaction');
+}
+
+sub init_bank_accounts_by {
+  my ($self) = @_;
+
+  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 };
+  foreach my $entry (@{ $self->controller->data }) {
+    $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
+
+    $self->check_bank_account($entry);
+    $self->check_currency($entry, take_default => 1);
+    $self->join_purposes($entry);
+    $self->join_remote_names($entry);
+    $self->check_existing($entry) unless @{ $entry->{errors} };
+  } continue {
+    $i++;
+  }
+
+  $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 {
+  my ($self) = @_;
+
+  $self->SUPER::setup_displayable_columns;
+
+  # TODO: don't show fields cleared, invoice_amount and transaction_id in the help text, as these should not be imported
+  $self->add_displayable_columns({ name => 'local_bank_code',       description => $::locale->text('Own bank code') },
+                                 { name => 'local_account_number',  description => $::locale->text('Own bank account number or IBAN') },
+                                 { name => 'local_bank_account_id', description => $::locale->text('ID of own bank account') },
+                                 { name => 'remote_bank_code',      description => $::locale->text('Bank code of the goal/source') },
+                                 { name => 'remote_account_number', description => $::locale->text('Account number of the goal/source') },
+                                 { name => 'transdate',             description => $::locale->text('Date of transaction') },
+                                 { name => 'valutadate',            description => $::locale->text('Valuta date') },
+                                 { name => 'amount',                description => $::locale->text('Amount') },
+                                 { name => 'currency',              description => $::locale->text('Currency') },
+                                 { name => 'currency_id',           description => $::locale->text('Currency (database ID)')          },
+                                 { name => 'remote_name',           description => $::locale->text('Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")') },
+                                 { name => 'purpose',               description => $::locale->text('Purpose (if field names purpose, purpose1, purpose2 ... exist they will all combined into the field "purpose")') },
+                                 );
+}
+
+sub check_bank_account {
+  my ($self, $entry) = @_;
+
+  my $object = $entry->{object};
+
+  # 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;
+  }
+
+  # Check whether or not local_bank_account ID, local_account_number and local_bank_code are consistent.
+  if ($object->local_bank_account_id && $entry->{raw_data}->{local_account_number}) {
+    my $bank_account = $self->bank_accounts_by->{id}->{ $object->local_bank_account_id };
+    if ($bank_account->account_number ne $entry->{raw_data}->{local_account_number}) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid local bank account');
+      return 0;
+    }
+    if ($entry->{raw_data}->{local_bank_code} && $entry->{raw_data}->{local_bank_code} ne $bank_account->bank_code) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid local bank account');
+      return 0;
+    }
+
+  }
+
+  # 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;
+    }
+    if ($entry->{raw_data}->{local_bank_code} && $entry->{raw_data}->{local_bank_code} ne $bank_account->bank_code) {
+      push @{ $entry->{errors} }, $::locale->text('Error: Invalid local bank account');
+      return 0;
+    }
+
+    $object->local_bank_account_id($bank_account->id);
+    $entry->{info_data}->{local_bank_name} = $bank_account->name;
+  }
+
+  return $object->local_bank_account_id ? 1 : 0;
+}
+
+sub join_purposes {
+  my ($self, $entry) = @_;
+
+  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} );
+  $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 9712697..3090182 100644 (file)
@@ -6,6 +6,8 @@ use English qw(-no_match_vars);
 use List::MoreUtils qw(pairwise any);
 
 use SL::Helper::Csv;
+
+use SL::DB::BankAccount;
 use SL::DB::Customer;
 use SL::DB::Language;
 use SL::DB::PaymentTerm;
@@ -19,7 +21,7 @@ use parent qw(Rose::Object);
 use Rose::Object::MakeMethods::Generic
 (
  scalar                  => [ qw(controller file csv test_run save_with_cascade) ],
- 'scalar --get_set_init' => [ qw(profile displayable_columns existing_objects class manager_class cvar_columns all_cvar_configs all_languages payment_terms_by delivery_terms_by all_vc vc_by clone_methods) ],
+ 'scalar --get_set_init' => [ qw(profile displayable_columns existing_objects class manager_class cvar_columns all_cvar_configs all_languages payment_terms_by delivery_terms_by all_bank_accounts all_vc vc_by clone_methods) ],
 );
 
 sub run {
@@ -141,6 +143,12 @@ sub init_all_languages {
   return SL::DB::Manager::Language->get_all;
 }
 
+sub init_all_bank_accounts {
+  my ($self) = @_;
+
+  return SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
+}
+
 sub init_payment_terms_by {
   my ($self) = @_;
 
index 3b03e04..73144db 100644 (file)
@@ -6,6 +6,8 @@ use parent 'SL::Controller::Helper::GetModels::Base';
 use Carp;
 use List::MoreUtils qw(uniq);
 
+use Data::Dumper;
+
 use Rose::Object::MakeMethods::Generic (
   scalar => [ qw(by dir specs form_data) ],
   'scalar --get_set_init' => [ qw(form_params) ],
index c9b0949..6049785 100644 (file)
@@ -22,6 +22,8 @@ use SL::DB::ProjectType;
 use SL::Helper::Flash;
 use SL::Locale::String;
 
+use Data::Dumper;
+
 use Rose::Object::MakeMethods::Generic
 (
  scalar => [ qw(project linked_records) ],
@@ -41,6 +43,7 @@ sub action_search {
   my %params;
 
   $params{CUSTOM_VARIABLES}  = CVar->get_configs(module => 'Projects');
+
   ($params{CUSTOM_VARIABLES_FILTER_CODE}, $params{CUSTOM_VARIABLES_INCLUSION_CODE})
     = CVar->render_search_options(variables      => $params{CUSTOM_VARIABLES},
                                   include_prefix => 'l_',
diff --git a/SL/Controller/Reconciliation.pm b/SL/Controller/Reconciliation.pm
new file mode 100644 (file)
index 0000000..c6faef1
--- /dev/null
@@ -0,0 +1,620 @@
+package SL::Controller::Reconciliation;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::Locale::String;
+use SL::JSON;
+use SL::Controller::Helper::ParseFilter;
+use SL::Helper::Flash;
+
+use SL::DB::BankTransaction;
+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) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('_bank_account');
+
+#
+# actions
+#
+
+sub action_search {
+  my ($self) = @_;
+
+  $self->render('reconciliation/search');
+}
+
+sub action_reconciliation {
+  my ($self) = @_;
+
+  $self->_get_linked_transactions;
+
+  $self->_get_balances;
+
+  $self->render('reconciliation/form',
+                title => t8('Reconciliation'));
+}
+
+sub action_load_overview {
+  my ($self) = @_;
+
+  $self->_get_proposals;
+
+  $self->_get_linked_transactions;
+
+  $self->_get_balances;
+
+  my $output = $self->render('reconciliation/tabs/overview', { output => 0 });
+  my %result = ( html => $output );
+
+  $self->render(\to_json(\%result), { type => 'json', process => 0 });
+}
+
+sub action_filter_overview {
+  my ($self) = @_;
+
+  $self->_get_linked_transactions;
+  $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)
+                 );
+
+  $self->render(\to_json(\%result), { type => 'json', process => 0 });
+}
+
+sub action_update_reconciliation_table {
+  my ($self) = @_;
+
+  my @errors = $self->_get_elements_and_validate();
+
+  my $output = $self->render('reconciliation/assigning_table', { output => 0 },
+                 bt_sum => $::form->format_amount(\%::myconfig, $self->{bt_sum}, 2),
+                 bb_sum => $::form->format_amount(\%::myconfig, -1 * $self->{bb_sum}, 2),
+                 show_button => !@errors
+                 );
+
+  my %result = ( html => $output );
+
+  $self->render(\to_json(\%result), { type => 'json', process => 0 });
+}
+
+sub action_reconcile {
+  my ($self) = @_;
+
+  #Check elements
+  my @errors = $self->_get_elements_and_validate;
+
+  if (@errors) {
+    unshift(@errors, (t8('Could not reconcile chosen elements!')));
+    flash('error', @errors);
+    $self->action_reconciliation;
+    return;
+  }
+
+  $self->_reconcile;
+
+  $self->action_reconciliation;
+}
+
+sub action_delete_reconciliation {
+  my ($self) = @_;
+
+  my $rec_links = SL::DB::Manager::ReconciliationLink->get_all(where => [ rec_group => $::form->{rec_group} ]);
+
+  foreach my $rec_link (@{ $rec_links }) {
+    my $bank_transaction = SL::DB::Manager::BankTransaction->find_by( id           => $rec_link->bank_transaction_id );
+    my $acc_transaction  = SL::DB::Manager::AccTransaction ->find_by( acc_trans_id => $rec_link->acc_trans_id        );
+
+    $bank_transaction->cleared('0');
+    $acc_transaction->cleared('0');
+
+    $bank_transaction->save;
+    $acc_transaction->save;
+
+    $rec_link->delete;
+  }
+
+  $self->_get_linked_transactions;
+  $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)
+                 );
+
+  $self->render(\to_json(\%result), { type => 'json', process => 0 });
+}
+
+sub action_load_proposals {
+  my ($self) = @_;
+
+  $self->_get_proposals;
+
+  my $output = $self->render('reconciliation/tabs/automatic', { output => 0 });
+  my %result = ( html => $output );
+
+  $self->render(\to_json(\%result), { type => 'json', process => 0 });
+}
+
+sub action_filter_proposals {
+  my ($self) = @_;
+
+  $self->_get_balances;
+  $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)
+                 );
+
+  $self->render(\to_json(\%result), { type => 'json', process => 0 });
+}
+
+sub action_reconcile_proposals {
+  my ($self) = @_;
+
+  my $counter = 0;
+
+  foreach my $bt_id ( @{ $::form->{bt_ids} }) {
+    my $rec_group = SL::DB::Manager::ReconciliationLink->get_new_rec_group();
+    my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
+    $bank_transaction->cleared('1');
+    if ( $bank_transaction->isa('SL::DB::BankTransaction') ) {
+      $bank_transaction->invoice_amount($bank_transaction->amount);
+    }
+    $bank_transaction->save;
+    foreach my $acc_trans_id (@{ $::form->{proposal_list}->{$bt_id}->{BB} }) {
+      SL::DB::ReconciliationLink->new(
+        rec_group => $rec_group,
+        bank_transaction_id => $bt_id,
+        acc_trans_id => $acc_trans_id
+      )->save;
+      my $acc_trans = SL::DB::Manager::AccTransaction->find_by(acc_trans_id => $acc_trans_id);
+      $acc_trans->cleared('1');
+      $acc_trans->save;
+    }
+    $counter++;
+  }
+
+  flash('ok', t8('#1 proposal(s) saved.', $counter));
+
+  $self->action_reconciliation;
+}
+
+#
+# filters
+#
+
+sub check_auth {
+  $::auth->assert('bank_transaction');
+}
+
+sub _bank_account {
+  my ($self) = @_;
+  $self->{bank_account} = SL::DB::Manager::BankAccount->find_by(id => $::form->{filter}->{"local_bank_account_id:number"});
+}
+
+#
+# helpers
+#
+
+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' ]);
+
+  my $check_sum;
+
+  my @proposals;
+
+  foreach my $bt (@{ $bank_transactions }) {
+    $check_sum = $bt->amount;
+    my $proposal;
+    $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;
+      if ($linked_record->to_table eq 'ar') {
+        $invoice = SL::DB::Manager::Invoice->find_by(id => $linked_record->to_id);
+        #find payments
+        my $payments = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, chart_link => { like => '%AR_paid%' }, transdate => $bt->transdate ]);
+        foreach my $payment (@{ $payments }) {
+          $check_sum += $payment->amount;
+          push @{ $proposal->{BB} }, $payment;
+        }
+      }
+      if ($linked_record->to_table eq 'ap') {
+        $invoice = SL::DB::Manager::PurchaseInvoice->find_by(id => $linked_record->to_id);
+        #find payments
+        my $payments = SL::DB::Manager::AccTransaction->get_all(where => [ trans_id => $invoice->id, chart_link => { like => '%AP_paid%' }, transdate => $bt->transdate ]);
+        foreach my $payment (@{ $payments }) {
+          $check_sum += $payment->amount;
+          push @{ $proposal->{BB} }, $payment;
+        }
+      }
+    }
+
+    #add proposal if something in acc_trans was found
+    #otherwise try to find another entry in acc_trans and add it
+    # 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',
+                                                                                 '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];
+        push @proposals, $proposal;
+      }
+    }
+  }
+
+  $self->{PROPOSALS} = \@proposals;
+}
+
+sub _get_elements_and_validate {
+  my ($self) = @_;
+
+  my @errors;
+
+  if ( not defined $::form->{bt_ids} ) {
+    push @errors, t8('No bank account chosen!');
+  }
+
+  if ( not defined $::form->{bb_ids} ) {
+    push @errors, t8('No transaction on chart bank chosen!');
+  }
+
+  if (!@errors) {
+    if (scalar @{ $::form->{bt_ids} } > 1 and scalar @{ $::form->{bb_ids} } > 1) {
+      push @errors, t8('No 1:n or n:1 relation');
+    }
+  }
+
+  my @elements;
+  my ($bt_sum, $bb_sum) = (0,0);
+
+  foreach my $bt_id (@{ $::form->{bt_ids} }) {
+    my $bt = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
+    $bt->{type} = 'BT';
+    $bt_sum += $bt->amount;
+    push @elements, $bt;
+  }
+
+  foreach my $bb_id (@{ $::form->{bb_ids} }) {
+    my $bb = SL::DB::Manager::AccTransaction->find_by(acc_trans_id => $bb_id);
+    $bb->{type} = 'BB';
+    $bb->{id} = $bb->acc_trans_id;
+    $bb_sum += $bb->amount;
+    push @elements, $bb;
+  }
+
+  if ($::form->round_amount($bt_sum + $bb_sum) != 0) {
+    push @errors, t8('Out of balance!');
+  }
+
+  $self->{ELEMENTS} = \@elements;
+  $self->{bt_sum} = $bt_sum;
+  $self->{bb_sum} = $bb_sum;
+
+  return @errors;
+}
+
+sub _reconcile {
+  my ($self) = @_;
+
+  # 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
+  my $rec_group = SL::DB::Manager::ReconciliationLink->get_new_rec_group();
+  #There is either a 1:n relation or a n:1 relation
+  if (scalar @{ $::form->{bt_ids} } == 1) {
+    my $bt_id = @{ $::form->{bt_ids} }[0];
+    foreach my $bb_id (@{ $::form->{bb_ids} }) {
+      my $rec_link = SL::DB::ReconciliationLink->new(bank_transaction_id => $bt_id,
+                                                     acc_trans_id        => $bb_id,
+                                                     rec_group           => $rec_group);
+      $rec_link->save;
+    }
+  } else {
+    my $bb_id = @{ $::form->{bb_ids} }[0];
+    foreach my $bt_id (@{ $::form->{bt_ids} }) {
+      my $rec_link = SL::DB::ReconciliationLink->new(bank_transaction_id => $bt_id,
+                                                     acc_trans_id        => $bb_id,
+                                                     rec_group           => $rec_group);
+      $rec_link->save;
+    }
+  }
+}
+
+sub _filter_to_where {
+  my ($self) = @_;
+
+  my %parse_filter = parse_filter($::form->{filter});
+  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});
+  @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_trans.transdate'        => $filter{fromdate},
+                                      'acc_trans.transdate'        => $filter{todate}   ],
+                             and => [ 'bank_transaction.transdate' => $filter{fromdate},
+                                      '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} );
+  }
+
+  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_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_trans.transdate'        => $filter{todate} ,
+                              'bank_transaction.transdate' => $filter{todate} ] );
+    push @bt_where, (transdate                    => $filter{todate} );
+    push @bb_where, (transdate                    => $filter{todate} );
+  }
+
+  if ($filter{cleared}) {
+    $filter{cleared} = $filter{cleared} eq 'FALSE' ? '0' : '1';
+    push @rl_where, ('acc_trans.cleared'        => $filter{cleared} );
+
+    push @bt_where, (cleared                    => $filter{cleared} );
+    push @bb_where, (cleared                    => $filter{cleared} );
+  }
+
+  $self->{rl_where} = \@rl_where;
+  $self->{bt_where} = \@bt_where;
+  $self->{bb_where} = \@bb_where;
+}
+
+sub _get_linked_transactions {
+  my ($self) = @_;
+
+  $self->_filter_to_where;
+
+  my (@where, @bt_where, @bb_where);
+  # don't try to reconcile opening and closing balances
+  # instead use an offset in configuration
+
+  @where    = (@{ $self->{rl_where} });
+  @bt_where = (@{ $self->{bt_where} }, cleared => '0');
+  @bb_where = (@{ $self->{bb_where} }, cleared => '0');
+
+  my @rows;
+
+  my $reconciliation_groups = SL::DB::Manager::ReconciliationLink->get_all(distinct => 1,
+                                                                           select => ['rec_group'],
+                                                                           where => \@where,
+                                                                           with_objects => ['bank_transaction', 'acc_trans']);
+
+  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_trans']);
+    my $line;
+    my $first_transaction = shift @{ $linked_transactions };
+    my $first_bt = $first_transaction->bank_transaction;
+    my $first_bb = $first_transaction->acc_trans;
+
+    if (defined $fromdate) {
+      $first_bt->{class} = 'out_of_balance' if ( $first_bt->transdate lt $fromdate );
+      $first_bb->{class} = 'out_of_balance' if ( $first_bb->transdate lt $fromdate );
+    }
+    if (defined $todate) {
+      $first_bt->{class} = 'out_of_balance' if ( $first_bt->transdate gt $todate );
+      $first_bb->{class} = 'out_of_balance' if ( $first_bb->transdate gt $todate );
+    }
+    $line->{BT} = [ $first_bt ];
+    $line->{BB} = [ $first_bb ];
+    $line->{rec_group} = $first_transaction->rec_group;
+    $line->{type} = 'Link';
+
+    #add the rest of transaction of this group
+    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_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 );
+      }
+      if (defined $todate) {
+        $bank_transaction->{class} = 'out_of_balance' if ( $bank_transaction->transdate gt $todate );
+        $acc_transaction->{class}  = 'out_of_balance' if ( $acc_transaction->transdate  gt $todate );
+      }
+      if ($bank_transaction->id != $previous_bt_id) {
+        push @{ $line->{BT} }, $bank_transaction;
+      }
+      if ($acc_transaction->acc_trans_id != $previous_acc_trans_id) {
+        push @{ $line->{BB} }, $acc_transaction;
+      }
+    }
+    push @rows, $line;
+  }
+
+  # add non-cleared bank transactions
+  my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(where => \@bt_where);
+  foreach my $bt (@{ $bank_transactions }) {
+    my $line;
+    $line->{BT} = [ $bt ];
+    $line->{type} = 'BT';
+    $line->{id} = $bt->id;
+    push @rows, $line;
+  }
+
+  # 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->record->storno) {
+      my $line;
+      $line->{BB} = [ $bb ];
+      $line->{type} = 'BB';
+      $line->{id} = $bb->acc_trans_id;
+      push @rows, $line;
+    }
+  }
+
+  #sort lines
+  @rows = sort sort_by_transdate @rows;
+
+  $self->{LINKED_TRANSACTIONS} = \@rows;
+}
+
+sub sort_by_transdate {
+  if ($a->{BT} and $b->{BT}) {
+    return $a->{BT}[0]->amount <=> $b->{BT}[0]->amount if $a->{BT}[0]->transdate eq $b->{BT}[0]->transdate;
+    return $a->{BT}[0]->transdate cmp $b->{BT}[0]->transdate;
+  }
+  if ($a->{BT}) {
+    return $a->{BT}[0]->amount <=> (-1 * $b->{BB}[0]->amount) if $a->{BT}[0]->transdate eq $b->{BB}[0]->transdate;
+    return $a->{BT}[0]->transdate cmp $b->{BB}[0]->transdate;
+  }
+  if ($b->{BT}) {
+    return (-1 * $a->{BB}[0]->amount) <=> $b->{BT}[0]->amount if $a->{BB}[0]->transdate eq $b->{BT}[0]->transdate;
+    return $a->{BB}[0]->transdate cmp $b->{BT}[0]->transdate;
+  }
+  return (-1 * $a->{BB}[0]->amount) <=> (-1 * $b->{BB}[0]->amount) if $a->{BB}[0]->transdate eq $b->{BB}[0]->transdate;
+  return $a->{BB}[0]->transdate cmp $b->{BB}[0]->transdate;
+}
+
+sub _get_balances {
+  my ($self) = @_;
+
+  $self->_filter_to_where;
+
+  my (@bt_where, @bb_where);
+  @bt_where = @{ $self->{bt_where} };
+  @bb_where = @{ $self->{bb_where} };
+
+  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);
+
+  $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 };
+
+
+  $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;
+
+  $self->{difference} = $bt_balance + $bb_balance;
+}
+
+sub init_cleared {
+  [ { title => t8("all"),       value => ''           },
+    { title => t8("cleared"),   value => 'TRUE'       },
+    { title => t8("uncleared"), value => 'FALSE'      }, ]
+}
+
+sub init_BANK_ACCOUNTS {
+  SL::DB::Manager::BankAccount->get_all_sorted( query => [ obsolete => 0 ] );
+}
+
+1;
index e79c080..ca74cd2 100644 (file)
@@ -199,6 +199,8 @@ sub trans_id {
     $self->{trans_id} = $_[0];
   }
 
+  die "illegal trans_id passed for DATEV export: " . $self->{trans_id} . "\n" unless $self->{trans_id} =~ m/^\d+$/;
+
   return $self->{trans_id};
 }
 
@@ -346,19 +348,25 @@ sub _sign {
 sub _get_transactions {
   $main::lxdebug->enter_sub();
   my $self     = shift;
-  my $fromto   =  shift;
+  my $fromto   = shift;
   my $progress_callback = shift || sub {};
 
   my $form     =  $main::form;
 
   my $trans_id_filter = '';
 
-  $trans_id_filter = 'AND ac.trans_id = ' . $self->trans_id if $self->trans_id;
+  if ( $self->{trans_id} ) {
+    # ignore dates when trans_id is passed so that the entire transaction is
+    # checked, not just either the initial bookings or the subsequent payments
+    # (the transdates will likely differ)
+    $fromto = '';
+    $trans_id_filter = 'ac.trans_id = ' . $self->trans_id;
+  } else {
+    $fromto      =~ s/transdate/ac\.transdate/g;
+  };
 
   my ($notsplitindex);
 
-  $fromto      =~ s/transdate/ac\.transdate/g;
-
   my $filter   = '';            # Useful for debugging purposes
 
   my %all_taxchart_ids = selectall_as_map($form, $self->dbh, qq|SELECT DISTINCT chart_id, TRUE AS is_set FROM tax|, 'chart_id', 'is_set');
@@ -421,7 +429,10 @@ sub _get_transactions {
   $self->{DATEV} = [];
 
   my $counter = 0;
-  while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
+  my $continue = 1; #
+  my $name;
+  while ( $continue && (my $ref = $sth->fetchrow_hashref("NAME_lc")) ) {
+    last unless $ref;  # for single transactions
     $counter++;
     if (($counter % 500) == 0) {
       $progress_callback->($counter);
@@ -445,13 +456,27 @@ sub _get_transactions {
     # keep fetching new acc_trans lines until the end of a balanced group is reached
     while (abs($count) > 0.01 || $firstrun || ($subcent && abs($count) > 0.005)) {
       my $ref2 = $sth->fetchrow_hashref("NAME_lc");
-      last unless ($ref2);
+      unless ( $ref2 ) {
+        $continue = 0;
+        last;
+      };
 
       # check if trans_id of current acc_trans line is still the same as the
-      # trans_id of the first line in group
+      # trans_id of the first line in group, i.e. we haven't finished a 0-group
+      # before moving on to the next trans_id, error will likely be in the old
+      # trans_id.
 
       if ($ref2->{trans_id} != $trans->[0]->{trans_id}) {
-        $self->add_error("Unbalanced ledger! old trans_id " . $trans->[0]->{trans_id} . " new trans_id " . $ref2->{trans_id} . " count $count");
+        require SL::DB::Manager::AccTransaction;
+        if ( $trans->[0]->{trans_id} ) {
+          my $acc_trans_old_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
+          $self->add_error("Unbalanced ledger! Old: " . $acc_trans_old_obj->transaction_name) if ref($acc_trans_old_obj);
+        };
+        if ( $ref2->{trans_id} ) {
+          my $acc_trans_curr_obj = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $ref2->{trans_id} ]);
+          $self->add_error("Unbalanced ledger! New:" . $acc_trans_curr_obj->transaction_name) if ref($acc_trans_curr_obj);
+        };
+        $self->add_error("count: $count");
         return;
       }
 
@@ -618,7 +643,9 @@ sub _get_transactions {
 
     $absumsatz = $form->round_amount($absumsatz, 2);
     if (abs($absumsatz) >= (0.01 * (1 + scalar @taxed))) {
-      $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion $trans->[0]->{trans_id} ($absumsatz)");
+      require SL::DB::Manager::AccTransaction;
+      my $acc_trans_obj  = SL::DB::Manager::AccTransaction->get_first(where => [ trans_id => $trans->[0]->{trans_id} ]);
+      $self->add_error("Datev-Export fehlgeschlagen! Bei Transaktion " . $acc_trans_obj->transaction_name . " ($absumsatz)");
 
     } elsif (abs($absumsatz) >= 0.01) {
       $self->add_net_gross_differences($absumsatz);
@@ -1066,6 +1093,8 @@ SL::DATEV - kivitendo DATEV Export module
 
   use SL::DATEV qw(:CONSTANTS);
 
+  my $startdate = DateTime->new(year => 2014, month => 9, day => 1);
+  my $enddate   = DateTime->new(year => 2014, month => 9, day => 31);
   my $datev = SL::DATEV->new(
     exporttype => DATEV_ET_BUCHUNGEN,
     format     => DATEV_FORMAT_KNE,
@@ -1073,6 +1102,14 @@ SL::DATEV - kivitendo DATEV Export module
     to         => $enddate,
   );
 
+  # To only export transactions from a specific trans_id: (from and to are ignored)
+  my $invoice = SL::DB::Manager::Invoice->find_by( invnumber => '216' );
+  my $datev = SL::DATEV->new(
+    exporttype => DATEV_ET_BUCHUNGEN,
+    format     => DATEV_FORMAT_KNE,
+    trans_id   => $invoice->trans_id,
+  );
+
   my $datev = SL::DATEV->new(
     exporttype => DATEV_ET_STAMM,
     format     => DATEV_FORMAT_KNE,
@@ -1084,7 +1121,7 @@ SL::DATEV - kivitendo DATEV Export module
   my $hashref = $datev->get_datev_stamm;
   $datev->save_datev_stamm($hashref);
 
-  # manually clean up temporary directories
+  # manually clean up temporary directories older than 8 hours
   $datev->clean_temporary_directories;
 
   # export
@@ -1117,7 +1154,7 @@ This module implements the DATEV export standard. For usage see above.
 
 =item new PARAMS
 
-Generic constructor. See section attributes for information about hat to pass.
+Generic constructor. See section attributes for information about what to pass.
 
 =item get_datev_stamm
 
@@ -1159,7 +1196,7 @@ Note: If either a download_token or export_path were set at the creation these a
 
 =item filenames
 
-Returns a list of filenames generated by this DATEV object. This only works if th files were generated during it's lifetime, not if the object was created from a download_token.
+Returns a list of filenames generated by this DATEV object. This only works if the files were generated during its lifetime, not if the object was created from a download_token.
 
 =item net_gross_differences
 
@@ -1211,7 +1248,14 @@ Can be set on creation to retrieve a prior export for download.
 
 =item to
 
-Set boundary dates for the export. Currently thse MUST be set for the export to work.
+Set boundary dates for the export. Unless a trans_id is passed these MUST be
+set for the export to work.
+
+=item trans_id
+
+To check only one gl/ar/ap transaction, pass the trans_id. The attributes
+L<from> and L<to> are currently still needed for the query to be assembled
+correctly.
 
 =item accnofrom
 
@@ -1255,7 +1299,7 @@ No or unrecognized exporttype or format was provided for an export
 
 =item *
 
-OBE rxport was called, which is not yet implemented.
+OBE export was called, which is not yet implemented.
 
 =item *
 
@@ -1274,7 +1318,7 @@ C<Unbalanced Ledger!>. Exactly that, your ledger is unbalanced. Should never occ
 =item *
 
 C<Datev-Export fehlgeschlagen! Bei Transaktion %d (%f).>  This error occurs if a
-transaction could not be reliably sorted out, or had rounding errors over the acceptable threshold.
+transaction could not be reliably sorted out, or had rounding errors above the acceptable threshold.
 
 =back
 
@@ -1284,7 +1328,7 @@ transaction could not be reliably sorted out, or had rounding errors over the ac
 
 =item *
 
-Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to to get a working export.
+Handling of Vollvorlauf is currently not fully implemented. You must provide both from and to in order to get a working export.
 
 =item *
 
index 053a30a..dc36281 100644 (file)
@@ -6,12 +6,33 @@ package SL::DB::AccTransaction;
 use strict;
 
 use SL::DB::MetaSetup::AccTransaction;
+use SL::DB::Manager::AccTransaction;
+use SL::Locale::String qw(t8);
+
+require SL::DB::GLTransaction;
+require SL::DB::Invoice;
+require SL::DB::PurchaseInvoice;
+
+__PACKAGE__->meta->add_relationship(
+  ar => {
+    type         => 'many to one',
+    class        => 'SL::DB::Invoice',
+    column_map   => { trans_id => 'id' },
+  },
+  ap => {
+    type         => 'many to one',
+    class        => 'SL::DB::PurchaseInvoice',
+    column_map   => { trans_id => 'id' },
+  },
+  gl => {
+    type         => 'many to one',
+    class        => 'SL::DB::GLTransaction',
+    column_map   => { trans_id => 'id' },
+  },
+);
 
 __PACKAGE__->meta->initialize;
 
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-__PACKAGE__->meta->make_manager_class;
-
 sub record {
   my ($self) = @_;
 
@@ -24,7 +45,43 @@ sub record {
   };
 
 };
+
+sub get_type {
+  my $self = shift;
+
+  my $ref = ref $self->record;
+
+  return "ar" if $ref->isa('SL::DB::Invoice');
+  return "ap" if $ref->isa('SL::DB::PurchaseInvoice');
+  return "gl" if $ref->isa('SL::DB::GLTransaction');
+
+  die "Can't find trans_id " . $self->trans_id . " in ar, ap or gl" unless $ref;
+
+};
+
+sub transaction_name {
+  my $self = shift;
+
+  my $ref = ref $self->record;
+  my $name = "trans_id: " . $self->trans_id;
+  if ( $self->get_type eq 'ar' ) {
+    $name .= " (" . $self->record->abbreviation . " " . t8("AR") . ") " . t8("Invoice Number") . ": " . $self->record->invnumber;
+  } elsif ( $self->get_type eq 'ap' ) {
+    $name .= " (" . $self->record->abbreviation . " " . t8("AP") . ") " . t8("Invoice Number") . ": " . $self->record->invnumber;
+  } elsif ( $self->get_type eq 'gl' ) {
+    $name = "trans_id: " . $self->trans_id . " (" . $self->record->abbreviation . ") " . $self->record->reference . " - " . $self->record->description;
+  } else {
+    die "can't determine type of acc_trans line with trans_id " . $self->trans_id;
+  };
+
+  $name .= "   " . t8("Date") . ": " . $self->transdate->to_kivitendo;
+
+  return $name;
+
+};
+
 1;
+
 __END__
 
 =pod
@@ -57,6 +114,22 @@ We use the Rose::DB::Object load function with the C<speculative> parameter for
 each record type, which returns true if the load was successful, so we don't
 bother to check the ref of the object.
 
+=item C<get_type>
+
+Returns the type of transaction the acc_trans entry belongs to: ar, ap or gl.
+
+Example:
+ my $acc = SL::DB::Manager::AccTransaction->get_first();
+ my $type = $acc->get_type;
+
+=item C<transaction_name>
+
+Generate a meaningful transaction name for an acc_trans line from the
+corresponding ar/ap/gl object, a combination of trans_id,
+invnumber/description, abbreviation. Can be used for better error output of the
+DATEV export and contains some database information, e.g. the trans_id, and is
+a kind of displayable_name for debugging or in the console.
+
 =back
 
 =head1 BUGS
index 4ef201b..7bbd897 100644 (file)
@@ -6,9 +6,46 @@ package SL::DB::BankAccount;
 use strict;
 
 use SL::DB::MetaSetup::BankAccount;
+use SL::DB::Manager::BankAccount;
+use SL::DB::Helper::ActsAsList;
 
 __PACKAGE__->meta->initialize;
-# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
-__PACKAGE__->meta->make_manager_class;
+
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+
+  if ( not $self->{chart_id} ) {
+    push @errors, $::locale->text('There is no connected chart.');
+  } else {
+    # check whether the assigned chart is valid or is already being used by
+    # another bank account (there is also a UNIQUE database constraint on
+    # chart_id)
+
+    my $chart_id = $self->chart_id;
+    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} )) {
+          push @errors, $::locale->text('The account #1 is already being used by bank account #2.', $chart->displayable_name, $linked_bank->{name});
+        };
+      };
+    } else {
+      push @errors, $::locale->text('The chart is not valid.');
+    };
+  };
+
+  push @errors, $::locale->text('The IBAN is missing.') unless $self->{iban};
+
+  return @errors;
+}
+
+sub displayable_name {
+  my ($self) = @_;
+
+  return join ' ', grep $_, $self->name, $self->bank, $self->iban;
+}
 
 1;
diff --git a/SL/DB/BankTransaction.pm b/SL/DB/BankTransaction.pm
new file mode 100644 (file)
index 0000000..d5d7c55
--- /dev/null
@@ -0,0 +1,272 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::BankTransaction;
+
+use strict;
+
+use SL::DB::MetaSetup::BankTransaction;
+use SL::DB::Manager::BankTransaction;
+use SL::DB::Helper::LinkedRecords;
+
+require SL::DB::Invoice;
+require SL::DB::PurchaseInvoice;
+
+__PACKAGE__->meta->initialize;
+
+
+# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
+#__PACKAGE__->meta->make_manager_class;
+
+sub compare_to {
+  my ($self, $other) = @_;
+
+  return  1 if  $self->transdate && !$other->transdate;
+  return -1 if !$self->transdate &&  $other->transdate;
+
+  my $result = 0;
+  $result    = $self->transdate <=> $other->transdate if $self->transdate;
+  return $result || ($self->id <=> $other->id);
+}
+
+sub linked_invoices {
+  my ($self) = @_;
+
+  #my $record_links = $self->linked_records(direction => 'both');
+
+  my @linked_invoices;
+
+  my $record_links = SL::DB::Manager::RecordLink->get_all(where => [ from_table => 'bank_transactions', from_id => $self->id ]);
+
+  foreach my $record_link (@{ $record_links }) {
+    push @linked_invoices, SL::DB::Manager::Invoice->find_by(id => $record_link->to_id)->invnumber         if $record_link->to_table eq 'ar';
+    push @linked_invoices, SL::DB::Manager::PurchaseInvoice->find_by(id => $record_link->to_id)->invnumber if $record_link->to_table eq 'ap';
+  }
+
+  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 52c8b6e..67a4d3a 100644 (file)
@@ -28,7 +28,19 @@ sub abbreviation {
   my $abbreviation = $::locale->text('GL Transaction (abbreviation)');
   $abbreviation   .= "(" . $::locale->text('Storno (one letter abbreviation)') . ")" if $self->storno;
   return $abbreviation;
+}
+
+sub link {
+  my ($self) = @_;
+
+  my $html;
+  $html   = SL::Presenter->get->gl_transaction($self, display => 'inline');
+
+  return $html;
+}
 
+sub invnumber {
+  return $_[0]->reference;
 }
 
 1;
index b48f873..fb9e833 100644 (file)
@@ -15,6 +15,7 @@ use SL::DB::AuthUserGroup;
 use SL::DB::BackgroundJob;
 use SL::DB::BackgroundJobHistory;
 use SL::DB::BankAccount;
+use SL::DB::BankTransaction;
 use SL::DB::Bin;
 use SL::DB::Buchungsgruppe;
 use SL::DB::Business;
@@ -82,6 +83,7 @@ use SL::DB::ProjectStatus;
 use SL::DB::ProjectType;
 use SL::DB::PurchaseInvoice;
 use SL::DB::RecordLink;
+use SL::DB::ReconciliationLink;
 use SL::DB::RequirementSpecAcceptanceStatus;
 use SL::DB::RequirementSpecComplexity;
 use SL::DB::RequirementSpecDependency;
index 9143402..31ae7b6 100644 (file)
@@ -99,6 +99,7 @@ my %kivitendo_package_names = (
   background_job_histories       => 'background_job_history',
   ap                             => 'purchase_invoice',
   bank_accounts                  => 'bank_account',
+  bank_transactions              => 'bank_transaction',
   buchungsgruppen                => 'buchungsgruppe',
   bin                            => 'bin',
   business                       => 'business',
@@ -162,6 +163,7 @@ my %kivitendo_package_names = (
   project_statuses               => 'project_status',
   project_types                  => 'project_type',
   record_links                   => 'record_link',
+  reconciliation_links           => 'reconciliation_link',
   requirement_spec_acceptance_statuses => 'RequirementSpecAcceptanceStatus',
   requirement_spec_complexities        => 'RequirementSpecComplexity',
   requirement_spec_item_dependencies   => 'RequirementSpecDependency',
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 0added1..065d2b3 100644 (file)
@@ -6,9 +6,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::AttrSorted;
 use SL::DB::Helper::FlattenToForm;
@@ -17,6 +17,7 @@ use SL::DB::Helper::PriceTaxCalculator;
 use SL::DB::Helper::PriceUpdater;
 use SL::DB::Helper::TransNumberGenerator;
 use SL::Locale::String qw(t8);
+use SL::DB::CustomVariable;
 
 __PACKAGE__->meta->add_relationship(
   invoiceitems => {
@@ -359,6 +360,16 @@ sub customervendor {
   goto &customer;
 }
 
+sub link {
+  my ($self) = @_;
+
+  my $html;
+  $html   = SL::Presenter->get->sales_invoice($self, display => 'inline') if $self->invoice;
+  $html   = SL::Presenter->get->ar_transaction($self, display => 'inline') if !$self->invoice;
+
+  return $html;
+}
+
 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
diff --git a/SL/DB/Manager/BankTransaction.pm b/SL/DB/Manager/BankTransaction.pm
new file mode 100644 (file)
index 0000000..50a2319
--- /dev/null
@@ -0,0 +1,29 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::BankTransaction;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+use SL::DB::Helper::Filtered;
+
+sub object_class { 'SL::DB::BankTransaction' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'transdate', 1 ],
+           columns => { SIMPLE => 'ALL',
+                        local_account_number => 'local_bank_account.account_number',
+                        local_bank_code      => 'local_bank_account.bank_code' }, );
+}
+
+sub default_objects_per_page {
+  40;
+}
+
+1;
index 03c2296..5dc943f 100644 (file)
@@ -20,6 +20,7 @@ sub type_filter {
   return (and => [ invoice => 1, amount  => { lt => 0 }, or => [ storno => 0, storno => undef ] ]) if $type eq 'credit_note';
   return (and => [ invoice => 1, amount  => { lt => 0 },         storno => 1                    ]) if $type =~ m/(?:invoice_)?storno/;
   return (and => [ invoice => 1, amount  => { ge => 0 },         storno => 1                    ]) if $type eq 'credit_note_storno';
+  return (amount => {gt => 'paid'}) if $type eq 'open';
 
   die "Unknown type $type";
 }
index e964f4f..ce673db 100644 (file)
@@ -46,6 +46,6 @@ sub _sort_spec {
          );
 }
 
-sub default_objects_per_page { 40 }
+sub default_objects_per_page { 15 }
 
 1;
diff --git a/SL/DB/Manager/ReconciliationLink.pm b/SL/DB/Manager/ReconciliationLink.pm
new file mode 100644 (file)
index 0000000..8f63a1c
--- /dev/null
@@ -0,0 +1,27 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::ReconciliationLink;
+
+use strict;
+
+use SL::DBUtils;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::ReconciliationLink' }
+
+__PACKAGE__->make_manager_methods;
+
+sub get_new_rec_group {
+  my $class = shift;
+
+  my $query = qq|SELECT max(rec_group) FROM reconciliation_links|;
+
+  my ($max) = selectfirst_array_query($::form, $class->object_class->init_db->dbh, $query);
+
+  return $max + 1;
+}
+
+1;
index 8057f44..8f1abea 100644 (file)
@@ -9,22 +9,29 @@ use base qw(SL::DB::Object);
 __PACKAGE__->meta->table('bank_accounts');
 
 __PACKAGE__->meta->columns(
-  account_number => { type => 'varchar', length => 100 },
-  bank           => { type => 'text' },
-  bank_code      => { type => 'varchar', length => 100 },
-  bic            => { type => 'varchar', length => 100 },
-  chart_id       => { type => 'integer', not_null => 1 },
-  iban           => { type => 'varchar', length => 100 },
-  id             => { type => 'integer', not_null => 1, sequence => 'id' },
-  name           => { type => 'text' },
+  account_number                  => { type => 'varchar', length => 100 },
+  bank                            => { type => 'text' },
+  bank_code                       => { type => 'varchar', length => 100 },
+  bic                             => { type => 'varchar', length => 100 },
+  chart_id                        => { type => 'integer', not_null => 1 },
+  iban                            => { type => 'varchar', length => 100 },
+  id                              => { type => 'integer', not_null => 1, sequence => 'id' },
+  name                            => { type => 'text' },
+  obsolete                        => { type => 'boolean', default => 'false', not_null => 1 },
+  reconciliation_starting_balance => { type => 'numeric', precision => 15, scale => 5 },
+  reconciliation_starting_date    => { type => 'date' },
+  sortkey                         => { type => 'integer', not_null => 1 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
 
+__PACKAGE__->meta->unique_keys([ 'chart_id' ]);
+
 __PACKAGE__->meta->foreign_keys(
   chart => {
     class       => 'SL::DB::Chart',
     key_columns => { chart_id => 'id' },
+    rel_type    => 'one to one',
   },
 );
 
diff --git a/SL/DB/MetaSetup/BankTransaction.pm b/SL/DB/MetaSetup/BankTransaction.pm
new file mode 100644 (file)
index 0000000..a2dd295
--- /dev/null
@@ -0,0 +1,45 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::BankTransaction;
+
+use strict;
+
+use base qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('bank_transactions');
+
+__PACKAGE__->meta->columns(
+  amount                => { type => 'numeric', not_null => 1, precision => 15, scale => 5 },
+  cleared               => { type => 'boolean', default => 'false', not_null => 1 },
+  currency_id           => { type => 'integer', not_null => 1 },
+  id                    => { type => 'serial', not_null => 1 },
+  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' },
+  transaction_id        => { type => 'integer' },
+  transdate             => { type => 'date', not_null => 1 },
+  valutadate            => { type => 'date', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  currency => {
+    class       => 'SL::DB::Currency',
+    key_columns => { currency_id => 'id' },
+  },
+
+  local_bank_account => {
+    class       => 'SL::DB::BankAccount',
+    key_columns => { local_bank_account_id => 'id' },
+  },
+);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/ReconciliationLink.pm b/SL/DB/MetaSetup/ReconciliationLink.pm
new file mode 100644 (file)
index 0000000..9b5c969
--- /dev/null
@@ -0,0 +1,33 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ReconciliationLink;
+
+use strict;
+
+use base qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('reconciliation_links');
+
+__PACKAGE__->meta->columns(
+  acc_trans_id        => { type => 'bigint', not_null => 1 },
+  bank_transaction_id => { type => 'integer', not_null => 1 },
+  id                  => { type => 'integer', not_null => 1, sequence => 'id' },
+  rec_group           => { type => 'integer', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->foreign_keys(
+  acc_trans => {
+    class       => 'SL::DB::AccTransaction',
+    key_columns => { acc_trans_id => 'acc_trans_id' },
+  },
+
+  bank_transaction => {
+    class       => 'SL::DB::BankTransaction',
+    key_columns => { bank_transaction_id => 'id' },
+  },
+);
+
+1;
+;
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 fbb9903..ab1bd49 100644 (file)
@@ -9,6 +9,7 @@ use SL::DB::Manager::PurchaseInvoice;
 use SL::DB::Helper::AttrHTML;
 use SL::DB::Helper::AttrSorted;
 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.
@@ -76,6 +77,16 @@ sub abbreviation {
   return t8('Invoice (one letter abbreviation)'). '(' . t8('Storno (one letter abbreviation)') . ')' if $self->storno;
   return t8('Invoice (one letter abbreviation)');
 
+};
+
+sub link {
+  my ($self) = @_;
+
+  my $html;
+  $html   = SL::Presenter->get->purchase_invoice($self, display => 'inline') if $self->invoice;
+  $html   = SL::Presenter->get->ap_transaction($self, display => 'inline') if !$self->invoice;
+
+  return $html;
 }
 
 1;
diff --git a/SL/DB/ReconciliationLink.pm b/SL/DB/ReconciliationLink.pm
new file mode 100644 (file)
index 0000000..0632d51
--- /dev/null
@@ -0,0 +1,13 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::ReconciliationLink;
+
+use strict;
+
+use SL::DB::MetaSetup::ReconciliationLink;
+use SL::DB::Manager::ReconciliationLink;
+
+__PACKAGE__->meta->initialize;
+
+1;
index 016601e..f3b547b 100644 (file)
--- a/SL/GL.pm
+++ b/SL/GL.pm
@@ -196,8 +196,6 @@ sub post_transaction {
       exporttype => DATEV_ET_BUCHUNGEN,
       format     => DATEV_FORMAT_KNE,
       dbh        => $dbh,
-      from       => $transdate,
-      to         => $transdate,
       trans_id   => $form->{id},
     );
 
index 07b22c0..77dda78 100644 (file)
@@ -9,7 +9,7 @@ our @EXPORT_OK = qw(render_flash delay_flash);
 
 my %valid_categories = (
   map({$_ => 'info'} qw(information message)),
-  map({$_ => $_}     qw(info error warning)),
+  map({$_ => $_}     qw(info error warning ok)),
 );
 
 #
index 17dcaf3..8bd2f5f 100644 (file)
--- a/SL/IR.pm
+++ b/SL/IR.pm
@@ -807,8 +807,6 @@ SQL
       exporttype => DATEV_ET_BUCHUNGEN,
       format     => DATEV_FORMAT_KNE,
       dbh        => $dbh,
-      from       => $transdate,
-      to         => $transdate,
       trans_id   => $form->{id},
     );
 
index cc3ce37..7bc724b 100644 (file)
--- a/SL/IS.pm
+++ b/SL/IS.pm
@@ -1274,8 +1274,6 @@ SQL
       exporttype => DATEV_ET_BUCHUNGEN,
       format     => DATEV_FORMAT_KNE,
       dbh        => $dbh,
-      from       => $transdate,
-      to         => $transdate,
       trans_id   => $form->{id},
     );
 
index 7b83e36..deeed1e 100644 (file)
@@ -23,6 +23,7 @@ use SL::Presenter::RequirementSpecTextBlock;
 use SL::Presenter::SepaExport;
 use SL::Presenter::Text;
 use SL::Presenter::Tag;
+use SL::Presenter::BankAccount;
 
 use Rose::Object::MakeMethods::Generic (
   scalar => [ qw(need_reinit_widgets) ],
diff --git a/SL/Presenter/BankAccount.pm b/SL/Presenter/BankAccount.pm
new file mode 100644 (file)
index 0000000..13a8cb2
--- /dev/null
@@ -0,0 +1,22 @@
+package SL::Presenter::BankAccount;
+
+use strict;
+
+use parent qw(Exporter);
+
+use Exporter qw(import);
+our @EXPORT = qw(account_number bank_code);
+
+use Carp;
+
+sub account_number {
+  my ($self, $bank_account) = @_;
+  return $self->escaped_text($bank_account->account_number);
+}
+
+sub bank_code {
+  my ($self, $bank_account) = @_;
+  return $self->escaped_text($bank_account->bank_code);
+}
+
+1;
index c078450..ebde34f 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);
+our @EXPORT = qw(invoice sales_invoice ar_transaction purchase_invoice ap_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) = @_;
 
@@ -79,10 +97,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 b46896d..b395e82 100644 (file)
@@ -55,6 +55,8 @@ sub grouped_record_list {
   $output .= _purchase_invoice_list(       $self, $groups{purchase_invoices},        %params) if $groups{purchase_invoices};
   $output .= _ap_transaction_list(         $self, $groups{ap_transactions},          %params) if $groups{ap_transactions};
 
+  $output .= _bank_transactions(           $self, $groups{bank_transactions},        %params) if $groups{bank_transactions};
+
   $output .= _sepa_collection_list(        $self, $groups{sepa_collections},         %params) if $groups{sepa_collections};
   $output .= _sepa_transfer_list(          $self, $groups{sepa_transfers},           %params) if $groups{sepa_transfers};
 
@@ -177,6 +179,7 @@ sub _group_records {
     sepa_collections         => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem')  &&  $_[0]->ar_id                        },
     sepa_transfers           => sub { (ref($_[0]) eq 'SL::DB::SepaExportItem')  &&  $_[0]->ap_id                        },
     gl_transactions          => sub { (ref($_[0]) eq 'SL::DB::GLTransaction')                                           },
+    bank_transactions        => sub { (ref($_[0]) eq 'SL::DB::BankTransaction') &&  $_[0]->id                           },
   );
 
   my %groups;
@@ -429,6 +432,29 @@ sub _ap_transaction_list {
   );
 }
 
+sub _bank_transactions {
+  my ($self, $list, %params) = @_;
+
+  return $self->record_list(
+    $list,
+    title   => $::locale->text('Bank transactions'),
+    type    => 'bank_transactions',
+    columns => [
+      [ $::locale->text('Transdate'),            'transdate'                      ],
+      [ $::locale->text('Local Bank Code'),      sub { $self->bank_code($_[0]->local_bank_account) }  ],
+      [ $::locale->text('Local account number'), sub { $self->account_number($_[0]->local_bank_account) }  ],
+      [ $::locale->text('Remote Bank Code'),     'remote_bank_code' ],
+      [ $::locale->text('Remote account number'),'remote_account_number' ],
+      [ $::locale->text('Valutadate'),           'valutadate' ],
+      [ $::locale->text('Amount'),               'amount' ],
+      [ $::locale->text('Currency'),             sub { $_[0]->currency->name } ],
+      [ $::locale->text('Remote name'),          'remote_name' ],
+      [ $::locale->text('Purpose'),              'purpose' ],
+    ],
+    %params,
+  );
+}
+
 sub _sepa_export_list {
   my ($self, $list, %params) = @_;
 
index 9c73d45..89904d8 100644 (file)
--- a/SL/RC.pm
+++ b/SL/RC.pm
@@ -116,6 +116,21 @@ sub payment_transactions {
     push(@values, conv_date($form->{todate}));
   }
 
+  if($form->{additional_fromdate}) {
+    $query .= qq|  AND ac.transdate >= ? |;
+    push(@values, conv_date($form->{additional_fromdate}));
+  }
+
+  if($form->{additional_todate}){
+    $query .= qq|  AND ac.transdate <= ? |;
+    push(@values, conv_date($form->{additional_todate}));
+  }
+
+  if($form->{filter_amount}){
+    $query .= qq|  AND ac.amount = ? |;
+    push(@values, conv_i($form->{filter_amount}));
+  }
+
   $query .=
     qq|UNION | .
 
@@ -141,6 +156,21 @@ sub payment_transactions {
     push(@values, conv_date($form->{todate}));
   }
 
+  if($form->{additional_fromdate}) {
+    $query .= qq| AND ac.transdate >= ? |;
+    push(@values, conv_date($form->{additional_fromdate}));
+  }
+
+  if($form->{additional_todate}){
+    $query .= qq| AND ac.transdate <= ? |;
+    push(@values, conv_date($form->{additional_todate}));
+  }
+
+  if($form->{filter_amount}){
+    $query .= qq| AND ac.amount = ? |;
+    push(@values, conv_i($form->{filter_amount}));
+  }
+
   $query .=
     qq|UNION | .
 
@@ -166,7 +196,22 @@ sub payment_transactions {
     push(@values, conv_date($form->{todate}));
   }
 
-  $query .= " ORDER BY 3,7,8";
+  if($form->{additional_fromdate}) {
+    $query .= qq| AND ac.transdate >= ? |;
+    push(@values, conv_date($form->{additional_fromdate}));
+  }
+
+  if($form->{additional_todate}){
+    $query .= qq| AND ac.transdate <= ? |;
+    push(@values, conv_date($form->{additional_todate}));
+  }
+
+  if($form->{filter_amount}){
+    $query .= qq| AND ac.amount = ? |;
+    push(@values, conv_i($form->{filter_amount}));
+  }
+
+  $query .= " ORDER BY 3,7,8 LIMIT 6";
 
   $form->{PR} = selectall_hashref_query($form, $dbh, $query, @values);
 
@@ -208,4 +253,33 @@ sub reconcile {
   $main::lxdebug->leave_sub();
 }
 
+sub get_statement_balance {
+  $main::lxdebug->enter_sub();
+
+  my ($self, $myconfig, $form) = @_;
+
+  # connect to database, turn AutoCommit off
+  my $dbh = $form->dbconnect_noauto($myconfig);
+
+  my ($query, @values);
+
+  $query = qq|SELECT sum(amount) FROM acc_trans where chart_id=45 AND cleared='1'|;
+
+  if($form->{fromdate}) {
+    $query .= qq| AND transdate >= ? |;
+    push(@values, conv_date($form->{fromdate}));
+  }
+
+  if($form->{todate}){
+    $query .= qq| AND transdate <= ? |;
+    push(@values, conv_date($form->{todate}));
+  }
+
+  ($form->{statement_balance}) = selectrow_query($form, $dbh, $query, @values);
+
+  $dbh->disconnect;
+
+  $main::lxdebug->leave_sub();
+}
+
 1;
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);
diff --git a/VERSION b/VERSION
index e4604e3..928d224 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.2.1
+3.2.1-rb
diff --git a/bin/mozilla/bankaccounts.pl b/bin/mozilla/bankaccounts.pl
deleted file mode 100644 (file)
index 941e407..0000000
+++ /dev/null
@@ -1,175 +0,0 @@
-use strict;
-
-use List::MoreUtils qw(any);
-use POSIX qw(strftime);
-
-use SL::BankAccount;
-use SL::Chart;
-use SL::Form;
-use SL::ReportGenerator;
-
-require "bin/mozilla/common.pl";
-require "bin/mozilla/reportgenerator.pl";
-
-sub bank_account_add {
-  $main::lxdebug->enter_sub();
-
-  bank_account_display_form('account' => {});
-
-  $main::lxdebug->leave_sub();
-}
-
-sub bank_account_edit {
-  $main::lxdebug->enter_sub();
-
-  my %params  = @_;
-  my $form    = $main::form;
-
-  my $account = SL::BankAccount->retrieve('id' => $params{id} || $form->{id});
-
-  bank_account_display_form('account' => $account);
-
-  $main::lxdebug->leave_sub();
-}
-
-sub bank_account_delete {
-  $::lxdebug->enter_sub();
-
-  SL::BankAccount->delete(id => $::form->{account}{id});
-
-  print $::form->redirect_header('bankaccounts.pl?action=bank_account_list');
-
-  $::lxdebug->leave_sub();
-}
-
-sub bank_account_display_form {
-  $main::lxdebug->enter_sub();
-
-  my %params     = @_;
-  my $account    = $params{account} || {};
-  my $form       = $main::form;
-  my $locale     = $main::locale;
-
-  my $charts     = SL::Chart->list('link' => 'AP_paid');
-  my $label_sub  = sub { join '--', map { $_[0]->{$_} } qw(accno description) };
-
-  $form->{title} = $account->{id} ? $locale->text('Edit bank account') : $locale->text('Add bank account');
-
-  $form->header();
-  print $form->parse_html_template('bankaccounts/bank_account_display_form',
-                                   { 'CHARTS'      => $charts,
-                                     'account'     => $account,
-                                     'chart_label' => $label_sub,
-                                     'params'      => \%params });
-
-  $main::lxdebug->leave_sub();
-}
-
-sub bank_account_save {
-  $main::lxdebug->enter_sub();
-
-  my $form    = $main::form;
-  my $locale  = $main::locale;
-
-  my $account = $form->{account} && (ref $form->{account} eq 'HASH') ? $form->{account} : { };
-
-  if (any { !$account->{$_} } qw(name account_number bank_code iban bic)) {
-    bank_account_display_form('account' => $account,
-                              'error'   => $locale->text('You have to fill in at least a name, an account number, the bank code, the IBAN and the BIC.'));
-
-    $main::lxdebug->leave_sub();
-    return;
-  }
-
-  my $id = SL::BankAccount->save(%{ $account });
-
-  if ($form->{callback}) {
-    $form->redirect();
-
-  } else {
-    bank_account_edit('id' => $id);
-  }
-
-  $main::lxdebug->leave_sub();
-}
-
-
-sub bank_account_list {
-  $main::lxdebug->enter_sub();
-
-  my $form   = $main::form;
-  my $locale = $main::locale;
-
-  $form->{title}     = $locale->text('List of bank accounts');
-
-  $form->{sort}    ||= 'account_number';
-  $form->{sortdir}   = '1' if (!defined $form->{sortdir});
-
-  $form->{callback}  = build_std_url('action=bank_account_list', 'sort', 'sortdir');
-
-  my $accounts       = SL::BankAccount->list('sortorder' => $form->{sort},
-                                             'sortdir'   => $form->{sortdir});
-
-  my $report         = SL::ReportGenerator->new(\%main::myconfig, $form);
-
-  my $href           = build_std_url('action=bank_account_list');
-
-  my %column_defs = (
-    'name'           => { 'text' => $locale->text('Name'), },
-    'account_number' => { 'text' => $locale->text('Account number'), },
-    'bank_code'      => { 'text' => $locale->text('Bank code'), },
-    'bank'           => { 'text' => $locale->text('Bank'), },
-    'bic'            => { 'text' => $locale->text('BIC'), },
-    'iban'           => { 'text' => $locale->text('IBAN'), },
-  );
-
-  my @columns = qw(name account_number bank bank_code bic iban);
-
-  foreach my $name (@columns) {
-    my $sortdir                 = $form->{sort} eq $name ? 1 - $form->{sortdir} : $form->{sortdir};
-    $column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir";
-  }
-
-  $report->set_options('raw_bottom_info_text'  => $form->parse_html_template('bankaccounts/bank_account_list_bottom'),
-                       'std_column_visibility' => 1,
-                       'output_format'         => 'HTML',
-                       'title'                 => $form->{title},
-                       'attachment_basename'   => $locale->text('bankaccounts') . strftime('_%Y%m%d', localtime time),
-    );
-  $report->set_options_from_form();
-  $locale->set_numberformat_wo_thousands_separator(\%::myconfig) if lc($report->{options}->{output_format}) eq 'csv';
-
-  $report->set_columns(%column_defs);
-  $report->set_column_order(@columns);
-  $report->set_export_options('bank_account_list');
-  $report->set_sort_indicator($form->{sort}, $form->{sortdir});
-
-  my $edit_url = build_std_url('action=bank_account_edit', 'callback');
-
-  foreach my $account (@{ $accounts }) {
-    my $row = { map { $_ => { 'data' => $account->{$_} } } keys %{ $account } };
-
-    $row->{account_number}->{link} = $edit_url . '&id=' . E($account->{id});
-
-    $report->add_data($row);
-  }
-
-  $report->generate_with_headers();
-
-  $main::lxdebug->leave_sub();
-}
-
-sub dispatcher {
-  my $form = $main::form;
-
-  foreach my $action (qw(bank_account_save bank_account_delete)) {
-    if ($form->{"action_${action}"}) {
-      call_sub($action);
-      return;
-    }
-  }
-
-  $form->error($main::locale->text('No action defined.'));
-}
-
-1;
index 83cf6be..5b1661b 100644 (file)
@@ -115,6 +115,16 @@ sub load_draft {
   my $form     = $main::form;
   my %myconfig = %main::myconfig;
 
+  # check and store certain form parameters that might have been passed as get, so we can later overwrite the values from the draft
+  # the overwrite happens at the end of this function
+  my @valid_overwrite_vars = qw(remove_draft amount_1 invnumber ordnumber transdate duedate notes datepaid_1 paid_1 callback AP_paid_1 currency);  # reference description
+  my $overwrite_hash;
+  # my @valid_fields;
+  foreach ( @valid_overwrite_vars ) {
+    $overwrite_hash->{$_} = $form->{$_} if exists $form->{$_};  # variant 1
+    # push(@valid_fields, $_) if exists $form->{$_}; # variant 2
+  };
+
   my ($old_form, $id, $description) = Drafts->load(\%myconfig, $form, $form->{id});
 
   if ($old_form) {
@@ -133,6 +143,14 @@ sub load_draft {
   # ungültige Belege. Vielleicht geht es anderen ähnlich jan 19.2.2011
   $form->{invdate} = $form->current_date(\%myconfig); # Aktuelles Rechnungsdatum  ...
   $form->{duedate} = $form->current_date(\%myconfig); # Aktuelles Fälligkeitsdatum  ...
+
+  if ( $overwrite_hash ) {
+    foreach ( keys $overwrite_hash ) {
+      $form->{$_} = $overwrite_hash->{$_};  # variante 1
+    };
+  };
+  # @{$form}{@valid_fields} = @{$overwrite_hash}{@valid_fields};  # variante 2
+
   update();
 
   $main::lxdebug->leave_sub();
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 b20f60d..d09f72b 100644 (file)
@@ -39,6 +39,18 @@ a.nomobile {
        background-color:transparent;
        border:none;
 }
+a.green {
+       background-color:#40FF00;
+       border:none;
+}
+a.orange {
+       background-color:#FF8000;
+       border:none;
+}
+a.red {
+       background-color:#FF0000;
+       border:none;
+}
 
 table {
     font-size: 90% !important;
@@ -236,6 +248,16 @@ body.menu {
        color: black;
        vertical-align: top;
 }
+.listrow_error1, .listrow_error:nth-child(even) {
+       background-color: #F6CECE;
+       color: black;
+       vertical-align: top;
+}
+.listrow_error0, .listrow_error:nth-child(odd) {
+       background-color: #F5A9A9;
+       color: black;
+       vertical-align: top;
+}
 .listrowempty {
        background-color: #FFFFFF;
        color: black;
@@ -328,6 +350,13 @@ label {
        margin-bottom: 5px;
        padding: 5px;
 }
+.flash_message_ok {
+       background-color: #ADFFB6;
+       border: 1px solid #007F0F;
+       margin-top: 5px;
+       margin-bottom: 5px;
+       padding: 5px;
+}
 .flash_message_warning {
        background-color: #FFE8C7;
        border: 1px solid #FF6600;
index ab761ad..8df983b 100644 (file)
@@ -4,6 +4,37 @@
 
 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:
 
@@ -44,6 +75,7 @@ Bugfixes:
 - Bugfix #50 Kundentyp-Rabatt wird falsch übernommen
 
 
+
 2015-04-10 - Release 3.2.1
 
 Dies ist ein Unstable Bugfix-Release für die 3.2. D.h. es wurden ein paar
diff --git a/image/bank-building.jpg b/image/bank-building.jpg
new file mode 100644 (file)
index 0000000..1de86ee
Binary files /dev/null and b/image/bank-building.jpg differ
index 874ccc9..d5276b7 100644 (file)
@@ -9,6 +9,7 @@ namespace("kivi").setupLocale({
 "Add text block":"Textblock erfassen",
 "Additional articles actions":"Aktionen zu zusätzlichen Artikeln",
 "Are you sure?":"Sind Sie sicher?",
+"Assign invoice":"Rechnung zuweisen",
 "Basic settings actions":"Aktionen zu Grundeinstellungen",
 "Cancel":"Abbrechen",
 "Chart picker":"Kontenauswahl",
@@ -19,6 +20,7 @@ namespace("kivi").setupLocale({
 "Create HTML":"HTML erzeugen",
 "Create PDF":"PDF erzeugen",
 "Create a new version":"Eine neue Version anlegen",
+"Create invoice":"Buchung erstellen",
 "Create new quotation/order":"Neues Angebot/neuen Auftrag anlegen",
 "Create new qutoation/order":"Neues Angebot/neuen Auftrag anlegen",
 "Create new version":"Neue Version anlegen",
@@ -62,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.",
@@ -69,6 +72,7 @@ namespace("kivi").setupLocale({
 "The recipient, subject or body is missing.":"Der Empfäger, der Betreff oder der Text ist leer.",
 "The selected database is still configured for client \"#1\". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?":"Die auswählte Datenbank ist noch für Mandant \"#1\" konfiguriert. Wenn Sie die Datenbank löschen, wird der Mandanten nicht mehr funktionieren, bis er anders konfiguriert wurde. Wollen Sie die Datenbank trotzdem löschen?",
 "There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?":"Einige der Lagerbewegungen sind nicht vollständig und Lagerbewegungen können nachträglich nicht mehr verändert werden. Wollen Sie wirklich fortfahren?",
+"There is no connected chart.":"Es fehlt ein verknüpftes Buchungskonto.",
 "There is one or more sections for which no part has been assigned yet; therefore creating the new record is not possible yet.":"Es gibt einen oder mehrere Abschnitte ohne Artikelzuweisung; daher kann der neue Beleg noch nicht erstellt werden.",
 "This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?":"Dieser Auftrag besitzt eine aktive Konfiguration für wiederkehrende Rechnungen. Wenn Sie jetzt speichern, so werden alle zukünftig hieraus erzeugten Rechnungen die Änderungen enthalten, nicht aber die bereits erzeugten Rechnungen. Wollen Sie speichern?",
 "Time/cost estimate actions":"Aktionen für Kosten-/Zeitabschätzung",
index a6a3645..01f55f2 100644 (file)
@@ -1,22 +1,30 @@
 /* This notice must be untouched at all times.
+Copyright (c) 2002-2008 Walter Zorn. All rights reserved.
 
-wz_tooltip.js    v. 3.38
+wz_tooltip.js   v. 5.31
 
 The latest version is available at
 http://www.walterzorn.com
 or http://www.devira.com
 or http://www.walterzorn.de
 
-Copyright (c) 2002-2005 Walter Zorn. All rights reserved.
-Created 1. 12. 2002 by Walter Zorn (Web: http://www.walterzorn.com )
-Last modified: 9. 12. 2005
+Created 1.12.2002 by Walter Zorn (Web: http://www.walterzorn.com )
+Last modified: 7.11.2008
 
-Cross-browser tooltips working even in Opera 5 and 6,
-as well as in NN 4, Gecko-Browsers, IE4+, Opera 7+ and Konqueror.
-No onmouseouts required.
-Appearance of tooltips can be individually configured
-via commands within the onmouseovers.
+Easy-to-use cross-browser tooltips.
+Just include the script at the beginning of the <body> section, and invoke
+Tip('Tooltip text') to show and UnTip() to hide the tooltip, from the desired
+HTML eventhandlers. Example:
+<a onmouseover="Tip('Some text')" onmouseout="UnTip()" href="index.htm">My home page</a>
+No container DIV required.
+By default, width and height of tooltips are automatically adapted to content.
+Is even capable of dynamically converting arbitrary HTML elements to tooltips
+by calling TagToTip('ID_of_HTML_element_to_be_converted') instead of Tip(),
+which means you can put important, search-engine-relevant stuff into tooltips.
+Appearance & behaviour of tooltips can be individually configured
+via commands passed to Tip() or TagToTip().
 
+Tab Width: 4
 LICENSE: LGPL
 
 This library is free software; you can redistribute it and/or
@@ -32,447 +40,1262 @@ For more details on the GNU Lesser General Public License,
 see http://www.gnu.org/copyleft/lesser.html
 */
 
+var config = new Object();
 
 
-////////////////  GLOBAL TOOPTIP CONFIGURATION  /////////////////////
-var ttAbove       = false;        // tooltip above mousepointer? Alternative: true
-var ttBgColor     = "#e6ecff";
-var ttBgImg       = "";           // path to background image;
-var ttBorderColor = "#003399";
-var ttBorderWidth = 1;
-var ttDelay       = 500;          // time span until tooltip shows up [milliseconds]
-var ttFontColor   = "#000066";
-var ttFontFace    = "arial,helvetica,sans-serif";
-var ttFontSize    = "11px";
-var ttFontWeight  = "normal";     // alternative: "bold";
-var ttLeft        = false;        // tooltip on the left of the mouse? Alternative: true
-var ttOffsetX     = 12;           // horizontal offset of left-top corner from mousepointer
-var ttOffsetY     = 15;           // vertical offset                   "
-var ttOpacity     = 100;          // opacity of tooltip in percent (must be integer between 0 and 100)
-var ttPadding     = 3;            // spacing between border and content
-var ttShadowColor = "";
-var ttShadowWidth = 0;
-var ttStatic      = false;        // tooltip NOT move with the mouse? Alternative: true
-var ttSticky      = false;        // do NOT hide tooltip on mouseout? Alternative: true
-var ttTemp        = 0;            // time span after which the tooltip disappears; 0 (zero) means "infinite timespan"
-var ttTextAlign   = "left";
-var ttTitleColor  = "#ffffff";    // color of caption text
-var ttWidth       = 300;
-////////////////////  END OF TOOLTIP CONFIG  ////////////////////////
-
-
-
-//////////////  TAGS WITH TOOLTIP FUNCTIONALITY  ////////////////////
-// List may be extended or shortened:
-var tt_tags = new Array("a","area","b","big","caption","center","code","dd","div","dl","dt","em","h1","h2","h3","h4","h5","h6","i","img","input","li","map","ol","p","pre","s", "select", "small","span","strike","strong","sub","sup","table","td","th","tr","tt","u","var","ul","layer");
-/////////////////////////////////////////////////////////////////////
-
-
-
-///////// DON'T CHANGE ANYTHING BELOW THIS LINE /////////////////////
-var tt_obj = null,         // current tooltip
-tt_ifrm = null,            // iframe to cover windowed controls in IE
-tt_objW = 0, tt_objH = 0,  // width and height of tt_obj
-tt_objX = 0, tt_objY = 0,
-tt_offX = 0, tt_offY = 0,
-xlim = 0, ylim = 0,        // right and bottom borders of visible client area
-tt_sup = false,            // true if T_ABOVE cmd
-tt_sticky = false,         // tt_obj sticky?
-tt_wait = false,
-tt_act = false,            // tooltip visibility flag
-tt_sub = false,            // true while tooltip below mousepointer
-tt_u = "undefined",
-tt_mf = null,              // stores previous mousemove evthandler
-// Opera: disable href when hovering <a>
-tt_tag = null;             // stores hovered dom node, href and previous statusbar txt
-
-
-var tt_db = (document.compatMode && document.compatMode != "BackCompat")? document.documentElement : document.body? document.body : null,
-tt_n = navigator.userAgent.toLowerCase(),
-tt_nv = navigator.appVersion;
-// Browser flags
-var tt_op = !!(window.opera && document.getElementById),
-tt_op6 = tt_op && !document.defaultView,
-tt_op7 = tt_op && !tt_op6,
-tt_ie = tt_n.indexOf("msie") != -1 && document.all && tt_db && !tt_op,
-tt_ie6 = tt_ie && parseFloat(tt_nv.substring(tt_nv.indexOf("MSIE")+5)) >= 5.5,
-tt_n4 = (document.layers && typeof document.classes != tt_u),
-tt_n6 = (!tt_op && document.defaultView && typeof document.defaultView.getComputedStyle != tt_u),
-tt_w3c = !tt_ie && !tt_n6 && !tt_op && document.getElementById;
-
-function tt_Int(t_x)
-{
-       var t_y;
-       return isNaN(t_y = parseInt(t_x))? 0 : t_y;
-}
-function wzReplace(t_x, t_y)
-{
-       var t_ret = "",
-       t_str = this,
-       t_xI;
-       while((t_xI = t_str.indexOf(t_x)) != -1)
-       {
-               t_ret += t_str.substring(0, t_xI) + t_y;
-               t_str = t_str.substring(t_xI + t_x.length);
-       }
-       return t_ret+t_str;
-}
-String.prototype.wzReplace = wzReplace;
-function tt_N4Tags(tagtyp, t_d, t_y)
-{
-       t_d = t_d || document;
-       t_y = t_y || new Array();
-       var t_x = (tagtyp=="a")? t_d.links : t_d.layers;
-       for(var z = t_x.length; z--;) t_y[t_y.length] = t_x[z];
-       for(z = t_d.layers.length; z--;) t_y = tt_N4Tags(tagtyp, t_d.layers[z].document, t_y);
-       return t_y;
-}
-function tt_Htm(tt, t_id, txt)
-{
-       var t_bgc = (typeof tt.T_BGCOLOR != tt_u)? tt.T_BGCOLOR : ttBgColor,
-       t_bgimg   = (typeof tt.T_BGIMG != tt_u)? tt.T_BGIMG : ttBgImg,
-       t_bc      = (typeof tt.T_BORDERCOLOR != tt_u)? tt.T_BORDERCOLOR : ttBorderColor,
-       t_bw      = (typeof tt.T_BORDERWIDTH != tt_u)? tt.T_BORDERWIDTH : ttBorderWidth,
-       t_ff      = (typeof tt.T_FONTFACE != tt_u)? tt.T_FONTFACE : ttFontFace,
-       t_fc      = (typeof tt.T_FONTCOLOR != tt_u)? tt.T_FONTCOLOR : ttFontColor,
-       t_fsz     = (typeof tt.T_FONTSIZE != tt_u)? tt.T_FONTSIZE : ttFontSize,
-       t_fwght   = (typeof tt.T_FONTWEIGHT != tt_u)? tt.T_FONTWEIGHT : ttFontWeight,
-       t_opa     = (typeof tt.T_OPACITY != tt_u)? tt.T_OPACITY : ttOpacity,
-       t_padd    = (typeof tt.T_PADDING != tt_u)? tt.T_PADDING : ttPadding,
-       t_shc     = (typeof tt.T_SHADOWCOLOR != tt_u)? tt.T_SHADOWCOLOR : (ttShadowColor || 0),
-       t_shw     = (typeof tt.T_SHADOWWIDTH != tt_u)? tt.T_SHADOWWIDTH : (ttShadowWidth || 0),
-       t_algn    = (typeof tt.T_TEXTALIGN != tt_u)? tt.T_TEXTALIGN : ttTextAlign,
-       t_tit     = (typeof tt.T_TITLE != tt_u)? tt.T_TITLE : "",
-       t_titc    = (typeof tt.T_TITLECOLOR != tt_u)? tt.T_TITLECOLOR : ttTitleColor,
-       t_w       = (typeof tt.T_WIDTH != tt_u)? tt.T_WIDTH  : ttWidth;
-       if(t_shc || t_shw)
-       {
-               t_shc = t_shc || "#cccccc";
-               t_shw = t_shw || 5;
-       }
-       if(tt_n4 && (t_fsz == "10px" || t_fsz == "11px")) t_fsz = "12px";
-
-       var t_optx = (tt_n4? '' : tt_n6? ('-moz-opacity:'+(t_opa/100.0)) : tt_ie? ('filter:Alpha(opacity='+t_opa+')') : ('opacity:'+(t_opa/100.0))) + ';';
-       var t_y = '<div id="'+t_id+'" style="position:absolute;z-index:1010;';
-       t_y += 'left:0px;top:0px;width:'+(t_w+t_shw)+'px;visibility:'+(tt_n4? 'hide' : 'hidden')+';'+t_optx+'">' +
-               '<table border="0" cellpadding="0" cellspacing="0"'+(t_bc? (' bgcolor="'+t_bc+'" style="background:'+t_bc+';"') : '')+' width="'+t_w+'">';
-       if(t_tit)
-       {
-               t_y += '<tr><td style="padding-left:3px;padding-right:3px;" align="'+t_algn+'"><font color="'+t_titc+'" face="'+t_ff+'" ' +
-                       'style="color:'+t_titc+';font-family:'+t_ff+';font-size:'+t_fsz+';"><b>' +
-                       (tt_n4? '&nbsp;' : '')+t_tit+'</b></font></td></tr>';
-       }
-       t_y += '<tr><td><table border="0" cellpadding="'+t_padd+'" cellspacing="'+t_bw+'" width="100%">' +
-               '<tr><td'+(t_bgc? (' bgcolor="'+t_bgc+'"') : '')+(t_bgimg? ' background="'+t_bgimg+'"' : '')+' style="text-align:'+t_algn+';';
-       if(tt_n6) t_y += 'padding:'+t_padd+'px;';
-       t_y += '" align="'+t_algn+'"><font color="'+t_fc+'" face="'+t_ff+'"' +
-               ' style="color:'+t_fc+';font-family:'+t_ff+';font-size:'+t_fsz+';font-weight:'+t_fwght+';">';
-       if(t_fwght == 'bold') t_y += '<b>';
-       t_y += txt;
-       if(t_fwght == 'bold') t_y += '</b>';
-       t_y += '</font></td></tr></table></td></tr></table>';
-       if(t_shw)
-       {
-               var t_spct = Math.round(t_shw*1.3);
-               if(tt_n4)
+//===================  GLOBAL TOOLTIP CONFIGURATION  =========================//
+var tt_Debug   = true          // false or true - recommended: false once you release your page to the public
+var tt_Enabled = true          // Allows to (temporarily) suppress tooltips, e.g. by providing the user with a button that sets this global variable to false
+var TagsToTip  = true          // false or true - if true, HTML elements to be converted to tooltips via TagToTip() are automatically hidden;
+                                                       // if false, you should hide those HTML elements yourself
+
+// For each of the following config variables there exists a command, which is
+// just the variablename in uppercase, to be passed to Tip() or TagToTip() to
+// configure tooltips individually. Individual commands override global
+// configuration. Order of commands is arbitrary.
+// Example: onmouseover="Tip('Tooltip text', LEFT, true, BGCOLOR, '#FF9900', FADEIN, 400)"
+
+config. Above                  = false         // false or true - tooltip above mousepointer
+config. BgColor                        = '#E2E7FF'     // Background colour (HTML colour value, in quotes)
+config. BgImg                  = ''            // Path to background image, none if empty string ''
+config. BorderColor            = '#003099'
+config. BorderStyle            = 'solid'       // Any permitted CSS value, but I recommend 'solid', 'dotted' or 'dashed'
+config. BorderWidth            = 1
+config. CenterMouse            = false         // false or true - center the tip horizontally below (or above) the mousepointer
+config. ClickClose             = false         // false or true - close tooltip if the user clicks somewhere
+config. ClickSticky            = false         // false or true - make tooltip sticky if user left-clicks on the hovered element while the tooltip is active
+config. CloseBtn               = false         // false or true - closebutton in titlebar
+config. CloseBtnColors = ['#990000', '#FFFFFF', '#DD3333', '#FFFFFF']  // [Background, text, hovered background, hovered text] - use empty strings '' to inherit title colours
+config. CloseBtnText   = '&nbsp;X&nbsp;'       // Close button text (may also be an image tag)
+config. CopyContent            = true          // When converting a HTML element to a tooltip, copy only the element's content, rather than converting the element by its own
+config. Delay                  = 400           // Time span in ms until tooltip shows up
+config. Duration               = 0                     // Time span in ms after which the tooltip disappears; 0 for infinite duration, < 0 for delay in ms _after_ the onmouseout until the tooltip disappears
+config. Exclusive              = false         // false or true - no other tooltip can appear until the current one has actively been closed
+config. FadeIn                 = 100           // Fade-in duration in ms, e.g. 400; 0 for no animation
+config. FadeOut                        = 100
+config. FadeInterval   = 30            // Duration of each fade step in ms (recommended: 30) - shorter is smoother but causes more CPU-load
+config. Fix                            = null          // Fixated position, two modes. Mode 1: x- an y-coordinates in brackets, e.g. [210, 480]. Mode 2: Show tooltip at a position related to an HTML element: [ID of HTML element, x-offset, y-offset from HTML element], e.g. ['SomeID', 10, 30]. Value null (default) for no fixated positioning.
+config. FollowMouse            = true          // false or true - tooltip follows the mouse
+config. FontColor              = '#000044'
+config. FontFace               = 'Verdana,Geneva,sans-serif'
+config. FontSize               = '8pt'         // E.g. '9pt' or '12px' - unit is mandatory
+config. FontWeight             = 'normal'      // 'normal' or 'bold';
+config. Height                 = 0                     // Tooltip height; 0 for automatic adaption to tooltip content, < 0 (e.g. -100) for a maximum for automatic adaption
+config. JumpHorz               = false         // false or true - jump horizontally to other side of mouse if tooltip would extend past clientarea boundary
+config. JumpVert               = true          // false or true - jump vertically              "
+config. Left                   = false         // false or true - tooltip on the left of the mouse
+config. OffsetX                        = 14            // Horizontal offset of left-top corner from mousepointer
+config. OffsetY                        = 8                     // Vertical offset
+config. Opacity                        = 100           // Integer between 0 and 100 - opacity of tooltip in percent
+config. Padding                        = 3                     // Spacing between border and content
+config. Shadow                 = false         // false or true
+config. ShadowColor            = '#C0C0C0'
+config. ShadowWidth            = 5
+config. Sticky                 = false         // false or true - fixate tip, ie. don't follow the mouse and don't hide on mouseout
+config. TextAlign              = 'left'        // 'left', 'right' or 'justify'
+config. Title                  = ''            // Default title text applied to all tips (no default title: empty string '')
+config. TitleAlign             = 'left'        // 'left' or 'right' - text alignment inside the title bar
+config. TitleBgColor   = ''            // If empty string '', BorderColor will be used
+config. TitleFontColor = '#FFFFFF'     // Color of title text - if '', BgColor (of tooltip body) will be used
+config. TitleFontFace  = ''            // If '' use FontFace (boldified)
+config. TitleFontSize  = ''            // If '' use FontSize
+config. TitlePadding   = 2
+config. Width                  = 0                     // Tooltip width; 0 for automatic adaption to tooltip content; < -1 (e.g. -240) for a maximum width for that automatic adaption;
+                                                                       // -1: tooltip width confined to the width required for the titlebar
+//=======  END OF TOOLTIP CONFIG, DO NOT CHANGE ANYTHING BELOW  ==============//
+
+
+
+
+//=====================  PUBLIC  =============================================//
+function Tip()
+{
+       tt_Tip(arguments, null);
+}
+function TagToTip()
+{
+       var t2t = tt_GetElt(arguments[0]);
+       if(t2t)
+               tt_Tip(arguments, t2t);
+}
+function UnTip()
+{
+       tt_OpReHref();
+       if(tt_aV[DURATION] < 0 && (tt_iState & 0x2))
+               tt_tDurt.Timer("tt_HideInit()", -tt_aV[DURATION], true);
+       else if(!(tt_aV[STICKY] && (tt_iState & 0x2)))
+               tt_HideInit();
+}
+
+//==================  PUBLIC PLUGIN API         =====================================//
+// Extension eventhandlers currently supported:
+// OnLoadConfig, OnCreateContentString, OnSubDivsCreated, OnShow, OnMoveBefore,
+// OnMoveAfter, OnHideInit, OnHide, OnKill
+
+var tt_aElt = new Array(10), // Container DIV, outer title & body DIVs, inner title & body TDs, closebutton SPAN, shadow DIVs, and IFRAME to cover windowed elements in IE
+tt_aV = new Array(),   // Caches and enumerates config data for currently active tooltip
+tt_sContent,                   // Inner tooltip text or HTML
+tt_t2t, tt_t2tDad,             // Tag converted to tip, and its DOM parent element
+tt_musX, tt_musY,
+tt_over,
+tt_x, tt_y, tt_w, tt_h; // Position, width and height of currently displayed tooltip
+
+function tt_Extension()
+{
+       tt_ExtCmdEnum();
+       tt_aExt[tt_aExt.length] = this;
+       return this;
+}
+function tt_SetTipPos(x, y)
+{
+       var css = tt_aElt[0].style;
+
+       tt_x = x;
+       tt_y = y;
+       css.left = x + "px";
+       css.top = y + "px";
+       if(tt_ie56)
+       {
+               var ifrm = tt_aElt[tt_aElt.length - 1];
+               if(ifrm)
                {
-                       t_y += '<layer bgcolor="'+t_shc+'" left="'+t_w+'" top="'+t_spct+'" width="'+t_shw+'" height="0"></layer>' +
-                               '<layer bgcolor="'+t_shc+'" left="'+t_spct+'" align="bottom" width="'+(t_w-t_spct)+'" height="'+t_shw+'"></layer>';
+                       ifrm.style.left = css.left;
+                       ifrm.style.top = css.top;
                }
-               else
+       }
+}
+function tt_HideInit()
+{
+       if(tt_iState)
+       {
+               tt_ExtCallFncs(0, "HideInit");
+               tt_iState &= ~(0x4 | 0x8);
+               if(tt_flagOpa && tt_aV[FADEOUT])
+               {
+                       tt_tFade.EndTimer();
+                       if(tt_opa)
+                       {
+                               var n = Math.round(tt_aV[FADEOUT] / (tt_aV[FADEINTERVAL] * (tt_aV[OPACITY] / tt_opa)));
+                               tt_Fade(tt_opa, tt_opa, 0, n);
+                               return;
+                       }
+               }
+               tt_tHide.Timer("tt_Hide();", 1, false);
+       }
+}
+function tt_Hide()
+{
+       if(tt_db && tt_iState)
+       {
+               tt_OpReHref();
+               if(tt_iState & 0x2)
+               {
+                       tt_aElt[0].style.visibility = "hidden";
+                       tt_ExtCallFncs(0, "Hide");
+               }
+               tt_tShow.EndTimer();
+               tt_tHide.EndTimer();
+               tt_tDurt.EndTimer();
+               tt_tFade.EndTimer();
+               if(!tt_op && !tt_ie)
                {
-                       t_optx = tt_n6? '-moz-opacity:0.85;' : tt_ie? 'filter:Alpha(opacity=85);' : 'opacity:0.85;';
-                       t_y += '<div id="'+t_id+'R" style="position:absolute;background:'+t_shc+';left:'+t_w+'px;top:'+t_spct+'px;width:'+t_shw+'px;height:1px;overflow:hidden;'+t_optx+'"></div>' +
-                               '<div style="position:relative;background:'+t_shc+';left:'+t_spct+'px;top:0px;width:'+(t_w-t_spct)+'px;height:'+t_shw+'px;overflow:hidden;'+t_optx+'"></div>';
+                       tt_tWaitMov.EndTimer();
+                       tt_bWait = false;
                }
+               if(tt_aV[CLICKCLOSE] || tt_aV[CLICKSTICKY])
+                       tt_RemEvtFnc(document, "mouseup", tt_OnLClick);
+               tt_ExtCallFncs(0, "Kill");
+               // In case of a TagToTip tip, hide converted DOM node and
+               // re-insert it into DOM
+               if(tt_t2t && !tt_aV[COPYCONTENT])
+                       tt_UnEl2Tip();
+               tt_iState = 0;
+               tt_over = null;
+               tt_ResetMainDiv();
+               if(tt_aElt[tt_aElt.length - 1])
+                       tt_aElt[tt_aElt.length - 1].style.display = "none";
        }
-       return(t_y+'</div>' +
-               (tt_ie6 ? '<iframe id="TTiEiFrM" src="javascript:false" scrolling="no" frameborder="0" style="filter:Alpha(opacity=0);position:absolute;top:0px;left:0px;display:none;"></iframe>' : ''));
 }
-function tt_EvX(t_e)
+function tt_GetElt(id)
+{
+       return(document.getElementById ? document.getElementById(id)
+                       : document.all ? document.all[id]
+                       : null);
+}
+function tt_GetDivW(el)
+{
+       return(el ? (el.offsetWidth || el.style.pixelWidth || 0) : 0);
+}
+function tt_GetDivH(el)
+{
+       return(el ? (el.offsetHeight || el.style.pixelHeight || 0) : 0);
+}
+function tt_GetScrollX()
+{
+       return(window.pageXOffset || (tt_db ? (tt_db.scrollLeft || 0) : 0));
+}
+function tt_GetScrollY()
+{
+       return(window.pageYOffset || (tt_db ? (tt_db.scrollTop || 0) : 0));
+}
+function tt_GetClientW()
+{
+       return tt_GetWndCliSiz("Width");
+}
+function tt_GetClientH()
+{
+       return tt_GetWndCliSiz("Height");
+}
+function tt_GetEvtX(e)
+{
+       return (e ? ((typeof(e.pageX) != tt_u) ? e.pageX : (e.clientX + tt_GetScrollX())) : 0);
+}
+function tt_GetEvtY(e)
+{
+       return (e ? ((typeof(e.pageY) != tt_u) ? e.pageY : (e.clientY + tt_GetScrollY())) : 0);
+}
+function tt_AddEvtFnc(el, sEvt, PFnc)
+{
+       if(el)
+       {
+               if(el.addEventListener)
+                       el.addEventListener(sEvt, PFnc, false);
+               else
+                       el.attachEvent("on" + sEvt, PFnc);
+       }
+}
+function tt_RemEvtFnc(el, sEvt, PFnc)
+{
+       if(el)
+       {
+               if(el.removeEventListener)
+                       el.removeEventListener(sEvt, PFnc, false);
+               else
+                       el.detachEvent("on" + sEvt, PFnc);
+       }
+}
+function tt_GetDad(el)
+{
+       return(el.parentNode || el.parentElement || el.offsetParent);
+}
+function tt_MovDomNode(el, dadFrom, dadTo)
+{
+       if(dadFrom)
+               dadFrom.removeChild(el);
+       if(dadTo)
+               dadTo.appendChild(el);
+}
+
+//======================  PRIVATE  ===========================================//
+var tt_aExt = new Array(),     // Array of extension objects
+
+tt_db, tt_op, tt_ie, tt_ie56, tt_bBoxOld,      // Browser flags
+tt_body,
+tt_ovr_,                               // HTML element the mouse is currently over
+tt_flagOpa,                            // Opacity support: 1=IE, 2=Khtml, 3=KHTML, 4=Moz, 5=W3C
+tt_maxPosX, tt_maxPosY,
+tt_iState = 0,                 // Tooltip active |= 1, shown |= 2, move with mouse |= 4, exclusive |= 8
+tt_opa,                                        // Currently applied opacity
+tt_bJmpVert, tt_bJmpHorz,// Tip temporarily on other side of mouse
+tt_elDeHref,                   // The tag from which we've removed the href attribute
+// Timer
+tt_tShow = new Number(0), tt_tHide = new Number(0), tt_tDurt = new Number(0),
+tt_tFade = new Number(0), tt_tWaitMov = new Number(0),
+tt_bWait = false,
+tt_u = "undefined";
+
+
+function tt_Init()
+{
+       tt_MkCmdEnum();
+       // Send old browsers instantly to hell
+       if(!tt_Browser() || !tt_MkMainDiv())
+               return;
+       tt_IsW3cBox();
+       tt_OpaSupport();
+       tt_AddEvtFnc(document, "mousemove", tt_Move);
+       // In Debug mode we search for TagToTip() calls in order to notify
+       // the user if they've forgotten to set the TagsToTip config flag
+       if(TagsToTip || tt_Debug)
+               tt_SetOnloadFnc();
+       // Ensure the tip be hidden when the page unloads
+       tt_AddEvtFnc(window, "unload", tt_Hide);
+}
+// Creates command names by translating config variable names to upper case
+function tt_MkCmdEnum()
 {
-       var t_y = tt_Int(t_e.pageX || t_e.clientX || 0) +
-               tt_Int(tt_ie? tt_db.scrollLeft : 0) +
-               tt_offX;
-       if(t_y > xlim) t_y = xlim;
-       var t_scr = tt_Int(window.pageXOffset || (tt_db? tt_db.scrollLeft : 0) || 0);
-       if(t_y < t_scr) t_y = t_scr;
-       return t_y;
+       var n = 0;
+       for(var i in config)
+               eval("window." + i.toString().toUpperCase() + " = " + n++);
+       tt_aV.length = n;
 }
-function tt_EvY(t_e)
+function tt_Browser()
 {
-       var t_y = tt_Int(t_e.pageY || t_e.clientY || 0) +
-               tt_Int(tt_ie? tt_db.scrollTop : 0);
-       if(tt_sup) t_y -= (tt_objH + tt_offY - 15);
-       else if(t_y > ylim || !tt_sub && t_y > ylim-24)
+       var n, nv, n6, w3c;
+
+       n = navigator.userAgent.toLowerCase(),
+       nv = navigator.appVersion;
+       tt_op = (document.defaultView && typeof(eval("w" + "indow" + "." + "o" + "p" + "er" + "a")) != tt_u);
+       tt_ie = n.indexOf("msie") != -1 && document.all && !tt_op;
+       if(tt_ie)
        {
-               t_y -= (tt_objH + 5);
-               tt_sub = false;
+               var ieOld = (!document.compatMode || document.compatMode == "BackCompat");
+               tt_db = !ieOld ? document.documentElement : (document.body || null);
+               if(tt_db)
+                       tt_ie56 = parseFloat(nv.substring(nv.indexOf("MSIE") + 5)) >= 5.5
+                                       && typeof document.body.style.maxHeight == tt_u;
        }
        else
        {
-               t_y += tt_offY;
-               tt_sub = true;
+               tt_db = document.documentElement || document.body ||
+                               (document.getElementsByTagName ? document.getElementsByTagName("body")[0]
+                               : null);
+               if(!tt_op)
+               {
+                       n6 = document.defaultView && typeof document.defaultView.getComputedStyle != tt_u;
+                       w3c = !n6 && document.getElementById;
+               }
+       }
+       tt_body = (document.getElementsByTagName ? document.getElementsByTagName("body")[0]
+                               : (document.body || null));
+       if(tt_ie || n6 || tt_op || w3c)
+       {
+               if(tt_body && tt_db)
+               {
+                       if(document.attachEvent || document.addEventListener)
+                               return true;
+               }
+               else
+                       tt_Err("wz_tooltip.js must be included INSIDE the body section,"
+                                       + " immediately after the opening <body> tag.", false);
        }
-       return t_y;
+       tt_db = null;
+       return false;
+}
+function tt_MkMainDiv()
+{
+       // Create the tooltip DIV
+       if(tt_body.insertAdjacentHTML)
+               tt_body.insertAdjacentHTML("afterBegin", tt_MkMainDivHtm());
+       else if(typeof tt_body.innerHTML != tt_u && document.createElement && tt_body.appendChild)
+               tt_body.appendChild(tt_MkMainDivDom());
+       if(window.tt_GetMainDivRefs /* FireFox Alzheimer */ && tt_GetMainDivRefs())
+               return true;
+       tt_db = null;
+       return false;
+}
+function tt_MkMainDivHtm()
+{
+       return(
+               '<div id="WzTtDiV"></div>' +
+               (tt_ie56 ? ('<iframe id="WzTtIfRm" src="javascript:false" scrolling="no" frameborder="0" style="filter:Alpha(opacity=0);position:absolute;top:0px;left:0px;display:none;"></iframe>')
+               : '')
+       );
 }
-function tt_ReleasMov()
+function tt_MkMainDivDom()
 {
-       if(document.onmousemove == tt_Move)
+       var el = document.createElement("div");
+       if(el)
+               el.id = "WzTtDiV";
+       return el;
+}
+function tt_GetMainDivRefs()
+{
+       tt_aElt[0] = tt_GetElt("WzTtDiV");
+       if(tt_ie56 && tt_aElt[0])
        {
-               if(!tt_mf && document.releaseEvents) document.releaseEvents(Event.MOUSEMOVE);
-               document.onmousemove = tt_mf;
+               tt_aElt[tt_aElt.length - 1] = tt_GetElt("WzTtIfRm");
+               if(!tt_aElt[tt_aElt.length - 1])
+                       tt_aElt[0] = null;
+       }
+       if(tt_aElt[0])
+       {
+               var css = tt_aElt[0].style;
+
+               css.visibility = "hidden";
+               css.position = "absolute";
+               css.overflow = "hidden";
+               return true;
        }
+       return false;
+}
+function tt_ResetMainDiv()
+{
+       tt_SetTipPos(0, 0);
+       tt_aElt[0].innerHTML = "";
+       tt_aElt[0].style.width = "0px";
+       tt_h = 0;
+}
+function tt_IsW3cBox()
+{
+       var css = tt_aElt[0].style;
+
+       css.padding = "10px";
+       css.width = "40px";
+       tt_bBoxOld = (tt_GetDivW(tt_aElt[0]) == 40);
+       css.padding = "0px";
+       tt_ResetMainDiv();
 }
-function tt_ShowIfrm(t_x)
+function tt_OpaSupport()
 {
-       if(!tt_obj || !tt_ifrm) return;
-       if(t_x)
+       var css = tt_body.style;
+
+       tt_flagOpa = (typeof(css.KhtmlOpacity) != tt_u) ? 2
+                               : (typeof(css.KHTMLOpacity) != tt_u) ? 3
+                               : (typeof(css.MozOpacity) != tt_u) ? 4
+                               : (typeof(css.opacity) != tt_u) ? 5
+                               : (typeof(css.filter) != tt_u) ? 1
+                               : 0;
+}
+// Ported from http://dean.edwards.name/weblog/2006/06/again/
+// (Dean Edwards et al.)
+function tt_SetOnloadFnc()
+{
+       tt_AddEvtFnc(document, "DOMContentLoaded", tt_HideSrcTags);
+       tt_AddEvtFnc(window, "load", tt_HideSrcTags);
+       if(tt_body.attachEvent)
+               tt_body.attachEvent("onreadystatechange",
+                       function() {
+                               if(tt_body.readyState == "complete")
+                                       tt_HideSrcTags();
+                       } );
+       if(/WebKit|KHTML/i.test(navigator.userAgent))
        {
-               tt_ifrm.style.width = tt_objW+'px';
-               tt_ifrm.style.height = tt_objH+'px';
-               tt_ifrm.style.display = "block";
+               var t = setInterval(function() {
+                                       if(/loaded|complete/.test(document.readyState))
+                                       {
+                                               clearInterval(t);
+                                               tt_HideSrcTags();
+                                       }
+                               }, 10);
        }
-       else tt_ifrm.style.display = "none";
 }
-function tt_GetDiv(t_id)
+function tt_HideSrcTags()
 {
-       return(
-               tt_n4? (document.layers[t_id] || null)
-               : tt_ie? (document.all[t_id] || null)
-               : (document.getElementById(t_id) || null)
-       );
+       if(!window.tt_HideSrcTags || window.tt_HideSrcTags.done)
+               return;
+       window.tt_HideSrcTags.done = true;
+       if(!tt_HideSrcTagsRecurs(tt_body))
+               tt_Err("There are HTML elements to be converted to tooltips.\nIf you"
+                               + " want these HTML elements to be automatically hidden, you"
+                               + " must edit wz_tooltip.js, and set TagsToTip in the global"
+                               + " tooltip configuration to true.", true);
 }
-function tt_GetDivW()
+function tt_HideSrcTagsRecurs(dad)
 {
-       return tt_Int(
-               tt_n4? tt_obj.clip.width
-               : (tt_obj.style.pixelWidth || tt_obj.offsetWidth)
-       );
+       var ovr, asT2t;
+       // Walk the DOM tree for tags that have an onmouseover or onclick attribute
+       // containing a TagToTip('...') call.
+       // (.childNodes first since .children is bugous in Safari)
+       var a = dad.childNodes || dad.children || null;
+
+       for(var i = a ? a.length : 0; i;)
+       {--i;
+               if(!tt_HideSrcTagsRecurs(a[i]))
+                       return false;
+               ovr = a[i].getAttribute ? (a[i].getAttribute("onmouseover") || a[i].getAttribute("onclick"))
+                               : (typeof a[i].onmouseover == "function") ? (a[i].onmouseover || a[i].onclick)
+                               : null;
+               if(ovr)
+               {
+                       asT2t = ovr.toString().match(/TagToTip\s*\(\s*'[^'.]+'\s*[\),]/);
+                       if(asT2t && asT2t.length)
+                       {
+                               if(!tt_HideSrcTag(asT2t[0]))
+                                       return false;
+                       }
+               }
+       }
+       return true;
 }
-function tt_GetDivH()
+function tt_HideSrcTag(sT2t)
 {
-       return tt_Int(
-               tt_n4? tt_obj.clip.height
-               : (tt_obj.style.pixelHeight || tt_obj.offsetHeight)
-       );
+       var id, el;
+
+       // The ID passed to the found TagToTip() call identifies an HTML element
+       // to be converted to a tooltip, so hide that element
+       id = sT2t.replace(/.+'([^'.]+)'.+/, "$1");
+       el = tt_GetElt(id);
+       if(el)
+       {
+               if(tt_Debug && !TagsToTip)
+                       return false;
+               else
+                       el.style.display = "none";
+       }
+       else
+               tt_Err("Invalid ID\n'" + id + "'\npassed to TagToTip()."
+                               + " There exists no HTML element with that ID.", true);
+       return true;
+}
+function tt_Tip(arg, t2t)
+{
+       if(!tt_db || (tt_iState & 0x8))
+               return;
+       if(tt_iState)
+               tt_Hide();
+       if(!tt_Enabled)
+               return;
+       tt_t2t = t2t;
+       if(!tt_ReadCmds(arg))
+               return;
+       tt_iState = 0x1 | 0x4;
+       tt_AdaptConfig1();
+       tt_MkTipContent(arg);
+       tt_MkTipSubDivs();
+       tt_FormatTip();
+       tt_bJmpVert = false;
+       tt_bJmpHorz = false;
+       tt_maxPosX = tt_GetClientW() + tt_GetScrollX() - tt_w - 1;
+       tt_maxPosY = tt_GetClientH() + tt_GetScrollY() - tt_h - 1;
+       tt_AdaptConfig2();
+       // Ensure the tip be shown and positioned before the first onmousemove
+       tt_OverInit();
+       tt_ShowInit();
+       tt_Move();
 }
+function tt_ReadCmds(a)
+{
+       var i;
 
-// Compat with DragDrop Lib: Ensure that z-index of tooltip is lifted beyond toplevel dragdrop element
-function tt_SetDivZ()
+       // First load the global config values, to initialize also values
+       // for which no command is passed
+       i = 0;
+       for(var j in config)
+               tt_aV[i++] = config[j];
+       // Then replace each cached config value for which a command is
+       // passed (ensure the # of command args plus value args be even)
+       if(a.length & 1)
+       {
+               for(i = a.length - 1; i > 0; i -= 2)
+                       tt_aV[a[i - 1]] = a[i];
+               return true;
+       }
+       tt_Err("Incorrect call of Tip() or TagToTip().\n"
+                       + "Each command must be followed by a value.", true);
+       return false;
+}
+function tt_AdaptConfig1()
 {
-       var t_i = tt_obj.style || tt_obj;
-       if(t_i)
+       tt_ExtCallFncs(0, "LoadConfig");
+       // Inherit unspecified title formattings from body
+       if(!tt_aV[TITLEBGCOLOR].length)
+               tt_aV[TITLEBGCOLOR] = tt_aV[BORDERCOLOR];
+       if(!tt_aV[TITLEFONTCOLOR].length)
+               tt_aV[TITLEFONTCOLOR] = tt_aV[BGCOLOR];
+       if(!tt_aV[TITLEFONTFACE].length)
+               tt_aV[TITLEFONTFACE] = tt_aV[FONTFACE];
+       if(!tt_aV[TITLEFONTSIZE].length)
+               tt_aV[TITLEFONTSIZE] = tt_aV[FONTSIZE];
+       if(tt_aV[CLOSEBTN])
        {
-               if(window.dd && dd.z)
-                       t_i.zIndex = Math.max(dd.z+1, t_i.zIndex);
-               if(tt_ifrm) tt_ifrm.style.zIndex = t_i.zIndex-1;
+               // Use title colours for non-specified closebutton colours
+               if(!tt_aV[CLOSEBTNCOLORS])
+                       tt_aV[CLOSEBTNCOLORS] = new Array("", "", "", "");
+               for(var i = 4; i;)
+               {--i;
+                       if(!tt_aV[CLOSEBTNCOLORS][i].length)
+                               tt_aV[CLOSEBTNCOLORS][i] = (i & 1) ? tt_aV[TITLEFONTCOLOR] : tt_aV[TITLEBGCOLOR];
+               }
+               // Enforce titlebar be shown
+               if(!tt_aV[TITLE].length)
+                       tt_aV[TITLE] = " ";
        }
+       // Circumvents broken display of images and fade-in flicker in Geckos < 1.8
+       if(tt_aV[OPACITY] == 100 && typeof tt_aElt[0].style.MozOpacity != tt_u && !Array.every)
+               tt_aV[OPACITY] = 99;
+       // Smartly shorten the delay for fade-in tooltips
+       if(tt_aV[FADEIN] && tt_flagOpa && tt_aV[DELAY] > 100)
+               tt_aV[DELAY] = Math.max(tt_aV[DELAY] - tt_aV[FADEIN], 100);
 }
-function tt_SetDivPos(t_x, t_y)
+function tt_AdaptConfig2()
 {
-       var t_i = tt_obj.style || tt_obj;
-       var t_px = (tt_op6 || tt_n4)? '' : 'px';
-       t_i.left = (tt_objX = t_x) + t_px;
-       t_i.top = (tt_objY = t_y) + t_px;
-       if(tt_ifrm)
+       if(tt_aV[CENTERMOUSE])
        {
-               tt_ifrm.style.left = t_i.left;
-               tt_ifrm.style.top = t_i.top;
+               tt_aV[OFFSETX] -= ((tt_w - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0)) >> 1);
+               tt_aV[JUMPHORZ] = false;
        }
 }
-function tt_ShowDiv(t_x)
+// Expose content globally so extensions can modify it
+function tt_MkTipContent(a)
 {
-       tt_ShowIfrm(t_x);
-       if(tt_n4) tt_obj.visibility = t_x? 'show' : 'hide';
-       else tt_obj.style.visibility = t_x? 'visible' : 'hidden';
-       tt_act = t_x;
+       if(tt_t2t)
+       {
+               if(tt_aV[COPYCONTENT])
+                       tt_sContent = tt_t2t.innerHTML;
+               else
+                       tt_sContent = "";
+       }
+       else
+               tt_sContent = a[0];
+       tt_ExtCallFncs(0, "CreateContentString");
+}
+function tt_MkTipSubDivs()
+{
+       var sCss = 'position:relative;margin:0px;padding:0px;border-width:0px;left:0px;top:0px;line-height:normal;width:auto;',
+       sTbTrTd = ' cellspacing="0" cellpadding="0" border="0" style="' + sCss + '"><tbody style="' + sCss + '"><tr><td ';
+
+       tt_aElt[0].style.width = tt_GetClientW() + "px";
+       tt_aElt[0].innerHTML =
+               (''
+               + (tt_aV[TITLE].length ?
+                       ('<div id="WzTiTl" style="position:relative;z-index:1;">'
+                       + '<table id="WzTiTlTb"' + sTbTrTd + 'id="WzTiTlI" style="' + sCss + '">'
+                       + tt_aV[TITLE]
+                       + '</td>'
+                       + (tt_aV[CLOSEBTN] ?
+                               ('<td align="right" style="' + sCss
+                               + 'text-align:right;">'
+                               + '<span id="WzClOsE" style="position:relative;left:2px;padding-left:2px;padding-right:2px;'
+                               + 'cursor:' + (tt_ie ? 'hand' : 'pointer')
+                               + ';" onmouseover="tt_OnCloseBtnOver(1)" onmouseout="tt_OnCloseBtnOver(0)" onclick="tt_HideInit()">'
+                               + tt_aV[CLOSEBTNTEXT]
+                               + '</span></td>')
+                               : '')
+                       + '</tr></tbody></table></div>')
+                       : '')
+               + '<div id="WzBoDy" style="position:relative;z-index:0;">'
+               + '<table' + sTbTrTd + 'id="WzBoDyI" style="' + sCss + '">'
+               + tt_sContent
+               + '</td></tr></tbody></table></div>'
+               + (tt_aV[SHADOW]
+                       ? ('<div id="WzTtShDwR" style="position:absolute;overflow:hidden;"></div>'
+                               + '<div id="WzTtShDwB" style="position:relative;overflow:hidden;"></div>')
+                       : '')
+               );
+       tt_GetSubDivRefs();
+       // Convert DOM node to tip
+       if(tt_t2t && !tt_aV[COPYCONTENT])
+               tt_El2Tip();
+       tt_ExtCallFncs(0, "SubDivsCreated");
+}
+function tt_GetSubDivRefs()
+{
+       var aId = new Array("WzTiTl", "WzTiTlTb", "WzTiTlI", "WzClOsE", "WzBoDy", "WzBoDyI", "WzTtShDwB", "WzTtShDwR");
+
+       for(var i = aId.length; i; --i)
+               tt_aElt[i] = tt_GetElt(aId[i - 1]);
 }
-function tt_OpDeHref(t_e)
+function tt_FormatTip()
 {
-       var t_tag;
-       if(t_e)
+       var css, w, h, pad = tt_aV[PADDING], padT, wBrd = tt_aV[BORDERWIDTH],
+       iOffY, iOffSh, iAdd = (pad + wBrd) << 1;
+
+       //--------- Title DIV ----------
+       if(tt_aV[TITLE].length)
        {
-               t_tag = t_e.target;
-               while(t_tag)
+               padT = tt_aV[TITLEPADDING];
+               css = tt_aElt[1].style;
+               css.background = tt_aV[TITLEBGCOLOR];
+               css.paddingTop = css.paddingBottom = padT + "px";
+               css.paddingLeft = css.paddingRight = (padT + 2) + "px";
+               css = tt_aElt[3].style;
+               css.color = tt_aV[TITLEFONTCOLOR];
+               if(tt_aV[WIDTH] == -1)
+                       css.whiteSpace = "nowrap";
+               css.fontFamily = tt_aV[TITLEFONTFACE];
+               css.fontSize = tt_aV[TITLEFONTSIZE];
+               css.fontWeight = "bold";
+               css.textAlign = tt_aV[TITLEALIGN];
+               // Close button DIV
+               if(tt_aElt[4])
                {
-                       if(t_tag.hasAttribute("href"))
-                       {
-                               tt_tag = t_tag
-                               tt_tag.t_href = tt_tag.getAttribute("href");
-                               tt_tag.removeAttribute("href");
-                               tt_tag.style.cursor = "hand";
-                               tt_tag.onmousedown = tt_OpReHref;
-                               tt_tag.stats = window.status;
-                               window.status = tt_tag.t_href;
-                               break;
-                       }
-                       t_tag = t_tag.parentElement;
+                       css = tt_aElt[4].style;
+                       css.background = tt_aV[CLOSEBTNCOLORS][0];
+                       css.color = tt_aV[CLOSEBTNCOLORS][1];
+                       css.fontFamily = tt_aV[TITLEFONTFACE];
+                       css.fontSize = tt_aV[TITLEFONTSIZE];
+                       css.fontWeight = "bold";
+               }
+               if(tt_aV[WIDTH] > 0)
+                       tt_w = tt_aV[WIDTH];
+               else
+               {
+                       tt_w = tt_GetDivW(tt_aElt[3]) + tt_GetDivW(tt_aElt[4]);
+                       // Some spacing between title DIV and closebutton
+                       if(tt_aElt[4])
+                               tt_w += pad;
+                       // Restrict auto width to max width
+                       if(tt_aV[WIDTH] < -1 && tt_w > -tt_aV[WIDTH])
+                               tt_w = -tt_aV[WIDTH];
+               }
+               // Ensure the top border of the body DIV be covered by the title DIV
+               iOffY = -wBrd;
+       }
+       else
+       {
+               tt_w = 0;
+               iOffY = 0;
+       }
+
+       //-------- Body DIV ------------
+       css = tt_aElt[5].style;
+       css.top = iOffY + "px";
+       if(wBrd)
+       {
+               css.borderColor = tt_aV[BORDERCOLOR];
+               css.borderStyle = tt_aV[BORDERSTYLE];
+               css.borderWidth = wBrd + "px";
+       }
+       if(tt_aV[BGCOLOR].length)
+               css.background = tt_aV[BGCOLOR];
+       if(tt_aV[BGIMG].length)
+               css.backgroundImage = "url(" + tt_aV[BGIMG] + ")";
+       css.padding = pad + "px";
+       css.textAlign = tt_aV[TEXTALIGN];
+       if(tt_aV[HEIGHT])
+       {
+               css.overflow = "auto";
+               if(tt_aV[HEIGHT] > 0)
+                       css.height = (tt_aV[HEIGHT] + iAdd) + "px";
+               else
+                       tt_h = iAdd - tt_aV[HEIGHT];
+       }
+       // TD inside body DIV
+       css = tt_aElt[6].style;
+       css.color = tt_aV[FONTCOLOR];
+       css.fontFamily = tt_aV[FONTFACE];
+       css.fontSize = tt_aV[FONTSIZE];
+       css.fontWeight = tt_aV[FONTWEIGHT];
+       css.textAlign = tt_aV[TEXTALIGN];
+       if(tt_aV[WIDTH] > 0)
+               w = tt_aV[WIDTH];
+       // Width like title (if existent)
+       else if(tt_aV[WIDTH] == -1 && tt_w)
+               w = tt_w;
+       else
+       {
+               // Measure width of the body's inner TD, as some browsers would expand
+               // the container and outer body DIV to 100%
+               w = tt_GetDivW(tt_aElt[6]);
+               // Restrict auto width to max width
+               if(tt_aV[WIDTH] < -1 && w > -tt_aV[WIDTH])
+                       w = -tt_aV[WIDTH];
+       }
+       if(w > tt_w)
+               tt_w = w;
+       tt_w += iAdd;
+
+       //--------- Shadow DIVs ------------
+       if(tt_aV[SHADOW])
+       {
+               tt_w += tt_aV[SHADOWWIDTH];
+               iOffSh = Math.floor((tt_aV[SHADOWWIDTH] * 4) / 3);
+               // Bottom shadow
+               css = tt_aElt[7].style;
+               css.top = iOffY + "px";
+               css.left = iOffSh + "px";
+               css.width = (tt_w - iOffSh - tt_aV[SHADOWWIDTH]) + "px";
+               css.height = tt_aV[SHADOWWIDTH] + "px";
+               css.background = tt_aV[SHADOWCOLOR];
+               // Right shadow
+               css = tt_aElt[8].style;
+               css.top = iOffSh + "px";
+               css.left = (tt_w - tt_aV[SHADOWWIDTH]) + "px";
+               css.width = tt_aV[SHADOWWIDTH] + "px";
+               css.background = tt_aV[SHADOWCOLOR];
+       }
+       else
+               iOffSh = 0;
+
+       //-------- Container DIV -------
+       tt_SetTipOpa(tt_aV[FADEIN] ? 0 : tt_aV[OPACITY]);
+       tt_FixSize(iOffY, iOffSh);
+}
+// Fixate the size so it can't dynamically change while the tooltip is moving.
+function tt_FixSize(iOffY, iOffSh)
+{
+       var wIn, wOut, h, add, pad = tt_aV[PADDING], wBrd = tt_aV[BORDERWIDTH], i;
+
+       tt_aElt[0].style.width = tt_w + "px";
+       tt_aElt[0].style.pixelWidth = tt_w;
+       wOut = tt_w - ((tt_aV[SHADOW]) ? tt_aV[SHADOWWIDTH] : 0);
+       // Body
+       wIn = wOut;
+       if(!tt_bBoxOld)
+               wIn -= (pad + wBrd) << 1;
+       tt_aElt[5].style.width = wIn + "px";
+       // Title
+       if(tt_aElt[1])
+       {
+               wIn = wOut - ((tt_aV[TITLEPADDING] + 2) << 1);
+               if(!tt_bBoxOld)
+                       wOut = wIn;
+               tt_aElt[1].style.width = wOut + "px";
+               tt_aElt[2].style.width = wIn + "px";
+       }
+       // Max height specified
+       if(tt_h)
+       {
+               h = tt_GetDivH(tt_aElt[5]);
+               if(h > tt_h)
+               {
+                       if(!tt_bBoxOld)
+                               tt_h -= (pad + wBrd) << 1;
+                       tt_aElt[5].style.height = tt_h + "px";
+               }
+       }
+       tt_h = tt_GetDivH(tt_aElt[0]) + iOffY;
+       // Right shadow
+       if(tt_aElt[8])
+               tt_aElt[8].style.height = (tt_h - iOffSh) + "px";
+       i = tt_aElt.length - 1;
+       if(tt_aElt[i])
+       {
+               tt_aElt[i].style.width = tt_w + "px";
+               tt_aElt[i].style.height = tt_h + "px";
+       }
+}
+function tt_DeAlt(el)
+{
+       var aKid;
+
+       if(el)
+       {
+               if(el.alt)
+                       el.alt = "";
+               if(el.title)
+                       el.title = "";
+               aKid = el.childNodes || el.children || null;
+               if(aKid)
+               {
+                       for(var i = aKid.length; i;)
+                               tt_DeAlt(aKid[--i]);
                }
        }
 }
+// This hack removes the native tooltips over links in Opera
+function tt_OpDeHref(el)
+{
+       if(!tt_op)
+               return;
+       if(tt_elDeHref)
+               tt_OpReHref();
+       while(el)
+       {
+               if(el.hasAttribute && el.hasAttribute("href"))
+               {
+                       el.t_href = el.getAttribute("href");
+                       el.t_stats = window.status;
+                       el.removeAttribute("href");
+                       el.style.cursor = "hand";
+                       tt_AddEvtFnc(el, "mousedown", tt_OpReHref);
+                       window.status = el.t_href;
+                       tt_elDeHref = el;
+                       break;
+               }
+               el = tt_GetDad(el);
+       }
+}
 function tt_OpReHref()
 {
-       if(tt_tag)
+       if(tt_elDeHref)
        {
-               tt_tag.setAttribute("href", tt_tag.t_href);
-               window.status = tt_tag.stats;
-               tt_tag = null;
+               tt_elDeHref.setAttribute("href", tt_elDeHref.t_href);
+               tt_RemEvtFnc(tt_elDeHref, "mousedown", tt_OpReHref);
+               window.status = tt_elDeHref.t_stats;
+               tt_elDeHref = null;
        }
 }
-function tt_Show(t_e, t_id, t_sup, t_delay, t_fix, t_left, t_offx, t_offy, t_static, t_sticky, t_temp)
+function tt_El2Tip()
+{
+       var css = tt_t2t.style;
+
+       // Store previous positioning
+       tt_t2t.t_cp = css.position;
+       tt_t2t.t_cl = css.left;
+       tt_t2t.t_ct = css.top;
+       tt_t2t.t_cd = css.display;
+       // Store the tag's parent element so we can restore that DOM branch
+       // when the tooltip is being hidden
+       tt_t2tDad = tt_GetDad(tt_t2t);
+       tt_MovDomNode(tt_t2t, tt_t2tDad, tt_aElt[6]);
+       css.display = "block";
+       css.position = "static";
+       css.left = css.top = css.marginLeft = css.marginTop = "0px";
+}
+function tt_UnEl2Tip()
+{
+       // Restore positioning and display
+       var css = tt_t2t.style;
+
+       css.display = tt_t2t.t_cd;
+       tt_MovDomNode(tt_t2t, tt_GetDad(tt_t2t), tt_t2tDad);
+       css.position = tt_t2t.t_cp;
+       css.left = tt_t2t.t_cl;
+       css.top = tt_t2t.t_ct;
+       tt_t2tDad = null;
+}
+function tt_OverInit()
 {
-       if(tt_obj) tt_Hide();
-       tt_mf = document.onmousemove || null;
-       if(window.dd && (window.DRAG && tt_mf == DRAG || window.RESIZE && tt_mf == RESIZE)) return;
-       var t_sh, t_h;
+       if(window.event)
+               tt_over = window.event.target || window.event.srcElement;
+       else
+               tt_over = tt_ovr_;
+       tt_DeAlt(tt_over);
+       tt_OpDeHref(tt_over);
+}
+function tt_ShowInit()
+{
+       tt_tShow.Timer("tt_Show()", tt_aV[DELAY], true);
+       if(tt_aV[CLICKCLOSE] || tt_aV[CLICKSTICKY])
+               tt_AddEvtFnc(document, "mouseup", tt_OnLClick);
+}
+function tt_Show()
+{
+       var css = tt_aElt[0].style;
 
-       tt_obj = tt_GetDiv(t_id);
-       if(tt_obj)
+       // Override the z-index of the topmost wz_dragdrop.js D&D item
+       css.zIndex = Math.max((window.dd && dd.z) ? (dd.z + 2) : 0, 1010);
+       if(tt_aV[STICKY] || !tt_aV[FOLLOWMOUSE])
+               tt_iState &= ~0x4;
+       if(tt_aV[EXCLUSIVE])
+               tt_iState |= 0x8;
+       if(tt_aV[DURATION] > 0)
+               tt_tDurt.Timer("tt_HideInit()", tt_aV[DURATION], true);
+       tt_ExtCallFncs(0, "Show")
+       css.visibility = "visible";
+       tt_iState |= 0x2;
+       if(tt_aV[FADEIN])
+               tt_Fade(0, 0, tt_aV[OPACITY], Math.round(tt_aV[FADEIN] / tt_aV[FADEINTERVAL]));
+       tt_ShowIfrm();
+}
+function tt_ShowIfrm()
+{
+       if(tt_ie56)
        {
-               t_e = t_e || window.event;
-               tt_sub = !(tt_sup = t_sup);
-               tt_sticky = t_sticky;
-               tt_objW = tt_GetDivW();
-               tt_objH = tt_GetDivH();
-               tt_offX = t_left? -(tt_objW+t_offx) : t_offx;
-               tt_offY = t_offy;
-               if(tt_op7) tt_OpDeHref(t_e);
-               if(tt_n4)
+               var ifrm = tt_aElt[tt_aElt.length - 1];
+               if(ifrm)
                {
-                       if(tt_obj.document.layers.length)
-                       {
-                               t_sh = tt_obj.document.layers[0];
-                               t_sh.clip.height = tt_objH - Math.round(t_sh.clip.width*1.3);
-                       }
+                       var css = ifrm.style;
+                       css.zIndex = tt_aElt[0].style.zIndex - 1;
+                       css.display = "block";
                }
+       }
+}
+function tt_Move(e)
+{
+       if(e)
+               tt_ovr_ = e.target || e.srcElement;
+       e = e || window.event;
+       if(e)
+       {
+               tt_musX = tt_GetEvtX(e);
+               tt_musY = tt_GetEvtY(e);
+       }
+       if(tt_iState & 0x4)
+       {
+               // Prevent jam of mousemove events
+               if(!tt_op && !tt_ie)
+               {
+                       if(tt_bWait)
+                               return;
+                       tt_bWait = true;
+                       tt_tWaitMov.Timer("tt_bWait = false;", 1, true);
+               }
+               if(tt_aV[FIX])
+               {
+                       tt_iState &= ~0x4;
+                       tt_PosFix();
+               }
+               else if(!tt_ExtCallFncs(e, "MoveBefore"))
+                       tt_SetTipPos(tt_Pos(0), tt_Pos(1));
+               tt_ExtCallFncs([tt_musX, tt_musY], "MoveAfter")
+       }
+}
+function tt_Pos(iDim)
+{
+       var iX, bJmpMod, cmdAlt, cmdOff, cx, iMax, iScrl, iMus, bJmp;
+
+       // Map values according to dimension to calculate
+       if(iDim)
+       {
+               bJmpMod = tt_aV[JUMPVERT];
+               cmdAlt = ABOVE;
+               cmdOff = OFFSETY;
+               cx = tt_h;
+               iMax = tt_maxPosY;
+               iScrl = tt_GetScrollY();
+               iMus = tt_musY;
+               bJmp = tt_bJmpVert;
+       }
+       else
+       {
+               bJmpMod = tt_aV[JUMPHORZ];
+               cmdAlt = LEFT;
+               cmdOff = OFFSETX;
+               cx = tt_w;
+               iMax = tt_maxPosX;
+               iScrl = tt_GetScrollX();
+               iMus = tt_musX;
+               bJmp = tt_bJmpHorz;
+       }
+       if(bJmpMod)
+       {
+               if(tt_aV[cmdAlt] && (!bJmp || tt_CalcPosAlt(iDim) >= iScrl + 16))
+                       iX = tt_PosAlt(iDim);
+               else if(!tt_aV[cmdAlt] && bJmp && tt_CalcPosDef(iDim) > iMax - 16)
+                       iX = tt_PosAlt(iDim);
+               else
+                       iX = tt_PosDef(iDim);
+       }
+       else
+       {
+               iX = iMus;
+               if(tt_aV[cmdAlt])
+                       iX -= cx + tt_aV[cmdOff] - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0);
+               else
+                       iX += tt_aV[cmdOff];
+       }
+       // Prevent tip from extending past clientarea boundary
+       if(iX > iMax)
+               iX = bJmpMod ? tt_PosAlt(iDim) : iMax;
+       // In case of insufficient space on both sides, ensure the left/upper part
+       // of the tip be visible
+       if(iX < iScrl)
+               iX = bJmpMod ? tt_PosDef(iDim) : iScrl;
+       return iX;
+}
+function tt_PosDef(iDim)
+{
+       if(iDim)
+               tt_bJmpVert = tt_aV[ABOVE];
+       else
+               tt_bJmpHorz = tt_aV[LEFT];
+       return tt_CalcPosDef(iDim);
+}
+function tt_PosAlt(iDim)
+{
+       if(iDim)
+               tt_bJmpVert = !tt_aV[ABOVE];
+       else
+               tt_bJmpHorz = !tt_aV[LEFT];
+       return tt_CalcPosAlt(iDim);
+}
+function tt_CalcPosDef(iDim)
+{
+       return iDim ? (tt_musY + tt_aV[OFFSETY]) : (tt_musX + tt_aV[OFFSETX]);
+}
+function tt_CalcPosAlt(iDim)
+{
+       var cmdOff = iDim ? OFFSETY : OFFSETX;
+       var dx = tt_aV[cmdOff] - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0);
+       if(tt_aV[cmdOff] > 0 && dx <= 0)
+               dx = 1;
+       return((iDim ? (tt_musY - tt_h) : (tt_musX - tt_w)) - dx);
+}
+function tt_PosFix()
+{
+       var iX, iY;
+
+       if(typeof(tt_aV[FIX][0]) == "number")
+       {
+               iX = tt_aV[FIX][0];
+               iY = tt_aV[FIX][1];
+       }
+       else
+       {
+               if(typeof(tt_aV[FIX][0]) == "string")
+                       el = tt_GetElt(tt_aV[FIX][0]);
+               // First slot in array is direct reference to HTML element
                else
+                       el = tt_aV[FIX][0];
+               iX = tt_aV[FIX][1];
+               iY = tt_aV[FIX][2];
+               // By default, vert pos is related to bottom edge of HTML element
+               if(!tt_aV[ABOVE] && el)
+                       iY += tt_GetDivH(el);
+               for(; el; el = el.offsetParent)
                {
-                       t_sh = tt_GetDiv(t_id+'R');
-                       if(t_sh)
-                       {
-                               t_h = tt_objH - tt_Int(t_sh.style.pixelTop || t_sh.style.top || 0);
-                               if(typeof t_sh.style.pixelHeight != tt_u) t_sh.style.pixelHeight = t_h;
-                               else t_sh.style.height = t_h+'px';
-                       }
+                       iX += el.offsetLeft || 0;
+                       iY += el.offsetTop || 0;
                }
+       }
+       // For a fixed tip positioned above the mouse, use the bottom edge as anchor
+       // (recommended by Christophe Rebeschini, 31.1.2008)
+       if(tt_aV[ABOVE])
+               iY -= tt_h;
+       tt_SetTipPos(iX, iY);
+}
+function tt_Fade(a, now, z, n)
+{
+       if(n)
+       {
+               now += Math.round((z - now) / n);
+               if((z > a) ? (now >= z) : (now <= z))
+                       now = z;
+               else
+                       tt_tFade.Timer(
+                               "tt_Fade("
+                               + a + "," + now + "," + z + "," + (n - 1)
+                               + ")",
+                               tt_aV[FADEINTERVAL],
+                               true
+                       );
+       }
+       now ? tt_SetTipOpa(now) : tt_Hide();
+}
+function tt_SetTipOpa(opa)
+{
+       // To circumvent the opacity nesting flaws of IE, we set the opacity
+       // for each sub-DIV separately, rather than for the container DIV.
+       tt_SetOpa(tt_aElt[5], opa);
+       if(tt_aElt[1])
+               tt_SetOpa(tt_aElt[1], opa);
+       if(tt_aV[SHADOW])
+       {
+               opa = Math.round(opa * 0.8);
+               tt_SetOpa(tt_aElt[7], opa);
+               tt_SetOpa(tt_aElt[8], opa);
+       }
+}
+function tt_OnCloseBtnOver(iOver)
+{
+       var css = tt_aElt[4].style;
 
-               xlim = tt_Int((tt_db && tt_db.clientWidth)? tt_db.clientWidth : window.innerWidth) +
-                       tt_Int(window.pageXOffset || (tt_db? tt_db.scrollLeft : 0) || 0) -
-                       tt_objW -
-                       (tt_n4? 21 : 0);
-               ylim = tt_Int(window.innerHeight || tt_db.clientHeight) +
-                       tt_Int(window.pageYOffset || (tt_db? tt_db.scrollTop : 0) || 0) -
-                       tt_objH - tt_offY;
-
-               tt_SetDivZ();
-               if(t_fix) tt_SetDivPos(tt_Int((t_fix = t_fix.split(','))[0]), tt_Int(t_fix[1]));
-               else tt_SetDivPos(tt_EvX(t_e), tt_EvY(t_e));
-
-               var t_txt = 'tt_ShowDiv(\'true\');';
-               if(t_sticky) t_txt += '{'+
-                               'tt_ReleasMov();'+
-                               'window.tt_upFunc = document.onmouseup || null;'+
-                               'if(document.captureEvents) document.captureEvents(Event.MOUSEUP);'+
-                               'document.onmouseup = new Function("window.setTimeout(\'tt_Hide();\', 10);");'+
-                       '}';
-               else if(t_static) t_txt += 'tt_ReleasMov();';
-               if(t_temp > 0) t_txt += 'window.tt_rtm = window.setTimeout(\'tt_sticky = false; tt_Hide();\','+t_temp+');';
-               window.tt_rdl = window.setTimeout(t_txt, t_delay);
-
-               if(!t_fix)
+       iOver <<= 1;
+       css.background = tt_aV[CLOSEBTNCOLORS][iOver];
+       css.color = tt_aV[CLOSEBTNCOLORS][iOver + 1];
+}
+function tt_OnLClick(e)
+{
+       //  Ignore right-clicks
+       e = e || window.event;
+       if(!((e.button && e.button & 2) || (e.which && e.which == 3)))
+       {
+               if(tt_aV[CLICKSTICKY] && (tt_iState & 0x4))
                {
-                       if(document.captureEvents) document.captureEvents(Event.MOUSEMOVE);
-                       document.onmousemove = tt_Move;
+                       tt_aV[STICKY] = true;
+                       tt_iState &= ~0x4;
                }
+               else if(tt_aV[CLICKCLOSE])
+                       tt_HideInit();
        }
 }
-var tt_area = false;
-function tt_Move(t_ev)
+function tt_Int(x)
+{
+       var y;
+
+       return(isNaN(y = parseInt(x)) ? 0 : y);
+}
+Number.prototype.Timer = function(s, iT, bUrge)
+{
+       if(!this.value || bUrge)
+               this.value = window.setTimeout(s, iT);
+}
+Number.prototype.EndTimer = function()
 {
-       if(!tt_obj) return;
-       if(tt_n6 || tt_w3c)
+       if(this.value)
        {
-               if(tt_wait) return;
-               tt_wait = true;
-               setTimeout('tt_wait = false;', 5);
+               window.clearTimeout(this.value);
+               this.value = 0;
        }
-       var t_e = t_ev || window.event;
-       tt_SetDivPos(tt_EvX(t_e), tt_EvY(t_e));
-       if(tt_op6)
+}
+function tt_GetWndCliSiz(s)
+{
+       var db, y = window["inner" + s], sC = "client" + s, sN = "number";
+       if(typeof y == sN)
        {
-               if(tt_area && t_e.target.tagName != 'AREA') tt_Hide();
-               else if(t_e.target.tagName == 'AREA') tt_area = true;
+               var y2;
+               return(
+                       // Gecko or Opera with scrollbar
+                       // ... quirks mode
+                       ((db = document.body) && typeof(y2 = db[sC]) == sN && y2 &&  y2 <= y) ? y2 
+                       // ... strict mode
+                       : ((db = document.documentElement) && typeof(y2 = db[sC]) == sN && y2 && y2 <= y) ? y2
+                       // No scrollbar, or clientarea size == 0, or other browser (KHTML etc.)
+                       : y
+               );
        }
+       // IE
+       return(
+               // document.documentElement.client+s functional, returns > 0
+               ((db = document.documentElement) && (y = db[sC])) ? y
+               // ... not functional, in which case document.body.client+s 
+               // is the clientarea size, fortunately
+               : document.body[sC]
+       );
 }
-function tt_Hide()
+function tt_SetOpa(el, opa)
 {
-       if(window.tt_obj)
+       var css = el.style;
+
+       tt_opa = opa;
+       if(tt_flagOpa == 1)
        {
-               if(window.tt_rdl) window.clearTimeout(tt_rdl);
-               if(!tt_sticky || !tt_act)
+               if(opa < 100)
                {
-                       if(window.tt_rtm) window.clearTimeout(tt_rtm);
-                       tt_ShowDiv(false);
-                       tt_SetDivPos(-tt_objW, -tt_objH);
-                       tt_obj = null;
-                       if(typeof window.tt_upFunc != tt_u) document.onmouseup = window.tt_upFunc;
+                       // Hacks for bugs of IE:
+                       // 1.) Once a CSS filter has been applied, fonts are no longer
+                       // anti-aliased, so we store the previous 'non-filter' to be
+                       // able to restore it
+                       if(typeof(el.filtNo) == tt_u)
+                               el.filtNo = css.filter;
+                       // 2.) A DIV cannot be made visible in a single step if an
+                       // opacity < 100 has been applied while the DIV was hidden
+                       var bVis = css.visibility != "hidden";
+                       // 3.) In IE6, applying an opacity < 100 has no effect if the
+                       //         element has no layout (position, size, zoom, ...)
+                       css.zoom = "100%";
+                       if(!bVis)
+                               css.visibility = "visible";
+                       css.filter = "alpha(opacity=" + opa + ")";
+                       if(!bVis)
+                               css.visibility = "hidden";
+               }
+               else if(typeof(el.filtNo) != tt_u)
+                       // Restore 'non-filter'
+                       css.filter = el.filtNo;
+       }
+       else
+       {
+               opa /= 100.0;
+               switch(tt_flagOpa)
+               {
+               case 2:
+                       css.KhtmlOpacity = opa; break;
+               case 3:
+                       css.KHTMLOpacity = opa; break;
+               case 4:
+                       css.MozOpacity = opa; break;
+               case 5:
+                       css.opacity = opa; break;
                }
-               tt_sticky = false;
-               if(tt_op6 && tt_area) tt_area = false;
-               tt_ReleasMov();
-               if(tt_op7) tt_OpReHref();
        }
 }
-function tt_Init()
+function tt_Err(sErr, bIfDebug)
+{
+       if(tt_Debug || !bIfDebug)
+               alert("Tooltip Script Error Message:\n\n" + sErr);
+}
+
+//============  EXTENSION (PLUGIN) MANAGER  ===============//
+function tt_ExtCmdEnum()
 {
-       if(!(tt_op || tt_n4 || tt_n6 || tt_ie || tt_w3c)) return;
-
-       var htm = tt_n4? '<div style="position:absolute;"></div>' : '',
-       tags,
-       t_tj,
-       over,
-       esc = 'return escape(';
-       var i = tt_tags.length; while(i--)
-       {
-               tags = tt_ie? (document.all.tags(tt_tags[i]) || 1)
-                       : document.getElementsByTagName? (document.getElementsByTagName(tt_tags[i]) || 1)
-                       : (!tt_n4 && tt_tags[i]=="a")? document.links
-                       : 1;
-               if(tt_n4 && (tt_tags[i] == "a" || tt_tags[i] == "layer")) tags = tt_N4Tags(tt_tags[i]);
-               var j = tags.length; while(j--)
+       var s;
+
+       // Add new command(s) to the commands enum
+       for(var i in config)
+       {
+               s = "window." + i.toString().toUpperCase();
+               if(eval("typeof(" + s + ") == tt_u"))
                {
-                       if(typeof (t_tj = tags[j]).onmouseover == "function" && t_tj.onmouseover.toString().indexOf(esc) != -1 && !tt_n6 || tt_n6 && (over = t_tj.getAttribute("onmouseover")) && over.indexOf(esc) != -1)
-                       {
-                               if(over) t_tj.onmouseover = new Function(over);
-                               var txt = unescape(t_tj.onmouseover());
-                               htm += tt_Htm(
-                                       t_tj,
-                                       "tOoLtIp"+i+""+j,
-                                       txt.wzReplace("& ","&")
-                               );
-
-                               t_tj.onmouseover = new Function('e',
-                                       'tt_Show(e,'+
-                                       '"tOoLtIp' +i+''+j+ '",'+
-                                       ((typeof t_tj.T_ABOVE != tt_u)? t_tj.T_ABOVE : ttAbove)+','+
-                                       ((typeof t_tj.T_DELAY != tt_u)? t_tj.T_DELAY : ttDelay)+','+
-                                       ((typeof t_tj.T_FIX != tt_u)? '"'+t_tj.T_FIX+'"' : '""')+','+
-                                       ((typeof t_tj.T_LEFT != tt_u)? t_tj.T_LEFT : ttLeft)+','+
-                                       ((typeof t_tj.T_OFFSETX != tt_u)? t_tj.T_OFFSETX : ttOffsetX)+','+
-                                       ((typeof t_tj.T_OFFSETY != tt_u)? t_tj.T_OFFSETY : ttOffsetY)+','+
-                                       ((typeof t_tj.T_STATIC != tt_u)? t_tj.T_STATIC : ttStatic)+','+
-                                       ((typeof t_tj.T_STICKY != tt_u)? t_tj.T_STICKY : ttSticky)+','+
-                                       ((typeof t_tj.T_TEMP != tt_u)? t_tj.T_TEMP : ttTemp)+
-                                       ');'
-                               );
-                               t_tj.onmouseout = tt_Hide;
-                               if(t_tj.alt) t_tj.alt = "";
-                               if(t_tj.title) t_tj.title = "";
-                       }
+                       eval(s + " = " + tt_aV.length);
+                       tt_aV[tt_aV.length] = null;
                }
        }
-       document.write(htm);
-       if(document.getElementById) tt_ifrm = document.getElementById("TTiEiFrM");
 }
+function tt_ExtCallFncs(arg, sFnc)
+{
+       var b = false;
+       for(var i = tt_aExt.length; i;)
+       {--i;
+               var fnc = tt_aExt[i]["On" + sFnc];
+               // Call the method the extension has defined for this event
+               if(fnc && fnc(arg))
+                       b = true;
+       }
+       return b;
+}
+
 tt_Init();
index 224bc26..dfc0f97 100755 (executable)
@@ -15,12 +15,12 @@ $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',
@@ -82,8 +82,12 @@ $self->{texts} = {
   'AUTOMATICALLY MATCH BINS'    => 'LAGERPLÄTZE AUTOMATISCH ZUWEISEN',
   'Abort'                       => 'Abbrechen',
   'Abrechnungsnummer'           => 'Abrechnungsnummer',
+  'Absolute BB Balance'         => 'Gesamtsaldo laut Bankbuchungen',
+  'Absolute BT Balance'         => 'Gesamtsaldo laut Kontoauszug',
   'Abteilung'                   => 'Abteilung',
   'Acceptance Statuses'         => 'Abnahmestatus',
+  'Acc Transaction'             => 'Hauptbuch',
+  'Acc transaction'             => 'Hauptbuch Buchung',
   'Access rights'               => 'Zugriffsrechte',
   'Access to clients'           => 'Zugriff auf Mandanten',
   'Account'                     => 'Konto',
@@ -121,6 +125,7 @@ $self->{texts} = {
   'Account for interest'        => 'Konto f&uuml;r Zinsen',
   'Account number'              => 'Kontonummer',
   'Account number not unique!'  => 'Kontonummer bereits vorhanden!',
+  'Account number of the goal/source' => 'Ziel- oder Quellkonto',
   'Account saved!'              => 'Konto gespeichert!',
   'Accounting method'           => 'Versteuerungsart',
   'Accrual'                     => 'Soll-Versteuerung',
@@ -141,6 +146,7 @@ $self->{texts} = {
   'Add Delivery Note'           => 'Lieferschein erfassen',
   'Add Delivery Order'          => 'Lieferschein erfassen',
   'Add Dunning'                 => 'Mahnung erzeugen',
+  'Add Exchangerate'            => '',
   'Add Follow-Up'               => 'Wiedervorlage erstellen',
   'Add Follow-Up for #1'        => 'Wiedervorlage f&uuml;r #1 erstellen',
   'Add General Ledger Transaction' => 'Dialogbuchen',
@@ -177,6 +183,7 @@ $self->{texts} = {
   'Add bank account'            => 'Bankkonto erfassen',
   'Add custom variable'         => 'Benutzerdefinierte Variable erfassen',
   'Add function block'          => 'Funktionsblock 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',
@@ -231,8 +238,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 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.',
@@ -283,6 +293,9 @@ $self->{texts} = {
   'Assign article'              => 'Artikel zuweisen',
   'Assign the following article to all sections' => 'Den folgenden Artikel allen Abschnitten zuweisen',
   'Assignment of articles to sections' => 'Zuweisung von Artikeln zu Abschnitten',
+  'Assign invoice'              => 'Rechnung zuweisen',
+  'Assigned'                    => 'Zugewiesen',
+  'Assigned invoices'           => 'Zugewiesene Rechnungen',
   'Assistant for general ledger corrections' => 'Assistent für die Korrektur von Hauptbucheinträgen',
   'Assume Tax Consultant Data in Tax Computation?' => 'Beraterdaten in UStVA übernehmen?',
   'At least'                    => 'Mindestens',
@@ -302,13 +315,17 @@ $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',
   'Available qty'               => 'Lagerbestand',
   'BALANCE SHEET'               => 'BILANZ',
+  'BB Balance'                  => 'Saldo Bank',
   'BIC'                         => 'BIC',
   'BOM'                         => 'Stückliste',
+  'BT Balance'                  => 'Saldo Buchungen',
   'BWA'                         => 'BWA',
   'Back'                        => 'Zurück',
   'Back to login'               => 'Zurück zur Anmeldung',
@@ -322,18 +339,25 @@ $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 account'                => 'Bankkonto',
   'Bank accounts'               => 'Bankkonten',
   'Bank code'                   => 'Bankleitzahl',
+  'Bank code of the goal/source' => 'Bankleitzahl von Ziel- oder Quellkonto',
   'Bank collection amount'      => 'Einzugsbetrag',
   'Bank collection payment list for export #1' => 'Bankeinzugszahlungsliste für SEPA-Export #1',
   'Bank collection via SEPA'    => 'Bankeinzug via SEPA',
   'Bank collections via SEPA'   => 'Bankeinzüge via SEPA',
+  'Bank transaction'            => 'Bankbuchung',
+  'Bank transactions'           => 'Bankbewegungen',
+  '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',
@@ -426,7 +450,10 @@ $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',
   'CSV import: inventories'     => 'CSV-Import: Lagerbewegungen/-bestände',
@@ -518,9 +545,11 @@ $self->{texts} = {
   'Choose Outputformat'         => 'Ausgabeformat auswählen...',
   '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',
   'City'                        => 'Stadt',
   'Clear fields'                => 'Felder leeren',
   'Cleared Balance'             => 'abgeschlossen',
+  'Cleared/uncleared only'      => 'Status abgeglichen',
   'Clearing Tax Received (No 71)' => 'Verrechnung des Erstattungsbetrages erwünscht (Zeile 71)',
   'Client'                      => 'Mandant',
   'Client #1'                   => 'Mandant #1',
@@ -580,6 +609,7 @@ $self->{texts} = {
   'Correct taxkey'              => 'Richtiger Steuerschlüssel',
   'Cost'                        => 'Kosten',
   'Costs'                       => 'Kosten',
+  'Could not load GL Transaction' => '',
   'Could not load class #1 (#2): "#3"' => 'Konnte Klasse #1 (#2) nicht laden: "#3"',
   'Could not load class #1, #2' => 'Konnte Klasse #1 nicht laden: "#2"',
   'Could not load employee'     => 'Konnte Benutzer nicht laden',
@@ -587,6 +617,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 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!',
@@ -643,6 +674,7 @@ $self->{texts} = {
   'Create customers and vendors. Edit all vendors. Edit all customers' => 'Kunden und Lieferanten erfassen. Alle Lieferanten bearbeiten. Alle Kunden bearbeiten',
   'Create customers and vendors. Edit all vendors. Edit only customers where salesman equals employee (login)' => 'Kunden und Lieferanten erfassen. Alle Lieferanten bearbeiten. Nur Kunden bearbeiten bei denen der Verkäufer gleich Bearbeiter (login) ist',
   'Create first invoice on'     => 'Erste Rechnung erzeugen am',
+  'Create invoice'              => 'Buchung erstellen',
   'Create invoice?'             => 'Rechnung erstellen?',
   'Create new'                  => 'Neu erfassen',
   'Create new background job'   => 'Neuen Hintergrund-Job anlegen',
@@ -717,6 +749,9 @@ $self->{texts} = {
   'Customer/Vendor (database ID)' => 'Kunde/Lieferant (Datenbank-ID)',
   'Customer/Vendor Name'        => 'Kunde/Lieferant',
   'Customer/Vendor Number'      => 'Kunden-/Lieferantennummer',
+  'Customer/Vendor name'        => 'Kunden-/Lieferantenname',
+  'Customer/Vendor number'      => 'Kunden-/Lieferantennummer',
+  'Customer/Vendor/Remote name' => 'Kunden/Lieferantenname laut Bank',
   'Customername'                => 'Kundenname',
   'Customernumberinit'          => 'Kunden-/Lieferantennummernkreis',
   'Customers'                   => 'Kunden',
@@ -756,6 +791,7 @@ $self->{texts} = {
   'Date Paid'                   => 'Zahlungsdatum',
   'Date and timestamp variables: If the default value equals \'NOW\' then the current date/current timestamp will be used. Otherwise the default value is copied as-is.' => 'Datums- und Uhrzeitvariablen: Wenn der Standardwert \'NOW\' ist, so wird das aktuelle Datum/die aktuelle Uhrzeit eingef&uuml;gt. Andernfalls wird der Standardwert so wie er ist benutzt.',
   'Date missing!'               => 'Datum fehlt!',
+  'Date of transaction'         => 'Buchungsdatum',
   'Date the payment is due in full' => 'Das Datum, bis die Rechnung in voller Höhe bezahlt werden muss',
   'Date the payment is due with discount' => 'Das Datum, bis die Rechnung unter Abzug von Skonto bezahlt werden kann',
   'Datevautomatik'              => 'Datev-Automatik',
@@ -864,6 +900,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',
@@ -915,7 +952,9 @@ $self->{texts} = {
   'Download picture'            => 'Bild herunterladen',
   'Download sample file'        => 'Beispieldatei herunterladen',
   'Draft for this Letter saved!' => 'Briefentwurf gespeichert!',
+  'Draft from:'                 => 'Entwurf vom:',
   'Draft saved.'                => 'Entwurf gespeichert.',
+  'Draft suggestions'           => 'Entwurfsvorschläge',
   'Drawing'                     => 'Zeichnung',
   'Dropdown Limit'              => 'Auswahllistenbegrenzung',
   'Due'                         => 'Fällig',
@@ -969,6 +1008,7 @@ $self->{texts} = {
   'Edit Employee #1'            => 'Benutzer #1 bearbeiten',
   'Edit Follow-Up'              => 'Wiedervorlage bearbeiten',
   'Edit Follow-Up for #1'       => 'Wiedervorlage f&uuml;r #1 bearbeiten',
+  'Edit GL Transaction with id' => '',
   'Edit General Ledger Transaction' => 'Buchung im Hauptbuch bearbeiten',
   'Edit Group'                  => 'Warengruppe editieren',
   'Edit Language'               => 'Sprache bearbeiten',
@@ -1085,6 +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' => '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',
@@ -1129,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',
@@ -1281,7 +1323,9 @@ $self->{texts} = {
   'I'                           => 'I',
   'IBAN'                        => 'IBAN',
   'ID'                          => 'Buchungsnummer',
+  'ID of own bank account'      => 'Datenbank-ID des Bankkontos',
   'ID-Nummer'                   => 'ID-Nummer (intern)',
+  'ID/Acc_ID'                   => 'ID/Acc_ID',
   'II'                          => 'II',
   'III'                         => 'III',
   'IV'                          => 'IV',
@@ -1311,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',
@@ -1349,6 +1395,7 @@ $self->{texts} = {
   'Initial version.'            => 'Initiale Version.',
   'Insert'                      => 'Einfügen',
   'Insert Date'                 => 'Erfassungsdatum',
+  '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',
@@ -1378,13 +1425,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',
@@ -1481,6 +1533,7 @@ $self->{texts} = {
   'Link to the following project:' => 'Mit dem folgenden Projekt verknüpfen:',
   'Linked Records'              => 'Verknüpfte Belege',
   'Liquidity projection'        => 'Liquiditätsübersicht',
+  'Linked invoices'             => 'Verknüpfte Rechnungen',
   'List Accounts'               => 'Konten anzeigen',
   'List Languages'              => 'Sprachen anzeigen',
   'List Price'                  => 'Listenpreis',
@@ -1489,7 +1542,6 @@ $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 transfers'      => 'Überweisungsliste',
   'List of custom variables'    => 'Liste der benutzerdefinierten Variablen',
@@ -1500,7 +1552,11 @@ $self->{texts} = {
   'Load letter draft'           => 'Briefentwurf laden',
   'Load profile'                => 'Profil laden',
   'Loading...'                  => 'Wird geladen...',
+  'Local Bank Code'             => 'Lokale Bankleitzahl',
   'Local Tax Office Preferences' => 'Angaben zum Finanzamt',
+  'Local account number'        => 'Lokale Kontonummer',
+  'Local bank account'          => 'Lokales Bankkonto',
+  'Local bank code'             => 'Lokale Bankleitzahl',
   'Lock System'                 => 'System sperren',
   'Lock and unlock installation' => 'Installation sperren/entsperren',
   'Lock file handling failed. Please verify that the directory "#1" is writeable by the webserver.' => 'Die Lockdateibehandlung schlug fehl. Bitte stellen Sie sicher, dass der Webserver das Verzeichnis "#1" beschreiben darf.',
@@ -1516,6 +1572,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',
@@ -1590,6 +1648,7 @@ $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.',
   'Net Income Statement'        => 'Einnahmenüberschußrechnung',
@@ -1600,7 +1659,6 @@ $self->{texts} = {
   'New Purchase Price Rule'     => 'Neue Einkaufspreisregel',
   'New Sales Price Rule'        => 'Neue Verkaufspreisregel',
   'New assembly'                => 'Neues Erzeugnis',
-  'New bank account'            => 'Neues Bankkonto',
   'New client #1: The database configuration fields "host", "port", "name" and "user" must not be empty.' => 'Neuer Mandant #1: Die Datenbankkonfigurationsfelder "Host", "Port" und "Name" dürfen nicht leer sein.',
   'New client #1: The name must be unique and not empty.' => 'Neuer Mandant #1: Der Name darf nicht leer und muss eindeutig sein.',
   'New contact'                 => 'Neue Ansprechperson',
@@ -1620,6 +1678,7 @@ $self->{texts} = {
   'Next run at'                 => 'Nächste Ausführung um',
   'No'                          => 'Nein',
   'No %s was found matching the search parameters.' => 'Es wurde kein %s gefunden, auf den die Suchparameter zutreffen.',
+  'No 1:n or n:1 relation'      => 'Keine 1:n oder n:1 Beziehung',
   'No Company Address given'    => 'Keine Firmenadresse hinterlegt!',
   'No Company Name given'       => 'Kein Firmenname hinterlegt!',
   'No Customer was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Endkunde gefunden',
@@ -1628,6 +1687,7 @@ $self->{texts} = {
   'No action defined.'          => 'Keine Aktion definiert.',
   'No articles have been added yet.' => 'Es wurden noch keine Artikel hinzugefügt.',
   'No background job has been created yet.' => 'Es wurden noch keine Hintergrund-Jobs angelegt.',
+  'No bank account chosen!'     => 'Kein Bankkonto ausgewählt!',
   'No bank information has been entered in this customer\'s master data entry. You cannot create bank collections unless you enter bank information.' => 'Für diesen Kunden wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.',
   'No bank information has been entered in this vendor\'s master data entry. You cannot create bank transfers unless you enter bank information.' => 'Für diesen Lieferanten wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.',
   'No bins have been added to this warehouse yet.' => 'Es wurden zu diesem Lager noch keine Lagerpl&auml;tze angelegt.',
@@ -1641,6 +1701,7 @@ $self->{texts} = {
   'No default currency'         => 'Keine Standardwährung',
   'No delivery term has been created yet.' => 'Es wurden noch keine Lieferbedingungen angelegt',
   'No department has been created yet.' => 'Es wurde noch keine Abteilung erfasst.',
+  'No draft was found.'         => 'Kein Entwurf gefunden.',
   'No dunnings have been selected for printing.' => 'Es wurden keine Mahnungen zum Drucken ausgew&auml;hlt.',
   'No file has been uploaded yet.' => 'Es wurde noch keine Datei hochgeladen.',
   'No function blocks have been created yet.' => 'Es wurden noch keine Funktionsblöcke angelegt.',
@@ -1672,6 +1733,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!' => '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.',
@@ -1778,14 +1840,17 @@ $self->{texts} = {
   'Otherwise the variable is only available for printing.' => 'Andernfalls steht die Variable nur beim Ausdruck zur Verfügung.',
   'Otherwise you can simply check create warehouse and bins and define a name for the warehouse (Bins will be created automatically) and then continue' => 'Andernfalls einfach <b>"Automatisches Zuweisen der Lagerplätze"</b>  anhaken und einen Namen für das Lager vergeben, bzw. per Auswahl auswählen (Lagerplätze werden dann automatisch hinzugefügt) danach auf weiter',
   'Out of balance transaction!' => 'Buchung ist nicht ausgeglichen!',
-  'Out of balance!'             => 'Summen stimmen nicht berein!',
+  'Out of balance!'             => 'Summen stimmen nicht überein!',
   'Output Number Format'        => 'Zahlenformat (Ausgabe)',
   'Outputformat'                => 'Ausgabeformat',
   'Overdue sales quotations and requests for quotations' => 'Überfällige Angebote und Preisanfragen',
   'Override'                    => 'Override',
   'Override invoice language'   => 'Diese Sprache verwenden',
+  'Overview'                    => 'Übersicht',
+  'Own bank account number or IBAN' => 'Eigene Kontonummer oder IBAN',
+  'Own bank code'               => 'Eigene Bankleitzahl',
   'Owner of account'            => 'Kontoinhaber',
-  'PAYMENT POSTED'              => 'Rechnung gebucht',
+  'PAYMENT POSTED'              => 'Rechung gebucht',
   'PDF'                         => 'PDF',
   'PDF (OpenDocument/OASIS)'    => 'PDF (OpenDocument/OASIS)',
   'PDF export -- options'       => 'PDF-Export -- Optionen',
@@ -1839,6 +1904,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.',
@@ -1993,6 +2059,8 @@ $self->{texts} = {
   'Project type'                => 'Projekttyp',
   'Projects'                    => 'Projekte',
   'Projecttransactions'         => 'Projektbuchungen',
+  'Proposal'                    => 'Vorschlag',
+  'Proposals'                   => 'Vorschläge',
   'Prozentual/Absolut'          => 'Prozentual/Absolut',
   'Purchase Delivery Orders'    => 'Einkaufslieferscheine',
   'Purchase Delivery Orders deleteable' => 'Einkaufslieferscheine löschbar',
@@ -2013,6 +2081,8 @@ $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',
   'Qty equal or less than #1'   => 'Menge gleich oder kleiner als #1',
@@ -2050,13 +2120,16 @@ $self->{texts} = {
   'Ranges of numbers'           => 'Nummernkreise',
   'Re-numbering all sections and function blocks in the order they are currently shown cannot be undone.' => 'Das Neu-Nummerieren aller Abschnitte und Funktionsblöcke kann nicht rückgängig gemacht werden.',
   'Re-run analysis'             => 'Analyse wiederholen',
+  'Really cancel link?'         => 'Verknüpfung wirklich aufheben?',
   'Receipt'                     => 'Zahlungseingang',
   'Receipt posted!'             => 'Beleg gebucht!',
   'Receipt, payment, reconciliation' => 'Zahlungseingang, Zahlungsausgang, Kontenabgleich',
   'Receipts'                    => 'Zahlungseingänge',
   'Receivables'                 => 'Forderungen',
   'Rechnungsnummer'             => 'Rechnungsnummer',
+  'Reconcile'                   => 'Abgleichen',
   'Reconciliation'              => 'Kontenabgleich',
+  'Reconciliation with bank'    => 'Kontenabgleich mit Bank',
   'Record Vendor Invoice'       => 'Einkaufsrechnung erfassen',
   'Record in'                   => 'Buchen auf',
   'Record number'               => 'Belegnummer',
@@ -2073,6 +2146,11 @@ $self->{texts} = {
   'Remaining Amount'            => 'abzurechnender Betrag',
   'Remaining Net Amount'        => 'abzurechnender Nettobetrag',
   'Remittance information prefix' => 'Verwendungszweckvorbelegung (Präfix)',
+  'Remote Bank Code'            => 'Fremde Bankleitzahl',
+  'Remote Name/Customer/Description' => 'Kunden/Lieferantenname und Beschreibung',
+  'Remote account number'       => 'Fremde Kontonummer',
+  'Remote bank code'            => 'Fremde Bankleitzahl',
+  'Remote name'                 => 'Fremder Kontoinhaber',
   'Removal'                     => 'Entnahme',
   'Removal from Warehouse'      => 'Lagerentnahme',
   'Removal from warehouse'      => 'Entnahme aus Lager',
@@ -2218,14 +2296,18 @@ $self->{texts} = {
   'Save as new'                 => 'als neu speichern',
   'Save document in WebDAV repository' => 'Dokument in WebDAV-Ablage speichern',
   'Save draft'                  => 'Entwurf speichern',
+  'Save invoices'               => 'Rechnungen speichern',
   'Save profile'                => 'Profil speichern',
+  'Save proposals'              => 'Vorschläge speichern',
   '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',
   'Search AR Aging'             => 'Offene Forderungen',
+  'Search bank transactions'    => 'Filter für Bankbuchungen',
   'Search contacts'             => 'Ansprechpersonensuche',
   'Search projects'             => 'Projektsuche',
   'Search term'                 => 'Suchbegriff',
@@ -2310,6 +2392,7 @@ $self->{texts} = {
   'Show Bestbefore'             => 'Mindesthaltbarkeit anzeigen',
   'Show Filter'                 => 'Filter zeigen',
   'Show Salesman'               => 'Verkäufer anzeigen',
+  'Show Stornos'                => 'Stornos anzeigen',
   'Show TODO list'              => 'Aufgabenliste anzeigen',
   'Show Transfer via default'   => 'Ein- / Auslagern über Standardlagerplatz anzeigen (zusätzlicher Knopf in Beleg Lieferschein)',
   'Show Value of Goods for Delivery Plan' => 'Warenverkaufswert im Lieferplan anzeigen.',
@@ -2350,11 +2433,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.',
@@ -2376,6 +2463,8 @@ $self->{texts} = {
   'Start the correction assistant' => 'Korrekturassistenten starten',
   'Startdate_coa'               => 'Gültig ab',
   'Starting Balance'            => 'Eröffnungsbilanzwerte',
+  'Starting balance'            => 'Anfangssaldo',
+  'Starting date'               => 'Anfangsdatum',
   'Starting the task server failed.' => 'Das Starten des Task-Servers schlug fehl.',
   'Starting with version 2.6.3 the configuration files in "config" have been consolidated.' => 'Ab Version 2.6.3 wurden die Konfiguration vereinfacht und es gibt nur noch eine Konfigurationsdatei im Verzeichnis config',
   'Statement'                   => 'Sammelrechnung',
@@ -2405,6 +2494,7 @@ $self->{texts} = {
   'Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1' => 'Zwischensummen können nicht zwischen den einzelnen Belegen unterscheiden, es wird nur "#1" angezeigt',
   'Subtotals per quarter'       => 'Zwischensummen pro Quartal',
   'Such entries cannot be exported into the DATEV format and have to be fixed as well.' => 'Solche Einträge sind aber nicht DATEV-exportiertbar und müssen ebenfalls korrigiert werden.',
+  'Suggested invoice'           => 'Rechnungsvorschlag',
   'Sum Credit'                  => 'Summe Haben',
   'Sum Debit'                   => 'Summe Soll',
   'Sum for'                     => 'Summe für',
@@ -2495,6 +2585,7 @@ $self->{texts} = {
   'The Buchungsgruppe has been created.' => 'Die Buchungsgruppe wurde erstellt.',
   'The Buchungsgruppe has been saved.' => 'Die Buchungsgruppe wurde gespeichert.',
   'The GL transaction #1 has been deleted.' => 'Die Dialogbuchung #1 wurde gelöscht.',
+  'The IBAN is missing.'        => 'Die IBAN fehlt.',
   'The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.' => 'Der LDAP-Server "#1:#2" ist nicht erreichbar. Bitte &uuml;berpr&uuml;fen Sie die Angaben in config/kivitendo.conf.',
   'The SEPA export has been created.' => 'Der SEPA-Export wurde erstellt',
   'The SEPA strings have been saved.' => 'Die bei SEPA-Überweisungen verwendeten Begriffe wurden gespeichert.',
@@ -2505,6 +2596,7 @@ $self->{texts} = {
   'The acceptance status is in use and cannot be deleted.' => 'Der Abnahmestatus wird verwendet und kann nicht gelöscht werden.',
   'The access rights a user has within a client instance is still governed by his group membership.' => 'Welche Zugriffsrechte ein Benutzer innerhalb eines Mandanten hat, wird weiterhin über Gruppenmitgliedschaften geregelt.',
   'The access rights have been saved.' => 'Die Zugriffsrechte wurden gespeichert.',
+  'The account #1 is already being used by bank account #2.' => 'Das Konto #1 wird schon von Bankkonto #2 benutzt.',
   'The account 3804 already exists, the update will be skipped.' => 'Das Konto 3804 existiert schon, das Update wird übersprungen.',
   'The account 3804 will not be added automatically.' => 'Das Konto 3804 wird nicht automatisch hinzugefügt.',
   'The action is missing or invalid.' => 'Die action fehlt, oder sie ist ungültig.',
@@ -2520,6 +2612,10 @@ $self->{texts} = {
   'The background job has been deleted.' => 'Der Hintergrund-Job wurde gelöscht.',
   'The background job has been saved.' => 'Der Hintergrund-Job wurde gespeichert.',
   'The background job was executed successfully.' => 'Der Hintergrund-Job wurde erfolgreich ausgeführt.',
+  'The bank account has been created.' => 'Das Bankkonto wurde erstellt.',
+  'The bank account has been deleted.' => 'Das Bankkonto wurde gelöscht.',
+  'The bank account has been saved.' => 'Das Bankkonto wurde gespeichert',
+  'The bank account has been used and cannot be deleted.' => 'Das Bankkonto wurde benutzt und kann nicht gelöscht werden.',
   'The bank information must not be empty.' => 'Die Bankinformationen müssen vollständig ausgefüllt werden.',
   'The base file name without a path or an extension to be used for printing for this type of requirement spec.' => 'Der Basisdateiname ohne Pfadanteil oder Erweiterung, der bei Drucken dieses Pflichtenhefttyps verwendet wird.',
   'The base unit does not exist or it is about to be deleted in row %d.' => 'Die Basiseinheit in Zeile %d existiert nicht oder soll gel&ouml;scht werden.',
@@ -2534,6 +2630,7 @@ $self->{texts} = {
   'The business has been saved.' => 'Der Kunden-/Lieferantentyp wurde gespeichert.',
   'The business is in use and cannot be deleted.' => 'Der Kunden-/Lieferantentyp wird benutzt und kann nicht gelöscht werden.',
   'The changing of tax-o-matic account is NOT recommended, but if you do so please also (re)configure buchungsgruppen and reconfigure ALL charts which point to this tax-o-matic account. ' => 'Es wird nicht empfohlen Steuerkonten (Umsatzsteuer oder Vorsteuer) "umzuhängen", aber falls es gemacht wird, bitte auch entsprechend konsequent die Buchungsgruppen und die Konten die mit dieser Steuer verknüpft sind umkonfigurieren.',
+  'The chart is not valid.'     => 'Das Konto ist nicht gültig.',
   'The client could not be deleted.' => 'Der Mandant konnte nicht gelöscht werden.',
   'The client has been created.' => 'Der Mandant wurde angelegt.',
   'The client has been deleted.' => 'Der Mandant wurde gelöscht.',
@@ -2763,6 +2860,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 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.',
@@ -2776,6 +2874,7 @@ $self->{texts} = {
   'There is an inconsistancy in your database.' => 'In Ihrer Datenbank sind Unstimmigkeiten vorhanden.',
   'There is at least one sales or purchase invoice for which kivitendo recorded an inventory transaction with taxkeys even though no tax was recorded.' => 'Es gibt mindestens eine Verkaufs- oder Einkaufsrechnung, für die kivitendo eine Warenbestandsbuchung ohne dazugehörige Steuerbuchung durchgeführt hat.',
   'There is at least one transaction for which the user has chosen a logically wrong taxkey.' => 'Es gibt mindestens eine Buchung, bei der ein logisch nicht passender Steuerschlüssel ausgewählt wurde.',
+  'There is no connected chart.' => 'Es fehlt ein verknüpftes Buchungskonto.',
   'There is not enough available of \'#1\' at warehouse \'#2\', bin \'#3\', #4, #5, for the transfer of #6.' => 'Von \'#1\' ist in Lager \'#2\', Lagerplatz \'#3\', #4, #5, nicht gen&uuml;gend eingelagert, um insgesamt #6 auszulagern.',
   'There is not enough available of \'#1\' at warehouse \'#2\', bin \'#3\', #4, for the transfer of #5.' => 'Von \'#1\' ist in Lager \'#2\', Lagerplatz \'#3\', #4 nicht gen&uuml;gend eingelagert, um insgesamt #5 auszulagern.',
   'There is not enough left of \'#1\' in bin \'#2\' for the removal of #3.' => 'In Lagerplatz \'#2\' ist nicht genug von \'#1\' vorhanden, um #3 zu entnehmen.',
@@ -2868,6 +2967,7 @@ $self->{texts} = {
   'Transaction'                 => 'Buchung',
   'Transaction %d cancelled.'   => 'Buchung %d erfolgreich storniert.',
   'Transaction Date missing!'   => 'Buchungsdatum fehlt!',
+  'Transaction ID'              => 'ID der Bankbewegung',
   'Transaction ID missing.'     => 'Die Buchungs-ID fehlt.',
   'Transaction deleted!'        => 'Buchung gelöscht!',
   'Transaction description'     => 'Vorgangsbezeichnung',
@@ -2879,9 +2979,11 @@ $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 to'                => 'Buchungsdatum bis',
   'Transfer'                    => 'Umlagern',
   'Transfer Quantity'           => 'Umlagermenge',
   'Transfer To Stock'           => 'Lagereingang',
@@ -2983,6 +3085,10 @@ $self->{texts} = {
   'Valid/Obsolete'              => 'Gültig/ungültig',
   'Value'                       => 'Wert',
   'Value of transferred goods'  => 'Verkaufswert der ausgelagerten Waren',
+  'Valuta date'                 => 'Valutadatum',
+  'Valutadate'                  => 'Valutadatum',
+  'Valutadate from'             => 'Valutadatum von',
+  'Valutadate to'               => 'Valutadatum bis',
   'Variable'                    => 'Variable',
   'Variable Description'        => 'Datenfeldbezeichnung',
   'Variable Name'               => 'Datenfeldname (intern)',
@@ -2997,6 +3103,7 @@ $self->{texts} = {
   'Vendor Order Number'         => 'Bestellnummer beim Lieferanten',
   'Vendor deleted!'             => 'Lieferant gelöscht!',
   'Vendor details'              => 'Lieferantendetails',
+  'Vendor filter for AP transaction drafts' => 'Filter für Entwürfe',
   'Vendor missing!'             => 'Lieferant fehlt!',
   'Vendor not on file or locked!' => 'Dieser Lieferant existiert nicht oder ist gesperrt.',
   'Vendor not on file!'         => 'Lieferant ist nicht in der Datenbank!',
@@ -3090,7 +3197,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.',
@@ -3122,6 +3228,7 @@ $self->{texts} = {
   'accrual'                     => 'Soll-Versteuerung',
   'action= not defined!'        => 'action= nicht definiert!',
   'active'                      => 'aktiv',
+  'all'                         => 'Alle',
   'all entries'                 => 'alle Einträge',
   'and'                         => 'und',
   'ap_aging_list'               => 'liste_offene_verbindlichkeiten',
@@ -3134,7 +3241,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',
@@ -3146,6 +3252,7 @@ $self->{texts} = {
   'chart_of_accounts'           => 'kontenuebersicht',
   'choice'                      => 'auswählen',
   'choice part'                 => 'Artikel auswählen',
+  'cleared'                     => 'Abgeglichen',
   'click here to edit cvars'    => 'Klicken Sie hier, um nach benutzerdefinierten Variablen zu suchen',
   'close'                       => 'schließen',
   'closed'                      => 'geschlossen',
@@ -3165,6 +3272,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',
@@ -3176,11 +3285,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',
@@ -3233,6 +3344,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',
@@ -3323,8 +3435,10 @@ $self->{texts} = {
   'transferred in / out'        => 'ein- / ausgelagert',
   'transferred out'             => 'ausgelagert',
   'trial_balance'               => 'susa',
+  'uncleared'                   => 'Nicht abgeglichen',
   'unconfigured'                => 'unkonfiguriert',
   'uncorrect partnumber '       => 'Unbekannte Teilenummer ',
+  'until'                       => 'bis',
   'use program settings'        => 'benutze Programmeinstellungen',
   'use user config'             => 'Verwende Benutzereinstellung',
   'used'                        => 'Verbraucht',
@@ -3334,6 +3448,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 ede0e2f..2bdde42 100644 (file)
@@ -2622,6 +2622,8 @@ $self->{texts} = {
   'delete'                      => '',
   'delivered'                   => '',
   'deliverydate'                => '',
+  'difference as skonto'        => '',
+  'difference_as_skonto'        => 'remainder as skonto',
   'direct debit'                => '',
   'disposed'                    => '',
   'do not include'              => '',
@@ -2780,6 +2782,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 c968475..05f2a29 100644 (file)
@@ -386,60 +386,100 @@ action=search
 
 
 [Cash]
-ACCESS=cash
 
 [Cash--Receipt]
+ACCESS=cash
 module=cp.pl
 action=payment
 type=receipt
 vc=customer
 
 [Cash--Payment]
+ACCESS=cash
 module=cp.pl
 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
 action=bank_transfer_add
 vc=customer
 
 [Cash--Bank transfer via SEPA]
+ACCESS=cash
 module=sepa.pl
 action=bank_transfer_add
 vc=vendor
 
+[Cash--Bank Import]
+module=menu.pl
+action=acc_menu
+submenu=1
+
+[Cash--Bank Import--CSV]
+ACCESS=bank_transaction
+module=controller.pl
+action=CsvImport/new
+profile.type=bank_transactions
+
+[Cash--Bank Import--MT940]
+ACCESS=bank_transaction
+module=controller.pl
+action=CsvImport/new
+profile.type=mt940
+
+[Cash--Bank transactions MT940]
+ACCESS=bank_transaction
+module=controller.pl
+action=BankTransaction/search
+
+[Cash--Reconciliation with bank]
+ACCESS=bank_transaction
+module=controller.pl
+action=Reconciliation/search
+next_sub=Reconciliation/reconciliation
+
+[Cash--Reconciliation]
+ACCESS=cash
+module=rc.pl
+action=reconciliation
+
 [Cash--Reports]
 module=menu.pl
 action=acc_menu
 submenu=1
 
 [Cash--Reports--Receipts]
+ACCESS=cash
 module=rp.pl
 action=report
 report=receipts
 
 [Cash--Reports--Payments]
+ACCESS=cash
 module=rp.pl
 action=report
 report=payments
 
 [Cash--Reports--Bank collections via SEPA]
+ACCESS=cash
 module=sepa.pl
 action=bank_transfer_search
 vc=customer
 
 [Cash--Reports--Bank transfers via SEPA]
+ACCESS=cash
 module=sepa.pl
 action=bank_transfer_search
 vc=vendor
 
+[Cash--Reports--Bank transactions]
+ACCESS=bank_transaction
+module=controller.pl
+action=BankTransaction/list_all
+
 [Reports]
 
 [Reports--Chart of Accounts]
@@ -619,8 +659,8 @@ module=am.pl
 action=list_tax
 
 [System--Bank accounts]
-module=bankaccounts.pl
-action=bank_account_list
+module=controller.pl
+action=BankAccount/list
 
 [System--Groups]
 module=pe.pl
diff --git a/sql/Pg-upgrade2/automatic_reconciliation.sql b/sql/Pg-upgrade2/automatic_reconciliation.sql
new file mode 100644 (file)
index 0000000..924fceb
--- /dev/null
@@ -0,0 +1,14 @@
+-- @tag: automatic_reconciliation
+-- @description: Erstellt Tabelle reconiliation_links für den automatischen Kontenabgleich.
+-- @depends: release_3_2_0 bank_transactions
+
+CREATE TABLE reconciliation_links (
+  id                      integer NOT NULL DEFAULT nextval('id'),
+  bank_transaction_id     integer NOT NULL,
+  acc_trans_id            bigint  NOT NULL,
+  rec_group               integer NOT NULL,
+
+  PRIMARY KEY (id),
+  FOREIGN KEY (bank_transaction_id)      REFERENCES bank_transactions (id),
+  FOREIGN KEY (acc_trans_id)             REFERENCES acc_trans (acc_trans_id)
+);
diff --git a/sql/Pg-upgrade2/bank_accounts_unique_chart_constraint.sql b/sql/Pg-upgrade2/bank_accounts_unique_chart_constraint.sql
new file mode 100644 (file)
index 0000000..c5eb53f
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: bank_accounts_unique_chart_constraint
+-- @description: Bankkonto - Constraint für eindeutiges Konto
+-- @depends: release_3_2_0 bank_accounts
+-- @encoding: utf-8
+
+ALTER TABLE bank_accounts ADD CONSTRAINT chart_id_unique UNIQUE (chart_id);
diff --git a/sql/Pg-upgrade2/bank_transactions.sql b/sql/Pg-upgrade2/bank_transactions.sql
new file mode 100644 (file)
index 0000000..6e9f917
--- /dev/null
@@ -0,0 +1,22 @@
+-- @tag: bank_transactions
+-- @description: Erstellen der Tabelle bank_transactions.
+-- @depends: release_3_2_0 currencies
+
+CREATE TABLE bank_transactions (
+  id SERIAL PRIMARY KEY,
+  transaction_id INTEGER,
+  remote_bank_code TEXT,
+  remote_account_number TEXT,
+  transdate DATE NOT NULL,
+  valutadate DATE NOT NULL,
+  amount numeric(15,5) NOT NULL,
+  remote_name 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)
+);
diff --git a/sql/Pg-upgrade2/bankaccounts_reconciliation.sql b/sql/Pg-upgrade2/bankaccounts_reconciliation.sql
new file mode 100644 (file)
index 0000000..cb9e53b
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: bankaccounts_reconciliation
+-- @description: Kontenabgleichsststartdatum und -saldo
+-- @depends: release_3_2_0
+-- @encoding: utf-8
+
+ALTER TABLE bank_accounts ADD COLUMN reconciliation_starting_date DATE;
+ALTER TABLE bank_accounts ADD COLUMN reconciliation_starting_balance numeric(15,5);
diff --git a/sql/Pg-upgrade2/bankaccounts_sortkey_and_obsolete.sql b/sql/Pg-upgrade2/bankaccounts_sortkey_and_obsolete.sql
new file mode 100644 (file)
index 0000000..cdda464
--- /dev/null
@@ -0,0 +1,13 @@
+-- @tag: bankaccounts_sortkey_and_obsolete
+-- @description: Bankkonto - Sortierreihenfolge und Ungültig
+-- @depends: release_3_2_0
+-- @encoding: utf-8
+
+-- 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;
+UPDATE bank_accounts SET sortkey = nextval('tmp_counter');
+DROP SEQUENCE tmp_counter;
+ALTER TABLE bank_accounts ALTER COLUMN sortkey SET NOT NULL;
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 );
index 8d1c170..9f55a0c 100644 (file)
@@ -45,13 +45,17 @@ sub init_common_state {
   $unit           = SL::DB::Manager::Unit->find_by(name => 'psch')                          || croak "No unit";
 }
 
+sub clear_up {
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(InvoiceItem Invoice OrderItem Order Customer Part);
+};
+
 sub create_invoices {
   my %params = @_;
 
   $params{$_} ||= {} for qw(customer part tax order orderitem periodic_invoices_config);
 
   # Clean up: remove invoices, orders, parts and customers
-  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(InvoiceItem Invoice OrderItem Order Customer Part);
+  clear_up();
 
   $customer     = SL::DB::Customer->new(
     name        => 'Test Customer',
@@ -237,4 +241,6 @@ are_invoices 'p=b ovp=5',[ '01.01.2009', 33.33 ], [ '01.07.2009', 33.33 ],
 create_invoices(periodic_invoices_config => { periodicity => 'y', order_value_periodicity => '5', start_date => DateTime->from_kivitendo('01.01.2009') });
 are_invoices 'p=y ovp=5',[ '01.01.2009', 66.67 ], [ '01.01.2010', 66.67 ], [ '01.01.2011', 66.67 ], [ '01.01.2012', 66.67 ], [ '01.01.2013', 66.65 ], [ '01.01.2014', 66.67 ];
 
+clear_up();
+
 done_testing();
index 9b278be..8994fad 100644 (file)
@@ -37,6 +37,10 @@ Support::TestSetup::login();
 
 our ($ar_chart, $buchungsgruppe, $ctrl, $currency_id, $customer, $employee, $order, $part, $tax_zone, $unit, @invoices);
 
+sub clear_up {
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(InvoiceItem Invoice OrderItem Order Customer Part);
+};
+
 sub init_common_state {
   $ar_chart       = SL::DB::Manager::Chart->find_by(accno => '1400')                        || croak "No AR chart";
   $buchungsgruppe = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%') || croak "No accounting group";
@@ -52,7 +56,7 @@ sub create_sales_order {
   $params{$_} ||= {} for qw(customer part tax order orderitem);
 
   # Clean up: remove invoices, orders, parts and customers
-  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(InvoiceItem Invoice OrderItem Order Customer Part);
+  clear_up();
 
   $customer     = SL::DB::Customer->new(
     name        => 'Test Customer',
@@ -203,4 +207,6 @@ create_sales_order(
 is_deeply($ctrl->data->{$_}, { months => [ (0) x 12 ], quarters => [ 0, 0, 0, 0 ], year => 0 }, "periodic conf p=q ovp=y, no invoices, starting and ending before current year, data for $_")
   for qw(purchase_invoices purchase_orders requests_for_quotation sales_invoices sales_orders sales_orders_per_inv sales_quotations);
 
+clear_up();
+
 done_testing();
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 220cd42..9846264 100644 (file)
@@ -24,16 +24,20 @@ use SL::DB::TaxZone;
 
 my ($customer, $currency_id, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $tax, $tax7, $taxzone);
 
-sub reset_state {
-  my %params = @_;
-
-  $params{$_} ||= {} for qw(buchungsgruppe unit customer part tax);
-
+sub clear_up {
   SL::DB::Manager::Order->delete_all(all => 1);
   SL::DB::Manager::DeliveryOrder->delete_all(all => 1);
   SL::DB::Manager::Invoice->delete_all(all => 1);
   SL::DB::Manager::Part->delete_all(all => 1);
   SL::DB::Manager::Customer->delete_all(all => 1);
+};
+
+sub reset_state {
+  my %params = @_;
+
+  $params{$_} ||= {} for qw(buchungsgruppe unit customer part tax);
+
+  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\%";
@@ -376,4 +380,5 @@ test_default_invoice_one_item_19_tax_not_included();
 test_default_invoice_two_items_19_7_tax_not_included();
 test_default_invoice_three_items_sellprice_rounding_discount();
 
+clear_up();
 done_testing();
index edfcf72..5df89a4 100644 (file)
@@ -25,16 +25,21 @@ use SL::DB::TaxZone;
 my ($customer, $currency_id, $buchungsgruppe, $employee, $vendor, $taxzone);
 my ($link, $links, $o1, $o2, $d, $i);
 
-sub reset_state {
-  my %params = @_;
-
-  $params{$_} ||= {} for qw(buchungsgruppe unit customer part tax);
-
+sub clear_up {
   SL::DB::Manager::DeliveryOrder->delete_all(all => 1);
   SL::DB::Manager::Order->delete_all(all => 1);
   SL::DB::Manager::Invoice->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);
+};
+
+sub reset_state {
+  my %params = @_;
+
+  $params{$_} ||= {} for qw(buchungsgruppe unit customer part tax);
+
+  clear_up();
 
   $buchungsgruppe  = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%', %{ $params{buchungsgruppe} }) || croak "No accounting group";
   $employee        = SL::DB::Manager::Employee->current                                                                    || croak "No employee";
@@ -104,7 +109,6 @@ Support::TestSetup::login();
 
 reset_state();
 
-
 $o1 = new_order();
 $i  = new_invoice();
 
@@ -313,4 +317,7 @@ is @$links, 3, 'recursive from i finds 3 (not i)';
 
 $links = $o1->linked_records(direction => 'both', recursive => 1, save_path => 1);
 is @$links, 4, 'recursive dir=both does not give duplicates';
+
+clear_up();
+
 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>
diff --git a/templates/webpages/bank_transactions/_filter.html b/templates/webpages/bank_transactions/_filter.html
new file mode 100644 (file)
index 0000000..0aaab14
--- /dev/null
@@ -0,0 +1,75 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE HTML %]
+<form action='controller.pl' method='post'>
+<div class='filter_toggle'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
+  [% SELF.filter_summary | html %]
+</div>
+<div class='filter_toggle' style='display:none'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Hide Filter' | $T8 %]</a>
+ <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_key='displayable_name', with_empty=1, style='width:500px') %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'Transdate from' | $T8 %]</th>
+     <td>[% L.date_tag('filter.transdate:date::ge', filter.transdate_date__ge) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'Transdate to' | $T8 %]</th>
+     <td>[% L.date_tag('filter.transdate:date::le', filter.transdate_date__le) %]</td>
+    </tr>
+
+    <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>
+ </table>
+
+[% L.hidden_tag('action', 'BankTransaction/dispatch') %]
+[% L.hidden_tag('sort_by', FORM.sort_by) %]
+[% L.hidden_tag('sort_dir', FORM.sort_dir) %]
+[% L.hidden_tag('page', FORM.page) %]
+[% L.input_tag('action_list_all', LxERP.t8('Continue'), type = 'submit', class='submit')%]
+
+
+<a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);'>[% 'Reset' | $T8 %]</a>
+
+</div>
+
+</form>
diff --git a/templates/webpages/bank_transactions/add_list.html b/templates/webpages/bank_transactions/add_list.html
new file mode 100644 (file)
index 0000000..ad48bfa
--- /dev/null
@@ -0,0 +1,39 @@
+[%- USE T8 -%][%- USE HTML -%][%- USE LxERP -%][%- USE P -%][%- USE L -%]
+[%- IF !INVOICES.size %]
+  <p class="message_hint">[% 'No data was found.' | $T8 %]</p>
+[%- ELSE %]
+  <table width="100%">
+   <tr class="listheading">
+    <th>[% L.checkbox_tag('invoices_check_all') %]</th>
+    <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>
+   </tr>
+
+  [%- FOREACH invoice = INVOICES %]
+   <tr class="listrow[% loop.count % 2 %]">
+    <td>[% L.checkbox_tag('invoice_id[]', value=invoice.id) %]</td>
+    <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>
+   </tr>
+  [%- END %]
+  </table>
+
+<script type="text/javascript">
+<!--
+$(function() {
+    $('#invoices_check_all').checkall('INPUT[name="invoice_id[]"]');
+    });
+-->
+</script>
+[%- END %]
+
diff --git a/templates/webpages/bank_transactions/assign_invoice.html b/templates/webpages/bank_transactions/assign_invoice.html
new file mode 100644 (file)
index 0000000..26229b8
--- /dev/null
@@ -0,0 +1,104 @@
+[%- USE HTML %][%- USE L %][%- USE LxERP %][%- USE T8 %]
+
+[% SET debug = 0 %]
+
+<form method="post" action="javascript:filter_invoices();">
+  <b>[%- LxERP.t8("Bank transaction") %]:</b>
+  <table>
+   <tr class="listheading">
+    [% IF debug %]<td>[%- LxERP.t8("ID") %]:</td>[% END %]
+    <td>[%- LxERP.t8("Transdate") %]:</td>
+    <td>[%- LxERP.t8("Amount") %]:</td>
+    <td>[%- LxERP.t8("Remote name") %]:</td>
+    <td>[%- LxERP.t8("Purpose") %]:</td>
+    <td>[%- LxERP.t8("Remote account number") %]:</td>
+    <td>[%- LxERP.t8("Remote bank code") %]:</td>
+   </tr>
+
+   <tr class="listrow">
+    [% 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_name %]</td>
+    <td>[% SELF.transaction.purpose %]</td>
+    <td>[% SELF.transaction.remote_account_number %]</td>
+    <td>[% SELF.transaction.remote_bank_code %]</td>
+   </tr>
+  </table>
+
+  <b>[%- LxERP.t8("Invoice filter") %]:</b>
+  <table>
+   <tr>
+    <th align="right">[%- LxERP.t8("Invoice number") %]</th>
+    <td>[% L.input_tag('invnumber', '', style=style) %]</td>
+
+    <th align="right">[%- LxERP.t8("Customer/Vendor name") %]</th>
+    <td>[% L.input_tag('vcname', '', style=style) %]</td>
+   </tr>
+
+   <tr>
+    <th align="right">[%- LxERP.t8("Amount") %]</th>
+    <td>[% L.input_tag('amount', '', style=style) %]</td>
+
+    <th align="right">[%- LxERP.t8("Customer/Vendor number") %]</th>
+    <td>[% L.input_tag('vcnumber', '', style=style) %]</td>
+   </tr>
+
+   <tr>
+    <th align="right">[%- LxERP.t8("Invdate from") %]</th>
+    <td>[% L.date_tag('transdatefrom') %]</td>
+
+    <th align="right">[%- LxERP.t8("to (date)") %]</th>
+    <td>[% L.date_tag('transdateto') %]</td>
+   </tr>
+  </table>
+
+  <p>
+   [% L.submit_tag('', LxERP.t8("Search")) %]
+   [% L.button_tag('add_selected_invoices()', LxERP.t8("Add invoices"), id='add_selected_record_links_button') %]
+   <a href="#" onclick="assign_invoice_reset_form();">[%- LxERP.t8("Reset") %]</a>
+   <a href="#" onclick="$('#assign_invoice_window').dialog('close');">[% LxERP.t8("Cancel") %]</a>
+  </p>
+
+  <hr>
+
+  <div id="record_list_filtered_list"></div>
+
+</form>
+
+<script type="text/javascript">
+<!--
+
+function filter_invoices() {
+  var url="controller.pl?action=BankTransaction/ajax_add_list&" + $("#assign_invoice_window form").serialize();
+  $.ajax({
+    url: url,
+    success: function(new_data) {
+      $("#record_list_filtered_list").html(new_data['html']);
+    }
+  });
+}
+
+function add_selected_invoices() {
+  var url="controller.pl?action=BankTransaction/ajax_accept_invoices&" + 'bt_id=[% SELF.transaction.id %]&' + $("#assign_invoice_window form").serialize();
+  $.ajax({
+    url: url,
+    success: function(new_html) {
+      var invoices = document.getElementById('assigned_invoices_[% SELF.transaction.id %]');
+      if (invoices.innerHTML == '') {
+        invoices.innerHTML = new_html;
+      } else {
+        invoices.innerHTML += '<br />' + new_html;
+      }
+      $('#assign_invoice_window').dialog('close');
+    }
+  });
+}
+
+function assign_invoice_reset_form() {
+  $('#assign_invoice_window form input[type=text]').val('');
+}
+
+//-->
+</script>
+
diff --git a/templates/webpages/bank_transactions/create_invoice.html b/templates/webpages/bank_transactions/create_invoice.html
new file mode 100644 (file)
index 0000000..cfc9ce8
--- /dev/null
@@ -0,0 +1,101 @@
+[%- USE HTML %][%- USE L %][%- USE LxERP %][%- USE T8 %]
+
+  <b>Transaction</b>
+  <table>
+   <tr class="listheading">
+    <td>[%- LxERP.t8("ID") %]:</td>
+    <td>[%- LxERP.t8("Amount") %]:</td>
+    <td>[%- LxERP.t8("Date") %]:</td>
+    <td>[%- LxERP.t8("Remote name") %]:</td>
+    <td>[%- LxERP.t8("Purpose") %]:</td>
+    <td>[%- LxERP.t8("Remote bank code") %]:</td>
+    <td>[%- LxERP.t8("Remote account number") %]:</td>
+   </tr>
+
+   <tr class="listrow">
+    <td>[% SELF.transaction.id %]</td>
+    <td>[% LxERP.format_amount(SELF.transaction.amount, 2) %]</td>
+    <td>[% SELF.transaction.valutadate_as_date %]</td>
+    <td>[% SELF.transaction.remote_name %]</td>
+    <td>[% SELF.transaction.purpose %]</td>
+    <td>[% SELF.transaction.remote_bank_code %]</td>
+    <td>[% SELF.transaction.remote_account_number %]</td>
+   </tr>
+  </table>
+
+
+<br>
+[% 'Vendor filter for AP transaction drafts' | $T8 %]:
+
+<form method="post" action="javascript:filter_drafts();">
+[% L.hidden_tag('bt_id', SELF.transaction.id) %]
+  <table>
+   <tr>
+    <th align="right">[%- LxERP.t8("Vendor") %]</th>
+    <td>
+            [%- INCLUDE 'generic/multibox.html'
+                 name          = 'vendor',
+                 select_name   = 'vendor_id',
+                 default       = ALL_VENDORS.size < limit ? vendor_id : vendor_name,
+                 style         = 'width: 250px',
+                 DATA          = ALL_VENDORS,
+                 id_key        = 'id',
+                 label_key     = 'name',
+                 limit         = limit,
+                 show_empty    = 1,
+                 allow_textbox = 1,
+                 class         = 'initial_focus',
+                 onChange      = 'filter_drafts();',
+                 -%]
+    </td>
+   </tr>
+  </table>
+</form>
+
+  <p>
+   <a href="#" onclick="$('#create_invoice_window').dialog('close');">[% LxERP.t8("Cancel") %]</a>
+  </p>
+
+  <hr>
+<div id="drafts">
+[% IF DRAFTS.size %]
+[% 'Draft suggestions' | $T8 %]:
+
+
+  <table>
+   <tr>
+    <th class="listheading">[% 'Description' | $T8 %]</th>
+    <th class="listheading">[% 'Vendor' | $T8 %]</th>
+    <th class="listheading">[% 'Employee' | $T8 %]</th>
+    <th class="listheading">[% 'Draft from:' | $T8 %]</th>
+   </tr>
+
+   [% 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=[% 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>
+    </tr>
+   [% END %]
+  </table>
+[% ELSE %]
+  <p class="message_hint">[% 'No draft was found.' | $T8 %]</p>
+[% END %]
+</div>
+
+<script type="text/javascript">
+<!--
+
+function filter_drafts() {
+  var url="controller.pl?action=BankTransaction/filter_drafts&" + $("#create_invoice_window form").serialize();
+  $.ajax({
+    url: url,
+    success: function(new_data) {
+      $("#drafts").html(new_data['html']);
+    }
+  });
+}
+//-->
+</script>
+
diff --git a/templates/webpages/bank_transactions/filter_drafts.html b/templates/webpages/bank_transactions/filter_drafts.html
new file mode 100644 (file)
index 0000000..7b2c8de
--- /dev/null
@@ -0,0 +1,23 @@
+[%- USE T8 -%][%- USE HTML -%][%- USE LxERP -%][%- USE P -%][%- USE L -%]
+[%- IF !DRAFTS.size %]
+  <p class="message_hint">[% 'No draft was found.' | $T8 %]</p>
+[%- ELSE %]
+  <table>
+   <tr>
+    <th class="listheading">[% 'Date' | $T8 %]</th>
+    <th class="listheading">[% 'Description' | $T8 %]</th>
+    <th class="listheading">[% 'Employee' | $T8 %]</th>
+    <th class="listheading">[% 'Vendor' | $T8 %]</th>
+   </tr>
+
+   [% FOREACH draft = DRAFTS %]
+    <tr class="listrow[% loop.count % 2 %]">
+     <td>[% HTML.escape(draft.itime_as_date) %]</td>
+     <td><a href="[% draft.module %].pl?action=load_draft&id=[% HTML.url(draft.id) %]&amount_1=[% SELF.transaction.amount_as_number %]&datepaid_1=[% HTML.url(SELF.transaction.transdate_as_date) %]&paid_1=[% SELF.transaction.amount_as_number %]&remove_draft=0&callback=[% HTML.url(callback) %]">[% HTML.escape(draft.description) %]</a></td>
+     <td>[% HTML.escape(draft.employee.name) %]</td>
+     <td>[% HTML.escape(draft.vendor) %]</td>
+    </tr>
+   [% END %]
+  </table>
+[%- END %]
+
diff --git a/templates/webpages/bank_transactions/invoices.html b/templates/webpages/bank_transactions/invoices.html
new file mode 100644 (file)
index 0000000..6ba716c
--- /dev/null
@@ -0,0 +1,8 @@
+[% USE L %]
+[% FOREACH invoice = INVOICES %]
+  <div id="[% bt_id %].[% invoice.id %]">
+    [% L.hidden_tag('invoice_ids.' _ bt_id _'[]', invoice.id) %]
+    [% invoice.invnumber %]
+    <a href=# onclick="delete_invoice([% bt_id %], [% invoice.id %])">x</a>
+  </div>
+[% END %]
diff --git a/templates/webpages/bank_transactions/list.html b/templates/webpages/bank_transactions/list.html
new file mode 100644 (file)
index 0000000..0a38b2b
--- /dev/null
@@ -0,0 +1,107 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+
+<script type="text/javascript" src="js/wz_tooltip.js"></script>
+
+<h1>[% title %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<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 %]
+</p>
+
+<form id="list_form">
+[% L.hidden_tag('filter.bank_account', FORM.filter.bank_account) %]
+[% L.hidden_tag('filter.fromdate', FORM.filter.fromdate) %]
+[% L.hidden_tag('filter.todate',   FORM.filter.todate) %]
+
+<div class="tabwidget">
+  <ul>
+    <li><a href="#all" onclick="show_invoice_button();">[% 'All transactions' | $T8 %]</a></li>
+    <li><a href="#automatic" onclick="show_proposal_button();">[% 'Proposals' | $T8 %]</a></li>
+  </ul>
+
+  <div id="all">[% PROCESS "bank_transactions/tabs/all.html" %]</div>
+  <div id="automatic">[% PROCESS "bank_transactions/tabs/automatic.html" %]</div>
+</div>
+
+[% L.hidden_tag('action', 'BankTransaction/dispatch') %]
+[% L.submit_tag('action_save_invoices', LxERP.t8('Save invoices')) %]
+[% L.submit_tag('action_save_proposals', LxERP.t8('Save proposals'), style='display: none') %]
+
+</form>
+
+<script type="text/javascript">
+<!--
+
+$(function() {
+  $('#check_all').checkall('INPUT[name^="proposal_ids"]');
+});
+
+$(function() {
+  $('.sort_link').each(function() {
+    var _href = $(this).attr("href");
+    $(this).attr("href", _href + "&filter.fromdate=" + "[% FORM.filter.fromdate %]" + "&filter.todate=" + "[% FORM.filter.todate %]");
+  });
+});
+
+function show_invoice_button () {
+  $("#action_save_proposals").hide();
+  $("#action_save_invoices").show();
+}
+
+function show_proposal_button () {
+  $("#action_save_invoices").hide();
+  $("#action_save_proposals").show();
+}
+
+function assign_invoice(bt_id) {
+  kivi.popup_dialog({
+    url:    'controller.pl?action=BankTransaction/assign_invoice',
+    data:   '&bt_id=' + bt_id,
+    type:   'POST',
+    id:     'assign_invoice_window',
+    dialog: { title: kivi.t8('Assign invoice') }
+  });
+  return true;
+}
+
+function add_invoices(bt_id, prop_id, prop_invnumber) {
+  // 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];
+    node.parentNode.removeChild(node);
+  }
+  UnTip();
+  var invoices = document.getElementById('assigned_invoices_' + bt_id);
+
+  $.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) {
+  $( "#" + bt_id + "\\." + prop_id ).remove();
+}
+
+function create_invoice(bt_id) {
+  kivi.popup_dialog({
+    url:    'controller.pl?action=BankTransaction/create_invoice',
+    data:   '&bt_id=' + bt_id + "&filter.bank_account=[% FORM.filter.bank_account %]&filter.todate=[% FORM.filter.todate %]&filter.fromdate=[% FORM.filter.fromdate %]",
+    type:   'POST',
+    id:     'create_invoice_window',
+    dialog: { title: kivi.t8('Create invoice') }
+  });
+  return true;
+}
+
+//-->
+</script>
+
diff --git a/templates/webpages/bank_transactions/report_bottom.html b/templates/webpages/bank_transactions/report_bottom.html
new file mode 100644 (file)
index 0000000..8868ff1
--- /dev/null
@@ -0,0 +1,2 @@
+[% USE L %]
+[%- L.paginate_controls(models=SELF.models) %]
diff --git a/templates/webpages/bank_transactions/report_top.html b/templates/webpages/bank_transactions/report_top.html
new file mode 100644 (file)
index 0000000..f6fbce1
--- /dev/null
@@ -0,0 +1,3 @@
+[%- USE L %]
+[%- PROCESS 'bank_transactions/_filter.html' filter=SELF.filter %]
+ <hr>
diff --git a/templates/webpages/bank_transactions/search.html b/templates/webpages/bank_transactions/search.html
new file mode 100644 (file)
index 0000000..3e234cd
--- /dev/null
@@ -0,0 +1,37 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+
+[%- INCLUDE 'common/flash.html' %]
+
+ <form method="post" action="controller.pl">
+
+  <div class="listtop">[% 'Search bank transactions' | $T8 %]</div>
+
+  <p>
+   <table>
+
+    <tr>
+     <th align="right">[% 'Bank account' | $T8 %]</th>
+     <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>
+     <th align="right">[% 'Transdate from' | $T8 %]</th>
+     <td>[% L.date_tag('filter.fromdate', filter.fromdate) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'to (date)' | $T8 %]</th>
+     <td>[% L.date_tag('filter.todate', filter.todate) %]</td>
+    </tr>
+   </table>
+  </p>
+
+  <hr size="3" noshade>
+
+  [% L.hidden_tag('action', 'BankTransaction/list') %]
+
+  <p>[% L.submit_tag('dummy', LxERP.t8('Continue')) %]</p>
+ </form>
diff --git a/templates/webpages/bank_transactions/tabs/all.html b/templates/webpages/bank_transactions/tabs/all.html
new file mode 100644 (file)
index 0000000..359ca5f
--- /dev/null
@@ -0,0 +1,104 @@
+[%- 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>
+        [% ELSE %]
+          <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=proposal&sort_dir=0" class="sort_link">
+            [% 'Proposal' | $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>
+        [% ELSE %]
+          <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=transdate&sort_dir=0" class="sort_link">
+            [% 'Transdate' | $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>
+        [% ELSE %]
+          <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=amount&sort_dir=0" class="sort_link">
+            [% 'Amount' | $T8 %]</a>
+        [% END %]
+    </th>
+    <th>[% 'Assigned' | $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>
+        [% ELSE %]
+          <a href="controller.pl?action=BankTransaction/list&filter.bank_account=[% bank_account.id %]&sort_by=remote_name&sort_dir=0" class="sort_link">
+            [% 'Remote name' | $T8 %]</a>
+        [% END %]
+    </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>
+
+  <tbody>
+   [%- FOREACH bt = BANK_TRANSACTIONS %]
+    <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 %]" 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><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 align=right>[% bt.transdate_as_date %]</td>
+     <td align=right>[% bt.amount_as_number %]</td>
+     <td align=right>[% bt.invoice_amount_as_number %]</td>
+     <td>[% HTML.escape(bt.remote_name) %]</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>
+ </table>
diff --git a/templates/webpages/bank_transactions/tabs/automatic.html b/templates/webpages/bank_transactions/tabs/automatic.html
new file mode 100644 (file)
index 0000000..42f387f
--- /dev/null
@@ -0,0 +1,51 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+
+<table>
+  <thead>
+    <tr class="listheading">
+      <th>[% L.checkbox_tag('check_all') %]</th>
+
+      <th>[% 'Type' | $T8 %]</th>
+      <th>[% 'ID' | $T8 %]</th>
+      <th>[% 'Transdate' | $T8 %]</th>
+      <th>[% 'Amount' | $T8 %]</th>
+      <th>[% 'Purpose/Reference' | $T8 %]</th>
+      <th>[% 'Customer/Vendor/Remote name' | $T8 %]</th>
+    </tr>
+  </thead>
+  [% IF !PROPOSALS.size %]
+    <tbody class="listrow">
+      <td colspan="7"><p class="message_hint">[% 'No data was found.' | $T8 %]</p></td>
+    </tbody>
+  [% ELSE %]
+    [% FOREACH proposal = PROPOSALS %]
+      <tbody class="listrow">
+        <tr>
+          <td rowspan=2 style="valign:center;">
+            [% L.checkbox_tag('proposal_ids[]', checked=0, value=proposal.id) %]
+          </td>
+
+          <td>[% 'Bank transaction' | $T8 %]</td>
+          <td>[% proposal.id %]</td>
+          <td>[% proposal.transdate_as_date %]</td>
+          <td>[% proposal.amount_as_number %]</td>
+          <td>[% HTML.escape(proposal.purpose) %]</td>
+          <td>[% HTML.escape(proposal.remote_name) %]</td>
+        </tr>
+
+      [% FOREACH proposed_invoice = proposal.proposals %]
+        <tr>
+
+          <td>[% 'Invoice' | $T8 %]</td>
+          <td>[% proposed_invoice.id %]</td>
+          <td>[% proposed_invoice.transdate_as_date %]</td>
+          <td>[% proposed_invoice.amount_as_number %]</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) %]
+      [% END %]
+      </tbody>
+    [% END %]
+  [% END %]
+</table>
diff --git a/templates/webpages/bankaccounts/bank_account_display_form.html b/templates/webpages/bankaccounts/bank_account_display_form.html
deleted file mode 100644 (file)
index dbc4596..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-[%- USE T8 %]
-[% USE HTML %]
-<h1>[% title %]</h1>
-
-[%- IF params.error %]
- <p><div class="message_error">[% params.error %]</div></p>
-[%- END %]
-
- <form method="post" action="bankaccounts.pl">
-
-  <p>
-   <table>
-    <tr>
-     <td align="right">[% 'Name' | $T8 %]</td>
-     <td><input name="account.name" size="20" maxlength="100" value="[% HTML.escape(account.name) %]"></td>
-    </tr>
-
-    <tr>
-     <td align="right">[% 'Account number' | $T8 %]</td>
-     <td><input name="account.account_number" size="20" maxlength="100" value="[% HTML.escape(account.account_number) %]"></td>
-    </tr>
-
-    <tr>
-     <td align="right">[% 'Bank code' | $T8 %]</td>
-     <td><input name="account.bank_code" size="20" maxlength="100" value="[% HTML.escape(account.bank_code) %]"></td>
-    </tr>
-
-    <tr>
-     <td align="right">[% 'Bank' | $T8 %]</td>
-     <td><input name="account.bank" size="30" value="[% HTML.escape(account.bank) %]"></td>
-    </tr>
-
-    <tr>
-     <td align="right">[% 'IBAN' | $T8 %]</td>
-     <td><input name="account.iban" size="30" maxlength="100" value="[% HTML.escape(account.iban) %]"></td>
-    </tr>
-
-    <tr>
-     <td align="right">[% 'BIC' | $T8 %]</td>
-     <td><input name="account.bic" size="30" maxlength="100" value="[% HTML.escape(account.bic) %]"></td>
-    </tr>
-
-    <tr>
-     <td align="right">[% 'Chart' | $T8 %]</td>
-     <td>
-      [%- INCLUDE generic/multibox.html
-            name      = 'account.chart_id',
-            DATA      = CHARTS,
-            id_key    = 'id',
-            label_sub = 'chart_label',
-            style     = 'width: 300px',
-      -%]
-     </td>
-    </tr>
-
-   </table>
-  </p>
-
-  <p>
-   <input type="hidden" name="action" value="dispatcher">
-   <input type="hidden" name="account.id" value="[% HTML.escape(account.id) %]">
-   <input type="hidden" name="callback" value="[% HTML.escape(callback) %]">
-
-[%- IF account.id %]
-   <input type="submit" name="action_bank_account_save" value="[% 'Save' | $T8 %]">
-   <input type="submit" name="action_bank_account_delete" value="[% 'Delete' | $T8 %]">
-[%- ELSE %]
-   <input type="submit" name="action_bank_account_save" value="[% 'Add' | $T8 %]">
-[%- END %]
-  </p>
- </form>
-
diff --git a/templates/webpages/bankaccounts/bank_account_list_bottom.html b/templates/webpages/bankaccounts/bank_account_list_bottom.html
deleted file mode 100644 (file)
index 202dd2d..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-[% USE T8 %][% USE HTML %]
- <p>
-  <a href="dispatcher.pl?M=bankaccounts&A=bank_account_add&callback=[% HTML.url(callback) %]">[%- 'New bank account' | $T8 %]</a>
- </p>
diff --git a/templates/webpages/bankaccounts/form.html b/templates/webpages/bankaccounts/form.html
new file mode 100644 (file)
index 0000000..e1c323c
--- /dev/null
@@ -0,0 +1,92 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+
+[% SET style="width: 400px" %]
+[% SET size=34 %]
+
+<h1>[% HTML.escape(title) %]</h1>
+
+<form action="controller.pl" method="post">
+
+[%- INCLUDE 'common/flash.html' %]
+
+[%- L.hidden_tag("id", SELF.bank_account.id) %]
+
+<table>
+  <tr>
+    <th align="right">[% 'Description' | $T8 %]</th>
+    <td>[%- L.input_tag("bank_account.name", SELF.bank_account.name, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'IBAN' | $T8 %]</th>
+    <td>[%- L.input_tag("bank_account.iban", SELF.bank_account.iban, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Bank' | $T8 %]</th>
+    <td>[%- L.input_tag("bank_account.bank", SELF.bank_account.bank, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Account number' | $T8 %]</th>
+    <td>[%- L.input_tag("bank_account.account_number", SELF.bank_account.account_number, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'BIC' | $T8 %]</th>
+    <td>[%- L.input_tag("bank_account.bic", SELF.bank_account.bic, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Bank code' | $T8 %]</th>
+    <td>[%- L.input_tag("bank_account.bank_code", SELF.bank_account.bank_code, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Chart' | $T8 %]</th>
+    <td>[% L.chart_picker('bank_account.chart_id', SELF.bank_account.chart_id, type='AR_paid,AP_paid', category='A,L,Q', choose=1, style=style) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Obsolete' | $T8 %]</th>
+    <td>[% L.checkbox_tag('bank_account.obsolete', checked = SELF.bank_account.obsolete, for_submit=1) %]</td>
+  </tr>
+  <tr>
+    <td align="left">[% 'Reconciliation' | $T8 %]:</td>
+    <td></td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Starting date' | $T8 %]</th>
+    <td>[% L.date_tag('bank_account.reconciliation_starting_date', SELF.bank_account.reconciliation_starting_date) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Starting balance' | $T8 %]</th>
+    <td>[%- L.input_tag('bank_account.reconciliation_starting_balance_as_number', SELF.bank_account.reconciliation_starting_balance_as_number) %]</td>
+  </tr>
+</table>
+
+ <p>
+  [% L.hidden_tag("action", "BankAccount/dispatch") %]
+  [% L.submit_tag("action_" _  (SELF.bank_account.id ? "update" : "create"), LxERP.t8('Save'), onclick="return check_prerequisites();") %]
+  [%- IF SELF.bank_account.id AND SELF.bank_account.number_of_bank_transactions == 0 -%]
+    [% L.submit_tag("action_delete", LxERP.t8('Delete')) %]
+  [%- END %]
+  <a href="[% SELF.url_for(action='list') %]">[%- LxERP.t8("Cancel") %]</a>
+ </p>
+
+ <hr>
+
+<script type="text/javascript">
+<!--
+function check_prerequisites() {
+  if ($('#bank_account_name').val() === "") {
+    alert(kivi.t8('The name is missing.'));
+    return false;
+  }
+  if ($('#bank_account_iban').val() === "") {
+    alert(kivi.t8('The IBAN is missing.'));
+    return false;
+  }
+  if ($('#bank_account_chart_id').val() === "") {
+    alert(kivi.t8('There is no connected chart.'));
+    return false;
+  }
+
+  return true;
+}
+-->
+</script>
+</form>
diff --git a/templates/webpages/bankaccounts/list.html b/templates/webpages/bankaccounts/list.html
new file mode 100644 (file)
index 0000000..c1a9ff0
--- /dev/null
@@ -0,0 +1,43 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%][%- INCLUDE 'common/flash.html' %]
+
+<h1>[% title %]</h1>
+
+<p>
+ <table width="100%" id="bankaccount_list">
+  <thead>
+   <tr class="listheading">
+    <th align="center" width="1%"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
+    <th>[% 'Name' | $T8 %]</th>
+    <th>[% 'IBAN' | $T8 %]</th>
+    <th>[% 'Bank' | $T8 %]</th>
+    <th>[% 'Bank code' | $T8 %]</th>
+    <th>[% 'BIC' | $T8 %]</th>
+    <th>[% 'Date' | $T8 %]</th>
+    <th>[% 'Balance' | $T8 %]</th>
+   </tr>
+  </thead>
+
+  <tbody>
+   [%- FOREACH account = BANKACCOUNTS %]
+    <tr class="listrow" id="account_id_[% account.id %]">
+     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
+     <td><a href="[% SELF.url_for(action='edit', id=account.id) %]">[% HTML.escape(account.name) %]</a></td>
+     <td>[% HTML.escape(account.iban) %]</a></td>
+     <td>[% HTML.escape(account.bank) %]</a></td>
+     <td>[% HTML.escape(account.bank_code) %]</a></td>
+     <td>[% HTML.escape(account.bic) %]</a></td>
+     <td>[% HTML.escape(account.reconciliation_starting_date.to_kivitendo) %]</a></td>
+     <td align="right">[% HTML.escape(account.reconciliation_starting_balance_as_number) %]</a></td>
+    </tr>
+   [%- END %]
+  </tbody>
+ </table>
+</p>
+
+<hr height="3">
+
+[% L.sortable_element('#bankaccount_list tbody', url=SELF.url_for(action='reorder'), with='account_id') %]
+
+<p>
+ <a href="[% SELF.url_for(action='new') %]">[%- 'Add' | $T8 %]</a>
+</p>
index cb592ed..4b705cd 100644 (file)
@@ -14,3 +14,4 @@
 [%- PROCESS output title=LxERP.t8('Error')       type='error'   messages = FLASH.error %]
 [%- PROCESS output title=LxERP.t8('Warning')     type='warning' messages = FLASH.warning %]
 [%- PROCESS output title=LxERP.t8('Information') type='info'    messages = FLASH.info %]
+[%- PROCESS output title=LxERP.t8('Ok')          type='ok'      messages = FLASH.ok %]
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>
diff --git a/templates/webpages/csv_import/_form_mt940.html b/templates/webpages/csv_import/_form_mt940.html
new file mode 100644 (file)
index 0000000..cb63e7d
--- /dev/null
@@ -0,0 +1,12 @@
+[% 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 8535f1a..745ef16 100644 (file)
  [%- INCLUDE 'csv_import/_form_inventories.html' %]
 [%- ELSIF SELF.type == 'orders' %]
  [%- 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 6b69894..41d448c 100644 (file)
                [% L.textarea_tag("notes", P.restricted_html(notes), class="texteditor", style="width: 600px; height: 200px") %]
               </td>
               <td>
-               <ilayer>
-                <layer onmouseover="this.T_STICKY=true;this.T_STATIC=true;return escape('[% 'The formula needs the following syntax:<br>For regular article:<br>Variablename= Variable Unit;<br>Variablename2= Variable2 Unit2;<br>...<br>###<br>Variable + ( Variable2 / Variable )<br><b>Please be beware of the spaces in the formula</b><br>' | $T8 %]')">
-                 <textarea name="formel" rows="[% HTML.escape(notes_rows) %]" cols="30" wrap="soft">[% HTML.escape(formel) %]</textarea></layer></ilayer>
+                 <textarea name="formel" rows="[% HTML.escape(notes_rows) %]" cols="30" wrap="soft" onmouseover="Tip('[% 'The formula needs the following syntax:<br>For regular article:<br>Variablename= Variable Unit;<br>Variablename2= Variable2 Unit2;<br>...<br>###<br>Variable + ( Variable2 / Variable )<br><b>Please be beware of the spaces in the formula</b><br>' | $T8 %]', STICKY, true)" onmouseout="UnTip()">[% HTML.escape(formel) %]</textarea>
                </td>
              </tr>
             </table>
diff --git a/templates/webpages/reconciliation/_linked_transactions.html b/templates/webpages/reconciliation/_linked_transactions.html
new file mode 100644 (file)
index 0000000..c05be85
--- /dev/null
@@ -0,0 +1,96 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- 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>
+  </tbody>
+[% ELSE %]
+  [% FOREACH link = SELF.LINKED_TRANSACTIONS %]
+    [% IF link.type == 'Link' %]
+      <tbody class="listrow">
+        [% FOREACH bt = link.BT %]
+          <tr>
+            [% IF loop.count == 1 %]
+              <td rowspan=[% link.BT.size + link.BB.size %] style="valign:center;">
+                [% L.button_tag('delete_reconciliation(' _ link.rec_group  _ ')', LxERP.t8("X")) %]
+              </td>
+            [% END %]
+
+            <td><img width="16px" height="16px" src="image/bank-building.jpg"></td>
+            <td>[% 'Bank Transaction' | $T8 %]</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>
+            <td>[% HTML.escape(bt.remote_name) %]</td>
+            <td>[% HTML.escape(bt.purpose) %]</td>
+            <td>[% HTML.escape(bt.remote_account_number) %]</td>
+            <td>[% HTML.escape(bt.remote_bank_code) %]</td>
+            <td></td>
+          </tr>
+        [% END %]
+        [% FOREACH bb = link.BB %]
+          <tr>
+            <td><div class="icon16 general-ledger--reports--journal"></div></td>
+            <td>[% 'Acc Transaction' | $T8 %]</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.record.customer.name) %][% HTML.escape(bb.record.vendor.name) %][% HTML.escape(bb.record.description) %]</td>
+            <td>[% bb.record.link %] [% HTML.escape(bb.source) %] [% HTML.escape(bb.memo) %]</td>
+            <td></td>
+            <td></td>
+            <td>[% HTML.escape(bb.source) %]</td>
+          </tr>
+        [% END %]
+      </tbody>
+    [% ELSE %]
+      <tbody class="listrow_error">
+        [% FOREACH bt = link.BT %]
+          <tr>
+            <td>
+              [%- L.checkbox_tag('bt_ids[]', value=link.id, onchange='update_reconciliation_table();')     %]
+            </td>
+
+            <td><img width="16px" height="16px" src="image/bank-building.jpg"></td>
+            <td>[% 'Bank Transaction' | $T8 %]</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>[% HTML.escape(bt.remote_name) %]</td>
+            <td>[% HTML.escape(bt.purpose) %]</td>
+            <td>[% HTML.escape(bt.remote_account_number) %]</td>
+            <td>[% HTML.escape(bt.remote_bank_code) %]</td>
+            <td></td>
+          </tr>
+        [% END %]
+        [% FOREACH bb = link.BB %]
+          <tr>
+            <td>
+              [%- L.checkbox_tag('bb_ids[]', value=link.id, onchange='update_reconciliation_table();')     %]
+            </td>
+
+            <td><div class="icon16 general-ledger--reports--journal"></div></td>
+            <td>[% 'Acc Transaction' | $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.record.customer.name) %][% HTML.escape(bb.record.vendor.name) %][% HTML.escape(bb.record.description) %]</td>
+            <td>[% bb.record.link %] [% HTML.escape(bb.source) %] [% HTML.escape(bb.memo) %]</td>
+            <td></td>
+            <td></td>
+            <td>[% HTML.escape(bb.source) %]</td>
+          </tr>
+        [% END %]
+      </tbody>
+    [% END %]
+  [% END %]
+[% END %]
diff --git a/templates/webpages/reconciliation/assigning_table.html b/templates/webpages/reconciliation/assigning_table.html
new file mode 100644 (file)
index 0000000..0da95a2
--- /dev/null
@@ -0,0 +1,39 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+
+[% IF SELF.ELEMENTS.size %]
+<table>
+  <thead>
+    <tr class="listheading">
+      <th></th>
+      <th>[% 'ID' | $T8 %]</th>
+      <th>[% 'Type' | $T8 %]</th>
+      <th>[% 'Transdate' | $T8 %]</th>
+      <th>[% 'Amount BT' | $T8 %]</th>
+      <th>[% 'Amount BB' | $T8 %]</th>
+    </tr>
+  </thead>
+  [% FOREACH element = SELF.ELEMENTS %]
+    <tbody id="assigned_elements">
+      <tr class="listrow" id='element[% element.type %][% element.id %]'>
+       <td><a href=# onclick="delete_element('[% element.id %]', '[% element.type %]')">x</a></td>
+       <td>[% HTML.escape(element.id) %]</td>
+       <td>[% IF element.type == 'BT' %][% 'Bank transaction' | $T8 %][% ELSE %][% 'Acc transaction' | $T8 %][% END %]</td>
+       <td>[% HTML.escape(element.transdate_as_date) %]</td>
+       <td align="right">[% IF element.type == 'BT' %][% HTML.escape(element.amount_as_number) %][% END %]</td>
+       <td align="right">[% IF element.type == 'BB' %][% LxERP.format_amount(-1 * element.amount, 2) %][% END %]</td>
+      </tr>
+    </tbody>
+  [% END %]
+  <tbody>
+    <tr class="listrow">
+      <td class="listtotal top_border"></td>
+      <td class="listtotal top_border"></td>
+      <td class="listtotal top_border"></td>
+      <td class="listtotal top_border"></td>
+      <td class="listtotal top_border">[% bt_sum %]</td>
+      <td class="listtotal top_border">[% bb_sum %]</td>
+    </tr>
+  </tbody>
+</table>
+  [% IF show_button %][% L.button_tag("submit_with_action('reconcile')", LxERP.t8("Reconcile")) %][% END %]
+[% END %]
diff --git a/templates/webpages/reconciliation/form.html b/templates/webpages/reconciliation/form.html
new file mode 100644 (file)
index 0000000..39847ac
--- /dev/null
@@ -0,0 +1,128 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+
+<style type="text/css">
+<!--
+html, body {
+  height: 100%;
+}
+.top_border {
+  border-top: solid black;
+  border-width: 4px;
+}
+.bottom_border {
+  border-bottom: solid black;
+  border-width: 4px;
+}
+#content.html-menu { height: 100%; }
+.out_of_balance {
+  color: #888888;
+}
+-->
+</style>
+
+<div class="listtop">[% title %]</div>
+
+[%- INCLUDE 'common/flash.html' %]
+
+<form id="reconciliation_form" method="post" action="controller.pl" style="height:100%">
+  <table>
+    <tr>
+     <th align="right">[% 'Bank account' | $T8 %]</th>
+     <td>[% L.select_tag('filter.local_bank_account_id:number',
+                          SELF.BANK_ACCOUNTS,
+                          default=FORM.filter.local_bank_account_id_number,
+                          title_key='displayable_name',
+                          with_empty=0,
+                          style='width:450px',
+                          onchange='filter_table();') %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'From' | $T8 %]</th>
+     <td>[% L.date_tag('filter.fromdate:date::ge', FORM.filter.fromdate_date__ge, onchange='filter_table();') %]</td>
+     <th align="right">[% 'to (date)' | $T8 %]</th>
+     <td>[% L.date_tag('filter.todate:date::le', FORM.filter.todate_date__le, onchange='filter_table();') %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'Cleared/uncleared only' | $T8 %]</th>
+     <td>[% L.select_tag('filter.cleared:eq_ignore_empty', SELF.cleared, value_key = 'value', title_key = 'title', default=FORM.filter.cleared_eq_ignore_empty, onchange='filter_table();') %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'Show Stornos' | $T8 %]</th>
+     <td>[% L.checkbox_tag('filter.show_stornos', checked=FORM.filter.show_stornos, value='1', onchange='filter_table();') %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'Absolute BT Balance' | $T8 %]</th>
+     <td class='absolut_bt_balance'>[% LxERP.format_amount(SELF.absolut_bt_balance, 2) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'Absolute BB Balance' | $T8 %]</th>
+     <td class='absolut_bb_balance'>[% LxERP.format_amount(-1 * SELF.absolut_bb_balance, 2) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'BT Balance' | $T8 %]</th>
+     <td class='bt_balance'>[% LxERP.format_amount(SELF.bt_balance, 2) %]</td>
+    </tr>
+
+    <tr>
+     <th align="right">[% 'BB Balance' | $T8 %]</th>
+     <td class='bb_balance'>[% LxERP.format_amount(-1 * SELF.bb_balance, 2) %]</td>
+    </tr>
+  </table>
+
+  [% L.submit_tag('submit_filter', LxERP.t8("Filter"), onclick='filter_table();return false;', style='display: none') %]
+
+  <div class="tabwidget" style="height:100%">
+    <ul>
+      <li><a href="#overview" onclick="load_overview();">[% 'Overview' | $T8 %]</a></li>
+      <li><a href="#automatic" onclick="load_proposals();">[% 'Proposals' | $T8 %]</a></li>
+    </ul>
+
+    <div id="overview" style="height:calc(100% - 60px);overflow: auto;">[% PROCESS "reconciliation/tabs/overview.html" %]</div>
+    <div id="automatic" style="height:calc(100% - 60px);overflow: auto;"></div>
+  </div>
+
+</form>
+
+<script type="text/javascript">
+<!--
+
+function load_proposals () {
+  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']);
+      $('#set_cleared').html('');
+    }
+  });
+}
+
+function load_overview () {
+  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('');
+      $('#set_cleared').html('');
+    }
+  });
+}
+
+//-->
+</script>
+
diff --git a/templates/webpages/reconciliation/proposals.html b/templates/webpages/reconciliation/proposals.html
new file mode 100644 (file)
index 0000000..df7bde4
--- /dev/null
@@ -0,0 +1,53 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+
+[% SET debug = 0 %]
+
+[% IF !SELF.PROPOSALS.size %]
+  <tbody class="listrow">
+    <td colspan="11"><p class="message_hint">[% 'No data was found.' | $T8 %]</p></td>
+  </tbody>
+[% ELSE %]
+  [% FOREACH proposal = SELF.PROPOSALS %]
+      <tbody class="listrow">
+        <tr>
+          <td rowspan=[% proposal.BB.size + 1 %] style="valign:center;">
+            [% L.checkbox_tag('bt_ids[]', checked=0, value=proposal.BT.id) %]
+          </td>
+
+          <td><img width="16px" height="16px" src="image/bank-building.jpg"></td>
+          <td>[% 'Bank Transaction' | $T8 %]</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>
+          <td>[% HTML.escape(proposal.BT.remote_name) %]</td>
+          <td>[% HTML.escape(proposal.BT.purpose) %]</td>
+          <td>[% HTML.escape(proposal.BT.remote_account_number) %]</td>
+          <td>[% HTML.escape(proposal.BT.remote_bank_code) %]</td>
+          <td></td>
+          [% L.hidden_tag('proposal_list.' _ proposal.BT.id _ '.BT', proposal.BT.id) %]
+        </tr>
+
+        [% FOREACH bb = proposal.BB %]
+          <tr>
+            <td><div class="icon16 general-ledger--reports--journal"></div></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.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>
+            [% L.hidden_tag('proposal_list.' _ proposal.BT.id _ '.BB[]', bb.acc_trans_id) %]
+          </tr>
+        [% END %]
+      </tbody>
+  [% END %]
+[% END %]
+
diff --git a/templates/webpages/reconciliation/search.html b/templates/webpages/reconciliation/search.html
new file mode 100644 (file)
index 0000000..3474b28
--- /dev/null
@@ -0,0 +1,44 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+
+<form method="post" action="controller.pl">
+
+<div class="listtop">[% 'Choose bank account for reconciliation' | $T8 %]</div>
+
+<p>
+ <table>
+  <tr>
+   <th align="right">[% 'Bank account' | $T8 %]</th>
+   <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>
+   <th align="right">[% 'From' | $T8 %]</th>
+   <td>[% L.date_tag('filter.fromdate:date::ge') %]</td>
+  </tr>
+
+  <tr>
+   <th align="right">[% 'to (date)' | $T8 %]</th>
+   <td>[% L.date_tag('filter.todate:date::le') %]</td>
+  </tr>
+
+  <tr>
+   <th align="right">[% 'Cleared/uncleared only' | $T8 %]</th>
+   <td>[% L.select_tag('filter.cleared:eq_ignore_empty', SELF.cleared, value_key = 'value', title_key = 'title', default = 'FALSE' ) %]</td>
+  </tr>
+
+  <tr>
+   <th align="right">[% 'Show Stornos' | $T8 %]</th>
+   <td>[% L.checkbox_tag('filter.show_stornos', value='1') %]</td>
+  </tr>
+ </table>
+</p>
+
+<hr size="3" noshade>
+
+[% L.hidden_tag('action', FORM.next_sub) %]
+
+<p>[% L.submit_tag('dummy', LxERP.t8('Continue')) %]</p>
+</form>
diff --git a/templates/webpages/reconciliation/tabs/automatic.html b/templates/webpages/reconciliation/tabs/automatic.html
new file mode 100644 (file)
index 0000000..7c49346
--- /dev/null
@@ -0,0 +1,66 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+
+[% SET debug = 0 %]
+
+<table width=100% id="proposal_table">
+  <thead>
+    <tr class="listheading">
+      <th>[% L.checkbox_tag('proposal_check_all') %]</th>
+
+      <th></th>
+      <th>[% 'Type' | $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>
+      <th>[% 'Remote Name/Customer/Description' | $T8 %]</th>
+      <th>[% 'Purpose/Reference' | $T8 %]</th>
+      <th>[% 'Remote account number' | $T8 %]</th>
+      <th>[% 'Remote bank code' | $T8 %]</th>
+      <th>[% 'Source' | $T8 %]</th>
+    </tr>
+  </thead>
+
+  [% PROCESS "reconciliation/proposals.html" %]
+<table>
+
+[% L.button_tag("reconcile_proposals()", LxERP.t8("Reconcile")) %]
+
+<script type="text/javascript">
+<!--
+
+function filter_table () {
+  var url="controller.pl?action=Reconciliation/filter_proposals&" + $('#reconciliation_form') . serialize();
+  $.ajax({
+    url: url,
+    success: function(new_data) {
+      $("tbody[class^='listrow']").remove();
+      $("#proposal_table").append(new_data['html']);
+      $(".absolut_bt_balance").html(new_data['absolut_bt_balance']);
+      $(".absolut_bb_balance").html(new_data['absolut_bb_balance']);
+      $(".bt_balance").html(new_data['bt_balance']);
+      $(".bb_balance").html(new_data['bb_balance']);
+    }
+  });
+}
+
+function reconcile_proposals() {
+  $('<input>').attr({
+    id : "action",
+    name : "action",
+    type : "hidden",
+    value : "Reconciliation/reconcile_proposals"
+  }).appendTo('#reconciliation_form');
+  $("#reconciliation_form").submit();
+}
+
+$(function() {
+  $('#proposal_check_all').checkall('INPUT[name^="bt_ids"]');
+});
+
+//-->
+</script>
+
diff --git a/templates/webpages/reconciliation/tabs/overview.html b/templates/webpages/reconciliation/tabs/overview.html
new file mode 100644 (file)
index 0000000..013ba54
--- /dev/null
@@ -0,0 +1,127 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+
+[% 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>
+          [% IF debug %]<th>[% 'ID/Acc_ID' | $T8 %]</th>[% END %]
+          <th>[% 'Transdate' | $T8 %]</th>
+          <th>[% 'Amount BT' | $T8 %]</th>
+          <th>[% 'Amount BB' | $T8 %]</th>
+          <th>[% 'Remote Name/Customer/Description' | $T8 %]</th>
+          <th>[% 'Purpose/Reference' | $T8 %]</th>
+          <th>[% 'Remote account number' | $T8 %]</th>
+          <th>[% 'Remote bank code' | $T8 %]</th>
+          <th>[% 'Source' | $T8 %]</th>
+        </tr>
+      </thead>
+
+      [% PROCESS 'reconciliation/_linked_transactions.html' %]
+
+      <tfoot>
+        <tr class="listtotal">
+          <td class="top_border"></td>
+          <td class="top_border"></td>
+          <td class="top_border"></td>
+          <td class="top_border"></td>
+          <td class="top_border"></td>
+          <td class="bt_balance top_border" align="right">[% LxERP.format_amount(SELF.bt_balance, 2) %]</td>
+          <td class="bb_balance top_border" align="right">[% LxERP.format_amount(-1 * SELF.bb_balance, 2) %]</td>
+          <td class="top_border"></td>
+          <td class="top_border"></td>
+          <td class="top_border"></td>
+          <td class="top_border"></td>
+          <td class="top_border"></td>
+        </tr>
+      </tfoot>
+    </table>
+  </div>
+
+  <hr size="3" noshade>
+
+  <div id="assigned_elements"></div>
+
+<script type="text/javascript">
+<!--
+
+function filter_table () {
+  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('');
+      $("#link_table").append(new_data['html']);
+      $(".absolut_bt_balance").html(new_data['absolut_bt_balance']);
+      $(".absolut_bb_balance").html(new_data['absolut_bb_balance']);
+      $(".bt_balance").html(new_data['bt_balance']);
+      $(".bb_balance").html(new_data['bb_balance']);
+    }
+  });
+}
+
+function update_reconciliation_table () {
+  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']);
+    }
+  });
+}
+
+function delete_element (id, type) {
+  if (type == 'BT') {
+    $("input[name^='bt_ids'][value=" + id + "]").attr('checked', false);
+  }
+  if (type == 'BB') {
+    $("input[name^='bb_ids'][value=" + id + "]").attr('checked', false);
+  }
+  update_reconciliation_table();
+}
+
+function submit_with_action(action) {
+  $('<input>').attr({
+    id : "action",
+    name : "action",
+    type : "hidden",
+    value : "Reconciliation/reconcile"
+  }).appendTo('#reconciliation_form');
+  $("#reconciliation_form").submit();
+}
+
+function delete_reconciliation(rec_group) {
+  var check = confirm('[% 'Really cancel link?' | $T8 %]');
+  if (check == true) {
+    var url="controller.pl?action=Reconciliation/delete_reconciliation&rec_group=" + rec_group + "&" + $('#reconciliation_form') . serialize();
+    $.ajax({
+      url: url,
+      success: function(new_data) {
+        $("tbody[class^='listrow']").remove();
+        $("#assigned_elements").html('');
+        $("#link_table").append(new_data['html']);
+        $(".absolut_bt_balance").html(new_data['absolut_bt_balance']);
+        $(".absolut_bb_balance").html(new_data['absolut_bb_balance']);
+        $(".bt_balance").html(new_data['bt_balance']);
+        $(".bb_balance").html(new_data['bb_balance']);
+      }
+    });
+  }
+}
+
+//-->
+</script>
+
diff --git a/templates/webpages/reconciliation/tabs/set_cleared.html b/templates/webpages/reconciliation/tabs/set_cleared.html
new file mode 100644 (file)
index 0000000..5e5644d
--- /dev/null
@@ -0,0 +1,8 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+
+<div id="set_cleared">
+Set cleared
+</div>
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 %]