BankTransaction: Kreditorenvorlagen: Vorlage direkt laden, wenn genau 1 Treffer
[kivitendo-erp.git] / SL / Controller / BankTransaction.pm
index a937451..e38f8a9 100644 (file)
@@ -68,32 +68,21 @@ sub action_list_all {
   $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 .= $::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 = ();
-  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
-  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(
@@ -101,24 +90,28 @@ sub action_list {
     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
-  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
@@ -174,8 +167,6 @@ sub action_list {
       }
       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
@@ -217,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
-  # * 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} })
-    && (@{ $_->{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
-  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 , \@proposals );
+  return ( $bank_transactions , \@proposals );
+}
+
+sub action_list {
+  my ($self) = @_;
+
+  if (!$::form->{filter}{bank_account}) {
+    flash('error', t8('No bank account chosen!'));
+    $self->action_search;
+    return;
   }
 
+  my $bank_account = SL::DB::BankAccount->load_cached($::form->{filter}->{bank_account});
+  my $fromdate     = $::locale->parse_date_to_object($::form->{filter}->{fromdate});
+  my $todate       = $::locale->parse_date_to_object($::form->{filter}->{todate});
+  $todate->add( days => 1 ) if $todate;
+
+  my ($bank_transactions, $proposals) = $self->gather_bank_transactions_and_proposals(
+    bank_account => $bank_account,
+    fromdate     => $fromdate,
+    todate       => $todate,
+    sort_by      => $::form->{sort_by},
+    sort_dir     => $::form->{sort_dir},
+  );
+
   $::request->layout->add_javascripts("kivi.BankTransaction.js");
   $self->render('bank_transactions/list',
                 title             => t8('Bank transactions MT940'),
                 BANK_TRANSACTIONS => $bank_transactions,
-                PROPOSALS         => \@proposals,
+                PROPOSALS         => $proposals,
                 bank_account      => $bank_account,
-                ui_tab            => scalar(@proposals) > 0?1:0,
+                ui_tab            => scalar(@{ $proposals }) > 0 ? 1 : 0,
               );
 }
 
@@ -273,12 +278,14 @@ sub action_create_invoice {
 
   my $templates_ap = SL::DB::Manager::RecordTemplate->get_all(
     where        => [ template_type => 'ap_transaction' ],
+    sort_by      => [ qw(template_name) ],
     with_objects => [ qw(employee vendor) ],
   );
   my $templates_gl = SL::DB::Manager::RecordTemplate->get_all(
     query        => [ template_type => 'gl_transaction',
                       chart_id      => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id,
                     ],
+    sort_by      => [ qw(template_name) ],
     with_objects => [ qw(employee record_template_items) ],
   );
 
@@ -292,14 +299,21 @@ sub action_create_invoice {
     '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,
-  );
+  # 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 {
@@ -616,6 +630,9 @@ sub save_single_bank_transaction {
       my $memo   = ($data{memos}   // [])->[$n_invoices];
 
       $n_invoices++ ;
+      # safety check invoice open
+      croak("Invoice closed. Cannot proceed.") unless ($invoice->open_amount);
+
       if (   ($payment_sent     && $bank_transaction->not_assigned_amount >= 0)
           || ($payment_received && $bank_transaction->not_assigned_amount <= 0)) {
         return {
@@ -674,6 +691,7 @@ sub save_single_bank_transaction {
                           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) {
@@ -788,9 +806,10 @@ sub action_unlink_bank_transaction {
         my $query = qq|UPDATE $type SET paid =
                         (SELECT COALESCE(abs(sum(amount)),0) FROM acc_trans
                          WHERE trans_id = ?
-                         AND chart_link ilike '%paid%')|;
+                         AND chart_link ilike '%paid%')
+                       WHERE id = ?|;
 
-        die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id) == -1);
+        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' ]);
@@ -799,6 +818,14 @@ sub action_unlink_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;
 
@@ -835,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->{"remote_name:substr::ilike"}, $::locale->text('Remote name')                                   ],
   );
 
   for (@filters) {
@@ -970,6 +998,7 @@ sub load_gl_record_template_url {
     '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,
   );
 }