BankTransaction: Kreditorenvorlagen: Vorlage direkt laden, wenn genau 1 Treffer
[kivitendo-erp.git] / SL / Controller / BankTransaction.pm
index 591ee7c..e38f8a9 100644 (file)
@@ -17,15 +17,20 @@ use SL::SEPA;
 use SL::DB::Invoice;
 use SL::DB::PurchaseInvoice;
 use SL::DB::RecordLink;
 use SL::DB::Invoice;
 use SL::DB::PurchaseInvoice;
 use SL::DB::RecordLink;
+use SL::DB::ReconciliationLink;
 use SL::JSON;
 use SL::DB::Chart;
 use SL::DB::AccTransaction;
 use SL::JSON;
 use SL::DB::Chart;
 use SL::DB::AccTransaction;
+use SL::DB::BankTransactionAccTrans;
 use SL::DB::Tax;
 use SL::DB::BankAccount;
 use SL::DB::Tax;
 use SL::DB::BankAccount;
+use SL::DB::GLTransaction;
 use SL::DB::RecordTemplate;
 use SL::DB::SepaExportItem;
 use SL::DB::RecordTemplate;
 use SL::DB::SepaExportItem;
-use SL::DBUtils qw(like);
+use SL::DBUtils qw(like do_query);
 
 
+use SL::Presenter::Tag qw(checkbox_tag html_tag);
+use Carp;
 use List::UtilsBy qw(partition_by);
 use List::MoreUtils qw(any);
 use List::Util qw(max);
 use List::UtilsBy qw(partition_by);
 use List::MoreUtils qw(any);
 use List::Util qw(max);
@@ -63,32 +68,21 @@ sub action_list_all {
   $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
 }
 
   $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;
-  }
+sub gather_bank_transactions_and_proposals {
+  my ($self, %params) = @_;
 
 
-  my $sort_by = $::form->{sort_by} || 'transdate';
+  my $sort_by = $params{sort_by} || 'transdate';
   $sort_by = 'transdate' if $sort_by eq 'proposal';
   $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;
+  $sort_by .= $params{sort_dir} ? ' DESC' : ' ASC';
 
   my @where = ();
 
   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} );
+  push @where, (transdate => { ge => $params{fromdate} }) if $params{fromdate};
+  push @where, (transdate => { lt => $params{todate} })   if $params{todate};
   # bank_transactions no younger than starting date,
   # including starting date (same search behaviour as fromdate)
   # but OPEN invoices to be matched may be from before
   # bank_transactions no younger than starting date,
   # including starting date (same search behaviour as fromdate)
   # but OPEN invoices to be matched may be from before
-  if ( $bank_account->reconciliation_starting_date ) {
-    push @where, (transdate => { ge => $bank_account->reconciliation_starting_date });
+  if ( $params{bank_account}->reconciliation_starting_date ) {
+    push @where, (transdate => { ge => $params{bank_account}->reconciliation_starting_date });
   };
 
   my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
   };
 
   my $bank_transactions = SL::DB::Manager::BankTransaction->get_all(
@@ -96,23 +90,28 @@ sub action_list {
     sort_by      => $sort_by,
     limit        => 10000,
     where        => [
     sort_by      => $sort_by,
     limit        => 10000,
     where        => [
-      amount                => {ne => \'invoice_amount'},
-      local_bank_account_id => $::form->{filter}{bank_account},
+      amount                => {ne => \'invoice_amount'},      # '} make emacs happy
+      local_bank_account_id => $params{bank_account}->id,
+      cleared               => 0,
       @where
     ],
   );
   # credit notes have a negative amount, treat differently
       @where
     ],
   );
   # credit notes have a negative amount, treat differently
