';
// -----------------------------------------------
diff --git a/class/banktransaction.class.php b/class/banktransaction.class.php
index 4c63631..4648030 100755
--- a/class/banktransaction.class.php
+++ b/class/banktransaction.class.php
@@ -1019,6 +1019,193 @@ class BankImportTransaction extends CommonObject
return 0;
}
+ /**
+ * Confirm payment: create Paiement or PaiementFourn in Dolibarr
+ * Links the bankimport transaction to the created payment and bank line.
+ *
+ * @param User $user User performing the action
+ * @param string $type 'facture' or 'facture_fourn'
+ * @param int $invoiceId Invoice ID
+ * @param int $bankAccountId Dolibarr bank account ID (llx_bank_account.rowid)
+ * @return int >0 if OK (payment ID), <0 if error
+ */
+ public function confirmPayment($user, $type, $invoiceId, $bankAccountId)
+ {
+ global $conf, $langs;
+
+ $error = 0;
+ $this->db->begin();
+
+ // Look up payment type ID for 'VIR' (bank transfer)
+ $paiementTypeId = 0;
+ $sql = "SELECT id FROM ".MAIN_DB_PREFIX."c_paiement WHERE code = 'VIR' AND active = 1";
+ $resql = $this->db->query($sql);
+ if ($resql && $this->db->num_rows($resql) > 0) {
+ $obj = $this->db->fetch_object($resql);
+ $paiementTypeId = (int) $obj->id;
+ }
+ if (empty($paiementTypeId)) {
+ $this->error = 'Payment type VIR not found in c_paiement';
+ $this->db->rollback();
+ return -1;
+ }
+
+ if ($type == 'facture') {
+ // Customer invoice payment
+ require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
+ require_once DOL_DOCUMENT_ROOT.'/compta/paiement/class/paiement.class.php';
+
+ $invoice = new Facture($this->db);
+ if ($invoice->fetch($invoiceId) <= 0) {
+ $this->error = 'Invoice not found: '.$invoiceId;
+ $this->db->rollback();
+ return -2;
+ }
+
+ // Calculate remaining amount
+ $alreadyPaid = $invoice->getSommePaiement();
+ $creditnotes = $invoice->getSumCreditNotesUsed();
+ $deposits = $invoice->getSumDepositsUsed();
+ $remaintopay = price2num($invoice->total_ttc - $alreadyPaid - $creditnotes - $deposits, 'MT');
+
+ if ($remaintopay <= 0) {
+ $this->error = $langs->trans("InvoiceAlreadyPaid");
+ $this->db->rollback();
+ return -5;
+ }
+
+ // Use the lesser of: transaction amount or remain to pay
+ $payAmount = min(abs($this->amount), $remaintopay);
+
+ $paiement = new Paiement($this->db);
+ $paiement->datepaye = $this->date_trans;
+ $paiement->amounts = array($invoiceId => $payAmount);
+ $paiement->multicurrency_amounts = array($invoiceId => $payAmount);
+ $paiement->paiementid = $paiementTypeId;
+ $paiement->paiementcode = 'VIR';
+ $paiement->num_payment = $this->end_to_end_id ?: $this->ref;
+ $paiement->note_private = $langs->trans("PaymentCreatedByBankImport").' - '.$this->name.' - '.dol_trunc($this->description, 100);
+
+ $paymentId = $paiement->create($user, 1);
+ if ($paymentId < 0) {
+ $this->error = $paiement->error;
+ $this->errors = $paiement->errors;
+ $error++;
+ }
+
+ if (!$error) {
+ $bankLineId = $paiement->addPaymentToBank(
+ $user,
+ 'payment',
+ '(CustomerInvoicePayment)',
+ $bankAccountId,
+ $this->name,
+ ''
+ );
+
+ if ($bankLineId > 0) {
+ $this->fk_paiement = $paymentId;
+ $this->fk_bank = $bankLineId;
+ $this->fk_facture = $invoiceId;
+ $this->fk_societe = $invoice->socid;
+ $this->status = self::STATUS_MATCHED;
+ $this->fk_user_match = $user->id;
+ $this->date_match = dol_now();
+ $this->update($user);
+ } else {
+ $this->error = 'Failed to add payment to bank';
+ $error++;
+ }
+ }
+
+ if ($error) {
+ $this->db->rollback();
+ return -3;
+ }
+
+ $this->db->commit();
+ return $paymentId;
+
+ } elseif ($type == 'facture_fourn') {
+ // Supplier invoice payment
+ require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
+ require_once DOL_DOCUMENT_ROOT.'/fourn/class/paiementfourn.class.php';
+
+ $invoice = new FactureFournisseur($this->db);
+ if ($invoice->fetch($invoiceId) <= 0) {
+ $this->error = 'Supplier invoice not found: '.$invoiceId;
+ $this->db->rollback();
+ return -2;
+ }
+
+ $alreadyPaid = $invoice->getSommePaiement();
+ $creditnotes = $invoice->getSumCreditNotesUsed();
+ $deposits = $invoice->getSumDepositsUsed();
+ $remaintopay = price2num($invoice->total_ttc - $alreadyPaid - $creditnotes - $deposits, 'MT');
+
+ if ($remaintopay <= 0) {
+ $this->error = $langs->trans("InvoiceAlreadyPaid");
+ $this->db->rollback();
+ return -5;
+ }
+
+ $payAmount = min(abs($this->amount), $remaintopay);
+
+ $paiementfourn = new PaiementFourn($this->db);
+ $paiementfourn->datepaye = $this->date_trans;
+ $paiementfourn->amounts = array($invoiceId => $payAmount);
+ $paiementfourn->multicurrency_amounts = array($invoiceId => $payAmount);
+ $paiementfourn->paiementid = $paiementTypeId;
+ $paiementfourn->paiementcode = 'VIR';
+ $paiementfourn->num_payment = $this->end_to_end_id ?: $this->ref;
+ $paiementfourn->note_private = $langs->trans("PaymentCreatedByBankImport").' - '.$this->name.' - '.dol_trunc($this->description, 100);
+
+ $paymentId = $paiementfourn->create($user, 1);
+ if ($paymentId < 0) {
+ $this->error = $paiementfourn->error;
+ $this->errors = $paiementfourn->errors;
+ $error++;
+ }
+
+ if (!$error) {
+ $bankLineId = $paiementfourn->addPaymentToBank(
+ $user,
+ 'payment_supplier',
+ '(SupplierInvoicePayment)',
+ $bankAccountId,
+ $this->name,
+ ''
+ );
+
+ if ($bankLineId > 0) {
+ $this->fk_paiementfourn = $paymentId;
+ $this->fk_bank = $bankLineId;
+ $this->fk_facture_fourn = $invoiceId;
+ $this->fk_societe = $invoice->socid;
+ $this->status = self::STATUS_MATCHED;
+ $this->fk_user_match = $user->id;
+ $this->date_match = dol_now();
+ $this->update($user);
+ } else {
+ $this->error = 'Failed to add payment to bank';
+ $error++;
+ }
+ }
+
+ if ($error) {
+ $this->db->rollback();
+ return -3;
+ }
+
+ $this->db->commit();
+ return $paymentId;
+ }
+
+ $this->error = 'Unknown type: '.$type;
+ $this->db->rollback();
+ return -4;
+ }
+
/**
* Link transaction to a Dolibarr object
*
diff --git a/confirm.php b/confirm.php
new file mode 100644
index 0000000..f42a8b8
--- /dev/null
+++ b/confirm.php
@@ -0,0 +1,350 @@
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+/**
+ * \file bankimport/confirm.php
+ * \ingroup bankimport
+ * \brief Payment confirmation page - match bank transactions to invoices
+ */
+
+// Load Dolibarr environment
+$res = 0;
+if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
+ $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
+}
+$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
+$tmp2 = realpath(__FILE__);
+$i = strlen($tmp) - 1;
+$j = strlen($tmp2) - 1;
+while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
+ $i--;
+ $j--;
+}
+if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
+ $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
+}
+if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
+ $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
+}
+if (!$res && file_exists("../main.inc.php")) {
+ $res = @include "../main.inc.php";
+}
+if (!$res && file_exists("../../main.inc.php")) {
+ $res = @include "../../main.inc.php";
+}
+if (!$res) {
+ die("Include of main fails");
+}
+
+require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
+require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
+dol_include_once('/bankimport/class/banktransaction.class.php');
+dol_include_once('/bankimport/lib/bankimport.lib.php');
+
+/**
+ * @var Conf $conf
+ * @var DoliDB $db
+ * @var Translate $langs
+ * @var User $user
+ */
+
+$langs->loadLangs(array("bankimport@bankimport", "banks", "bills"));
+
+$action = GETPOST('action', 'aZ09');
+
+// Security check
+if (!$user->hasRight('bankimport', 'write')) {
+ accessforbidden();
+}
+
+$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
+
+/*
+ * Actions
+ */
+
+// Confirm single payment
+if ($action == 'confirmpayment' && !empty($bankAccountId)) {
+ $transid = GETPOSTINT('transid');
+ $matchtype = GETPOST('matchtype', 'alpha');
+ $matchid = GETPOSTINT('matchid');
+
+ if ($transid > 0 && !empty($matchtype) && $matchid > 0) {
+ $trans = new BankImportTransaction($db);
+ if ($trans->fetch($transid) > 0) {
+ if ($trans->status != BankImportTransaction::STATUS_NEW) {
+ setEventMessages($langs->trans("TransactionAlreadyProcessed"), null, 'warnings');
+ } else {
+ $result = $trans->confirmPayment($user, $matchtype, $matchid, $bankAccountId);
+ if ($result > 0) {
+ setEventMessages($langs->trans("PaymentCreatedSuccessfully", dol_escape_htmltag($trans->name), price(abs($trans->amount))), null, 'mesgs');
+ } else {
+ setEventMessages($trans->error, $trans->errors, 'errors');
+ }
+ }
+ }
+ }
+
+ header("Location: ".$_SERVER["PHP_SELF"]."?token=".newToken());
+ exit;
+}
+
+// Confirm all high-score matches
+if ($action == 'confirmall' && !empty($bankAccountId)) {
+ $transaction = new BankImportTransaction($db);
+ $transactions = $transaction->fetchAll('date_trans', 'DESC', 0, 0, array('status' => BankImportTransaction::STATUS_NEW));
+
+ $created = 0;
+ $failed = 0;
+
+ if (is_array($transactions)) {
+ foreach ($transactions as $trans) {
+ $matches = $trans->findMatches();
+ if (!empty($matches) && $matches[0]['match_score'] >= 80) {
+ $bestMatch = $matches[0];
+ $result = $trans->confirmPayment($user, $bestMatch['type'], $bestMatch['id'], $bankAccountId);
+ if ($result > 0) {
+ $created++;
+ } else {
+ $failed++;
+ }
+ }
+ }
+ }
+
+ if ($created > 0 || $failed > 0) {
+ setEventMessages($langs->trans("PaymentsCreatedSummary", $created, $failed), null, $failed > 0 ? 'warnings' : 'mesgs');
+ } else {
+ setEventMessages($langs->trans("NoNewMatchesFound"), null, 'warnings');
+ }
+
+ header("Location: ".$_SERVER["PHP_SELF"]."?token=".newToken());
+ exit;
+}
+
+// Ignore transaction
+if ($action == 'ignore') {
+ $transid = GETPOSTINT('transid');
+ if ($transid > 0) {
+ $trans = new BankImportTransaction($db);
+ if ($trans->fetch($transid) > 0 && $trans->status == BankImportTransaction::STATUS_NEW) {
+ $trans->setStatus(BankImportTransaction::STATUS_IGNORED, $user);
+ setEventMessages($langs->trans("StatusUpdated"), null, 'mesgs');
+ }
+ }
+ header("Location: ".$_SERVER["PHP_SELF"]."?token=".newToken());
+ exit;
+}
+
+
+/*
+ * View
+ */
+
+$form = new Form($db);
+
+$title = $langs->trans("PaymentConfirmation");
+llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-bankimport page-confirm');
+
+print load_fiche_titre($title, '', 'bank');
+
+// Check if bank account is configured
+if (empty($bankAccountId)) {
+ print '
';
+ llxFooter();
+ $db->close();
+ exit;
+}
+
+// Description
+print '
'.$langs->trans("PaymentConfirmationDesc").'
';
+
+// Fetch all new transactions and find matches
+$transaction = new BankImportTransaction($db);
+$transactions = $transaction->fetchAll('date_trans', 'DESC', 0, 0, array('status' => BankImportTransaction::STATUS_NEW));
+
+$pendingMatches = array(); // transactions with matches
+$noMatches = array(); // transactions without matches
+
+if (is_array($transactions)) {
+ foreach ($transactions as $trans) {
+ $matches = $trans->findMatches();
+ if (!empty($matches)) {
+ $pendingMatches[] = array('transaction' => $trans, 'matches' => $matches);
+ } else {
+ $noMatches[] = $trans;
+ }
+ }
+}
+
+// Confirm all button (if high-score matches exist)
+$highScoreCount = 0;
+foreach ($pendingMatches as $pm) {
+ if ($pm['matches'][0]['match_score'] >= 80) {
+ $highScoreCount++;
+ }
+}
+
+if ($highScoreCount > 0) {
+ print '
';
+}
+
+// Match reasons translation
+$reasonLabels = array(
+ 'ref' => $langs->trans("MatchByRef"),
+ 'ref_client' => $langs->trans("MatchByClientRef"),
+ 'ref_supplier' => $langs->trans("MatchBySupplierRef"),
+ 'amount' => $langs->trans("MatchByAmount"),
+ 'amount_close' => $langs->trans("MatchByAmountClose"),
+ 'name_exact' => $langs->trans("MatchByNameExact"),
+ 'name_similar' => $langs->trans("MatchByNameSimilar"),
+ 'iban' => $langs->trans("MatchByIBAN")
+);
+
+if (!empty($pendingMatches)) {
+ print '
';
+ print '
';
+ print '';
+ print '| '.$langs->trans("Date").' | ';
+ print ''.$langs->trans("Counterparty").' | ';
+ print ''.$langs->trans("Amount").' ('.$langs->trans("Transaction").') | ';
+ print ' | ';
+ print ''.$langs->trans("Invoice").' | ';
+ print ''.$langs->trans("ThirdParty").' | ';
+ print ''.$langs->trans("Amount").' ('.$langs->trans("Invoice").') | ';
+ print ''.$langs->trans("Score").' | ';
+ print ''.$langs->trans("MatchReason").' | ';
+ print ''.$langs->trans("Action").' | ';
+ print '
';
+
+ foreach ($pendingMatches as $pm) {
+ $trans = $pm['transaction'];
+ $bestMatch = $pm['matches'][0];
+
+ // Score color
+ $scoreColor = $bestMatch['match_score'] >= 80 ? '#4caf50' : ($bestMatch['match_score'] >= 60 ? '#ff9800' : '#9e9e9e');
+
+ print '';
+
+ // Transaction date
+ print '| '.dol_print_date($trans->date_trans, 'day').' | ';
+
+ // Counterparty name + description
+ print '';
+ print ''.dol_escape_htmltag(dol_trunc($trans->name, 30)).'';
+ if ($trans->description) {
+ print ' '.dol_escape_htmltag(dol_trunc($trans->description, 50)).'';
+ }
+ print ' | ';
+
+ // Transaction amount
+ print '';
+ if ($trans->amount >= 0) {
+ print '+'.price($trans->amount, 0, $langs, 1, -1, 2, $trans->currency).'';
+ } else {
+ print ''.price($trans->amount, 0, $langs, 1, -1, 2, $trans->currency).'';
+ }
+ print ' | ';
+
+ // Arrow
+ print '↔ | ';
+
+ // Invoice reference
+ print '';
+ if ($bestMatch['type'] == 'facture') {
+ require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
+ $inv = new Facture($db);
+ $inv->fetch($bestMatch['id']);
+ print $inv->getNomUrl(1);
+ } else {
+ require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
+ $inv = new FactureFournisseur($db);
+ $inv->fetch($bestMatch['id']);
+ print $inv->getNomUrl(1);
+ }
+ print ' | ';
+
+ // Third party
+ print '';
+ if ($bestMatch['socid'] > 0) {
+ require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
+ $soc = new Societe($db);
+ $soc->fetch($bestMatch['socid']);
+ print $soc->getNomUrl(1);
+ } else {
+ print dol_escape_htmltag($bestMatch['socname']);
+ }
+ print ' | ';
+
+ // Invoice amount
+ print ''.price($bestMatch['amount'], 0, $langs, 1, -1, 2, 'EUR').' | ';
+
+ // Score
+ print ''.$bestMatch['match_score'].'% | ';
+
+ // Match reasons
+ print '';
+ if (!empty($bestMatch['match_reasons'])) {
+ foreach ($bestMatch['match_reasons'] as $reason) {
+ $label = $reasonLabels[$reason] ?? $reason;
+ print ''.$label.' ';
+ }
+ }
+ print ' | ';
+
+ // Actions
+ print '';
+ // Confirm payment button
+ print 'id.'&matchtype='.urlencode($bestMatch['type']).'&matchid='.$bestMatch['id'].'&token='.newToken().'">';
+ print $langs->trans("ConfirmPayment");
+ print '';
+ print ' ';
+ // Ignore button
+ print 'id.'&token='.newToken().'">';
+ print $langs->trans("SetAsIgnored");
+ print '';
+
+ // Show alternatives if multiple matches
+ if (count($pm['matches']) > 1) {
+ print ' ';
+ print '+'.($count = count($pm['matches']) - 1).' '.$langs->trans("Alternatives");
+ print '';
+ }
+ print ' | ';
+
+ print '
';
+ }
+
+ print '
';
+ print '
';
+} else {
+ print '
';
+ print $langs->trans("NoNewMatchesFound");
+ print '
';
+}
+
+// Show unmatched transactions count
+if (!empty($noMatches)) {
+ print '
';
+ print '
';
+ print img_picto('', 'info', 'class="pictofixedwidth"');
+ print $langs->trans("UnmatchedTransactions", count($noMatches));
+ print '
'.$langs->trans("ShowAll").'';
+ print '
';
+}
+
+llxFooter();
+$db->close();
diff --git a/core/modules/modBankImport.class.php b/core/modules/modBankImport.class.php
index d264f82..f7fb10a 100755
--- a/core/modules/modBankImport.class.php
+++ b/core/modules/modBankImport.class.php
@@ -76,7 +76,7 @@ class modBankImport extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@bankimport'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
- $this->version = '1.2';
+ $this->version = '1.4';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
@@ -369,6 +369,21 @@ class modBankImport extends DolibarrModules
'target' => '',
'user' => 2,
);
+ $this->menu[$r++] = array(
+ 'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
+ 'type' => 'left',
+ 'titre' => 'PaymentConfirmation',
+ 'prefix' => img_picto('', 'payment', 'class="pictofixedwidth valignmiddle paddingright"'),
+ 'mainmenu' => 'bank',
+ 'leftmenu' => 'bankimport_confirm',
+ 'url' => '/bankimport/confirm.php?mainmenu=bank&leftmenu=bankimport',
+ 'langs' => 'bankimport@bankimport',
+ 'position' => 203,
+ 'enabled' => 'isModEnabled("bankimport")',
+ 'perms' => '$user->hasRight("bankimport", "write")',
+ 'target' => '',
+ 'user' => 2,
+ );
$this->menu[$r++] = array(
'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
'type' => 'left',
@@ -378,7 +393,7 @@ class modBankImport extends DolibarrModules
'leftmenu' => 'bankimport_pdfstatements',
'url' => '/bankimport/pdfstatements.php?mainmenu=bank&leftmenu=bankimport',
'langs' => 'bankimport@bankimport',
- 'position' => 203,
+ 'position' => 204,
'enabled' => 'isModEnabled("bankimport")',
'perms' => '$user->hasRight("bankimport", "read")',
'target' => '',