Compare commits
3 commits
31daf7ec94
...
7f011424bb
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f011424bb | |||
| c1c06e19ae | |||
| be8a02e88e |
15 changed files with 2068 additions and 15 deletions
|
|
@ -199,6 +199,12 @@ if ($action == 'update') {
|
||||||
$error++;
|
$error++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dolibarr Bank Account mapping
|
||||||
|
$res = dolibarr_set_const($db, "BANKIMPORT_BANK_ACCOUNT_ID", GETPOSTINT('BANKIMPORT_BANK_ACCOUNT_ID'), 'chaine', 0, '', $conf->entity);
|
||||||
|
if (!($res > 0)) {
|
||||||
|
$error++;
|
||||||
|
}
|
||||||
|
|
||||||
// Reminder setting
|
// Reminder setting
|
||||||
$res = dolibarr_set_const($db, "BANKIMPORT_REMINDER_ENABLED", GETPOSTINT('BANKIMPORT_REMINDER_ENABLED'), 'chaine', 0, '', $conf->entity);
|
$res = dolibarr_set_const($db, "BANKIMPORT_REMINDER_ENABLED", GETPOSTINT('BANKIMPORT_REMINDER_ENABLED'), 'chaine', 0, '', $conf->entity);
|
||||||
if (!($res > 0)) {
|
if (!($res > 0)) {
|
||||||
|
|
@ -433,6 +439,41 @@ print '</tr>';
|
||||||
|
|
||||||
print '</table>';
|
print '</table>';
|
||||||
|
|
||||||
|
// Bank Account Mapping Section
|
||||||
|
print '<br>';
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<td colspan="2">'.$langs->trans("BankAccountMapping").'</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
// Bank Account Dropdown
|
||||||
|
$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td class="titlefield fieldrequired">'.$langs->trans("DolibarrBankAccount").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
|
||||||
|
// Build select from llx_bank_account
|
||||||
|
$sql_ba = "SELECT rowid, label, iban_prefix FROM ".MAIN_DB_PREFIX."bank_account";
|
||||||
|
$sql_ba .= " WHERE entity = ".((int) $conf->entity);
|
||||||
|
$sql_ba .= " AND clos = 0";
|
||||||
|
$sql_ba .= " ORDER BY label ASC";
|
||||||
|
|
||||||
|
$bankAccounts = array('' => $langs->trans("SelectBankAccount"));
|
||||||
|
$resql_ba = $db->query($sql_ba);
|
||||||
|
if ($resql_ba) {
|
||||||
|
while ($obj_ba = $db->fetch_object($resql_ba)) {
|
||||||
|
$ibanDisplay = $obj_ba->iban_prefix ? ' ('.dol_trunc($obj_ba->iban_prefix, 20).')' : '';
|
||||||
|
$bankAccounts[$obj_ba->rowid] = $obj_ba->label.$ibanDisplay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print $form->selectarray('BANKIMPORT_BANK_ACCOUNT_ID', $bankAccounts, $bankAccountId, 0, 0, 0, '', 0, 0, 0, '', 'minwidth300');
|
||||||
|
print '<br><span class="opacitymedium small">'.$langs->trans("DolibarrBankAccountHelp").'</span>';
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
// Automatic Import Section
|
// Automatic Import Section
|
||||||
print '<br>';
|
print '<br>';
|
||||||
print '<table class="noborder centpercent">';
|
print '<table class="noborder centpercent">';
|
||||||
|
|
|
||||||
102
ajax/checkpending.php
Normal file
102
ajax/checkpending.php
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX endpoint to check for pending bank transaction matches
|
||||||
|
* Used by browser notification system
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) {
|
||||||
|
define('NOTOKENRENEWAL', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREMENU')) {
|
||||||
|
define('NOREQUIREMENU', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREHTML')) {
|
||||||
|
define('NOREQUIREHTML', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREAJAX')) {
|
||||||
|
define('NOREQUIREAJAX', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('bankimport', 'read')) {
|
||||||
|
echo json_encode(array('error' => 'access_denied'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||||
|
if (empty($bankAccountId)) {
|
||||||
|
echo json_encode(array('pending' => 0, 'incoming' => 0));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count new unmatched transactions (incoming payments = positive amount)
|
||||||
|
$sqlIncoming = "SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total";
|
||||||
|
$sqlIncoming .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
||||||
|
$sqlIncoming .= " WHERE entity IN (".getEntity('banktransaction').")";
|
||||||
|
$sqlIncoming .= " AND status = 0 AND amount > 0";
|
||||||
|
$resIncoming = $db->query($sqlIncoming);
|
||||||
|
$incoming = 0;
|
||||||
|
$incomingTotal = 0;
|
||||||
|
if ($resIncoming) {
|
||||||
|
$obj = $db->fetch_object($resIncoming);
|
||||||
|
$incoming = (int) $obj->cnt;
|
||||||
|
$incomingTotal = (float) $obj->total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count all new transactions
|
||||||
|
$sqlAll = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
||||||
|
$sqlAll .= " WHERE entity IN (".getEntity('banktransaction').")";
|
||||||
|
$sqlAll .= " AND status = 0";
|
||||||
|
$resAll = $db->query($sqlAll);
|
||||||
|
$pending = 0;
|
||||||
|
if ($resAll) {
|
||||||
|
$obj = $db->fetch_object($resAll);
|
||||||
|
$pending = (int) $obj->cnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(array(
|
||||||
|
'pending' => $pending,
|
||||||
|
'incoming' => $incoming,
|
||||||
|
'incoming_total' => $incomingTotal,
|
||||||
|
));
|
||||||
|
|
||||||
|
$db->close();
|
||||||
|
|
@ -113,6 +113,36 @@ if ($reminderEnabled) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Payment matching notification
|
||||||
|
$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||||
|
if (!empty($bankAccountId)) {
|
||||||
|
$sqlNewCount = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
||||||
|
$sqlNewCount .= " WHERE entity IN (".getEntity('banktransaction').")";
|
||||||
|
$sqlNewCount .= " AND status = 0";
|
||||||
|
$resNewCount = $db->query($sqlNewCount);
|
||||||
|
$newCount = 0;
|
||||||
|
if ($resNewCount) {
|
||||||
|
$objNewCount = $db->fetch_object($resNewCount);
|
||||||
|
$newCount = (int) $objNewCount->cnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newCount > 0) {
|
||||||
|
print '<div class="info" style="border-left: 4px solid #2196F3; background: #e3f2fd; padding: 12px; margin-bottom: 15px;">';
|
||||||
|
print img_picto('', 'payment', 'class="pictofixedwidth"');
|
||||||
|
print '<strong>'.$langs->trans("PendingPaymentMatches", $newCount).'</strong>';
|
||||||
|
print '<br>'.$langs->trans("PendingPaymentMatchesDesc");
|
||||||
|
print ' <a class="butAction" style="margin-left: 10px;" href="'.dol_buildpath('/bankimport/confirm.php', 1).'?mainmenu=bank&leftmenu=bankimport">';
|
||||||
|
print $langs->trans("ReviewAndConfirm");
|
||||||
|
print '</a>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print '<div class="warning" style="margin-bottom: 15px;">';
|
||||||
|
print img_warning().' '.$langs->trans("NoBankAccountConfigured");
|
||||||
|
print ' <a href="'.dol_buildpath('/bankimport/admin/setup.php', 1).'">'.$langs->trans("GoToSetup").'</a>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
print '<div class="fichecenter"><div class="fichethirdleft">';
|
print '<div class="fichecenter"><div class="fichethirdleft">';
|
||||||
|
|
||||||
// -----------------------------------------------
|
// -----------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,11 @@ class BankImportStatement extends CommonObject
|
||||||
@unlink($this->filepath);
|
@unlink($this->filepath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete statement lines
|
||||||
|
$sqlLines = "DELETE FROM ".MAIN_DB_PREFIX."bankimport_statement_line";
|
||||||
|
$sqlLines .= " WHERE fk_statement = ".((int) $this->id);
|
||||||
|
$this->db->query($sqlLines);
|
||||||
|
|
||||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX."bankimport_statement";
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."bankimport_statement";
|
||||||
$sql .= " WHERE rowid = ".((int) $this->id);
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
|
@ -755,6 +760,531 @@ class BankImportStatement extends CommonObject
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile bank entries using parsed statement lines.
|
||||||
|
*
|
||||||
|
* Strategy: Match each statement_line (parsed from PDF) to a llx_bank entry
|
||||||
|
* by amount + date (with tolerance). This is authoritative because the
|
||||||
|
* statement lines come directly from the bank's PDF and represent exactly
|
||||||
|
* what transactions belong to this statement.
|
||||||
|
*
|
||||||
|
* Matching priority:
|
||||||
|
* 1. Exact amount + exact date
|
||||||
|
* 2. Exact amount + date within 4 days tolerance
|
||||||
|
*
|
||||||
|
* @param User $user User performing the reconciliation
|
||||||
|
* @param int $bankAccountId Dolibarr bank account ID (llx_bank_account.rowid)
|
||||||
|
* @return int Number of reconciled entries, or -1 on error
|
||||||
|
*/
|
||||||
|
public function reconcileBankEntries($user, $bankAccountId)
|
||||||
|
{
|
||||||
|
if (empty($this->id) || empty($bankAccountId)) {
|
||||||
|
$this->error = 'Missing required fields for reconciliation';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
|
||||||
|
|
||||||
|
// Format statement number: "NR/YYYY" (e.g., "3/2025")
|
||||||
|
$numReleve = $this->statement_number.'/'.$this->statement_year;
|
||||||
|
|
||||||
|
// Get statement lines
|
||||||
|
$lines = $this->getStatementLines();
|
||||||
|
if (!is_array($lines) || empty($lines)) {
|
||||||
|
// No statement lines parsed yet — nothing to reconcile
|
||||||
|
$this->copyToDolibarrStatementDir($bankAccountId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reconciled = 0;
|
||||||
|
$usedBankIds = array();
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$amount = (float) $line->amount;
|
||||||
|
$dateBooking = $line->date_booking; // YYYY-MM-DD string
|
||||||
|
|
||||||
|
// Step 1: Try exact amount + exact date
|
||||||
|
$sqlMatch = "SELECT b.rowid";
|
||||||
|
$sqlMatch .= " FROM ".MAIN_DB_PREFIX."bank as b";
|
||||||
|
$sqlMatch .= " WHERE b.fk_account = ".((int) $bankAccountId);
|
||||||
|
$sqlMatch .= " AND b.rappro = 0";
|
||||||
|
$sqlMatch .= " AND b.amount = ".$amount;
|
||||||
|
$sqlMatch .= " AND b.datev = '".$this->db->escape($dateBooking)."'";
|
||||||
|
if (!empty($usedBankIds)) {
|
||||||
|
$sqlMatch .= " AND b.rowid NOT IN (".implode(',', $usedBankIds).")";
|
||||||
|
}
|
||||||
|
$sqlMatch .= " LIMIT 1";
|
||||||
|
|
||||||
|
$resMatch = $this->db->query($sqlMatch);
|
||||||
|
$matched = false;
|
||||||
|
|
||||||
|
if ($resMatch && $this->db->num_rows($resMatch) > 0) {
|
||||||
|
$match = $this->db->fetch_object($resMatch);
|
||||||
|
$matched = $this->reconcileBankLine($user, $match->rowid, $numReleve, $line->rowid, $usedBankIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Try exact amount + date within 4 days tolerance
|
||||||
|
if (!$matched) {
|
||||||
|
$sqlMatch2 = "SELECT b.rowid, ABS(DATEDIFF(b.datev, '".$this->db->escape($dateBooking)."')) as date_diff";
|
||||||
|
$sqlMatch2 .= " FROM ".MAIN_DB_PREFIX."bank as b";
|
||||||
|
$sqlMatch2 .= " WHERE b.fk_account = ".((int) $bankAccountId);
|
||||||
|
$sqlMatch2 .= " AND b.rappro = 0";
|
||||||
|
$sqlMatch2 .= " AND b.amount = ".$amount;
|
||||||
|
$sqlMatch2 .= " AND ABS(DATEDIFF(b.datev, '".$this->db->escape($dateBooking)."')) <= 4";
|
||||||
|
if (!empty($usedBankIds)) {
|
||||||
|
$sqlMatch2 .= " AND b.rowid NOT IN (".implode(',', $usedBankIds).")";
|
||||||
|
}
|
||||||
|
$sqlMatch2 .= " ORDER BY date_diff ASC LIMIT 1";
|
||||||
|
|
||||||
|
$resMatch2 = $this->db->query($sqlMatch2);
|
||||||
|
if ($resMatch2 && $this->db->num_rows($resMatch2) > 0) {
|
||||||
|
$match2 = $this->db->fetch_object($resMatch2);
|
||||||
|
$matched = $this->reconcileBankLine($user, $match2->rowid, $numReleve, $line->rowid, $usedBankIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Match by supplier invoice numbers in description
|
||||||
|
if (!$matched && !empty($line->description)) {
|
||||||
|
$matched = $this->reconcileByInvoiceNumbers($user, $line, $bankAccountId, $numReleve, $usedBankIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($matched) {
|
||||||
|
$reconciled++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy PDF to Dolibarr's bank statement document directory
|
||||||
|
$this->copyToDolibarrStatementDir($bankAccountId);
|
||||||
|
|
||||||
|
return $reconciled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile a single bank line: set num_releve, rappro=1, link statement_line
|
||||||
|
*
|
||||||
|
* @param User $user User
|
||||||
|
* @param int $bankRowId llx_bank.rowid
|
||||||
|
* @param string $numReleve Statement number (e.g. "3/2025")
|
||||||
|
* @param int $lineRowId llx_bankimport_statement_line.rowid
|
||||||
|
* @param array &$usedBankIds Reference to array of already used bank IDs
|
||||||
|
* @return bool True if reconciled successfully
|
||||||
|
*/
|
||||||
|
private function reconcileBankLine($user, $bankRowId, $numReleve, $lineRowId, &$usedBankIds)
|
||||||
|
{
|
||||||
|
$bankLine = new AccountLine($this->db);
|
||||||
|
$bankLine->fetch($bankRowId);
|
||||||
|
$bankLine->num_releve = $numReleve;
|
||||||
|
|
||||||
|
$result = $bankLine->update_conciliation($user, 0, 1);
|
||||||
|
if ($result >= 0) {
|
||||||
|
$usedBankIds[] = (int) $bankRowId;
|
||||||
|
|
||||||
|
// Link statement line to this bank entry
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."bankimport_statement_line SET";
|
||||||
|
$sql .= " fk_bank = ".((int) $bankRowId);
|
||||||
|
$sql .= ", match_status = 'reconciled'";
|
||||||
|
$sql .= " WHERE rowid = ".((int) $lineRowId);
|
||||||
|
$this->db->query($sql);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a statement line as pending review (matched by invoice number but amount
|
||||||
|
* difference exceeds threshold). Sets fk_bank as candidate but does NOT reconcile
|
||||||
|
* the bank entry (rappro stays 0).
|
||||||
|
*
|
||||||
|
* @param int $bankRowId llx_bank.rowid (candidate)
|
||||||
|
* @param int $lineRowId llx_bankimport_statement_line.rowid
|
||||||
|
* @return bool True if saved
|
||||||
|
*/
|
||||||
|
private function markPendingReview($bankRowId, $lineRowId)
|
||||||
|
{
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."bankimport_statement_line SET";
|
||||||
|
$sql .= " fk_bank = ".((int) $bankRowId);
|
||||||
|
$sql .= ", match_status = 'pending_review'";
|
||||||
|
$sql .= " WHERE rowid = ".((int) $lineRowId);
|
||||||
|
|
||||||
|
return (bool) $this->db->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract supplier invoice numbers from statement line description
|
||||||
|
* and find matching llx_bank entries via the payment chain.
|
||||||
|
*
|
||||||
|
* Patterns recognized:
|
||||||
|
* - /INV/9009414207 (Firmenlastschrift)
|
||||||
|
* - /ADV/0014494147 (Avise)
|
||||||
|
* - Beleg-Nr.: 9008468982 (Überweisungsauftrag)
|
||||||
|
*
|
||||||
|
* Chain: ref_supplier → llx_facture_fourn → llx_paiementfourn_facturefourn
|
||||||
|
* → llx_paiementfourn → llx_bank_url → llx_bank
|
||||||
|
*
|
||||||
|
* @param User $user User
|
||||||
|
* @param object $line Statement line object
|
||||||
|
* @param int $bankAccountId Dolibarr bank account ID
|
||||||
|
* @param string $numReleve Statement number (e.g. "3/2025")
|
||||||
|
* @param array &$usedBankIds Reference to array of already used bank IDs
|
||||||
|
* @return bool True if at least one bank entry was reconciled
|
||||||
|
*/
|
||||||
|
private function reconcileByInvoiceNumbers($user, $line, $bankAccountId, $numReleve, &$usedBankIds)
|
||||||
|
{
|
||||||
|
$desc = $line->description.' '.$line->name;
|
||||||
|
|
||||||
|
// Extract invoice/reference numbers from description
|
||||||
|
$refNumbers = array();
|
||||||
|
|
||||||
|
// Pattern 1: /INV/XXXXXXXXXX
|
||||||
|
if (preg_match_all('/\/INV\/(\d+)/', $desc, $m)) {
|
||||||
|
$refNumbers = array_merge($refNumbers, $m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern 2: /ADV/XXXXXXXXXX
|
||||||
|
if (preg_match_all('/\/ADV\/(\d+)/', $desc, $m)) {
|
||||||
|
$refNumbers = array_merge($refNumbers, $m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern 3: Beleg-Nr.: XXXXXXXXXX or Beleg-Nr. XXXXXXXXXX
|
||||||
|
if (preg_match_all('/Beleg-Nr\.?\s*:?\s*(\d{5,})/', $desc, $m)) {
|
||||||
|
$refNumbers = array_merge($refNumbers, $m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($refNumbers)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$refNumbers = array_unique($refNumbers);
|
||||||
|
|
||||||
|
// Build escaped list for SQL IN clause
|
||||||
|
$escapedRefs = array();
|
||||||
|
foreach ($refNumbers as $ref) {
|
||||||
|
$escapedRefs[] = "'".$this->db->escape($ref)."'";
|
||||||
|
}
|
||||||
|
$inClause = implode(',', $escapedRefs);
|
||||||
|
|
||||||
|
// Find llx_bank entries linked to supplier payments for these invoice numbers
|
||||||
|
$sql = "SELECT DISTINCT b.rowid, b.amount";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX."bank as b";
|
||||||
|
$sql .= " JOIN ".MAIN_DB_PREFIX."bank_url as bu ON bu.fk_bank = b.rowid AND bu.type = 'payment_supplier'";
|
||||||
|
$sql .= " JOIN ".MAIN_DB_PREFIX."paiementfourn as pf ON pf.rowid = bu.url_id";
|
||||||
|
$sql .= " JOIN ".MAIN_DB_PREFIX."paiementfourn_facturefourn as pfff ON pfff.fk_paiementfourn = pf.rowid";
|
||||||
|
$sql .= " JOIN ".MAIN_DB_PREFIX."facture_fourn as ff ON ff.rowid = pfff.fk_facturefourn";
|
||||||
|
$sql .= " WHERE b.fk_account = ".((int) $bankAccountId);
|
||||||
|
$sql .= " AND b.rappro = 0";
|
||||||
|
$sql .= " AND ff.ref_supplier IN (".$inClause.")";
|
||||||
|
if (!empty($usedBankIds)) {
|
||||||
|
$sql .= " AND b.rowid NOT IN (".implode(',', $usedBankIds).")";
|
||||||
|
}
|
||||||
|
|
||||||
|
dol_syslog(get_class($this)."::reconcileByInvoiceNumbers refs=".implode(',', $refNumbers), LOG_DEBUG);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
|
||||||
|
if (!$resql) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$matched = false;
|
||||||
|
$stmtAmount = abs((float) $line->amount);
|
||||||
|
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$bankAmount = abs((float) $obj->amount);
|
||||||
|
$diff = abs($stmtAmount - $bankAmount);
|
||||||
|
|
||||||
|
if ($diff > 5.0) {
|
||||||
|
// Differenz > 5 EUR: nur als Kandidat markieren, nicht abgleichen
|
||||||
|
$this->markPendingReview($obj->rowid, $line->rowid);
|
||||||
|
dol_syslog(get_class($this)."::reconcileByInvoiceNumbers PENDING bank=".$obj->rowid." diff=".$diff, LOG_WARNING);
|
||||||
|
} else {
|
||||||
|
// Differenz <= 5 EUR: automatisch abgleichen
|
||||||
|
if ($this->reconcileBankLine($user, $obj->rowid, $numReleve, $line->rowid, $usedBankIds)) {
|
||||||
|
$matched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
|
||||||
|
return $matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the PDF file to Dolibarr's bank statement document directory
|
||||||
|
* so it appears in the Documents tab of account_statement_document.php
|
||||||
|
*
|
||||||
|
* Target: $conf->bank->dir_output."/".$bankAccountId."/statement/".dol_sanitizeFileName($numReleve)."/"
|
||||||
|
*
|
||||||
|
* @param int $bankAccountId Dolibarr bank account ID
|
||||||
|
* @return int 1 if OK, 0 if nothing to copy, -1 on error
|
||||||
|
*/
|
||||||
|
public function copyToDolibarrStatementDir($bankAccountId)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
if (empty($this->filepath) || !file_exists($this->filepath)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($bankAccountId) || empty($this->statement_number) || empty($this->statement_year)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$numReleve = $this->statement_number.'/'.$this->statement_year;
|
||||||
|
$targetDir = $conf->bank->dir_output.'/'.((int) $bankAccountId).'/statement/'.dol_sanitizeFileName($numReleve);
|
||||||
|
|
||||||
|
if (!is_dir($targetDir)) {
|
||||||
|
dol_mkdir($targetDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetFile = $targetDir.'/'.$this->filename;
|
||||||
|
|
||||||
|
// Don't copy if already exists with same size
|
||||||
|
if (file_exists($targetFile) && filesize($targetFile) == filesize($this->filepath)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = @copy($this->filepath, $targetFile);
|
||||||
|
if ($result) {
|
||||||
|
@chmod($targetFile, 0664);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->error = 'Failed to copy PDF to '.$targetDir;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse individual transaction lines from a PDF bank statement.
|
||||||
|
*
|
||||||
|
* Extracts booking date, value date, transaction type, amount, counterparty name
|
||||||
|
* and description text from VR-Bank PDF statement format.
|
||||||
|
*
|
||||||
|
* @param string $filepath Path to PDF file (uses $this->filepath if empty)
|
||||||
|
* @return array Array of transaction arrays, each with keys:
|
||||||
|
* date_booking, date_value, transaction_type, amount, name, description
|
||||||
|
*/
|
||||||
|
public function parsePdfTransactions($filepath = '')
|
||||||
|
{
|
||||||
|
if (empty($filepath)) {
|
||||||
|
$filepath = $this->filepath;
|
||||||
|
}
|
||||||
|
if (empty($filepath) || !file_exists($filepath)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$escapedPath = escapeshellarg($filepath);
|
||||||
|
$textlines = array();
|
||||||
|
exec("pdftotext -layout ".$escapedPath." - 2>/dev/null", $textlines);
|
||||||
|
|
||||||
|
// Determine statement year from metadata
|
||||||
|
$stmtYear = !empty($this->statement_year) ? (int) $this->statement_year : (int) date('Y');
|
||||||
|
|
||||||
|
$transactions = array();
|
||||||
|
$currentTx = null;
|
||||||
|
$inTransactionBlock = false;
|
||||||
|
$skipPageBreak = false; // True between "Übertrag auf" and "Übertrag von"
|
||||||
|
|
||||||
|
foreach ($textlines as $line) {
|
||||||
|
// Stop parsing at "Anlage" (fee detail section) or "Der ausgewiesene Kontostand"
|
||||||
|
if (preg_match('/^\s*Anlage\s+\d/', $line) || preg_match('/Der ausgewiesene Kontostand/', $line)) {
|
||||||
|
if ($currentTx !== null) {
|
||||||
|
$transactions[] = $currentTx;
|
||||||
|
$currentTx = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle page breaks: skip everything between "Übertrag auf Blatt" and "Übertrag von Blatt"
|
||||||
|
if (preg_match('/Übertrag\s+auf\s+Blatt/', $line)) {
|
||||||
|
// Save current transaction before page break
|
||||||
|
if ($currentTx !== null) {
|
||||||
|
$transactions[] = $currentTx;
|
||||||
|
$currentTx = null;
|
||||||
|
}
|
||||||
|
$skipPageBreak = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preg_match('/Übertrag\s+von\s+Blatt/', $line)) {
|
||||||
|
$skipPageBreak = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($skipPageBreak) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip blank lines and decorative lines
|
||||||
|
if (preg_match('/^\s*$/', $line) || preg_match('/────/', $line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect start of transaction block
|
||||||
|
if (preg_match('/Bu-Tag\s+Wert\s+Vorgang/', $line)) {
|
||||||
|
$inTransactionBlock = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$inTransactionBlock) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip balance lines
|
||||||
|
if (preg_match('/alter Kontostand/', $line) || preg_match('/neuer Kontostand/', $line)) {
|
||||||
|
if ($currentTx !== null) {
|
||||||
|
$transactions[] = $currentTx;
|
||||||
|
$currentTx = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction line: "DD.MM. DD.MM. Vorgangsart ... Betrag S/H"
|
||||||
|
if (preg_match('/^\s+(\d{2})\.(\d{2})\.\s+(\d{2})\.(\d{2})\.\s+(.+)/', $line, $m)) {
|
||||||
|
// Save previous transaction
|
||||||
|
if ($currentTx !== null) {
|
||||||
|
$transactions[] = $currentTx;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bookDay = (int) $m[1];
|
||||||
|
$bookMonth = (int) $m[2];
|
||||||
|
$valDay = (int) $m[3];
|
||||||
|
$valMonth = (int) $m[4];
|
||||||
|
$rest = $m[5];
|
||||||
|
|
||||||
|
$bookYear = $stmtYear;
|
||||||
|
$valYear = $stmtYear;
|
||||||
|
|
||||||
|
// Parse amount and S/H from the rest of the line
|
||||||
|
$amount = 0;
|
||||||
|
$txType = '';
|
||||||
|
|
||||||
|
if (preg_match('/^(.+?)\s+([\d.,]+)\s+(S|H)\s*$/', $rest, $am)) {
|
||||||
|
$txType = trim($am[1]);
|
||||||
|
$amount = self::parseGermanAmount($am[2]);
|
||||||
|
if ($am[3] === 'S') {
|
||||||
|
$amount = -$amount;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$txType = trim($rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build date strings
|
||||||
|
$dateBooking = sprintf('%04d-%02d-%02d', $bookYear, $bookMonth, $bookDay);
|
||||||
|
$dateValue = sprintf('%04d-%02d-%02d', $valYear, $valMonth, $valDay);
|
||||||
|
|
||||||
|
$currentTx = array(
|
||||||
|
'date_booking' => $dateBooking,
|
||||||
|
'date_value' => $dateValue,
|
||||||
|
'transaction_type' => $txType,
|
||||||
|
'amount' => $amount,
|
||||||
|
'name' => '',
|
||||||
|
'description' => '',
|
||||||
|
);
|
||||||
|
} elseif ($currentTx !== null) {
|
||||||
|
// Continuation line (counterparty name or description)
|
||||||
|
$detail = trim($line);
|
||||||
|
if (!empty($detail)) {
|
||||||
|
if (empty($currentTx['name'])) {
|
||||||
|
$currentTx['name'] = $detail;
|
||||||
|
} else {
|
||||||
|
$currentTx['description'] .= ($currentTx['description'] ? ' ' : '').$detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't forget the last transaction
|
||||||
|
if ($currentTx !== null) {
|
||||||
|
$transactions[] = $currentTx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save parsed transaction lines to the database.
|
||||||
|
* Deletes existing lines for this statement first, then inserts new ones.
|
||||||
|
*
|
||||||
|
* @param array $transactions Array from parsePdfTransactions()
|
||||||
|
* @return int Number of lines saved, or -1 on error
|
||||||
|
*/
|
||||||
|
public function saveStatementLines($transactions)
|
||||||
|
{
|
||||||
|
if (empty($this->id)) {
|
||||||
|
$this->error = 'Statement not saved yet';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// Delete existing lines for this statement
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."bankimport_statement_line";
|
||||||
|
$sql .= " WHERE fk_statement = ".((int) $this->id);
|
||||||
|
$this->db->query($sql);
|
||||||
|
|
||||||
|
$now = dol_now();
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($transactions as $i => $tx) {
|
||||||
|
$lineNum = $i + 1;
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."bankimport_statement_line (";
|
||||||
|
$sql .= "fk_statement, entity, line_number, date_booking, date_value,";
|
||||||
|
$sql .= "transaction_type, amount, currency, name, description, datec";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= ((int) $this->id).",";
|
||||||
|
$sql .= ((int) $this->entity).",";
|
||||||
|
$sql .= ((int) $lineNum).",";
|
||||||
|
$sql .= "'".$this->db->escape($tx['date_booking'])."',";
|
||||||
|
$sql .= (!empty($tx['date_value']) ? "'".$this->db->escape($tx['date_value'])."'" : "NULL").",";
|
||||||
|
$sql .= (!empty($tx['transaction_type']) ? "'".$this->db->escape($tx['transaction_type'])."'" : "NULL").",";
|
||||||
|
$sql .= ((float) $tx['amount']).",";
|
||||||
|
$sql .= "'EUR',";
|
||||||
|
$sql .= (!empty($tx['name']) ? "'".$this->db->escape($tx['name'])."'" : "NULL").",";
|
||||||
|
$sql .= (!empty($tx['description']) ? "'".$this->db->escape($tx['description'])."'" : "NULL").",";
|
||||||
|
$sql .= "'".$this->db->idate($now)."'";
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$count++;
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->commit();
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statement lines from database
|
||||||
|
*
|
||||||
|
* @return array Array of line objects, or -1 on error
|
||||||
|
*/
|
||||||
|
public function getStatementLines()
|
||||||
|
{
|
||||||
|
$sql = "SELECT rowid, fk_statement, line_number, date_booking, date_value,";
|
||||||
|
$sql .= " transaction_type, amount, currency, name, description, fk_bank, match_status";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX."bankimport_statement_line";
|
||||||
|
$sql .= " WHERE fk_statement = ".((int) $this->id);
|
||||||
|
$sql .= " ORDER BY line_number ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = array();
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$lines[] = $obj;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Link transactions to this statement based on date range and IBAN
|
* Link transactions to this statement based on date range and IBAN
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1019,6 +1019,193 @@ class BankImportTransaction extends CommonObject
|
||||||
return 0;
|
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
|
* Link transaction to a Dolibarr object
|
||||||
*
|
*
|
||||||
|
|
|
||||||
350
confirm.php
Normal file
350
confirm.php
Normal file
|
|
@ -0,0 +1,350 @@
|
||||||
|
<?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/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 '<div class="warning">';
|
||||||
|
print img_warning().' '.$langs->trans("ErrorNoBankAccountConfigured");
|
||||||
|
print ' <a href="'.dol_buildpath('/bankimport/admin/setup.php', 1).'">'.$langs->trans("GoToSetup").'</a>';
|
||||||
|
print '</div>';
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
print '<div class="opacitymedium" style="margin-bottom: 15px;">'.$langs->trans("PaymentConfirmationDesc").'</div>';
|
||||||
|
|
||||||
|
// 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 '<div class="tabsAction">';
|
||||||
|
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?action=confirmall&token='.newToken().'">';
|
||||||
|
print $langs->trans("ConfirmAllHighScore").' ('.$highScoreCount.')';
|
||||||
|
print '</a>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 '<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("Counterparty").'</th>';
|
||||||
|
print '<th class="right">'.$langs->trans("Amount").' ('.$langs->trans("Transaction").')</th>';
|
||||||
|
print '<th style="text-align: center; width: 30px;"></th>';
|
||||||
|
print '<th>'.$langs->trans("Invoice").'</th>';
|
||||||
|
print '<th>'.$langs->trans("ThirdParty").'</th>';
|
||||||
|
print '<th class="right">'.$langs->trans("Amount").' ('.$langs->trans("Invoice").')</th>';
|
||||||
|
print '<th class="center">'.$langs->trans("Score").'</th>';
|
||||||
|
print '<th>'.$langs->trans("MatchReason").'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans("Action").'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
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 '<tr class="oddeven">';
|
||||||
|
|
||||||
|
// Transaction date
|
||||||
|
print '<td class="nowraponall">'.dol_print_date($trans->date_trans, 'day').'</td>';
|
||||||
|
|
||||||
|
// Counterparty name + description
|
||||||
|
print '<td class="tdoverflowmax200">';
|
||||||
|
print '<a href="'.dol_buildpath('/bankimport/card.php', 1).'?id='.$trans->id.'">'.dol_escape_htmltag(dol_trunc($trans->name, 30)).'</a>';
|
||||||
|
if ($trans->description) {
|
||||||
|
print '<br><span class="opacitymedium small">'.dol_escape_htmltag(dol_trunc($trans->description, 50)).'</span>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Transaction amount
|
||||||
|
print '<td class="right nowraponall">';
|
||||||
|
if ($trans->amount >= 0) {
|
||||||
|
print '<span style="color: green; font-weight: bold;">+'.price($trans->amount, 0, $langs, 1, -1, 2, $trans->currency).'</span>';
|
||||||
|
} else {
|
||||||
|
print '<span style="color: red; font-weight: bold;">'.price($trans->amount, 0, $langs, 1, -1, 2, $trans->currency).'</span>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Arrow
|
||||||
|
print '<td class="center" style="font-size: 1.3em;">↔</td>';
|
||||||
|
|
||||||
|
// Invoice reference
|
||||||
|
print '<td class="nowraponall">';
|
||||||
|
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 '</td>';
|
||||||
|
|
||||||
|
// Third party
|
||||||
|
print '<td class="tdoverflowmax150">';
|
||||||
|
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 '</td>';
|
||||||
|
|
||||||
|
// Invoice amount
|
||||||
|
print '<td class="right nowraponall">'.price($bestMatch['amount'], 0, $langs, 1, -1, 2, 'EUR').'</td>';
|
||||||
|
|
||||||
|
// Score
|
||||||
|
print '<td class="center"><span style="color: '.$scoreColor.'; font-weight: bold; font-size: 1.1em;">'.$bestMatch['match_score'].'%</span></td>';
|
||||||
|
|
||||||
|
// Match reasons
|
||||||
|
print '<td>';
|
||||||
|
if (!empty($bestMatch['match_reasons'])) {
|
||||||
|
foreach ($bestMatch['match_reasons'] as $reason) {
|
||||||
|
$label = $reasonLabels[$reason] ?? $reason;
|
||||||
|
print '<span class="badge badge-secondary">'.$label.'</span> ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
print '<td class="center nowraponall">';
|
||||||
|
// Confirm payment button
|
||||||
|
print '<a class="butActionSmall" href="'.$_SERVER["PHP_SELF"].'?action=confirmpayment&transid='.$trans->id.'&matchtype='.urlencode($bestMatch['type']).'&matchid='.$bestMatch['id'].'&token='.newToken().'">';
|
||||||
|
print $langs->trans("ConfirmPayment");
|
||||||
|
print '</a>';
|
||||||
|
print '<br>';
|
||||||
|
// Ignore button
|
||||||
|
print '<a class="butActionSmall button-cancel" style="margin-top: 3px;" href="'.$_SERVER["PHP_SELF"].'?action=ignore&transid='.$trans->id.'&token='.newToken().'">';
|
||||||
|
print $langs->trans("SetAsIgnored");
|
||||||
|
print '</a>';
|
||||||
|
|
||||||
|
// Show alternatives if multiple matches
|
||||||
|
if (count($pm['matches']) > 1) {
|
||||||
|
print '<br><a class="small opacitymedium" style="margin-top: 3px;" href="'.dol_buildpath('/bankimport/card.php', 1).'?id='.$trans->id.'&action=findmatches&token='.newToken().'">';
|
||||||
|
print '+'.($count = count($pm['matches']) - 1).' '.$langs->trans("Alternatives");
|
||||||
|
print '</a>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
print '</div>';
|
||||||
|
} else {
|
||||||
|
print '<div class="opacitymedium" style="padding: 20px; text-align: center;">';
|
||||||
|
print $langs->trans("NoNewMatchesFound");
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show unmatched transactions count
|
||||||
|
if (!empty($noMatches)) {
|
||||||
|
print '<br>';
|
||||||
|
print '<div class="info">';
|
||||||
|
print img_picto('', 'info', 'class="pictofixedwidth"');
|
||||||
|
print $langs->trans("UnmatchedTransactions", count($noMatches));
|
||||||
|
print ' <a href="'.dol_buildpath('/bankimport/list.php', 1).'?search_status=0">'.$langs->trans("ShowAll").'</a>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
167
core/boxes/box_bankimport_pending.php
Normal file
167
core/boxes/box_bankimport_pending.php
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
<?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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
include_once DOL_DOCUMENT_ROOT.'/core/boxes/modules_boxes.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard widget showing pending bank transaction matches
|
||||||
|
*/
|
||||||
|
class box_bankimport_pending extends ModeleBoxes
|
||||||
|
{
|
||||||
|
public $boxcode = "bankimport_pending";
|
||||||
|
public $boximg = "fa-money-check-alt";
|
||||||
|
public $boxlabel = "BoxBankImportPending";
|
||||||
|
public $depends = array("bankimport");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
* @param string $param More parameters
|
||||||
|
*/
|
||||||
|
public function __construct($db, $param = '')
|
||||||
|
{
|
||||||
|
global $user;
|
||||||
|
$this->db = $db;
|
||||||
|
$this->hidden = !$user->hasRight('bankimport', 'read');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load data into info_box_contents array to show on dashboard
|
||||||
|
*
|
||||||
|
* @param int $max Maximum number of records to load
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function loadBox($max = 5)
|
||||||
|
{
|
||||||
|
global $user, $langs, $conf;
|
||||||
|
|
||||||
|
$langs->loadLangs(array("bankimport@bankimport", "banks"));
|
||||||
|
|
||||||
|
$this->max = $max;
|
||||||
|
|
||||||
|
// Box header
|
||||||
|
$this->info_box_head = array(
|
||||||
|
'text' => $langs->trans("BoxBankImportPending"),
|
||||||
|
'sublink' => dol_buildpath('/bankimport/confirm.php', 1).'?mainmenu=bank&leftmenu=bankimport',
|
||||||
|
'subtext' => $langs->trans("ReviewAndConfirm"),
|
||||||
|
'subpicto' => 'payment',
|
||||||
|
);
|
||||||
|
|
||||||
|
$line = 0;
|
||||||
|
$bankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||||
|
|
||||||
|
if (empty($bankAccountId)) {
|
||||||
|
// No bank account configured
|
||||||
|
$this->info_box_contents[$line][] = array(
|
||||||
|
'td' => 'class="center" colspan="4"',
|
||||||
|
'text' => img_warning().' '.$langs->trans("NoBankAccountConfigured"),
|
||||||
|
'url' => dol_buildpath('/bankimport/admin/setup.php', 1),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count new (unmatched) transactions
|
||||||
|
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
||||||
|
$sql .= " WHERE entity IN (".getEntity('banktransaction').")";
|
||||||
|
$sql .= " AND status = 0";
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
$newCount = 0;
|
||||||
|
if ($resql) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
$newCount = (int) $obj->cnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newCount > 0) {
|
||||||
|
// Summary line: X transactions pending
|
||||||
|
$this->info_box_contents[$line][] = array(
|
||||||
|
'td' => 'class="left" colspan="3"',
|
||||||
|
'text' => '<strong>'.$langs->trans("PendingPaymentMatches", $newCount).'</strong>',
|
||||||
|
'asis' => 1,
|
||||||
|
);
|
||||||
|
$this->info_box_contents[$line][] = array(
|
||||||
|
'td' => 'class="right"',
|
||||||
|
'text' => '<a class="butActionSmall" href="'.dol_buildpath('/bankimport/confirm.php', 1).'?mainmenu=bank&leftmenu=bankimport">'.$langs->trans("ReviewAndConfirm").'</a>',
|
||||||
|
'asis' => 1,
|
||||||
|
);
|
||||||
|
$line++;
|
||||||
|
|
||||||
|
// Show last few unmatched transactions
|
||||||
|
$sql2 = "SELECT t.rowid, t.date_trans, t.name, t.amount, t.currency";
|
||||||
|
$sql2 .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction as t";
|
||||||
|
$sql2 .= " WHERE t.entity IN (".getEntity('banktransaction').")";
|
||||||
|
$sql2 .= " AND t.status = 0 AND t.amount > 0";
|
||||||
|
$sql2 .= " ORDER BY t.date_trans DESC";
|
||||||
|
$sql2 .= $this->db->plimit($max, 0);
|
||||||
|
|
||||||
|
$resql2 = $this->db->query($sql2);
|
||||||
|
if ($resql2) {
|
||||||
|
$num = $this->db->num_rows($resql2);
|
||||||
|
$i = 0;
|
||||||
|
while ($i < $num && $line < $max + 1) {
|
||||||
|
$obj2 = $this->db->fetch_object($resql2);
|
||||||
|
if (!$obj2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date
|
||||||
|
$this->info_box_contents[$line][] = array(
|
||||||
|
'td' => 'class="nowraponall"',
|
||||||
|
'text' => dol_print_date($this->db->jdate($obj2->date_trans), 'day'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Name
|
||||||
|
$this->info_box_contents[$line][] = array(
|
||||||
|
'td' => 'class="tdoverflowmax150"',
|
||||||
|
'text' => dol_trunc($obj2->name, 28),
|
||||||
|
'url' => dol_buildpath('/bankimport/card.php', 1).'?id='.$obj2->rowid.'&mainmenu=bank&leftmenu=bankimport',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
$amountColor = $obj2->amount >= 0 ? 'color: green;' : 'color: red;';
|
||||||
|
$amountPrefix = $obj2->amount >= 0 ? '+' : '';
|
||||||
|
$this->info_box_contents[$line][] = array(
|
||||||
|
'td' => 'class="right nowraponall"',
|
||||||
|
'text' => '<span style="'.$amountColor.'">'.$amountPrefix.price($obj2->amount, 0, $langs, 1, -1, 2, $obj2->currency).'</span>',
|
||||||
|
'asis' => 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Status badge
|
||||||
|
$this->info_box_contents[$line][] = array(
|
||||||
|
'td' => 'class="right"',
|
||||||
|
'text' => '<span class="badge badge-status4 badge-status">'.$langs->trans("New").'</span>',
|
||||||
|
'asis' => 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
$line++;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No pending transactions
|
||||||
|
$this->info_box_contents[$line][] = array(
|
||||||
|
'td' => 'class="center opacitymedium" colspan="4"',
|
||||||
|
'text' => $langs->trans("NoNewMatchesFound"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the box
|
||||||
|
*
|
||||||
|
* @param array|null $head Optional head array
|
||||||
|
* @param array|null $contents Optional contents array
|
||||||
|
* @param int $nooutput No print, return output
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function showBox($head = null, $contents = null, $nooutput = 0)
|
||||||
|
{
|
||||||
|
return parent::showBox($this->info_box_head, $this->info_box_contents, $nooutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'
|
$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'
|
// 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.5';
|
||||||
// Url to the file with your last numberversion of this module
|
// Url to the file with your last numberversion of this module
|
||||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@ class modBankImport extends DolibarrModules
|
||||||
),
|
),
|
||||||
// Set this to relative path of js file if module must load a js on all pages
|
// Set this to relative path of js file if module must load a js on all pages
|
||||||
'js' => array(
|
'js' => array(
|
||||||
// '/bankimport/js/bankimport.js.php',
|
'/bankimport/js/bankimport_notify.js.php',
|
||||||
),
|
),
|
||||||
// Set here all hooks context managed by module. To find available hook context, make a "grep -r '>initHooks(' *" on source code. You can also set hook context to 'all'
|
// Set here all hooks context managed by module. To find available hook context, make a "grep -r '>initHooks(' *" on source code. You can also set hook context to 'all'
|
||||||
/* BEGIN MODULEBUILDER HOOKSCONTEXTS */
|
/* BEGIN MODULEBUILDER HOOKSCONTEXTS */
|
||||||
|
|
@ -255,12 +255,11 @@ class modBankImport extends DolibarrModules
|
||||||
// Add here list of php file(s) stored in bankimport/core/boxes that contains a class to show a widget.
|
// Add here list of php file(s) stored in bankimport/core/boxes that contains a class to show a widget.
|
||||||
/* BEGIN MODULEBUILDER WIDGETS */
|
/* BEGIN MODULEBUILDER WIDGETS */
|
||||||
$this->boxes = array(
|
$this->boxes = array(
|
||||||
// 0 => array(
|
0 => array(
|
||||||
// 'file' => 'bankimportwidget1.php@bankimport',
|
'file' => 'box_bankimport_pending.php@bankimport',
|
||||||
// 'note' => 'Widget provided by BankImport',
|
'note' => 'Pending bank transaction matches',
|
||||||
// 'enabledbydefaulton' => 'Home',
|
'enabledbydefaulton' => 'Home',
|
||||||
// ),
|
),
|
||||||
// ...
|
|
||||||
);
|
);
|
||||||
/* END MODULEBUILDER WIDGETS */
|
/* END MODULEBUILDER WIDGETS */
|
||||||
|
|
||||||
|
|
@ -369,6 +368,21 @@ class modBankImport extends DolibarrModules
|
||||||
'target' => '',
|
'target' => '',
|
||||||
'user' => 2,
|
'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(
|
$this->menu[$r++] = array(
|
||||||
'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
|
'fk_menu' => 'fk_mainmenu=bank,fk_leftmenu=bankimport',
|
||||||
'type' => 'left',
|
'type' => 'left',
|
||||||
|
|
@ -378,7 +392,7 @@ class modBankImport extends DolibarrModules
|
||||||
'leftmenu' => 'bankimport_pdfstatements',
|
'leftmenu' => 'bankimport_pdfstatements',
|
||||||
'url' => '/bankimport/pdfstatements.php?mainmenu=bank&leftmenu=bankimport',
|
'url' => '/bankimport/pdfstatements.php?mainmenu=bank&leftmenu=bankimport',
|
||||||
'langs' => 'bankimport@bankimport',
|
'langs' => 'bankimport@bankimport',
|
||||||
'position' => 203,
|
'position' => 204,
|
||||||
'enabled' => 'isModEnabled("bankimport")',
|
'enabled' => 'isModEnabled("bankimport")',
|
||||||
'perms' => '$user->hasRight("bankimport", "read")',
|
'perms' => '$user->hasRight("bankimport", "read")',
|
||||||
'target' => '',
|
'target' => '',
|
||||||
|
|
|
||||||
174
js/bankimport_notify.js.php
Normal file
174
js/bankimport_notify.js.php
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
<?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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaScript for browser push notifications about incoming payments
|
||||||
|
* Loaded on every Dolibarr page via module_parts['js']
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Define MIME type
|
||||||
|
if (!defined('NOTOKENRENEWAL')) {
|
||||||
|
define('NOTOKENRENEWAL', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREMENU')) {
|
||||||
|
define('NOREQUIREMENU', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREHTML')) {
|
||||||
|
define('NOREQUIREHTML', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREAJAX')) {
|
||||||
|
define('NOREQUIREAJAX', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
$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";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/javascript; charset=UTF-8');
|
||||||
|
header('Cache-Control: max-age=3600');
|
||||||
|
|
||||||
|
if (!isModEnabled('bankimport') || empty($user->id) || !$user->hasRight('bankimport', 'read')) {
|
||||||
|
echo '/* bankimport: no access */';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkUrl = dol_buildpath('/bankimport/ajax/checkpending.php', 1);
|
||||||
|
$confirmUrl = dol_buildpath('/bankimport/confirm.php', 1).'?mainmenu=bank&leftmenu=bankimport';
|
||||||
|
$checkInterval = 5 * 60 * 1000; // 5 Minuten
|
||||||
|
?>
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var STORAGE_KEY = 'bankimport_last_pending';
|
||||||
|
var CHECK_URL = <?php echo json_encode($checkUrl); ?>;
|
||||||
|
var CONFIRM_URL = <?php echo json_encode($confirmUrl); ?>;
|
||||||
|
var CHECK_INTERVAL = <?php echo $checkInterval; ?>;
|
||||||
|
|
||||||
|
// Erst nach Seitenload starten
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
// Berechtigung anfragen beim ersten Mal
|
||||||
|
if ('Notification' in window && Notification.permission === 'default') {
|
||||||
|
// Dezent um Berechtigung bitten - nicht sofort, sondern nach 10 Sekunden
|
||||||
|
setTimeout(function() {
|
||||||
|
Notification.requestPermission();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sofort prüfen
|
||||||
|
checkPending();
|
||||||
|
|
||||||
|
// Regelmäßig prüfen
|
||||||
|
setInterval(checkPending, CHECK_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPending() {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', CHECK_URL, true);
|
||||||
|
xhr.timeout = 15000;
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status !== 200) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var data = JSON.parse(xhr.responseText);
|
||||||
|
} catch(e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastKnown = parseInt(localStorage.getItem(STORAGE_KEY) || '0', 10);
|
||||||
|
var currentPending = data.pending || 0;
|
||||||
|
var incoming = data.incoming || 0;
|
||||||
|
var incomingTotal = data.incoming_total || 0;
|
||||||
|
|
||||||
|
// Neue Buchungen seit letztem Check?
|
||||||
|
if (currentPending > lastKnown && currentPending > 0) {
|
||||||
|
var newCount = currentPending - lastKnown;
|
||||||
|
|
||||||
|
if (incoming > 0) {
|
||||||
|
showNotification(
|
||||||
|
'Zahlungseingang',
|
||||||
|
incoming + ' Zahlungseingang' + (incoming > 1 ? 'e' : '') + ' (' + formatAmount(incomingTotal) + ' €)\nBestätigung erforderlich',
|
||||||
|
'incoming'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showNotification(
|
||||||
|
'Bankimport',
|
||||||
|
newCount + ' neue Buchung' + (newCount > 1 ? 'en' : '') + ' warten auf Zuordnung',
|
||||||
|
'pending'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktuellen Stand merken
|
||||||
|
localStorage.setItem(STORAGE_KEY, currentPending.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = function() {};
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(title, body, type) {
|
||||||
|
if (!('Notification' in window)) return;
|
||||||
|
if (Notification.permission !== 'granted') return;
|
||||||
|
|
||||||
|
var icon = type === 'incoming'
|
||||||
|
? '/theme/common/mime/money.png'
|
||||||
|
: '/theme/common/mime/doc.png';
|
||||||
|
|
||||||
|
var notification = new Notification(title, {
|
||||||
|
body: body,
|
||||||
|
icon: icon,
|
||||||
|
tag: 'bankimport-' + type,
|
||||||
|
requireInteraction: true
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.onclick = function() {
|
||||||
|
window.focus();
|
||||||
|
window.location.href = CONFIRM_URL;
|
||||||
|
notification.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nach 30 Sekunden automatisch schließen
|
||||||
|
setTimeout(function() {
|
||||||
|
notification.close();
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(amount) {
|
||||||
|
return parseFloat(amount).toFixed(2).replace('.', ',').replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
@ -246,3 +246,69 @@ ViewPDFStatement = PDF-Kontoauszug anzeigen
|
||||||
PermBankImportRead = Bankimport: Buchungen und Kontoauszüge ansehen
|
PermBankImportRead = Bankimport: Buchungen und Kontoauszüge ansehen
|
||||||
PermBankImportWrite = Bankimport: Kontoauszüge abrufen und PDF hochladen
|
PermBankImportWrite = Bankimport: Kontoauszüge abrufen und PDF hochladen
|
||||||
PermBankImportDelete = Bankimport: Buchungen und Kontoauszüge löschen
|
PermBankImportDelete = Bankimport: Buchungen und Kontoauszüge löschen
|
||||||
|
|
||||||
|
#
|
||||||
|
# Zahlungsabgleich
|
||||||
|
#
|
||||||
|
PaymentConfirmation = Zahlungsabgleich
|
||||||
|
PaymentConfirmationDesc = Bankbuchungen mit Rechnungen abgleichen und Zahlungen in Dolibarr erstellen.
|
||||||
|
PendingPaymentMatches = %s neue Bankbuchungen warten auf Zuordnung
|
||||||
|
PendingPaymentMatchesDesc = Prüfen und bestätigen Sie die Zuordnungen, um Zahlungen in Dolibarr zu erstellen.
|
||||||
|
ReviewAndConfirm = Zuordnungen prüfen
|
||||||
|
ConfirmPayment = Zahlung bestätigen
|
||||||
|
ConfirmAllHighScore = Alle sicheren Zuordnungen bestätigen (Score >= 80%%)
|
||||||
|
PaymentCreatedSuccessfully = Zahlung erstellt: %s - %s
|
||||||
|
PaymentCreatedByBankImport = Automatisch erstellt durch Bankimport
|
||||||
|
ErrorNoBankAccountConfigured = Kein Dolibarr-Bankkonto konfiguriert. Bitte in den Einstellungen zuordnen.
|
||||||
|
NoBankAccountConfigured = Es ist noch kein Dolibarr-Bankkonto zugeordnet.
|
||||||
|
TransactionAlreadyProcessed = Buchung wurde bereits verarbeitet
|
||||||
|
PaymentsCreatedSummary = %s Zahlungen erstellt, %s fehlgeschlagen
|
||||||
|
NoNewMatchesFound = Keine neuen Zuordnungen gefunden
|
||||||
|
Alternatives = weitere Zuordnung(en)
|
||||||
|
UnmatchedTransactions = %s neue Buchungen ohne Rechnungszuordnung
|
||||||
|
InvoiceAlreadyPaid = Rechnung ist bereits vollständig bezahlt
|
||||||
|
|
||||||
|
#
|
||||||
|
# Bankkonto-Zuordnung
|
||||||
|
#
|
||||||
|
BankAccountMapping = Dolibarr-Bankkonto Zuordnung
|
||||||
|
DolibarrBankAccount = Dolibarr Bankkonto
|
||||||
|
DolibarrBankAccountHelp = Wählen Sie das Dolibarr-Bankkonto, das der konfigurierten IBAN entspricht. Zahlungen werden auf dieses Konto gebucht.
|
||||||
|
SelectBankAccount = -- Bankkonto auswählen --
|
||||||
|
|
||||||
|
#
|
||||||
|
# Kontoauszugsabgleich
|
||||||
|
#
|
||||||
|
BankEntriesReconciled = %s Bankbuchungen mit Auszug %s abgeglichen
|
||||||
|
BankEntriesReconciledTotal = %s Bankbuchungen über %s Kontoauszüge abgeglichen
|
||||||
|
ReconcileStatement = Kontoauszug abgleichen
|
||||||
|
ReconcileAllStatements = Alle Kontoauszüge abgleichen
|
||||||
|
NoBankEntriesToReconcile = Keine offenen Bankbuchungen zum Abgleichen gefunden
|
||||||
|
StatementMissingDates = Kontoauszug hat keinen Zeitraum (Von/Bis) - Abgleich nicht möglich
|
||||||
|
|
||||||
|
#
|
||||||
|
# Kontoauszugs-Positionen (Statement Lines)
|
||||||
|
#
|
||||||
|
StatementLinesExtracted = %s Buchungszeilen aus Auszug %s extrahiert
|
||||||
|
StatementLines = Buchungszeilen
|
||||||
|
BookingDate = Buchungstag
|
||||||
|
ValueDate = Wertstellung
|
||||||
|
TransactionType = Vorgangsart
|
||||||
|
NoStatementLines = Keine Buchungszeilen vorhanden
|
||||||
|
|
||||||
|
#
|
||||||
|
# Ausstehende Zuordnungen (Pending Review)
|
||||||
|
#
|
||||||
|
PendingReconciliationMatches = Zuordnungen mit Betragsabweichung
|
||||||
|
PendingReconciliationMatchesDesc = Diese Zuordnungen wurden über Rechnungsnummern erkannt, aber der Betrag weicht um mehr als 5 EUR ab. Bitte prüfen und bestätigen.
|
||||||
|
AmountStatement = Betrag (Auszug)
|
||||||
|
AmountDolibarr = Betrag (Dolibarr)
|
||||||
|
Difference = Differenz
|
||||||
|
BankEntry = Bank-Eintrag
|
||||||
|
ReconciliationConfirmed = Zuordnung bestätigt und abgeglichen
|
||||||
|
Confirm = Bestätigen
|
||||||
|
|
||||||
|
#
|
||||||
|
# Dashboard-Widget
|
||||||
|
#
|
||||||
|
BoxBankImportPending = Bankimport - Offene Zuordnungen
|
||||||
|
|
|
||||||
|
|
@ -142,3 +142,69 @@ BankImportAboutPage = BankImport about page
|
||||||
# Home
|
# Home
|
||||||
#
|
#
|
||||||
BankImportArea = BankImport Home
|
BankImportArea = BankImport Home
|
||||||
|
|
||||||
|
#
|
||||||
|
# Payment Confirmation
|
||||||
|
#
|
||||||
|
PaymentConfirmation = Payment Matching
|
||||||
|
PaymentConfirmationDesc = Match bank transactions to invoices and create payments in Dolibarr.
|
||||||
|
PendingPaymentMatches = %s new bank transactions waiting for matching
|
||||||
|
PendingPaymentMatchesDesc = Review and confirm matches to create payments in Dolibarr.
|
||||||
|
ReviewAndConfirm = Review matches
|
||||||
|
ConfirmPayment = Confirm payment
|
||||||
|
ConfirmAllHighScore = Confirm all high-score matches (score >= 80%%)
|
||||||
|
PaymentCreatedSuccessfully = Payment created: %s - %s
|
||||||
|
PaymentCreatedByBankImport = Automatically created by BankImport
|
||||||
|
ErrorNoBankAccountConfigured = No Dolibarr bank account configured. Please configure in settings.
|
||||||
|
NoBankAccountConfigured = No Dolibarr bank account mapped yet.
|
||||||
|
TransactionAlreadyProcessed = Transaction already processed
|
||||||
|
PaymentsCreatedSummary = %s payments created, %s failed
|
||||||
|
NoNewMatchesFound = No new matches found
|
||||||
|
Alternatives = more match(es)
|
||||||
|
UnmatchedTransactions = %s new transactions without invoice match
|
||||||
|
InvoiceAlreadyPaid = Invoice is already fully paid
|
||||||
|
|
||||||
|
#
|
||||||
|
# Bank Account Mapping
|
||||||
|
#
|
||||||
|
BankAccountMapping = Dolibarr Bank Account Mapping
|
||||||
|
DolibarrBankAccount = Dolibarr Bank Account
|
||||||
|
DolibarrBankAccountHelp = Select the Dolibarr bank account corresponding to the configured IBAN. Payments will be booked to this account.
|
||||||
|
SelectBankAccount = -- Select bank account --
|
||||||
|
|
||||||
|
#
|
||||||
|
# Statement Reconciliation
|
||||||
|
#
|
||||||
|
BankEntriesReconciled = %s bank entries reconciled with statement %s
|
||||||
|
BankEntriesReconciledTotal = %s bank entries reconciled across %s statements
|
||||||
|
ReconcileStatement = Reconcile statement
|
||||||
|
ReconcileAllStatements = Reconcile all statements
|
||||||
|
NoBankEntriesToReconcile = No open bank entries found to reconcile
|
||||||
|
StatementMissingDates = Statement has no date range (from/to) - reconciliation not possible
|
||||||
|
|
||||||
|
#
|
||||||
|
# Statement Lines
|
||||||
|
#
|
||||||
|
StatementLinesExtracted = %s transaction lines extracted from statement %s
|
||||||
|
StatementLines = Transaction Lines
|
||||||
|
BookingDate = Booking Date
|
||||||
|
ValueDate = Value Date
|
||||||
|
TransactionType = Transaction Type
|
||||||
|
NoStatementLines = No transaction lines available
|
||||||
|
|
||||||
|
#
|
||||||
|
# Pending Review Matches
|
||||||
|
#
|
||||||
|
PendingReconciliationMatches = Matches with Amount Deviation
|
||||||
|
PendingReconciliationMatchesDesc = These matches were found via invoice numbers but the amount differs by more than 5 EUR. Please review and confirm.
|
||||||
|
AmountStatement = Amount (Statement)
|
||||||
|
AmountDolibarr = Amount (Dolibarr)
|
||||||
|
Difference = Difference
|
||||||
|
BankEntry = Bank Entry
|
||||||
|
ReconciliationConfirmed = Match confirmed and reconciled
|
||||||
|
Confirm = Confirm
|
||||||
|
|
||||||
|
#
|
||||||
|
# Dashboard Widget
|
||||||
|
#
|
||||||
|
BoxBankImportPending = BankImport - Pending Matches
|
||||||
|
|
|
||||||
|
|
@ -249,9 +249,33 @@ if ($action == 'upload' && !empty($_FILES['pdffile'])) {
|
||||||
$result = $stmt->create($user);
|
$result = $stmt->create($user);
|
||||||
|
|
||||||
if ($result > 0) {
|
if ($result > 0) {
|
||||||
// Link matching transactions to this statement
|
// Link matching FinTS transactions to this statement
|
||||||
$linked = $stmt->linkTransactions();
|
$linked = $stmt->linkTransactions();
|
||||||
$totalLinked += max(0, $linked);
|
$totalLinked += max(0, $linked);
|
||||||
|
|
||||||
|
// Parse PDF transaction lines and save to database
|
||||||
|
$pdfLines = $stmt->parsePdfTransactions();
|
||||||
|
if (!empty($pdfLines)) {
|
||||||
|
$linesSaved = $stmt->saveStatementLines($pdfLines);
|
||||||
|
if ($linesSaved > 0) {
|
||||||
|
setEventMessages($langs->trans("StatementLinesExtracted", $linesSaved, $stmt->statement_number.'/'.$stmt->statement_year), null, 'mesgs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy PDF to Dolibarr's bank statement document directory
|
||||||
|
$uploadBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||||
|
if ($uploadBankAccountId > 0) {
|
||||||
|
$stmt->copyToDolibarrStatementDir($uploadBankAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconcile bank entries if bank account is configured
|
||||||
|
if ($uploadBankAccountId > 0) {
|
||||||
|
$reconciledCount = $stmt->reconcileBankEntries($user, $uploadBankAccountId);
|
||||||
|
if ($reconciledCount > 0) {
|
||||||
|
setEventMessages($langs->trans("BankEntriesReconciled", $reconciledCount, $stmt->statement_number.'/'.$stmt->statement_year), null, 'mesgs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$uploadedCount++;
|
$uploadedCount++;
|
||||||
$lastYear = $stmt->statement_year;
|
$lastYear = $stmt->statement_year;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -329,6 +353,123 @@ if ($action == 'view') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reconcile single statement
|
||||||
|
if ($action == 'reconcile') {
|
||||||
|
if (!$user->hasRight('bankimport', 'write')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
$id = GETPOSTINT('id');
|
||||||
|
$reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||||
|
|
||||||
|
if (empty($reconcileBankAccountId)) {
|
||||||
|
setEventMessages($langs->trans("ErrorNoBankAccountConfigured"), null, 'errors');
|
||||||
|
} elseif ($statement->fetch($id) > 0) {
|
||||||
|
// Parse statement lines if not yet done
|
||||||
|
$existingLines = $statement->getStatementLines();
|
||||||
|
if (is_array($existingLines) && empty($existingLines)) {
|
||||||
|
$pdfLines = $statement->parsePdfTransactions();
|
||||||
|
if (!empty($pdfLines)) {
|
||||||
|
$statement->saveStatementLines($pdfLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reconciledCount = $statement->reconcileBankEntries($user, $reconcileBankAccountId);
|
||||||
|
if ($reconciledCount > 0) {
|
||||||
|
setEventMessages($langs->trans("BankEntriesReconciled", $reconciledCount, $statement->statement_number.'/'.$statement->statement_year), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($langs->trans("NoBankEntriesToReconcile"), null, 'warnings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconcile all statements
|
||||||
|
if ($action == 'reconcileall') {
|
||||||
|
if (!$user->hasRight('bankimport', 'write')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
$reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||||
|
|
||||||
|
if (empty($reconcileBankAccountId)) {
|
||||||
|
setEventMessages($langs->trans("ErrorNoBankAccountConfigured"), null, 'errors');
|
||||||
|
} else {
|
||||||
|
$allStatements = $statement->fetchAll('statement_year,statement_number', 'ASC', 0, 0, array());
|
||||||
|
$totalReconciled = 0;
|
||||||
|
$stmtCount = 0;
|
||||||
|
|
||||||
|
if (is_array($allStatements)) {
|
||||||
|
foreach ($allStatements as $stmt) {
|
||||||
|
// Parse statement lines if not yet done
|
||||||
|
$existingLines = $stmt->getStatementLines();
|
||||||
|
if (is_array($existingLines) && empty($existingLines)) {
|
||||||
|
$pdfLines = $stmt->parsePdfTransactions();
|
||||||
|
if (!empty($pdfLines)) {
|
||||||
|
$stmt->saveStatementLines($pdfLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = $stmt->reconcileBankEntries($user, $reconcileBankAccountId);
|
||||||
|
if ($count > 0) {
|
||||||
|
$totalReconciled += $count;
|
||||||
|
$stmtCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalReconciled > 0) {
|
||||||
|
setEventMessages($langs->trans("BankEntriesReconciledTotal", $totalReconciled, $stmtCount), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($langs->trans("NoBankEntriesToReconcile"), null, 'warnings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm a pending reconciliation match
|
||||||
|
if ($action == 'confirmreconcile') {
|
||||||
|
if (!$user->hasRight('bankimport', 'write')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
$lineId = GETPOSTINT('lineid');
|
||||||
|
$bankId = GETPOSTINT('bankid');
|
||||||
|
$reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||||
|
|
||||||
|
if ($lineId > 0 && $bankId > 0 && $reconcileBankAccountId > 0) {
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
|
||||||
|
|
||||||
|
// Get statement info from line
|
||||||
|
$sqlLine = "SELECT sl.fk_statement, s.statement_number, s.statement_year";
|
||||||
|
$sqlLine .= " FROM ".MAIN_DB_PREFIX."bankimport_statement_line sl";
|
||||||
|
$sqlLine .= " JOIN ".MAIN_DB_PREFIX."bankimport_statement s ON s.rowid = sl.fk_statement";
|
||||||
|
$sqlLine .= " WHERE sl.rowid = ".((int) $lineId);
|
||||||
|
$resLine = $db->query($sqlLine);
|
||||||
|
|
||||||
|
if ($resLine && $db->num_rows($resLine) > 0) {
|
||||||
|
$lineObj = $db->fetch_object($resLine);
|
||||||
|
$numReleve = $lineObj->statement_number.'/'.$lineObj->statement_year;
|
||||||
|
|
||||||
|
// Reconcile the bank entry
|
||||||
|
$bankLine = new AccountLine($db);
|
||||||
|
$bankLine->fetch($bankId);
|
||||||
|
$bankLine->num_releve = $numReleve;
|
||||||
|
|
||||||
|
$result = $bankLine->update_conciliation($user, 0, 1);
|
||||||
|
if ($result >= 0) {
|
||||||
|
// Update statement line status
|
||||||
|
$sqlUpd = "UPDATE ".MAIN_DB_PREFIX."bankimport_statement_line SET";
|
||||||
|
$sqlUpd .= " match_status = 'reconciled'";
|
||||||
|
$sqlUpd .= " WHERE rowid = ".((int) $lineId);
|
||||||
|
$db->query($sqlUpd);
|
||||||
|
|
||||||
|
setEventMessages($langs->trans("ReconciliationConfirmed"), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($langs->trans("Error"), null, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
// Delete confirmation
|
// Delete confirmation
|
||||||
if ($action == 'delete' && $confirm == 'yes') {
|
if ($action == 'delete' && $confirm == 'yes') {
|
||||||
$id = GETPOSTINT('id');
|
$id = GETPOSTINT('id');
|
||||||
|
|
@ -561,6 +702,21 @@ print ' <input type="submit" class="button smallpaddingimp" value="'.$langs->tra
|
||||||
print '</div>';
|
print '</div>';
|
||||||
print '</form>';
|
print '</form>';
|
||||||
|
|
||||||
|
// Reconcile All button
|
||||||
|
$reconcileBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
|
||||||
|
if (!empty($reconcileBankAccountId)) {
|
||||||
|
print '<div class="right" style="margin-bottom: 10px;">';
|
||||||
|
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?action=reconcileall&year='.$year.'&token='.newToken().'">';
|
||||||
|
print img_picto('', 'bank', 'class="pictofixedwidth"').$langs->trans("ReconcileAllStatements");
|
||||||
|
print '</a>';
|
||||||
|
print '</div>';
|
||||||
|
} else {
|
||||||
|
print '<div class="warning" style="margin-bottom: 10px;">';
|
||||||
|
print img_warning().' '.$langs->trans("NoBankAccountConfigured");
|
||||||
|
print ' <a href="'.dol_buildpath('/bankimport/admin/setup.php', 1).'">'.$langs->trans("GoToSetup").'</a>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
// List of existing PDF statements
|
// List of existing PDF statements
|
||||||
print '<div class="div-table-responsive">';
|
print '<div class="div-table-responsive">';
|
||||||
print '<table class="noborder centpercent">';
|
print '<table class="noborder centpercent">';
|
||||||
|
|
@ -573,7 +729,7 @@ print '<th class="right">'.$langs->trans("OpeningBalance").'</th>';
|
||||||
print '<th class="right">'.$langs->trans("ClosingBalance").'</th>';
|
print '<th class="right">'.$langs->trans("ClosingBalance").'</th>';
|
||||||
print '<th class="right">'.$langs->trans("Size").'</th>';
|
print '<th class="right">'.$langs->trans("Size").'</th>';
|
||||||
print '<th class="center">'.$langs->trans("DateCreation").'</th>';
|
print '<th class="center">'.$langs->trans("DateCreation").'</th>';
|
||||||
print '<th class="center" width="150">'.$langs->trans("Actions").'</th>';
|
print '<th class="center" width="200">'.$langs->trans("Actions").'</th>';
|
||||||
print '</tr>';
|
print '</tr>';
|
||||||
|
|
||||||
$filter = array();
|
$filter = array();
|
||||||
|
|
@ -657,21 +813,28 @@ if (is_array($records) && count($records) > 0) {
|
||||||
print '</td>';
|
print '</td>';
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
print '<td class="center nowraponall">';
|
print '<td class="center nowraponall" style="white-space: nowrap;">';
|
||||||
if ($obj->filepath && file_exists($obj->filepath)) {
|
if ($obj->filepath && file_exists($obj->filepath)) {
|
||||||
// View (inline)
|
// View (inline)
|
||||||
print '<a class="paddingright" href="'.$_SERVER["PHP_SELF"].'?action=view&id='.$obj->id.'&token='.newToken().'" target="_blank" title="'.$langs->trans("View").'">';
|
print '<a style="margin: 0 6px;" href="'.$_SERVER["PHP_SELF"].'?action=view&id='.$obj->id.'&token='.newToken().'" target="_blank" title="'.$langs->trans("View").'">';
|
||||||
print img_picto($langs->trans("View"), 'eye');
|
print img_picto($langs->trans("View"), 'eye');
|
||||||
print '</a>';
|
print '</a>';
|
||||||
|
|
||||||
// Download
|
// Download
|
||||||
print '<a class="paddingright" href="'.$_SERVER["PHP_SELF"].'?action=download&id='.$obj->id.'&token='.newToken().'" title="'.$langs->trans("Download").'">';
|
print '<a style="margin: 0 6px;" href="'.$_SERVER["PHP_SELF"].'?action=download&id='.$obj->id.'&token='.newToken().'" title="'.$langs->trans("Download").'">';
|
||||||
print img_picto($langs->trans("Download"), 'download');
|
print img_picto($langs->trans("Download"), 'download');
|
||||||
print '</a>';
|
print '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reconcile
|
||||||
|
if (!empty($reconcileBankAccountId) && $obj->date_from && $obj->date_to) {
|
||||||
|
print '<a style="margin: 0 6px;" href="'.$_SERVER["PHP_SELF"].'?action=reconcile&id='.$obj->id.'&year='.$year.'&token='.newToken().'" title="'.$langs->trans("ReconcileStatement").'">';
|
||||||
|
print img_picto($langs->trans("ReconcileStatement"), 'bank');
|
||||||
|
print '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
// Delete
|
// Delete
|
||||||
print '<a class="paddingright" href="'.$_SERVER["PHP_SELF"].'?action=delete&id='.$obj->id.'&year='.$year.'&token='.newToken().'" title="'.$langs->trans("Delete").'">';
|
print '<a style="margin: 0 6px;" href="'.$_SERVER["PHP_SELF"].'?action=delete&id='.$obj->id.'&year='.$year.'&token='.newToken().'" title="'.$langs->trans("Delete").'">';
|
||||||
print img_picto($langs->trans("Delete"), 'delete');
|
print img_picto($langs->trans("Delete"), 'delete');
|
||||||
print '</a>';
|
print '</a>';
|
||||||
|
|
||||||
|
|
@ -688,6 +851,93 @@ if (is_array($records) && count($records) > 0) {
|
||||||
print '</table>';
|
print '</table>';
|
||||||
print '</div>';
|
print '</div>';
|
||||||
|
|
||||||
|
// Pending review matches
|
||||||
|
$sqlPending = "SELECT sl.rowid as line_id, sl.fk_statement, sl.line_number, sl.date_booking, sl.amount as stmt_amount,";
|
||||||
|
$sqlPending .= " sl.name as stmt_name, sl.fk_bank,";
|
||||||
|
$sqlPending .= " b.rowid as bank_id, b.datev, b.amount as bank_amount, b.label as bank_label,";
|
||||||
|
$sqlPending .= " s.statement_number, s.statement_year";
|
||||||
|
$sqlPending .= " FROM ".MAIN_DB_PREFIX."bankimport_statement_line sl";
|
||||||
|
$sqlPending .= " JOIN ".MAIN_DB_PREFIX."bankimport_statement s ON s.rowid = sl.fk_statement";
|
||||||
|
$sqlPending .= " JOIN ".MAIN_DB_PREFIX."bank b ON b.rowid = sl.fk_bank";
|
||||||
|
$sqlPending .= " WHERE sl.match_status = 'pending_review'";
|
||||||
|
$sqlPending .= " AND sl.entity = ".((int) $conf->entity);
|
||||||
|
$sqlPending .= " ORDER BY s.statement_year, s.statement_number, sl.line_number";
|
||||||
|
|
||||||
|
$resPending = $db->query($sqlPending);
|
||||||
|
if ($resPending && $db->num_rows($resPending) > 0) {
|
||||||
|
print '<br>';
|
||||||
|
print '<div class="div-table-responsive">';
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<td colspan="8">';
|
||||||
|
print img_warning().' <strong>'.$langs->trans("PendingReconciliationMatches").'</strong>';
|
||||||
|
print ' - '.$langs->trans("PendingReconciliationMatchesDesc");
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th class="center">'.$langs->trans("StatementNumber").'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans("BookingDate").'</th>';
|
||||||
|
print '<th>'.$langs->trans("Name").'</th>';
|
||||||
|
print '<th class="right">'.$langs->trans("AmountStatement").'</th>';
|
||||||
|
print '<th class="right">'.$langs->trans("AmountDolibarr").'</th>';
|
||||||
|
print '<th class="right">'.$langs->trans("Difference").'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans("BankEntry").'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans("Action").'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
while ($pendObj = $db->fetch_object($resPending)) {
|
||||||
|
$diff = abs(abs((float) $pendObj->stmt_amount) - abs((float) $pendObj->bank_amount));
|
||||||
|
$diffColor = ($diff > 10) ? 'color: red; font-weight: bold;' : 'color: #e68a00;';
|
||||||
|
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
|
||||||
|
// Statement number
|
||||||
|
print '<td class="center nowraponall">'.$pendObj->statement_number.'/'.$pendObj->statement_year.'</td>';
|
||||||
|
|
||||||
|
// Booking date
|
||||||
|
print '<td class="center">'.dol_print_date($db->jdate($pendObj->date_booking), 'day').'</td>';
|
||||||
|
|
||||||
|
// Name
|
||||||
|
print '<td>'.dol_escape_htmltag($pendObj->stmt_name).'</td>';
|
||||||
|
|
||||||
|
// Amount from PDF statement
|
||||||
|
print '<td class="right nowraponall">';
|
||||||
|
$stmtColor = $pendObj->stmt_amount >= 0 ? '' : 'color: red;';
|
||||||
|
print '<span style="'.$stmtColor.'">'.price($pendObj->stmt_amount, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Amount from Dolibarr bank
|
||||||
|
print '<td class="right nowraponall">';
|
||||||
|
$bankColor = $pendObj->bank_amount >= 0 ? '' : 'color: red;';
|
||||||
|
print '<span style="'.$bankColor.'">'.price($pendObj->bank_amount, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Difference
|
||||||
|
print '<td class="right nowraponall">';
|
||||||
|
print '<span style="'.$diffColor.'">'.price($diff, 0, $langs, 1, -1, 2, 'EUR').'</span>';
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Bank entry link
|
||||||
|
print '<td class="center">';
|
||||||
|
print '<a href="'.DOL_URL_ROOT.'/compta/bank/line.php?rowid='.$pendObj->bank_id.'" target="_blank">#'.$pendObj->bank_id.'</a>';
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Action: confirm button
|
||||||
|
print '<td class="center nowraponall">';
|
||||||
|
print '<a class="butAction butActionSmall" href="'.$_SERVER["PHP_SELF"].'?action=confirmreconcile&lineid='.$pendObj->line_id.'&bankid='.$pendObj->bank_id.'&year='.$year.'&token='.newToken().'">';
|
||||||
|
print $langs->trans("Confirm");
|
||||||
|
print '</a>';
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
$db->free($resPending);
|
||||||
|
|
||||||
// Statistics
|
// Statistics
|
||||||
$totalCount = $statement->fetchAll('', '', 0, 0, array(), 'count');
|
$totalCount = $statement->fetchAll('', '', 0, 0, array(), 'count');
|
||||||
$yearCount = is_array($records) ? count($records) : 0;
|
$yearCount = is_array($records) ? count($records) : 0;
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,30 @@
|
||||||
-- v1.1: Add fk_statement to transaction table
|
-- v1.1: Add fk_statement to transaction table
|
||||||
ALTER TABLE llx_bankimport_transaction ADD COLUMN fk_statement INTEGER AFTER fk_societe;
|
ALTER TABLE llx_bankimport_transaction ADD COLUMN fk_statement INTEGER AFTER fk_societe;
|
||||||
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_statement (fk_statement);
|
ALTER TABLE llx_bankimport_transaction ADD INDEX idx_bankimport_transaction_fk_statement (fk_statement);
|
||||||
|
|
||||||
|
-- v1.3: Add statement_line table for parsed PDF transactions
|
||||||
|
CREATE TABLE IF NOT EXISTS llx_bankimport_statement_line (
|
||||||
|
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
fk_statement INTEGER NOT NULL,
|
||||||
|
entity INTEGER DEFAULT 1 NOT NULL,
|
||||||
|
line_number INTEGER DEFAULT 0,
|
||||||
|
date_booking DATE NOT NULL,
|
||||||
|
date_value DATE,
|
||||||
|
transaction_type VARCHAR(100),
|
||||||
|
amount DOUBLE(24,8) NOT NULL,
|
||||||
|
currency VARCHAR(3) DEFAULT 'EUR',
|
||||||
|
name VARCHAR(255),
|
||||||
|
description TEXT,
|
||||||
|
fk_bank INTEGER,
|
||||||
|
datec DATETIME,
|
||||||
|
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_fk_statement (fk_statement);
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_entity (entity);
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_date_booking (date_booking);
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_amount (amount);
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_fk_bank (fk_bank);
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD UNIQUE INDEX uk_bankimport_stmtline (fk_statement, line_number, entity);
|
||||||
|
|
||||||
|
-- v1.4: Add match_status to statement_line for approval workflow
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD COLUMN match_status VARCHAR(20) DEFAULT NULL AFTER fk_bank;
|
||||||
|
|
|
||||||
15
sql/llx_bankimport_statement_line.key.sql
Normal file
15
sql/llx_bankimport_statement_line.key.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- 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.
|
||||||
|
|
||||||
|
-- Indexes for llx_bankimport_statement_line
|
||||||
|
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_fk_statement (fk_statement);
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_entity (entity);
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_date_booking (date_booking);
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_amount (amount);
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD INDEX idx_bankimport_stmtline_fk_bank (fk_bank);
|
||||||
|
ALTER TABLE llx_bankimport_statement_line ADD UNIQUE INDEX uk_bankimport_stmtline (fk_statement, line_number, entity);
|
||||||
34
sql/llx_bankimport_statement_line.sql
Normal file
34
sql/llx_bankimport_statement_line.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
-- 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.
|
||||||
|
|
||||||
|
-- Table for individual transaction lines parsed from PDF bank statements
|
||||||
|
|
||||||
|
CREATE TABLE llx_bankimport_statement_line (
|
||||||
|
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
fk_statement INTEGER NOT NULL, -- Link to llx_bankimport_statement
|
||||||
|
entity INTEGER DEFAULT 1 NOT NULL,
|
||||||
|
line_number INTEGER DEFAULT 0, -- Position within statement (1, 2, 3...)
|
||||||
|
|
||||||
|
-- Transaction data from PDF
|
||||||
|
date_booking DATE NOT NULL, -- Buchungstag (Bu-Tag)
|
||||||
|
date_value DATE, -- Wertstellungstag (Wert)
|
||||||
|
transaction_type VARCHAR(100), -- Vorgangsart (e.g. Überweisungsgutschr., Basislastschrift)
|
||||||
|
amount DOUBLE(24,8) NOT NULL, -- Amount (positive = credit, negative = debit)
|
||||||
|
currency VARCHAR(3) DEFAULT 'EUR',
|
||||||
|
|
||||||
|
-- Counterparty and description
|
||||||
|
name VARCHAR(255), -- Counterparty name (first detail line)
|
||||||
|
description TEXT, -- Full description (all detail lines)
|
||||||
|
|
||||||
|
-- Matching
|
||||||
|
fk_bank INTEGER, -- Link to llx_bank when reconciled
|
||||||
|
match_status VARCHAR(20) DEFAULT NULL, -- NULL=unmatched, reconciled=auto, pending_review=needs approval
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
datec DATETIME,
|
||||||
|
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
Loading…
Reference in a new issue