-  my $all_open_ar_invoices = SL::DB::Manager::Invoice        ->get_all(where => [ or => [ amount => { gt => \'paid' },
-                                                                                          and => [ type    => 'credit_note',
-                                                                                                   amount  => { lt => \'paid' }
-                                                                                                 ],
-                                                                                        ],
-                                                                                ],
-                                                                       with_objects => ['customer','payment_terms']);
-
-  my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { ne => \'paid' }], with_objects => ['vendor'  ,'payment_terms']);
-  my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $bank_account->chart_id ,
-                                                                             'sepa_export.executed' => 0, 'sepa_export.closed' => 0 ], with_objects => ['sepa_export']);
+  my $all_open_ar_invoices = SL::DB::Manager::Invoice->get_all(where        => [ or => [ amount => { gt => \'paid' },                 # '} make emacs happy
+                                                                                         and    => [ type    => 'credit_note',
+                                                                                                     amount  => { lt => \'paid' }     # '} make emacs happy
+                                                                                         ],
+                                                                                 ],
+                                                               ],
+                                                               with_objects => ['customer','payment_terms']);
+
+  my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where        => [amount => { ne => \'paid' }],                 #  '}] make emacs happy
+                                                                       with_objects => ['vendor'  ,'payment_terms']);
+  my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where        => [chart_id               => $params{bank_account}->chart_id ,
+                                                                                             'sepa_export.executed' => 0,
+                                                                                             'sepa_export.closed'   => 0
+                                                                            ],
+                                                                            with_objects => ['sepa_export']);
 
   my @all_open_invoices;
   # filter out invoices with less than 1 cent outstanding
 
   my @all_open_invoices;
   # filter out invoices with less than 1 cent outstanding
@@ -168,8 +167,6 @@ sub action_list {
       }
       next if $found;
       # batch transaction has no remotename !!
       }
       next if $found;
       # batch transaction has no remotename !!
-    } else {
-      next unless $bt->{remote_name};  # bank has no name, usually fees, use create invoice to assign
     }
 
     # try to match the current $bt to each of the open_invoices, saving the
     }
 
     # try to match the current $bt to each of the open_invoices, saving the
@@ -211,38 +208,52 @@ sub action_list {
   # to qualify as a proposal there has to be
   # * agreement >= 5  TODO: make threshold configurable in configuration
   # * there must be only one exact 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 @otherproposals = grep {
        ($_->{agreement} >= $proposal_threshold)
     && (1 == scalar @{ $_->{proposals} })
   my $proposal_threshold = 5;
   my @otherproposals = grep {
        ($_->{agreement} >= $proposal_threshold)
     && (1 == scalar @{ $_->{proposals} })
-    && (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01
-                                          : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01)
   } @{ $bank_transactions };
 
   push @proposals, @otherproposals;
 
   # sort bank transaction proposals by quality (score) of proposal
   } @{ $bank_transactions };
 
   push @proposals, @otherproposals;
 
   # sort bank transaction proposals by quality (score) of proposal
-  if ($::form->{sort_by} && $::form->{sort_by} eq 'proposal') {
-    if ($::form->{sort_dir}) {
-      $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ];
-    } else {
-      $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ];
-    }
+  if ($params{sort_by} && $params{sort_by} eq 'proposal') {
+    my $dir = $params{sort_dir} ? 1 : -1;
+    $bank_transactions = [ sort { ($a->{agreement} <=> $b->{agreement}) * $dir } @{ $bank_transactions } ];
   }
 
   }
 
-  # for testing with t/bank/banktransaction.t :
-  if ( $::form->{dont_render_for_test} ) {
-    return $bank_transactions;
+  return ( $bank_transactions , \@proposals );
+}
+
+sub action_list {
+  my ($self) = @_;
+
+  if (!$::form->{filter}{bank_account}) {
+    flash('error', t8('No bank account chosen!'));
+    $self->action_search;
+    return;
   }
 
   }
 
+  my $bank_account = SL::DB::BankAccount->load_cached($::form->{filter}->{bank_account});
+  my $fromdate     = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
+  my $todate       = $::locale->parse_date_to_object($::form->{filter}->{todate});
+  $todate->add( days => 1 ) if $todate;
+
+  my ($bank_transactions, $proposals) = $self->gather_bank_transactions_and_proposals(
+    bank_account => $bank_account,
+    fromdate     => $fromdate,
+    todate       => $todate,
+    sort_by      => $::form->{sort_by},
+    sort_dir     => $::form->{sort_dir},
+  );
+
   $::request->layout->add_javascripts("kivi.BankTransaction.js");
   $self->render('bank_transactions/list',
                 title             => t8('Bank transactions MT940'),
                 BANK_TRANSACTIONS => $bank_transactions,
   $::request->layout->add_javascripts("kivi.BankTransaction.js");
   $self->render('bank_transactions/list',
                 title             => t8('Bank transactions MT940'),
                 BANK_TRANSACTIONS => $bank_transactions,
-                PROPOSALS         => \@proposals,
+                PROPOSALS         => $proposals,
                 bank_account      => $bank_account,
                 bank_account      => $bank_account,
-                ui_tab            => scalar(@proposals) > 0?1:0,
+                ui_tab            => scalar(@{ $proposals }) > 0 ? 1 : 0,
               );
 }
 
               );
 }
 
