Add repair page for orphaned transactions and fix paid invoice filter

- New repair.php page for admin to fix orphaned BankImport transactions
  that are still marked as "New" but have existing payments in Dolibarr
- Fix invoice filter in card.php to only check BankImport linkage,
  not payment.fk_bank - allows linking paid invoices not yet in BankImport
- Add translations for repair page (DE/EN)
- Bump version to 1.7.1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-20 12:04:20 +01:00
parent 48b8fe2773
commit 95220dda14
5 changed files with 437 additions and 23 deletions

View file

@ -765,21 +765,20 @@ if ($object->id > 0) {
$isPaid = ($obj->fk_statut == 2);
// For paid invoices, check if payment is already linked to a bank entry
$hasLinkedBankEntry = false;
// For paid invoices, check if already linked via BankImport transaction
$isLinkedViaBankImport = false;
if ($isPaid) {
$sqlPay = "SELECT pf.fk_bank FROM ".MAIN_DB_PREFIX."paiementfourn pf";
$sqlPay .= " JOIN ".MAIN_DB_PREFIX."paiementfourn_facturefourn pff ON pf.rowid = pff.fk_paiementfourn";
$sqlPay .= " WHERE pff.fk_facturefourn = ".((int) $obj->rowid);
$sqlPay .= " AND pf.fk_bank IS NOT NULL AND pf.fk_bank > 0";
$resqlPay = $db->query($sqlPay);
if ($resqlPay && $db->num_rows($resqlPay) > 0) {
$hasLinkedBankEntry = true;
$sqlBi = "SELECT rowid FROM ".MAIN_DB_PREFIX."bankimport_transaction";
$sqlBi .= " WHERE fk_facture_fourn = ".((int) $obj->rowid);
$sqlBi .= " AND status > 0";
$resqlBi = $db->query($sqlBi);
if ($resqlBi && $db->num_rows($resqlBi) > 0) {
$isLinkedViaBankImport = true;
}
}
// Show if unpaid OR if paid but not yet linked to bank
if ($remainToPay > 0 || ($isPaid && !$hasLinkedBankEntry)) {
// Show if unpaid OR if paid but not yet linked via BankImport
if ($remainToPay > 0 || ($isPaid && !$isLinkedViaBankImport)) {
$invoiceList[] = array(
'id' => $obj->rowid,
'ref' => $obj->ref,
@ -831,21 +830,20 @@ if ($object->id > 0) {
$isPaid = ($obj->fk_statut == 2);
// For paid invoices, check if payment is already linked to a bank entry
$hasLinkedBankEntry = false;
// For paid invoices, check if already linked via BankImport transaction
$isLinkedViaBankImport = false;
if ($isPaid) {
$sqlPay = "SELECT p.fk_bank FROM ".MAIN_DB_PREFIX."paiement p";
$sqlPay .= " JOIN ".MAIN_DB_PREFIX."paiement_facture pf ON p.rowid = pf.fk_paiement";
$sqlPay .= " WHERE pf.fk_facture = ".((int) $obj->rowid);
$sqlPay .= " AND p.fk_bank IS NOT NULL AND p.fk_bank > 0";
$resqlPay = $db->query($sqlPay);
if ($resqlPay && $db->num_rows($resqlPay) > 0) {
$hasLinkedBankEntry = true;
$sqlBi = "SELECT rowid FROM ".MAIN_DB_PREFIX."bankimport_transaction";
$sqlBi .= " WHERE fk_facture = ".((int) $obj->rowid);
$sqlBi .= " AND status > 0";
$resqlBi = $db->query($sqlBi);
if ($resqlBi && $db->num_rows($resqlBi) > 0) {
$isLinkedViaBankImport = true;
}
}
// Show if unpaid OR if paid but not yet linked to bank
if ($remainToPay > 0 || ($isPaid && !$hasLinkedBankEntry)) {
// Show if unpaid OR if paid but not yet linked via BankImport
if ($remainToPay > 0 || ($isPaid && !$isLinkedViaBankImport)) {
$invoiceList[] = array(
'id' => $obj->rowid,
'ref' => $obj->ref,

View file

@ -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 = '2.0';
$this->version = '2.5';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';

View file

@ -354,3 +354,13 @@ CannotUnlinkThisStatus = Verknüpfung kann bei diesem Status nicht aufgehoben we
Payment = Zahlung
LinkedInvoices = Verknüpfte Rechnungen
NoInvoicesLinkedToPayment = Keine Rechnungen mit dieser Zahlung verknüpft
#
# Repair Page
#
RepairOrphanedTransactions = Verwaiste Buchungen reparieren
RepairOrphanedTransactionsDesc = Diese Seite findet BankImport-Buchungen, die noch als "Neu" markiert sind, obwohl die Zahlung bereits in Dolibarr existiert und mit der Bank verknüpft ist. Dies kann bei älteren Modulversionen vorkommen.
NoOrphanedTransactionsFound = Keine verwaisten Buchungen gefunden. Alle BankImport-Buchungen sind korrekt verknüpft.
OrphanedTransactionsFound = %s verwaiste Buchung(en) gefunden
RepairAll = Alle reparieren
Repair = Reparieren

View file

@ -250,3 +250,13 @@ CannotUnlinkThisStatus = Cannot unlink with this status
Payment = Payment
LinkedInvoices = Linked Invoices
NoInvoicesLinkedToPayment = No invoices linked to this payment
#
# Repair Page
#
RepairOrphanedTransactions = Repair Orphaned Transactions
RepairOrphanedTransactionsDesc = This page finds BankImport transactions that are still marked as "New" even though the payment already exists in Dolibarr and is linked to the bank. This can happen with older module versions.
NoOrphanedTransactionsFound = No orphaned transactions found. All BankImport transactions are correctly linked.
OrphanedTransactionsFound = %s orphaned transaction(s) found
RepairAll = Repair All
Repair = Repair

396
repair.php Normal file
View file

@ -0,0 +1,396 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* 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/repair.php
* \ingroup bankimport
* \brief Repair orphaned BankImport transactions
*/
// 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';
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", "admin"));
// Security check - only admin
if (!$user->admin) {
accessforbidden();
}
$action = GETPOST('action', 'aZ09');
/*
* Actions
*/
// Repair a single transaction
if ($action == 'repair' && GETPOSTINT('transid') > 0) {
$transid = GETPOSTINT('transid');
$paymentId = GETPOSTINT('payment_id');
$bankId = GETPOSTINT('bank_id');
$invoiceId = GETPOSTINT('invoice_id');
$invoiceType = GETPOST('invoice_type', 'alpha');
$db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX."bankimport_transaction SET";
$sql .= " status = 1";
if ($paymentId > 0) {
if ($invoiceType == 'facture') {
$sql .= ", fk_paiement = ".((int) $paymentId);
} else {
$sql .= ", fk_paiementfourn = ".((int) $paymentId);
}
}
if ($bankId > 0) {
$sql .= ", fk_bank = ".((int) $bankId);
}
if ($invoiceId > 0) {
if ($invoiceType == 'facture') {
$sql .= ", fk_facture = ".((int) $invoiceId);
} else {
$sql .= ", fk_facture_fourn = ".((int) $invoiceId);
}
}
$sql .= " WHERE rowid = ".((int) $transid);
$result = $db->query($sql);
if ($result) {
$db->commit();
setEventMessages($langs->trans("RecordModifiedSuccessfully"), null, 'mesgs');
} else {
$db->rollback();
setEventMessages($db->lasterror(), null, 'errors');
}
header("Location: ".$_SERVER["PHP_SELF"]."?token=".newToken());
exit;
}
// Repair all found orphans
if ($action == 'repairall') {
$orphans = findOrphanedTransactions($db, $conf);
$repaired = 0;
$failed = 0;
$db->begin();
foreach ($orphans as $orphan) {
$sql = "UPDATE ".MAIN_DB_PREFIX."bankimport_transaction SET";
$sql .= " status = 1";
if (!empty($orphan['payment_id'])) {
if ($orphan['invoice_type'] == 'facture') {
$sql .= ", fk_paiement = ".((int) $orphan['payment_id']);
} else {
$sql .= ", fk_paiementfourn = ".((int) $orphan['payment_id']);
}
}
if (!empty($orphan['bank_id'])) {
$sql .= ", fk_bank = ".((int) $orphan['bank_id']);
}
if (!empty($orphan['invoice_id'])) {
if ($orphan['invoice_type'] == 'facture') {
$sql .= ", fk_facture = ".((int) $orphan['invoice_id']);
} else {
$sql .= ", fk_facture_fourn = ".((int) $orphan['invoice_id']);
}
}
$sql .= " WHERE rowid = ".((int) $orphan['trans_id']);
if ($db->query($sql)) {
$repaired++;
} else {
$failed++;
}
}
if ($failed == 0) {
$db->commit();
setEventMessages($langs->trans("RecordsModified", $repaired), null, 'mesgs');
} else {
$db->rollback();
setEventMessages($langs->trans("ErrorsOccurred", $failed), null, 'errors');
}
header("Location: ".$_SERVER["PHP_SELF"]."?token=".newToken());
exit;
}
/**
* Find orphaned BankImport transactions
* These are transactions with status=0 (NEW) but where payments already exist in Dolibarr
*/
function findOrphanedTransactions($db, $conf)
{
$orphans = array();
// Find customer invoice orphans
$sql = "SELECT bt.rowid as trans_id, bt.ref as trans_ref, bt.amount, bt.name, bt.date_trans,";
$sql .= " f.rowid as invoice_id, f.ref as invoice_ref, f.total_ttc,";
$sql .= " p.rowid as payment_id, p.fk_bank as bank_id";
$sql .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction bt";
$sql .= " JOIN ".MAIN_DB_PREFIX."facture f ON (";
$sql .= " bt.description LIKE CONCAT('%', f.ref, '%')";
$sql .= " OR bt.end_to_end_id LIKE CONCAT('%', f.ref, '%')";
$sql .= " )";
$sql .= " JOIN ".MAIN_DB_PREFIX."paiement_facture pf ON pf.fk_facture = f.rowid";
$sql .= " JOIN ".MAIN_DB_PREFIX."paiement p ON p.rowid = pf.fk_paiement";
$sql .= " WHERE bt.status = 0";
$sql .= " AND bt.fk_paiement IS NULL";
$sql .= " AND p.fk_bank IS NOT NULL";
$sql .= " AND f.entity = ".$conf->entity;
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$orphans[] = array(
'trans_id' => $obj->trans_id,
'trans_ref' => $obj->trans_ref,
'trans_amount' => $obj->amount,
'trans_name' => $obj->name,
'trans_date' => $obj->date_trans,
'invoice_id' => $obj->invoice_id,
'invoice_ref' => $obj->invoice_ref,
'invoice_amount' => $obj->total_ttc,
'payment_id' => $obj->payment_id,
'bank_id' => $obj->bank_id,
'invoice_type' => 'facture'
);
}
}
// Find supplier invoice orphans
$sql = "SELECT bt.rowid as trans_id, bt.ref as trans_ref, bt.amount, bt.name, bt.date_trans,";
$sql .= " f.rowid as invoice_id, f.ref as invoice_ref, f.ref_supplier, f.total_ttc,";
$sql .= " p.rowid as payment_id, p.fk_bank as bank_id";
$sql .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction bt";
$sql .= " JOIN ".MAIN_DB_PREFIX."facture_fourn f ON (";
$sql .= " bt.description LIKE CONCAT('%', f.ref, '%')";
$sql .= " OR bt.description LIKE CONCAT('%', f.ref_supplier, '%')";
$sql .= " OR bt.end_to_end_id LIKE CONCAT('%', f.ref, '%')";
$sql .= " )";
$sql .= " JOIN ".MAIN_DB_PREFIX."paiementfourn_facturefourn pff ON pff.fk_facturefourn = f.rowid";
$sql .= " JOIN ".MAIN_DB_PREFIX."paiementfourn p ON p.rowid = pff.fk_paiementfourn";
$sql .= " WHERE bt.status = 0";
$sql .= " AND bt.fk_paiementfourn IS NULL";
$sql .= " AND p.fk_bank IS NOT NULL";
$sql .= " AND f.entity = ".$conf->entity;
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$orphans[] = array(
'trans_id' => $obj->trans_id,
'trans_ref' => $obj->trans_ref,
'trans_amount' => $obj->amount,
'trans_name' => $obj->name,
'trans_date' => $obj->date_trans,
'invoice_id' => $obj->invoice_id,
'invoice_ref' => $obj->invoice_ref,
'invoice_ref_supplier' => $obj->ref_supplier,
'invoice_amount' => $obj->total_ttc,
'payment_id' => $obj->payment_id,
'bank_id' => $obj->bank_id,
'invoice_type' => 'facture_fourn'
);
}
}
return $orphans;
}
/*
* View
*/
$title = $langs->trans("RepairOrphanedTransactions");
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-bankimport page-repair');
print load_fiche_titre($title, '', 'tools');
// Find orphans
$orphans = findOrphanedTransactions($db, $conf);
print '<div class="info">';
print img_picto('', 'info', 'class="pictofixedwidth"');
print $langs->trans("RepairOrphanedTransactionsDesc");
print '</div>';
print '<br>';
if (empty($orphans)) {
print '<div class="opacitymedium">'.$langs->trans("NoOrphanedTransactionsFound").'</div>';
} else {
print '<div class="warning">';
print img_picto('', 'warning', 'class="pictofixedwidth"');
print sprintf($langs->trans("OrphanedTransactionsFound"), count($orphans));
print '</div>';
print '<br>';
// Repair all button
print '<div class="tabsAction">';
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?action=repairall&token='.newToken().'">'.$langs->trans("RepairAll").' ('.count($orphans).')</a>';
print '</div>';
print '<br>';
// Table
print '<div class="div-table-responsive">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans("Date").'</th>';
print '<th>'.$langs->trans("TransactionRef").'</th>';
print '<th>'.$langs->trans("Counterparty").'</th>';
print '<th class="right">'.$langs->trans("Amount").'</th>';
print '<th>&rarr;</th>';
print '<th>'.$langs->trans("Invoice").'</th>';
print '<th class="right">'.$langs->trans("Amount").'</th>';
print '<th>'.$langs->trans("Payment").'</th>';
print '<th>'.$langs->trans("BankEntry").'</th>';
print '<th class="center">'.$langs->trans("Action").'</th>';
print '</tr>';
foreach ($orphans as $orphan) {
print '<tr class="oddeven">';
// Date
print '<td class="nowraponall">'.dol_print_date($orphan['trans_date'], 'day').'</td>';
// Transaction ref
print '<td>';
print '<a href="'.dol_buildpath('/bankimport/card.php', 1).'?id='.$orphan['trans_id'].'">';
print dol_trunc($orphan['trans_ref'], 12);
print '</a>';
print '</td>';
// Counterparty
print '<td>'.dol_escape_htmltag($orphan['trans_name']).'</td>';
// Transaction amount
print '<td class="right nowraponall">';
if ($orphan['trans_amount'] >= 0) {
print '<span style="color: green;">'.price($orphan['trans_amount'], 0, $langs, 1, -1, 2, 'EUR').'</span>';
} else {
print '<span style="color: red;">'.price($orphan['trans_amount'], 0, $langs, 1, -1, 2, 'EUR').'</span>';
}
print '</td>';
// Arrow
print '<td class="center">&rarr;</td>';
// Invoice
print '<td class="nowraponall">';
if ($orphan['invoice_type'] == 'facture') {
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
$inv = new Facture($db);
$inv->fetch($orphan['invoice_id']);
print $inv->getNomUrl(1);
} else {
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
$inv = new FactureFournisseur($db);
$inv->fetch($orphan['invoice_id']);
print $inv->getNomUrl(1);
if (!empty($orphan['invoice_ref_supplier'])) {
print ' <span class="opacitymedium small">('.$orphan['invoice_ref_supplier'].')</span>';
}
}
print '</td>';
// Invoice amount
print '<td class="right nowraponall">'.price($orphan['invoice_amount'], 0, $langs, 1, -1, 2, 'EUR').'</td>';
// Payment
print '<td class="nowraponall">';
if ($orphan['invoice_type'] == 'facture') {
require_once DOL_DOCUMENT_ROOT.'/compta/paiement/class/paiement.class.php';
$pay = new Paiement($db);
$pay->fetch($orphan['payment_id']);
print $pay->getNomUrl(1);
} else {
require_once DOL_DOCUMENT_ROOT.'/fourn/class/paiementfourn.class.php';
$pay = new PaiementFourn($db);
$pay->fetch($orphan['payment_id']);
print $pay->getNomUrl(1);
}
print '</td>';
// Bank entry
print '<td class="nowraponall">';
print '<a href="'.DOL_URL_ROOT.'/compta/bank/line.php?rowid='.$orphan['bank_id'].'">';
print img_picto('', 'bank_account', 'class="pictofixedwidth"');
print '#'.$orphan['bank_id'];
print '</a>';
print '</td>';
// Action
print '<td class="center nowraponall">';
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline;">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="repair">';
print '<input type="hidden" name="transid" value="'.$orphan['trans_id'].'">';
print '<input type="hidden" name="payment_id" value="'.$orphan['payment_id'].'">';
print '<input type="hidden" name="bank_id" value="'.$orphan['bank_id'].'">';
print '<input type="hidden" name="invoice_id" value="'.$orphan['invoice_id'].'">';
print '<input type="hidden" name="invoice_type" value="'.$orphan['invoice_type'].'">';
print '<button type="submit" class="butActionSmall">'.$langs->trans("Repair").'</button>';
print '</form>';
print '</td>';
print '</tr>';
}
print '</table>';
print '</div>';
}
// Back link
print '<br>';
print '<div class="tabsAction">';
print '<a class="butAction" href="'.dol_buildpath('/bankimport/list.php', 1).'">'.$langs->trans("BackToList").'</a>';
print '</div>';
llxFooter();
$db->close();