@@ -267,12 +278,14 @@ sub action_create_invoice {
 
   my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
     where        => [ template_type => 'ap_transaction' ],
 
   my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
     where        => [ template_type => 'ap_transaction' ],
+    sort_by      => [ qw(template_name) ],
     with_objects => [ qw(employee vendor) ],
   );
   my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
     query        => [ template_type => 'gl_transaction',
                       chart_id      => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
                     ],
     with_objects => [ qw(employee vendor) ],
   );
   my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
     query        => [ template_type => 'gl_transaction',
                       chart_id      => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
                     ],
+    sort_by      => [ qw(template_name) ],
     with_objects => [ qw(employee record_template_items) ],
   );
 
     with_objects => [ qw(employee record_template_items) ],
   );
 
@@ -286,15 +299,21 @@ sub action_create_invoice {
     'filter.fromdate'     => $::form->{filter}->{fromdate},
   ));
 
     'filter.fromdate'     => $::form->{filter}->{fromdate},
   ));
 
-  $self->render(
-    'bank_transactions/create_invoice',
-    { layout => 0 },
-    title        => t8('Create invoice'),
-    TEMPLATES_GL => $use_vendor_filter && @{ $templates_ap } ? undef : $templates_gl,
-    TEMPLATES_AP => $templates_ap,
-    vendor_name  => $use_vendor_filter && @{ $templates_ap } ? $vendor_of_transaction->name : undef,
-    BT_ID        => $::form->{bt_id},
-  );
+  # if we have exactly one ap match, use this directly
+  if (1 == scalar @{ $templates_ap }) {
+    $self->redirect_to($self->load_ap_record_template_url($templates_ap->[0]));
+
+  } else {
+    my $dialog_html = $self->render(
+      'bank_transactions/create_invoice',
+      { layout => 0, output => 0 },
+      title        => t8('Create invoice'),
+      TEMPLATES_GL => $use_vendor_filter && @{ $templates_ap } ? undef : $templates_gl,
+      TEMPLATES_AP => $templates_ap,
+      vendor_name  => $use_vendor_filter && @{ $templates_ap } ? $vendor_of_transaction->name : undef,
+    );
+    $self->js->run('kivi.BankTransaction.show_create_invoice_dialog', $dialog_html)->render;
+  }
 }
 
 sub action_ajax_payment_suggestion {
 }
 
 sub action_ajax_payment_suggestion {
@@ -304,20 +323,16 @@ sub action_ajax_payment_suggestion {
   # 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
 
   # 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} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
+  croak("Need bt_id") unless $::form->{bt_id};
 
 
-  die unless $bt and $invoice;
+  my $invoice = SL::DB::Manager::Invoice->find_by( id => $::form->{prop_id} ) || SL::DB::Manager::PurchaseInvoice->find_by( id => $::form->{prop_id} );
 
 
-  my @select_options = $invoice->get_payment_select_options_for_bank_transaction($::form->{bt_id});
+  croak("No valid invoice found") unless $invoice;
 
 
-  my $html;
-  $html = $self->render(
+  my $html = $self->render(
     'bank_transactions/_payment_suggestion', { output => 0 },
     bt_id          => $::form->{bt_id},
     'bank_transactions/_payment_suggestion', { output => 0 },
     bt_id          => $::form->{bt_id},
-    prop_id        => $::form->{prop_id},
     invoice        => $invoice,
     invoice        => $invoice,
-    SELECT_OPTIONS => \@select_options,
   );
 
   $self->render(\ SL::JSON::to_json( { 'html' => "$html" } ), { layout => 0, type => 'json', process => 0 });
   );
 
   $self->render(\ SL::JSON::to_json( { 'html' => "$html" } ), { layout => 0, type => 'json', process => 0 });
@@ -555,13 +570,20 @@ sub save_single_bank_transaction {
     };
   }
 
     };
   }
 
+  my $bank_transaction = $data{bank_transaction};
+
+  if ($bank_transaction->closed_period) {
+    return {
+      %data,
+      result => 'error',
+      message => $::locale->text('Cannot post payment for a closed period!'),
+    };
+  }
   my (@warnings);
 
   my $worker = sub {
     my $bt_id                 = $data{bank_transaction_id};
   my (@warnings);
 
   my $worker = sub {
     my $bt_id                 = $data{bank_transaction_id};
-    my $bank_transaction      = $data{bank_transaction};
     my $sign                  = $bank_transaction->amount < 0 ? -1 : 1;
     my $sign                  = $bank_transaction->amount < 0 ? -1 : 1;
-    my $amount_of_transaction = $sign * $bank_transaction->amount;
     my $payment_received      = $bank_transaction->amount > 0;
     my $payment_sent          = $bank_transaction->amount < 0;
 
     my $payment_received      = $bank_transaction->amount > 0;
     my $payment_sent          = $bank_transaction->amount < 0;
 
@@ -608,18 +630,11 @@ sub save_single_bank_transaction {
       my $memo   = ($data{memos}   // [])->[$n_invoices];
 
       $n_invoices++ ;
       my $memo   = ($data{memos}   // [])->[$n_invoices];
 
       $n_invoices++ ;
+      # safety check invoice open
+      croak("Invoice closed. Cannot proceed.") unless ($invoice->open_amount);
 
 
-      # Check if bank_transaction already has a link to the invoice, may only be linked once per invoice
-      # This might be caused by the user reloading a page and resending the form
-      if (_existing_record_link($bank_transaction, $invoice)) {
-        return {
-          %data,
-          result  => 'error',
-          message => $::locale->text("Bank transaction with id #1 has already been linked to #2.", $bank_transaction->id, $invoice->displayable_name),
-        };
-      }
-
-      if (!$amount_of_transaction && $invoice->open_amount) {
+      if (   ($payment_sent     && $bank_transaction->not_assigned_amount >= 0)
+          || ($payment_received && $bank_transaction->not_assigned_amount <= 0)) {
         return {
           %data,
           result  => 'error',
         return {
           %data,
           result  => 'error',
@@ -627,79 +642,82 @@ sub save_single_bank_transaction {
         };
       }
 
         };
       }
 
-      my $payment_type;
+      my ($payment_type, $free_skonto_amount);
       if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
         $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
       } else {
         $payment_type = 'without_skonto';
       if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) {
         $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} });
       } else {
         $payment_type = 'without_skonto';
-      };
-
+      }
 
 
-      # pay invoice or go to the next bank transaction if the amount is not sufficiently high
-      if ($invoice->open_amount <= $amount_of_transaction && $n_invoices < $max_invoices) {
-        my $open_amount = ($payment_type eq 'with_skonto_pt'?$invoice->amount_less_skonto:$invoice->open_amount);
-        # first calculate new bank transaction amount ...
-        if ($invoice->is_sales) {
-          $amount_of_transaction -= $sign * $open_amount;
-          $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $open_amount);
+      if ($payment_type eq 'free_skonto') {
+        # parse user input > 0
+        if ($::form->parse_amount(\%::myconfig, $::form->{"free_skonto_amount"}->{"$bt_id"}{$invoice->id}) > 0) {
+          $free_skonto_amount = $::form->parse_amount(\%::myconfig, $::form->{"free_skonto_amount"}->{"$bt_id"}{$invoice->id});
         } else {
         } else {
-          $amount_of_transaction += $sign * $open_amount;
-          $bank_transaction->invoice_amount($bank_transaction->invoice_amount - $open_amount);
-        }
-        # ... and then pay the invoice
-        $invoice->pay_invoice(chart_id     => $bank_transaction->local_bank_account->chart_id,
-                              trans_id     => $invoice->id,
-                              amount       => $open_amount,
-                              payment_type => $payment_type,
-                              source       => $source,
-                              memo         => $memo,
-                              transdate    => $bank_transaction->transdate->to_kivitendo);
-      } else {
-        # use the whole amount of the bank transaction for the invoice, overpay the invoice if necessary
-
-        # this catches credit_notes and negative sales invoices
-        if ( $invoice->is_sales && $invoice->amount < 0 ) {
-          # $invoice->open_amount     is negative for credit_notes
-          # $bank_transaction->amount is negative for outgoing transactions
-          # so $amount_of_transaction is negative but needs positive
-          $amount_of_transaction *= -1;
-
-        } elsif (!$invoice->is_sales && $invoice->invoice_type =~ m/ap_transaction|purchase_invoice/) {
-          # $invoice->open_amount may be negative for ap_transaction but may be positiv for negativ ap_transaction
-          # if $invoice->open_amount is negative $bank_transaction->amount is positve
-          # if $invoice->open_amount is positive $bank_transaction->amount is negative
-          # but amount of transaction is for both positive
-          $amount_of_transaction *= -1 if $invoice->open_amount == - $amount_of_transaction;
-        }
-
-        my $overpaid_amount = $amount_of_transaction - $invoice->open_amount;
-        $invoice->pay_invoice(chart_id     => $bank_transaction->local_bank_account->chart_id,
-                              trans_id     => $invoice->id,
-                              amount       => $amount_of_transaction,
-                              payment_type => $payment_type,
-                              source       => $source,
-                              memo         => $memo,
-                              transdate    => $bank_transaction->transdate->to_kivitendo);
-        $bank_transaction->invoice_amount($bank_transaction->amount);
-        $amount_of_transaction = 0;
-
-        if ($overpaid_amount >= 0.01) {
-          push @warnings, {
+          return {
             %data,
             %data,
-            result  => 'warning',
-            message => $::locale->text('Invoice #1 was overpaid by #2.', $invoice->invnumber, $::form->format_amount(\%::myconfig, $overpaid_amount, 2)),
+            result  => 'error',
+            message => $::locale->text("Free skonto amount has to be a positive number."),
           };
         }
       }
           };
         }
       }
+    # pay invoice
+    # TODO rewrite this: really booked amount should be a return value of Payment.pm
+    # also this controller shouldnt care about how to calc skonto. we simply delegate the
+    # payment_type to the helper and get the corresponding bank_transaction values back
+    # hotfix to get the signs right - compare absolute values and later set the signs
+    # should be better done elsewhere - changing not_assigned_amount to abs feels seriously bogus
+
+    my $open_amount = $payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto : $invoice->open_amount;
+    $open_amount            = abs($open_amount);
+    $open_amount           -= $free_skonto_amount if ($payment_type eq 'free_skonto');
+    my $not_assigned_amount = abs($bank_transaction->not_assigned_amount);
+    my $amount_for_booking  = ($open_amount < $not_assigned_amount) ? $open_amount : $not_assigned_amount;
+    my $amount_for_payment  = $amount_for_booking;
+
+    # get the right direction for the payment bookings (all amounts < 0 are stornos, credit notes or negative ap)
+    $amount_for_payment *= -1 if $invoice->amount < 0;
+    $free_skonto_amount *= -1 if ($free_skonto_amount && $invoice->amount < 0);
+    # get the right direction for the bank transaction
+    $amount_for_booking *= $sign;
+
+    $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking);
+
+    # ... and then pay the invoice
+    my @acc_ids = $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id,
+                          trans_id      => $invoice->id,
+                          amount        => $amount_for_payment,
+                          payment_type  => $payment_type,
+                          source        => $source,
+                          memo          => $memo,
+                          skonto_amount => $free_skonto_amount,
+                          bt_id         => $bt_id,
+                          transdate     => $bank_transaction->valutadate->to_kivitendo);
+    # ... and record the origin via BankTransactionAccTrans
+    if (scalar(@acc_ids) < 2) {
+      return {
+        %data,
+        result  => 'error',
+        message => $::locale->text("Unable to book transactions for bank purpose #1", $bank_transaction->purpose),
+      };
+    }
+    foreach my $acc_trans_id (@acc_ids) {
+        my $id_type = $invoice->is_sales ? 'ar' : 'ap';
+        my  %props_acc = (
+          acc_trans_id        => $acc_trans_id,
+          bank_transaction_id => $bank_transaction->id,
+          $id_type            => $invoice->id,
+        );
+        SL::DB::BankTransactionAccTrans->new(%props_acc)->save;
+    }
       # Record a record link from the bank transaction to the invoice
       # Record a record link from the bank transaction to the invoice
-      my @props = (
+      my %props = (
         from_table => 'bank_transactions',
         from_id    => $bt_id,
         to_table   => $invoice->is_sales ? 'ar' : 'ap',
         to_id      => $invoice->id,
       );
         from_table => 'bank_transactions',
         from_id    => $bt_id,
         to_table   => $invoice->is_sales ? 'ar' : 'ap',
         to_id      => $invoice->id,
       );
-
-      SL::DB::RecordLink->new(@props)->save;
+      SL::DB::RecordLink->new(%props)->save;
 
       # "close" a sepa_export_item if it exists
       # code duplicated in action_save_proposals!
 
       # "close" a sepa_export_item if it exists
       # code duplicated in action_save_proposals!
@@ -742,7 +760,83 @@ sub save_single_bank_transaction {
 
   return grep { $_ } ($error, @warnings);
 }
 
   return grep { $_ } ($error, @warnings);
 }
+sub action_unlink_bank_transaction {
+  my ($self, %params) = @_;
+
+  croak("No bank transaction ids") unless scalar @{ $::form->{ids}} > 0;
+
+  my $success_count;
+
+  foreach my $bt_id (@{ $::form->{ids}} )  {
+
+    my $bank_transaction = SL::DB::Manager::BankTransaction->find_by(id => $bt_id);
+    croak("No valid bank transaction found") unless (ref($bank_transaction)  eq 'SL::DB::BankTransaction');
+    croak t8('Cannot unlink payment for a closed period!') if $bank_transaction->closed_period;
+
+    # everything in one transaction
+    my $rez = $bank_transaction->db->with_transaction(sub {
+      # 1. remove all reconciliations (due to underlying trigger, this has to be the first step)
+      my $rec_links = SL::DB::Manager::ReconciliationLink->get_all(where => [ bank_transaction_id => $bt_id ]);
+      $_->delete for @{ $rec_links };
+
+      my %trans_ids;
+      foreach my $acc_trans_id_entry (@{ SL::DB::Manager::BankTransactionAccTrans->get_all(where => [bank_transaction_id => $bt_id ] )}) {
+
+        my $acc_trans = SL::DB::Manager::AccTransaction->get_all(where => [acc_trans_id => $acc_trans_id_entry->acc_trans_id]);
+
+        # save trans_id and type
+        die "no type" unless ($acc_trans_id_entry->ar_id || $acc_trans_id_entry->ap_id || $acc_trans_id_entry->gl_id);
+        $trans_ids{$acc_trans_id_entry->ar_id} = 'ar' if $acc_trans_id_entry->ar_id;
+        $trans_ids{$acc_trans_id_entry->ap_id} = 'ap' if $acc_trans_id_entry->ap_id;
+        $trans_ids{$acc_trans_id_entry->gl_id} = 'gl' if $acc_trans_id_entry->gl_id;
+        # 2. all good -> ready to delete acc_trans and bt_acc link
+        $acc_trans_id_entry->delete;
+        $_->delete for @{ $acc_trans };
+      }
+      # 3. update arap.paid (may not be 0, yet)
+      #    or in case of gl, delete whole entry
+      while (my ($trans_id, $type) = each %trans_ids) {
+        if ($type eq 'gl') {
+          SL::DB::Manager::GLTransaction->delete_all(where => [ id => $trans_id ]);
+          next;
+        }
+        die ("invalid type") unless $type =~ m/^(ar|ap)$/;
+
+        # recalc and set paid via database query
+        my $query = qq|UPDATE $type SET paid =
+                        (SELECT COALESCE(abs(sum(amount)),0) FROM acc_trans
+                         WHERE trans_id = ?
+                         AND chart_link ilike '%paid%')
+                       WHERE id = ?|;
 
 
+        die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id, $trans_id) == -1);
+      }
+      # 4. and delete all (if any) record links
+      my $rl = SL::DB::Manager::RecordLink->delete_all(where => [ from_id => $bt_id, from_table => 'bank_transactions' ]);
+
+      # 5. finally reset  this bank transaction
+      $bank_transaction->invoice_amount(0);
+      $bank_transaction->cleared(0);
+      $bank_transaction->save;
+      # 6. and add a log entry in history_erp
+      SL::DB::History->new(
+        trans_id    => $bank_transaction->id,
+        snumbers    => 'bank_transaction_unlink_' . $bank_transaction->id,
+        employee_id => SL::DB::Manager::Employee->current->id,
+        what_done   => 'bank_transaction',
+        addition    => 'UNLINKED',
+      )->save();
+
+      1;
+
+    }) || die t8('error while unlinking payment #1 : ', $bank_transaction->purpose) . $bank_transaction->db->error . "\n";
+
+    $success_count++;
+  }
+
+  flash('ok', t8('#1 bank transaction bookings undone.', $success_count));
+  $self->action_list_all() unless $params{testcase};
+}
 #
 # filters
 #
 #
 # filters
 #
@@ -768,6 +862,7 @@ sub make_filter_summary {
     [ $filter->{"valutadate:date::le"},     $::locale->text('Valutadate') . " " . $::locale->text('To Date')   ],
     [ $filter->{"amount:number"},           $::locale->text('Amount')                                          ],
     [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account')                              ],
     [ $filter->{"valutadate:date::le"},     $::locale->text('Valutadate') . " " . $::locale->text('To Date')   ],
     [ $filter->{"amount:number"},           $::locale->text('Amount')                                          ],
     [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account')                              ],
+    [ $filter->{"remote_name:substr::ilike"}, $::locale->text('Remote name')                                   ],
   );
 
   for (@filters) {
   );
 
   for (@filters) {
@@ -785,10 +880,19 @@ sub prepare_report {
   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
   $self->{report} = $report;
 
   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 @columns     = qw(ids 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 = (
   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 = (
+    ids                 => { raw_header_data => checkbox_tag("", id => "check_all", checkall  => "[data-checkall=1]"),
+                             'align'         => 'center',
+                             raw_data        => sub { if (@{ $_[0]->linked_invoices }) {
+                                                        if ($_[0]->closed_period) {
+                                                          html_tag('text', "X"); #, tooltip => t8('Bank Transaction is in a closed period.')),
+                                                        } else {
+                                                          checkbox_tag("ids[]", value => $_[0]->id, "data-checkall" => 1);
+                                                        }
+                                                } } },
     transdate             => { sub   => sub { $_[0]->transdate_as_date } },
     valutadate            => { sub   => sub { $_[0]->valutadate_as_date } },
     remote_name           => { },
     transdate             => { sub   => sub { $_[0]->transdate_as_date } },
     valutadate            => { sub   => sub { $_[0]->valutadate_as_date } },
     remote_name           => { },
@@ -798,7 +902,8 @@ sub prepare_report {
                                align => 'right' },
     invoice_amount        => { sub   => sub { $_[0]->invoice_amount_as_number },
                                align => 'right' },
                                align => 'right' },
     invoice_amount        => { sub   => sub { $_[0]->invoice_amount_as_number },
                                align => 'right' },
-    invoices              => { sub   => sub { $_[0]->linked_invoices } },
+    invoices              => { sub   => sub { my @invnumbers; for my $obj (@{ $_[0]->linked_invoices }) {
+                                                                next unless $obj; push @invnumbers, $obj->invnumber } return \@invnumbers } },
     currency              => { sub   => sub { $_[0]->currency->name } },
     purpose               => { },
     local_account_number  => { sub   => sub { $_[0]->local_bank_account->account_number } },
     currency              => { sub   => sub { $_[0]->currency->name } },
     purpose               => { },
     local_account_number  => { sub   => sub { $_[0]->local_bank_account->account_number } },
@@ -833,20 +938,6 @@ sub prepare_report {
   );
 }
 
   );
 }
 
-sub _existing_record_link {
-  my ($bt, $invoice) = @_;
-
-  # check whether a record link from banktransaction $bt already exists to
-  # invoice $invoice, returns 1 if that is the case
-
-  die unless $bt->isa("SL::DB::BankTransaction") && ( $invoice->isa("SL::DB::Invoice") || $invoice->isa("SL::DB::PurchaseInvoice") );
-
-  my $linked_record_to_table = $invoice->is_sales ? 'Invoice' : 'PurchaseInvoice';
-  my $linked_records = $bt->linked_records( direction => 'to', to => $linked_record_to_table, query => [ id => $invoice->id ]  );
-
-  return @$linked_records ? 1 : 0;
-};
-
 sub init_problems { [] }
 
 sub init_models {
 sub init_problems { [] }
 
 sub init_models {
@@ -859,6 +950,7 @@ sub init_models {
         by  => 'transdate',
         dir => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
       },
         by  => 'transdate',
         dir => 0,   # 1 = ASC, 0 = DESC : default sort is newest at top
       },
+      id                    => t8('ID'),
       transdate             => t8('Transdate'),
       remote_name           => t8('Remote name'),
       amount                => t8('Amount'),
       transdate             => t8('Transdate'),
       remote_name           => t8('Remote name'),
       amount                => t8('Amount'),
@@ -895,16 +987,18 @@ sub load_ap_record_template_url {
 }
 
 sub load_gl_record_template_url {
 }
 
 sub load_gl_record_template_url {
-  my ($self, $template, $bt_id) = @_;
+  my ($self, $template) = @_;
 
   return $self->url_for(
     controller                           => 'gl.pl',
     action                               => 'load_record_template',
     id                                   => $template->id,
 
   return $self->url_for(
     controller                           => 'gl.pl',
     action                               => 'load_record_template',
     id                                   => $template->id,
-    'form_defaults.amount_1'             => abs($self->transaction->amount), # always positive
+    'form_defaults.amount_1'             => abs($self->transaction->not_assigned_amount), # always positive
     'form_defaults.transdate'            => $self->transaction->transdate_as_date,
     'form_defaults.callback'             => $self->callback,
     'form_defaults.bt_id'                => $self->transaction->id,
     'form_defaults.transdate'            => $self->transaction->transdate_as_date,
     'form_defaults.callback'             => $self->callback,
     'form_defaults.bt_id'                => $self->transaction->id,
+    'form_defaults.bt_chart_id'          => $self->transaction->local_bank_account->chart->id,
+    'form_defaults.description'          => $self->transaction->purpose,
   );
 }
 
   );
 }
 
@@ -927,9 +1021,18 @@ sub setup_list_all_action_bar {
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
 
   for my $bar ($::request->layout->get('actionbar')) {
     $bar->add(
-      action => [
-        t8('Filter'),
-        submit    => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
+      combobox => [
+        action => [ t8('Actions') ],
+        action => [
+          t8('Unlink bank transactions'),
+            submit => [ '#form', { action => 'BankTransaction/unlink_bank_transaction' } ],
+            checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ],
+            disabled  => $::instance_conf->get_payments_changeable ? t8('Cannot safely unlink bank transactions, please set the posting configuration for payments to unchangeable.') : undef,
+          ],
+        ],
+        action => [
+          t8('Filter'),
+          submit    => [ '#filter_form', { action => 'BankTransaction/list_all' } ],
         accesskey => 'enter',
       ],
     );
         accesskey => 'enter',
       ],
     );
@@ -959,18 +1062,15 @@ tries to post its amount to a certain number of invoices (parameter
 C<invoice_ids>, an array ref of database IDs to purchase or sales
 invoice objects).
 
 C<invoice_ids>, an array ref of database IDs to purchase or sales
 invoice objects).
 
+This method handles already partly assigned bank transactions.
+
 This method cannot handle already partly assigned bank transactions, i.e.
 a bank transaction that has a invoice_amount <> 0 but not the fully
 transaction amount (invoice_amount == amount).
 This method cannot handle already partly assigned bank transactions, i.e.
 a bank transaction that has a invoice_amount <> 0 but not the fully
 transaction amount (invoice_amount == amount).
-Currently this state is impossible from the point of the user interface,
-but for double safety and further reliance posting an bank_transaction
-where some invoice_amount is already assigned or a RecordLink from
-bank to document exists will not be accepted.
 
 If the amount of the bank transaction is higher than the sum of
 
 If the amount of the bank transaction is higher than the sum of
-the assigned invoices (1 .. n) the last invoice will be overpayed.
-
-Therefore this function implements not all valid uses cases.
+the assigned invoices (1 .. n) the bank transaction will only be
+partly assigned.
 
 The whole function is wrapped in a database transaction. If an
 exception occurs the bank transaction is not posted at all. The same
 
 The whole function is wrapped in a database transaction. If an
 exception occurs the bank transaction is not posted at all. The same
@@ -1007,6 +1107,18 @@ C<invoice_ids>
 
 =back
 
 
 =back
 
+=item C<action_unlink_bank_transaction>
+
+Takes one or more bank transaction ID (as parameter C<form::ids>) and
+tries to revert all payment bookings including already cleared bookings.
+
+This method won't undo payments that are in a closed period and assumes
+that payments are not manually changed, i.e. only imported payments.
+
+GL-records will be deleted completely if a bank transaction was the source.
+
+TODO: we still rely on linked_records for the check boxes
+
 =back
 
 =head1 AUTHOR
 =back
 
 =head1 AUTHOR