Matches die innerhalb von 10% des Transaktionsbetrags liegen werden jetzt vor Matches mit größerer Abweichung angezeigt. Bei 523€ Transaktion: - Multi-Match 529€ (Diff 6€) → wird bevorzugt - Einzelmatch 60€ (Diff 463€) → kommt danach Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2502 lines
74 KiB
PHP
Executable file
2502 lines
74 KiB
PHP
Executable file
<?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/class/banktransaction.class.php
|
|
* \ingroup bankimport
|
|
* \brief Class for bank transactions from FinTS import
|
|
*/
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
|
|
|
/**
|
|
* Class BankImportTransaction
|
|
* Represents a bank transaction imported via FinTS
|
|
*/
|
|
class BankImportTransaction extends CommonObject
|
|
{
|
|
/**
|
|
* Send notification via GlobalNotify (if available)
|
|
*
|
|
* @param string $type 'error', 'warning', 'info', 'action'
|
|
* @param string $title Title
|
|
* @param string $message Message
|
|
* @param string $actionUrl URL for action button
|
|
* @param string $actionLabel Label for action button
|
|
* @return bool True if sent via GlobalNotify
|
|
*/
|
|
public static function notify($type, $title, $message, $actionUrl = '', $actionLabel = '')
|
|
{
|
|
global $db;
|
|
|
|
if (!isModEnabled('globalnotify')) {
|
|
dol_syslog("BankImport [{$type}]: {$title} - {$message}", LOG_WARNING);
|
|
return false;
|
|
}
|
|
|
|
$classFile = dol_buildpath('/globalnotify/class/globalnotify.class.php', 0);
|
|
if (!file_exists($classFile)) {
|
|
dol_syslog("BankImport [{$type}]: {$title} - {$message}", LOG_WARNING);
|
|
return false;
|
|
}
|
|
|
|
require_once $classFile;
|
|
|
|
if (!class_exists('GlobalNotify')) {
|
|
dol_syslog("BankImport [{$type}]: {$title} - {$message}", LOG_WARNING);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
switch ($type) {
|
|
case 'error':
|
|
GlobalNotify::error('bankimport', $title, $message, $actionUrl, $actionLabel);
|
|
break;
|
|
case 'warning':
|
|
GlobalNotify::warning('bankimport', $title, $message, $actionUrl, $actionLabel);
|
|
break;
|
|
case 'action':
|
|
GlobalNotify::actionRequired('bankimport', $title, $message, $actionUrl, $actionLabel ?: 'Aktion erforderlich');
|
|
break;
|
|
default:
|
|
GlobalNotify::info('bankimport', $title, $message, $actionUrl, $actionLabel);
|
|
}
|
|
return true;
|
|
} catch (Exception $e) {
|
|
dol_syslog("GlobalNotify error: ".$e->getMessage(), LOG_ERR);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @var string ID to identify managed object
|
|
*/
|
|
public $element = 'banktransaction';
|
|
|
|
/**
|
|
* @var string Name of table without prefix where object is stored
|
|
*/
|
|
public $table_element = 'bankimport_transaction';
|
|
|
|
/**
|
|
* @var string Primary key name
|
|
*/
|
|
public $pk = 'rowid';
|
|
|
|
/**
|
|
* @var int Entity
|
|
*/
|
|
public $entity;
|
|
|
|
/**
|
|
* @var string Unique reference (hash)
|
|
*/
|
|
public $ref;
|
|
|
|
/**
|
|
* @var string IBAN
|
|
*/
|
|
public $iban;
|
|
|
|
/**
|
|
* @var string BIC
|
|
*/
|
|
public $bic;
|
|
|
|
/**
|
|
* @var int Creation timestamp
|
|
*/
|
|
public $datec;
|
|
|
|
/**
|
|
* @var int Transaction date
|
|
*/
|
|
public $date_trans;
|
|
|
|
/**
|
|
* @var int Value date
|
|
*/
|
|
public $date_value;
|
|
|
|
/**
|
|
* @var string Counterparty name
|
|
*/
|
|
public $name;
|
|
|
|
/**
|
|
* @var string Counterparty IBAN
|
|
*/
|
|
public $counterparty_iban;
|
|
|
|
/**
|
|
* @var string Counterparty BIC
|
|
*/
|
|
public $counterparty_bic;
|
|
|
|
/**
|
|
* @var float Amount
|
|
*/
|
|
public $amount;
|
|
|
|
/**
|
|
* @var string Currency
|
|
*/
|
|
public $currency = 'EUR';
|
|
|
|
/**
|
|
* @var string Short label
|
|
*/
|
|
public $label;
|
|
|
|
/**
|
|
* @var string Full description
|
|
*/
|
|
public $description;
|
|
|
|
/**
|
|
* @var string End-to-end ID
|
|
*/
|
|
public $end_to_end_id;
|
|
|
|
/**
|
|
* @var string Mandate ID
|
|
*/
|
|
public $mandate_id;
|
|
|
|
/**
|
|
* @var int Link to llx_bank
|
|
*/
|
|
public $fk_bank;
|
|
|
|
/**
|
|
* @var int Link to llx_facture
|
|
*/
|
|
public $fk_facture;
|
|
|
|
/**
|
|
* @var int Link to llx_facture_fourn
|
|
*/
|
|
public $fk_facture_fourn;
|
|
|
|
/**
|
|
* @var int Link to llx_paiement
|
|
*/
|
|
public $fk_paiement;
|
|
|
|
/**
|
|
* @var int Link to llx_paiementfourn
|
|
*/
|
|
public $fk_paiementfourn;
|
|
|
|
/**
|
|
* @var int Link to llx_salary
|
|
*/
|
|
public $fk_salary;
|
|
|
|
/**
|
|
* @var int Link to llx_don
|
|
*/
|
|
public $fk_don;
|
|
|
|
/**
|
|
* @var int Link to llx_loan
|
|
*/
|
|
public $fk_loan;
|
|
|
|
/**
|
|
* @var int Link to llx_societe
|
|
*/
|
|
public $fk_societe;
|
|
|
|
/**
|
|
* @var int Link to llx_bankimport_statement
|
|
*/
|
|
public $fk_statement;
|
|
|
|
/**
|
|
* @var int Status (0=new, 1=matched, 2=reconciled, 9=ignored)
|
|
*/
|
|
public $status = 0;
|
|
|
|
/**
|
|
* @var string Import batch key
|
|
*/
|
|
public $import_key;
|
|
|
|
/**
|
|
* @var int User who created
|
|
*/
|
|
public $fk_user_creat;
|
|
|
|
/**
|
|
* @var int User who modified
|
|
*/
|
|
public $fk_user_modif;
|
|
|
|
/**
|
|
* @var int User who matched
|
|
*/
|
|
public $fk_user_match;
|
|
|
|
/**
|
|
* @var int Match date
|
|
*/
|
|
public $date_match;
|
|
|
|
/**
|
|
* @var string Private note
|
|
*/
|
|
public $note_private;
|
|
|
|
/**
|
|
* @var string Public note
|
|
*/
|
|
public $note_public;
|
|
|
|
// Status constants
|
|
const STATUS_NEW = 0;
|
|
const STATUS_MATCHED = 1;
|
|
const STATUS_RECONCILED = 2;
|
|
const STATUS_IGNORED = 9;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
global $conf;
|
|
|
|
$this->db = $db;
|
|
$this->entity = $conf->entity;
|
|
}
|
|
|
|
/**
|
|
* Create transaction in database
|
|
*
|
|
* @param User $user User that creates
|
|
* @param bool $notrigger Disable triggers
|
|
* @return int <0 if KO, Id of created object if OK
|
|
*/
|
|
public function create($user, $notrigger = false)
|
|
{
|
|
global $conf;
|
|
|
|
$error = 0;
|
|
$now = dol_now();
|
|
|
|
// Generate reference hash if not set
|
|
if (empty($this->ref)) {
|
|
$this->ref = $this->generateRef();
|
|
}
|
|
|
|
$this->db->begin();
|
|
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."bankimport_transaction (";
|
|
$sql .= "ref, entity, iban, bic, datec, date_trans, date_value,";
|
|
$sql .= "name, counterparty_iban, counterparty_bic,";
|
|
$sql .= "amount, currency, label, description, end_to_end_id, mandate_id,";
|
|
$sql .= "status, import_key, fk_user_creat";
|
|
$sql .= ") VALUES (";
|
|
$sql .= "'".$this->db->escape($this->ref)."',";
|
|
$sql .= ((int) $this->entity).",";
|
|
$sql .= ($this->iban ? "'".$this->db->escape($this->iban)."'" : "NULL").",";
|
|
$sql .= ($this->bic ? "'".$this->db->escape($this->bic)."'" : "NULL").",";
|
|
$sql .= "'".$this->db->idate($now)."',";
|
|
$sql .= "'".$this->db->idate($this->date_trans)."',";
|
|
$sql .= ($this->date_value ? "'".$this->db->idate($this->date_value)."'" : "NULL").",";
|
|
$sql .= ($this->name ? "'".$this->db->escape($this->name)."'" : "NULL").",";
|
|
$sql .= ($this->counterparty_iban ? "'".$this->db->escape($this->counterparty_iban)."'" : "NULL").",";
|
|
$sql .= ($this->counterparty_bic ? "'".$this->db->escape($this->counterparty_bic)."'" : "NULL").",";
|
|
$sql .= ((float) $this->amount).",";
|
|
$sql .= "'".$this->db->escape($this->currency)."',";
|
|
$sql .= ($this->label ? "'".$this->db->escape($this->label)."'" : "NULL").",";
|
|
$sql .= ($this->description ? "'".$this->db->escape($this->description)."'" : "NULL").",";
|
|
$sql .= ($this->end_to_end_id ? "'".$this->db->escape($this->end_to_end_id)."'" : "NULL").",";
|
|
$sql .= ($this->mandate_id ? "'".$this->db->escape($this->mandate_id)."'" : "NULL").",";
|
|
$sql .= ((int) $this->status).",";
|
|
$sql .= ($this->import_key ? "'".$this->db->escape($this->import_key)."'" : "NULL").",";
|
|
$sql .= ((int) $user->id);
|
|
$sql .= ")";
|
|
|
|
dol_syslog(get_class($this)."::create", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql) {
|
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."bankimport_transaction");
|
|
$this->datec = $now;
|
|
$this->fk_user_creat = $user->id;
|
|
|
|
$this->db->commit();
|
|
return $this->id;
|
|
} else {
|
|
$this->error = $this->db->lasterror();
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load transaction from database
|
|
*
|
|
* @param int $id Id of transaction to load
|
|
* @param string $ref Reference
|
|
* @return int <0 if KO, 0 if not found, >0 if OK
|
|
*/
|
|
public function fetch($id, $ref = '')
|
|
{
|
|
$sql = "SELECT t.*";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction as t";
|
|
if ($id > 0) {
|
|
$sql .= " WHERE t.rowid = ".((int) $id);
|
|
} else {
|
|
$sql .= " WHERE t.ref = '".$this->db->escape($ref)."' AND t.entity = ".((int) $this->entity);
|
|
}
|
|
|
|
dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql) {
|
|
if ($this->db->num_rows($resql)) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
|
|
$this->id = $obj->rowid;
|
|
$this->ref = $obj->ref;
|
|
$this->entity = $obj->entity;
|
|
$this->iban = $obj->iban;
|
|
$this->bic = $obj->bic;
|
|
$this->datec = $this->db->jdate($obj->datec);
|
|
$this->date_trans = $this->db->jdate($obj->date_trans);
|
|
$this->date_value = $this->db->jdate($obj->date_value);
|
|
$this->name = $obj->name;
|
|
$this->counterparty_iban = $obj->counterparty_iban;
|
|
$this->counterparty_bic = $obj->counterparty_bic;
|
|
$this->amount = $obj->amount;
|
|
$this->currency = $obj->currency;
|
|
$this->label = $obj->label;
|
|
$this->description = $obj->description;
|
|
$this->end_to_end_id = $obj->end_to_end_id;
|
|
$this->mandate_id = $obj->mandate_id;
|
|
$this->fk_bank = $obj->fk_bank;
|
|
$this->fk_facture = $obj->fk_facture;
|
|
$this->fk_facture_fourn = $obj->fk_facture_fourn;
|
|
$this->fk_paiement = $obj->fk_paiement;
|
|
$this->fk_paiementfourn = $obj->fk_paiementfourn;
|
|
$this->fk_salary = $obj->fk_salary;
|
|
$this->fk_don = $obj->fk_don;
|
|
$this->fk_loan = $obj->fk_loan;
|
|
$this->fk_societe = $obj->fk_societe;
|
|
$this->fk_statement = $obj->fk_statement;
|
|
$this->status = $obj->status;
|
|
$this->import_key = $obj->import_key;
|
|
$this->fk_user_creat = $obj->fk_user_creat;
|
|
$this->fk_user_modif = $obj->fk_user_modif;
|
|
$this->fk_user_match = $obj->fk_user_match;
|
|
$this->date_match = $this->db->jdate($obj->date_match);
|
|
$this->note_private = $obj->note_private;
|
|
$this->note_public = $obj->note_public;
|
|
|
|
$this->db->free($resql);
|
|
return 1;
|
|
} else {
|
|
$this->db->free($resql);
|
|
return 0;
|
|
}
|
|
} else {
|
|
$this->error = $this->db->lasterror();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update transaction in database
|
|
*
|
|
* @param User $user User that modifies
|
|
* @param bool $notrigger Disable triggers
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function update($user, $notrigger = false)
|
|
{
|
|
$error = 0;
|
|
|
|
$this->db->begin();
|
|
|
|
$sql = "UPDATE ".MAIN_DB_PREFIX."bankimport_transaction SET";
|
|
$sql .= " iban = ".($this->iban ? "'".$this->db->escape($this->iban)."'" : "NULL").",";
|
|
$sql .= " bic = ".($this->bic ? "'".$this->db->escape($this->bic)."'" : "NULL").",";
|
|
$sql .= " date_trans = '".$this->db->idate($this->date_trans)."',";
|
|
$sql .= " date_value = ".($this->date_value ? "'".$this->db->idate($this->date_value)."'" : "NULL").",";
|
|
$sql .= " name = ".($this->name ? "'".$this->db->escape($this->name)."'" : "NULL").",";
|
|
$sql .= " counterparty_iban = ".($this->counterparty_iban ? "'".$this->db->escape($this->counterparty_iban)."'" : "NULL").",";
|
|
$sql .= " counterparty_bic = ".($this->counterparty_bic ? "'".$this->db->escape($this->counterparty_bic)."'" : "NULL").",";
|
|
$sql .= " amount = ".((float) $this->amount).",";
|
|
$sql .= " currency = '".$this->db->escape($this->currency)."',";
|
|
$sql .= " label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL").",";
|
|
$sql .= " description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL").",";
|
|
$sql .= " end_to_end_id = ".($this->end_to_end_id ? "'".$this->db->escape($this->end_to_end_id)."'" : "NULL").",";
|
|
$sql .= " mandate_id = ".($this->mandate_id ? "'".$this->db->escape($this->mandate_id)."'" : "NULL").",";
|
|
$sql .= " fk_bank = ".($this->fk_bank > 0 ? ((int) $this->fk_bank) : "NULL").",";
|
|
$sql .= " fk_facture = ".($this->fk_facture > 0 ? ((int) $this->fk_facture) : "NULL").",";
|
|
$sql .= " fk_facture_fourn = ".($this->fk_facture_fourn > 0 ? ((int) $this->fk_facture_fourn) : "NULL").",";
|
|
$sql .= " fk_paiement = ".($this->fk_paiement > 0 ? ((int) $this->fk_paiement) : "NULL").",";
|
|
$sql .= " fk_paiementfourn = ".($this->fk_paiementfourn > 0 ? ((int) $this->fk_paiementfourn) : "NULL").",";
|
|
$sql .= " fk_salary = ".($this->fk_salary > 0 ? ((int) $this->fk_salary) : "NULL").",";
|
|
$sql .= " fk_don = ".($this->fk_don > 0 ? ((int) $this->fk_don) : "NULL").",";
|
|
$sql .= " fk_loan = ".($this->fk_loan > 0 ? ((int) $this->fk_loan) : "NULL").",";
|
|
$sql .= " fk_societe = ".($this->fk_societe > 0 ? ((int) $this->fk_societe) : "NULL").",";
|
|
$sql .= " fk_statement = ".($this->fk_statement > 0 ? ((int) $this->fk_statement) : "NULL").",";
|
|
$sql .= " status = ".((int) $this->status).",";
|
|
$sql .= " fk_user_modif = ".((int) $user->id).",";
|
|
$sql .= " note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL").",";
|
|
$sql .= " note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
|
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
|
|
|
dol_syslog(get_class($this)."::update", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql) {
|
|
$this->fk_user_modif = $user->id;
|
|
$this->db->commit();
|
|
return 1;
|
|
} else {
|
|
$this->error = $this->db->lasterror();
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete transaction from database
|
|
*
|
|
* @param User $user User that deletes
|
|
* @param bool $notrigger Disable triggers
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function delete($user, $notrigger = false)
|
|
{
|
|
$this->db->begin();
|
|
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
|
|
|
dol_syslog(get_class($this)."::delete", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql) {
|
|
$this->db->commit();
|
|
return 1;
|
|
} else {
|
|
$this->error = $this->db->lasterror();
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate unique reference hash
|
|
*
|
|
* @return string Reference hash
|
|
*/
|
|
public function generateRef()
|
|
{
|
|
// Create hash from: date + amount + name + description
|
|
$data = $this->date_trans . '|' . $this->amount . '|' . $this->name . '|' . $this->description;
|
|
return substr(md5($data), 0, 32);
|
|
}
|
|
|
|
/**
|
|
* Check if transaction already exists (duplicate detection)
|
|
*
|
|
* @return int 0 if not exists, rowid if exists
|
|
*/
|
|
public function exists()
|
|
{
|
|
$ref = $this->generateRef();
|
|
|
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
|
$sql .= " WHERE ref = '".$this->db->escape($ref)."'";
|
|
$sql .= " AND entity = ".((int) $this->entity);
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
if ($this->db->num_rows($resql) > 0) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
return $obj->rowid;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Import transactions from FinTS fetch result
|
|
*
|
|
* @param array $transactions Array of transaction data from FinTS
|
|
* @param string $iban IBAN of the account
|
|
* @param User $user User doing the import
|
|
* @return array Array with 'imported' and 'skipped' counts
|
|
*/
|
|
public function importFromFinTS($transactions, $iban, $user)
|
|
{
|
|
$imported = 0;
|
|
$skipped = 0;
|
|
$errors = array();
|
|
|
|
$importKey = date('YmdHis') . '_' . $user->id;
|
|
|
|
foreach ($transactions as $tx) {
|
|
$trans = new BankImportTransaction($this->db);
|
|
$trans->iban = $iban;
|
|
$trans->date_trans = $tx['date'];
|
|
$trans->date_value = $tx['bookingDate'] ?? $tx['date'];
|
|
$trans->name = $tx['name'] ?? '';
|
|
$trans->counterparty_iban = $tx['iban'] ?? '';
|
|
$trans->counterparty_bic = $tx['bic'] ?? '';
|
|
$trans->amount = $tx['amount'];
|
|
$trans->currency = $tx['currency'] ?? 'EUR';
|
|
$trans->label = $tx['bookingText'] ?? '';
|
|
$trans->description = $tx['reference'] ?? '';
|
|
$trans->end_to_end_id = $tx['endToEndId'] ?? '';
|
|
$trans->import_key = $importKey;
|
|
$trans->status = self::STATUS_NEW;
|
|
|
|
// Check for duplicate
|
|
if ($trans->exists()) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
|
|
// Create transaction
|
|
$result = $trans->create($user);
|
|
if ($result > 0) {
|
|
$imported++;
|
|
} else {
|
|
$errors[] = $trans->error;
|
|
}
|
|
}
|
|
|
|
// Send notification if new transactions were imported
|
|
if ($imported > 0) {
|
|
// Count unmatched transactions
|
|
$unmatchedCount = $this->countUnmatchedTransactions();
|
|
|
|
if ($unmatchedCount > 0) {
|
|
self::notify(
|
|
'action',
|
|
$imported.' neue Bankbuchungen',
|
|
"{$unmatchedCount} Buchungen warten auf Zuordnung zu Rechnungen",
|
|
dol_buildpath('/bankimport/list.php?status=0', 1),
|
|
'Zahlungsabgleich'
|
|
);
|
|
} else {
|
|
// All matched - just info
|
|
self::notify(
|
|
'info',
|
|
$imported.' Bankbuchungen importiert',
|
|
'Alle Buchungen wurden automatisch zugeordnet',
|
|
dol_buildpath('/bankimport/list.php', 1),
|
|
'Anzeigen'
|
|
);
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'imported' => $imported,
|
|
'skipped' => $skipped,
|
|
'errors' => $errors,
|
|
'import_key' => $importKey
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Count unmatched/new transactions waiting for reconciliation
|
|
*
|
|
* @return int Number of unmatched transactions
|
|
*/
|
|
public function countUnmatchedTransactions()
|
|
{
|
|
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."bankimport_transaction";
|
|
$sql .= " WHERE status = ".self::STATUS_NEW;
|
|
$sql .= " AND entity = ".((int) $this->entity);
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
return (int) $obj->cnt;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Fetch list of transactions with filters
|
|
*
|
|
* @param string $sortfield Sort field
|
|
* @param string $sortorder Sort order (ASC/DESC)
|
|
* @param int $limit Limit
|
|
* @param int $offset Offset
|
|
* @param array $filter Filters array
|
|
* @param string $mode 'list' returns array of objects, 'count' returns total count
|
|
* @return array|int Array of transactions, count or -1 on error
|
|
*/
|
|
public function fetchAll($sortfield = 'date_trans', $sortorder = 'DESC', $limit = 0, $offset = 0, $filter = array(), $mode = 'list')
|
|
{
|
|
$sql = "SELECT t.rowid";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."bankimport_transaction as t";
|
|
$sql .= " WHERE t.entity = ".((int) $this->entity);
|
|
|
|
// Apply filters
|
|
if (!empty($filter['ref'])) {
|
|
$sql .= " AND t.ref LIKE '%".$this->db->escape($filter['ref'])."%'";
|
|
}
|
|
if (!empty($filter['name'])) {
|
|
$sql .= " AND t.name LIKE '%".$this->db->escape($filter['name'])."%'";
|
|
}
|
|
if (!empty($filter['description'])) {
|
|
$sql .= " AND (t.description LIKE '%".$this->db->escape($filter['description'])."%'";
|
|
$sql .= " OR t.label LIKE '%".$this->db->escape($filter['description'])."%')";
|
|
}
|
|
if (!empty($filter['amount_min'])) {
|
|
$sql .= " AND t.amount >= ".((float) $filter['amount_min']);
|
|
}
|
|
if (!empty($filter['amount_max'])) {
|
|
$sql .= " AND t.amount <= ".((float) $filter['amount_max']);
|
|
}
|
|
if (!empty($filter['date_from'])) {
|
|
$sql .= " AND t.date_trans >= '".$this->db->idate($filter['date_from'])."'";
|
|
}
|
|
if (!empty($filter['date_to'])) {
|
|
$sql .= " AND t.date_trans <= '".$this->db->idate($filter['date_to'])."'";
|
|
}
|
|
if (isset($filter['status']) && $filter['status'] !== '' && $filter['status'] >= 0) {
|
|
$sql .= " AND t.status = ".((int) $filter['status']);
|
|
}
|
|
if (!empty($filter['iban'])) {
|
|
$sql .= " AND t.iban LIKE '%".$this->db->escape($filter['iban'])."%'";
|
|
}
|
|
|
|
// Count mode - just return total count
|
|
if ($mode == 'count') {
|
|
$sqlcount = preg_replace('/SELECT t\.rowid/', 'SELECT COUNT(*) as total', $sql);
|
|
$resqlcount = $this->db->query($sqlcount);
|
|
if ($resqlcount) {
|
|
$objcount = $this->db->fetch_object($resqlcount);
|
|
return (int) $objcount->total;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Sort and limit
|
|
$sql .= $this->db->order($sortfield, $sortorder);
|
|
if ($limit > 0) {
|
|
$sql .= $this->db->plimit($limit, $offset);
|
|
}
|
|
|
|
dol_syslog(get_class($this)."::fetchAll", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql) {
|
|
$result = array();
|
|
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$trans = new BankImportTransaction($this->db);
|
|
$trans->fetch($obj->rowid);
|
|
$result[] = $trans;
|
|
}
|
|
|
|
$this->db->free($resql);
|
|
return $result;
|
|
} else {
|
|
$this->error = $this->db->lasterror();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get status label
|
|
*
|
|
* @param int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto
|
|
* @return string Label
|
|
*/
|
|
public function getLibStatut($mode = 0)
|
|
{
|
|
return $this->LibStatut($this->status, $mode);
|
|
}
|
|
|
|
/**
|
|
* Get status label for a given status
|
|
*
|
|
* @param int $status Status value
|
|
* @param int $mode Mode
|
|
* @return string Label
|
|
*/
|
|
public function LibStatut($status, $mode = 0)
|
|
{
|
|
global $langs;
|
|
|
|
$statusLabels = array(
|
|
self::STATUS_NEW => array('short' => 'New', 'long' => 'NewTransaction', 'picto' => 'status0'),
|
|
self::STATUS_MATCHED => array('short' => 'Matched', 'long' => 'TransactionMatched', 'picto' => 'status4'),
|
|
self::STATUS_RECONCILED => array('short' => 'Reconciled', 'long' => 'TransactionReconciled', 'picto' => 'status6'),
|
|
self::STATUS_IGNORED => array('short' => 'Ignored', 'long' => 'TransactionIgnored', 'picto' => 'status5'),
|
|
);
|
|
|
|
$statusInfo = $statusLabels[$status] ?? array('short' => 'Unknown', 'long' => 'Unknown', 'picto' => 'status0');
|
|
|
|
if ($mode == 0) {
|
|
return $langs->trans($statusInfo['long']);
|
|
} elseif ($mode == 1) {
|
|
return $langs->trans($statusInfo['short']);
|
|
} elseif ($mode == 2) {
|
|
return img_picto($langs->trans($statusInfo['long']), $statusInfo['picto']).' '.$langs->trans($statusInfo['short']);
|
|
} elseif ($mode == 3) {
|
|
return img_picto($langs->trans($statusInfo['long']), $statusInfo['picto']);
|
|
} elseif ($mode == 4) {
|
|
return img_picto($langs->trans($statusInfo['long']), $statusInfo['picto']).' '.$langs->trans($statusInfo['long']);
|
|
} elseif ($mode == 5) {
|
|
return $langs->trans($statusInfo['short']).' '.img_picto($langs->trans($statusInfo['long']), $statusInfo['picto']);
|
|
} elseif ($mode == 6) {
|
|
return $langs->trans($statusInfo['long']).' '.img_picto($langs->trans($statusInfo['long']), $statusInfo['picto']);
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Set status of transaction
|
|
*
|
|
* @param int $status New status
|
|
* @param User $user User making the change
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function setStatus($status, $user)
|
|
{
|
|
$this->status = $status;
|
|
if ($status == self::STATUS_MATCHED || $status == self::STATUS_RECONCILED) {
|
|
$this->fk_user_match = $user->id;
|
|
$this->date_match = dol_now();
|
|
}
|
|
return $this->update($user);
|
|
}
|
|
|
|
/**
|
|
* Try to auto-match transaction with Dolibarr invoices
|
|
* Uses multiple criteria: amount, name, invoice reference, IBAN
|
|
*
|
|
* @return array Array of potential matches with scores
|
|
*/
|
|
public function findMatches()
|
|
{
|
|
$matches = array();
|
|
$seenIds = array();
|
|
|
|
// Skip if already matched
|
|
if ($this->status == self::STATUS_MATCHED || $this->status == self::STATUS_RECONCILED) {
|
|
return $matches;
|
|
}
|
|
|
|
// Build search text from description and end-to-end-id
|
|
$searchText = strtoupper($this->description . ' ' . $this->end_to_end_id . ' ' . $this->label);
|
|
|
|
// For incoming payments (positive amount), search customer invoices
|
|
if ($this->amount > 0) {
|
|
$matches = array_merge($matches, $this->findCustomerInvoiceMatches($searchText, $seenIds));
|
|
}
|
|
|
|
// For outgoing payments (negative amount), search supplier invoices
|
|
if ($this->amount < 0) {
|
|
$matches = array_merge($matches, $this->findSupplierInvoiceMatches($searchText, $seenIds));
|
|
|
|
// FALLBACK: If best match has amount difference > 5 EUR, try multi-invoice matching
|
|
$absAmount = abs($this->amount);
|
|
$needsMultiMatch = true;
|
|
|
|
if (!empty($matches)) {
|
|
$bestMatch = $matches[0];
|
|
// If best single match is close enough in amount, no need for multi-match
|
|
if (abs($bestMatch['amount'] - $absAmount) <= 5.00) {
|
|
$needsMultiMatch = false;
|
|
}
|
|
}
|
|
|
|
if ($needsMultiMatch) {
|
|
// Try to find supplier by IBAN or name
|
|
$socid = $this->findSupplierForMultiMatch($searchText);
|
|
|
|
if ($socid > 0) {
|
|
// Tolerance: 3% of amount (for cash discounts/skonto) or minimum 5 EUR
|
|
$tolerance = max($absAmount * 0.03, 5.00);
|
|
$multiMatch = $this->findMultipleSupplierInvoiceMatches($searchText, $socid, $absAmount, $tolerance);
|
|
|
|
if ($multiMatch && count($multiMatch['invoices']) > 1) {
|
|
// Add as a special "multi" match
|
|
$matches[] = array(
|
|
'type' => 'multi_facture_fourn',
|
|
'id' => 0, // Special marker
|
|
'ref' => count($multiMatch['invoices']).' '.$this->getLangsTransNoLoad("Invoices"),
|
|
'amount' => $multiMatch['total'],
|
|
'socname' => $multiMatch['invoices'][0]['socname'],
|
|
'socid' => $socid,
|
|
'match_score' => $multiMatch['match_score'],
|
|
'match_reasons' => array('multi_invoice', 'ref_supplier'),
|
|
'invoices' => $multiMatch['invoices'],
|
|
'difference' => $multiMatch['difference']
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by: 1) Amount difference (closer to transaction amount is better), 2) Score
|
|
$absAmount = abs($this->amount);
|
|
usort($matches, function($a, $b) use ($absAmount) {
|
|
$diffA = abs($a['amount'] - $absAmount);
|
|
$diffB = abs($b['amount'] - $absAmount);
|
|
|
|
// If one match is much closer in amount (within 10% of transaction), prefer it
|
|
$threshold = $absAmount * 0.10;
|
|
if ($diffA < $threshold && $diffB >= $threshold) {
|
|
return -1; // A is better (closer amount)
|
|
}
|
|
if ($diffB < $threshold && $diffA >= $threshold) {
|
|
return 1; // B is better (closer amount)
|
|
}
|
|
|
|
// Otherwise sort by score
|
|
return $b['match_score'] - $a['match_score'];
|
|
});
|
|
|
|
return $matches;
|
|
}
|
|
|
|
/**
|
|
* Find supplier ID for multi-invoice matching
|
|
* Uses IBAN or name matching
|
|
*
|
|
* @param string $searchText Search text
|
|
* @return int Societe ID or 0
|
|
*/
|
|
protected function findSupplierForMultiMatch($searchText)
|
|
{
|
|
// First try by IBAN
|
|
if (!empty($this->counterparty_iban)) {
|
|
$socid = $this->findSocieteByIban($this->counterparty_iban);
|
|
if ($socid > 0) {
|
|
return $socid;
|
|
}
|
|
}
|
|
|
|
// Then try by name similarity
|
|
if (!empty($this->name)) {
|
|
$sql = "SELECT s.rowid, s.nom";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."societe as s";
|
|
$sql .= " WHERE s.entity = ".((int) $this->entity);
|
|
$sql .= " AND s.fournisseur = 1"; // Must be a supplier
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$bestSocid = 0;
|
|
$bestSimilarity = 0;
|
|
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$similarity = $this->calculateNameSimilarity($this->name, $obj->nom);
|
|
if ($similarity > 70 && $similarity > $bestSimilarity) {
|
|
$bestSimilarity = $similarity;
|
|
$bestSocid = $obj->rowid;
|
|
}
|
|
}
|
|
|
|
if ($bestSocid > 0) {
|
|
return $bestSocid;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get translation without loading langs (fallback)
|
|
*
|
|
* @param string $key Translation key
|
|
* @return string
|
|
*/
|
|
protected function getLangsTransNoLoad($key)
|
|
{
|
|
global $langs;
|
|
if (is_object($langs)) {
|
|
return $langs->trans($key);
|
|
}
|
|
return $key;
|
|
}
|
|
|
|
/**
|
|
* Find matching customer invoices
|
|
*
|
|
* @param string $searchText Text to search in
|
|
* @param array &$seenIds Already seen invoice IDs
|
|
* @return array Matches
|
|
*/
|
|
protected function findCustomerInvoiceMatches($searchText, &$seenIds)
|
|
{
|
|
$matches = array();
|
|
|
|
// 1. Search by invoice reference in description (highest priority)
|
|
$sql = "SELECT f.rowid, f.ref, f.ref_client, f.total_ttc, f.date_lim_reglement,";
|
|
$sql .= " s.nom as socname, s.rowid as socid";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON f.fk_soc = s.rowid";
|
|
$sql .= " WHERE f.entity = ".((int) $this->entity);
|
|
$sql .= " AND f.fk_statut = 1"; // Unpaid
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$score = 0;
|
|
$matchReasons = array();
|
|
|
|
// Check if invoice reference appears in description
|
|
if (stripos($searchText, $obj->ref) !== false) {
|
|
$score += 80;
|
|
$matchReasons[] = 'ref';
|
|
}
|
|
|
|
// Check client reference
|
|
if (!empty($obj->ref_client) && stripos($searchText, $obj->ref_client) !== false) {
|
|
$score += 70;
|
|
$matchReasons[] = 'ref_client';
|
|
}
|
|
|
|
// Check exact amount match
|
|
if (abs($obj->total_ttc - $this->amount) < 0.01) {
|
|
$score += 50;
|
|
$matchReasons[] = 'amount';
|
|
} elseif (abs($obj->total_ttc - $this->amount) < 1.00) {
|
|
// Close amount (within 1 EUR)
|
|
$score += 20;
|
|
$matchReasons[] = 'amount_close';
|
|
}
|
|
|
|
// Check name similarity
|
|
if (!empty($this->name) && !empty($obj->socname)) {
|
|
$nameSimilarity = $this->calculateNameSimilarity($this->name, $obj->socname);
|
|
if ($nameSimilarity > 80) {
|
|
$score += 40;
|
|
$matchReasons[] = 'name_exact';
|
|
} elseif ($nameSimilarity > 50) {
|
|
$score += 20;
|
|
$matchReasons[] = 'name_similar';
|
|
}
|
|
}
|
|
|
|
// Check IBAN match
|
|
if (!empty($this->counterparty_iban)) {
|
|
$ibanMatch = $this->checkSocieteIban($obj->socid, $this->counterparty_iban);
|
|
if ($ibanMatch) {
|
|
$score += 60;
|
|
$matchReasons[] = 'iban';
|
|
}
|
|
}
|
|
|
|
// Only include if score is significant
|
|
if ($score >= 50 && !isset($seenIds['facture_'.$obj->rowid])) {
|
|
$seenIds['facture_'.$obj->rowid] = true;
|
|
$matches[] = array(
|
|
'type' => 'facture',
|
|
'id' => $obj->rowid,
|
|
'ref' => $obj->ref,
|
|
'amount' => $obj->total_ttc,
|
|
'socname' => $obj->socname,
|
|
'socid' => $obj->socid,
|
|
'match_score' => min($score, 100),
|
|
'match_reasons' => $matchReasons,
|
|
'date_due' => $obj->date_lim_reglement
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $matches;
|
|
}
|
|
|
|
/**
|
|
* Find matching supplier invoices
|
|
*
|
|
* @param string $searchText Text to search in
|
|
* @param array &$seenIds Already seen invoice IDs
|
|
* @return array Matches
|
|
*/
|
|
protected function findSupplierInvoiceMatches($searchText, &$seenIds)
|
|
{
|
|
$matches = array();
|
|
$absAmount = abs($this->amount);
|
|
|
|
$sql = "SELECT f.rowid, f.ref, f.ref_supplier, f.total_ttc, f.date_lim_reglement,";
|
|
$sql .= " s.nom as socname, s.rowid as socid";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn as f";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON f.fk_soc = s.rowid";
|
|
$sql .= " WHERE f.entity = ".((int) $this->entity);
|
|
$sql .= " AND f.fk_statut = 1"; // Unpaid
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$score = 0;
|
|
$matchReasons = array();
|
|
|
|
// Check if invoice reference appears in description
|
|
if (stripos($searchText, $obj->ref) !== false) {
|
|
$score += 80;
|
|
$matchReasons[] = 'ref';
|
|
}
|
|
|
|
// Check supplier reference
|
|
if (!empty($obj->ref_supplier) && stripos($searchText, $obj->ref_supplier) !== false) {
|
|
$score += 70;
|
|
$matchReasons[] = 'ref_supplier';
|
|
}
|
|
|
|
// Check exact amount match
|
|
if (abs($obj->total_ttc - $absAmount) < 0.01) {
|
|
$score += 50;
|
|
$matchReasons[] = 'amount';
|
|
} elseif (abs($obj->total_ttc - $absAmount) < 1.00) {
|
|
$score += 20;
|
|
$matchReasons[] = 'amount_close';
|
|
}
|
|
|
|
// Check name similarity
|
|
if (!empty($this->name) && !empty($obj->socname)) {
|
|
$nameSimilarity = $this->calculateNameSimilarity($this->name, $obj->socname);
|
|
if ($nameSimilarity > 80) {
|
|
$score += 40;
|
|
$matchReasons[] = 'name_exact';
|
|
} elseif ($nameSimilarity > 50) {
|
|
$score += 20;
|
|
$matchReasons[] = 'name_similar';
|
|
}
|
|
}
|
|
|
|
// Check IBAN match
|
|
if (!empty($this->counterparty_iban)) {
|
|
$ibanMatch = $this->checkSocieteIban($obj->socid, $this->counterparty_iban);
|
|
if ($ibanMatch) {
|
|
$score += 60;
|
|
$matchReasons[] = 'iban';
|
|
}
|
|
}
|
|
|
|
// Only include if score is significant
|
|
if ($score >= 50 && !isset($seenIds['facture_fourn_'.$obj->rowid])) {
|
|
$seenIds['facture_fourn_'.$obj->rowid] = true;
|
|
$matches[] = array(
|
|
'type' => 'facture_fourn',
|
|
'id' => $obj->rowid,
|
|
'ref' => $obj->ref,
|
|
'amount' => $obj->total_ttc,
|
|
'socname' => $obj->socname,
|
|
'socid' => $obj->socid,
|
|
'match_score' => min($score, 100),
|
|
'match_reasons' => $matchReasons,
|
|
'date_due' => $obj->date_lim_reglement
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $matches;
|
|
}
|
|
|
|
/**
|
|
* Calculate name similarity percentage
|
|
*
|
|
* @param string $name1 First name
|
|
* @param string $name2 Second name
|
|
* @return int Similarity percentage (0-100)
|
|
*/
|
|
protected function calculateNameSimilarity($name1, $name2)
|
|
{
|
|
// Normalize names
|
|
$name1 = strtolower(trim($name1));
|
|
$name2 = strtolower(trim($name2));
|
|
|
|
// Remove common suffixes
|
|
$suffixes = array(' gmbh', ' ag', ' kg', ' ohg', ' e.k.', ' ug', ' gbr', ' se', ' co. kg', ' gmbh & co. kg');
|
|
foreach ($suffixes as $suffix) {
|
|
$name1 = str_replace($suffix, '', $name1);
|
|
$name2 = str_replace($suffix, '', $name2);
|
|
}
|
|
|
|
$name1 = trim($name1);
|
|
$name2 = trim($name2);
|
|
|
|
// Check if one contains the other
|
|
if (strpos($name1, $name2) !== false || strpos($name2, $name1) !== false) {
|
|
return 90;
|
|
}
|
|
|
|
// Use similar_text for fuzzy matching
|
|
$similarity = 0;
|
|
similar_text($name1, $name2, $similarity);
|
|
|
|
return (int) $similarity;
|
|
}
|
|
|
|
/**
|
|
* Find multiple supplier invoices that together match the transaction amount
|
|
* Uses the logic: oldest invoices first, check if ref_supplier appears in description
|
|
*
|
|
* @param string $searchText Text to search in (description)
|
|
* @param int $socid Societe ID (supplier)
|
|
* @param float $targetAmount Target amount (absolute value of transaction)
|
|
* @param float $tolerance Tolerance for amount matching (default 5.00)
|
|
* @return array|null Array with 'invoices' and 'total' or null if no match
|
|
*/
|
|
protected function findMultipleSupplierInvoiceMatches($searchText, $socid, $targetAmount, $tolerance = 5.00)
|
|
{
|
|
$matchedInvoices = array();
|
|
$runningTotal = 0.0;
|
|
|
|
// Get all unpaid supplier invoices for this supplier, ordered by date (oldest first)
|
|
$sql = "SELECT f.rowid, f.ref, f.ref_supplier, f.total_ttc, f.datef, f.date_lim_reglement,";
|
|
$sql .= " s.nom as socname, s.rowid as socid";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn as f";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON f.fk_soc = s.rowid";
|
|
$sql .= " WHERE f.entity = ".((int) $this->entity);
|
|
$sql .= " AND f.fk_statut = 1"; // Unpaid
|
|
$sql .= " AND f.fk_soc = ".((int) $socid);
|
|
$sql .= " ORDER BY f.datef ASC, f.rowid ASC"; // Oldest first
|
|
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
return null;
|
|
}
|
|
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
// Check if this invoice's ref_supplier appears in the search text
|
|
$refFound = false;
|
|
if (!empty($obj->ref_supplier)) {
|
|
// Try different formats: with and without leading zeros, etc.
|
|
$refClean = preg_replace('/^0+/', '', $obj->ref_supplier); // Remove leading zeros
|
|
if (stripos($searchText, $obj->ref_supplier) !== false ||
|
|
stripos($searchText, $refClean) !== false) {
|
|
$refFound = true;
|
|
}
|
|
}
|
|
|
|
// Also check the internal ref
|
|
if (!$refFound && stripos($searchText, $obj->ref) !== false) {
|
|
$refFound = true;
|
|
}
|
|
|
|
if ($refFound) {
|
|
// Calculate remaining amount for this invoice
|
|
$remainToPay = $this->getInvoiceRemainToPay('facture_fourn', $obj->rowid, $obj->total_ttc);
|
|
|
|
if ($remainToPay > 0) {
|
|
$matchedInvoices[] = array(
|
|
'type' => 'facture_fourn',
|
|
'id' => $obj->rowid,
|
|
'ref' => $obj->ref,
|
|
'ref_supplier' => $obj->ref_supplier,
|
|
'amount' => $remainToPay,
|
|
'total_ttc' => $obj->total_ttc,
|
|
'socname' => $obj->socname,
|
|
'socid' => $obj->socid,
|
|
'datef' => $obj->datef,
|
|
'date_due' => $obj->date_lim_reglement
|
|
);
|
|
$runningTotal += $remainToPay;
|
|
}
|
|
}
|
|
|
|
// Stop if we've reached or exceeded the target
|
|
if ($runningTotal >= $targetAmount - $tolerance) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
$this->db->free($resql);
|
|
|
|
// Check if total matches within tolerance
|
|
if (!empty($matchedInvoices) && abs($runningTotal - $targetAmount) <= $tolerance) {
|
|
return array(
|
|
'invoices' => $matchedInvoices,
|
|
'total' => $runningTotal,
|
|
'difference' => $targetAmount - $runningTotal,
|
|
'match_score' => abs($runningTotal - $targetAmount) < 0.01 ? 100 : 98
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get remaining amount to pay for an invoice
|
|
*
|
|
* @param string $type 'facture' or 'facture_fourn'
|
|
* @param int $invoiceId Invoice ID
|
|
* @param float $totalTtc Total TTC of invoice
|
|
* @return float Remaining amount
|
|
*/
|
|
protected function getInvoiceRemainToPay($type, $invoiceId, $totalTtc)
|
|
{
|
|
if ($type == 'facture_fourn') {
|
|
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
|
$invoice = new FactureFournisseur($this->db);
|
|
if ($invoice->fetch($invoiceId) > 0) {
|
|
$alreadyPaid = $invoice->getSommePaiement();
|
|
$creditnotes = $invoice->getSumCreditNotesUsed();
|
|
$deposits = $invoice->getSumDepositsUsed();
|
|
return price2num($invoice->total_ttc - $alreadyPaid - $creditnotes - $deposits, 'MT');
|
|
}
|
|
} else {
|
|
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
|
|
$invoice = new Facture($this->db);
|
|
if ($invoice->fetch($invoiceId) > 0) {
|
|
$alreadyPaid = $invoice->getSommePaiement();
|
|
$creditnotes = $invoice->getSumCreditNotesUsed();
|
|
$deposits = $invoice->getSumDepositsUsed();
|
|
return price2num($invoice->total_ttc - $alreadyPaid - $creditnotes - $deposits, 'MT');
|
|
}
|
|
}
|
|
return $totalTtc;
|
|
}
|
|
|
|
/**
|
|
* Check if IBAN matches any bank account of a societe
|
|
*
|
|
* @param int $socid Societe ID
|
|
* @param string $iban IBAN to check
|
|
* @return bool True if match found
|
|
*/
|
|
protected function checkSocieteIban($socid, $iban)
|
|
{
|
|
if (empty($socid) || empty($iban)) {
|
|
return false;
|
|
}
|
|
|
|
// Normalize IBAN (remove spaces)
|
|
$iban = str_replace(' ', '', strtoupper($iban));
|
|
|
|
$sql = "SELECT iban_prefix FROM ".MAIN_DB_PREFIX."societe_rib";
|
|
$sql .= " WHERE fk_soc = ".((int) $socid);
|
|
$sql .= " AND status = 1"; // Active
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$storedIban = str_replace(' ', '', strtoupper($obj->iban_prefix));
|
|
if ($storedIban === $iban) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Find third party by IBAN
|
|
*
|
|
* @param string $iban IBAN to search
|
|
* @return int|null Societe ID or null
|
|
*/
|
|
public function findSocieteByIban($iban)
|
|
{
|
|
if (empty($iban)) {
|
|
return null;
|
|
}
|
|
|
|
$iban = str_replace(' ', '', strtoupper($iban));
|
|
|
|
$sql = "SELECT fk_soc FROM ".MAIN_DB_PREFIX."societe_rib";
|
|
$sql .= " WHERE REPLACE(UPPER(iban_prefix), ' ', '') = '".$this->db->escape($iban)."'";
|
|
$sql .= " AND status = 1";
|
|
$sql .= " LIMIT 1";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
return (int) $obj->fk_soc;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Auto-detect and link third party by IBAN
|
|
*
|
|
* @param User $user User doing the action
|
|
* @return int Societe ID if found and linked, 0 if not found
|
|
*/
|
|
public function autoLinkSociete($user)
|
|
{
|
|
if (!empty($this->fk_societe)) {
|
|
return $this->fk_societe; // Already linked
|
|
}
|
|
|
|
$socid = $this->findSocieteByIban($this->counterparty_iban);
|
|
if ($socid > 0) {
|
|
$this->fk_societe = $socid;
|
|
$this->update($user);
|
|
return $socid;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Confirm payment for multiple invoices (batch payment)
|
|
* Creates a single payment that covers multiple invoices
|
|
*
|
|
* @param User $user User performing the action
|
|
* @param array $invoices Array of invoice data: [['type' => 'facture_fourn', 'id' => X, 'amount' => Y], ...]
|
|
* @param int $bankAccountId Dolibarr bank account ID
|
|
* @return int >0 if OK (payment ID), <0 if error
|
|
*/
|
|
public function confirmMultiplePayment($user, $invoices, $bankAccountId)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
if (empty($invoices)) {
|
|
$this->error = 'No invoices provided';
|
|
return -1;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
|
|
// Determine if these are customer or supplier invoices
|
|
$firstType = $invoices[0]['type'];
|
|
$isSupplier = ($firstType == 'facture_fourn');
|
|
|
|
// Build amounts array for payment
|
|
$amounts = array();
|
|
$multicurrency_amounts = array();
|
|
$totalPayment = 0;
|
|
$socid = 0;
|
|
|
|
foreach ($invoices as $inv) {
|
|
$invoiceId = (int) $inv['id'];
|
|
$payAmount = (float) $inv['amount'];
|
|
|
|
if ($payAmount <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$amounts[$invoiceId] = $payAmount;
|
|
$multicurrency_amounts[$invoiceId] = $payAmount;
|
|
$totalPayment += $payAmount;
|
|
|
|
// Get socid from first invoice
|
|
if ($socid == 0) {
|
|
if ($isSupplier) {
|
|
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
|
$invoice = new FactureFournisseur($this->db);
|
|
if ($invoice->fetch($invoiceId) > 0) {
|
|
$socid = $invoice->socid;
|
|
}
|
|
} else {
|
|
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
|
|
$invoice = new Facture($this->db);
|
|
if ($invoice->fetch($invoiceId) > 0) {
|
|
$socid = $invoice->socid;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($amounts)) {
|
|
$this->error = 'No valid invoice amounts';
|
|
$this->db->rollback();
|
|
return -2;
|
|
}
|
|
|
|
// Check for cash discount (Skonto): If paid amount is less than invoice sum
|
|
$actualAmount = abs($this->amount);
|
|
$hasDiscount = false;
|
|
$discountAmount = 0;
|
|
$discountFactor = 1.0;
|
|
|
|
if ($actualAmount < $totalPayment && $actualAmount > 0) {
|
|
$discountAmount = $totalPayment - $actualAmount;
|
|
// Only apply discount logic if within 5% (typical cash discount range)
|
|
if ($discountAmount <= ($totalPayment * 0.05)) {
|
|
$hasDiscount = true;
|
|
$discountFactor = $actualAmount / $totalPayment;
|
|
// Adjust amounts proportionally
|
|
foreach ($amounts as $invoiceId => $amount) {
|
|
$amounts[$invoiceId] = price2num($amount * $discountFactor, 'MT');
|
|
$multicurrency_amounts[$invoiceId] = $amounts[$invoiceId];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($isSupplier) {
|
|
// Supplier invoice payment
|
|
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/fourn/class/paiementfourn.class.php';
|
|
|
|
$paiementfourn = new PaiementFourn($this->db);
|
|
$paiementfourn->datepaye = $this->date_trans;
|
|
$paiementfourn->amounts = $amounts;
|
|
$paiementfourn->multicurrency_amounts = $multicurrency_amounts;
|
|
$paiementfourn->paiementid = $paiementTypeId;
|
|
$paiementfourn->paiementcode = 'VIR';
|
|
$paiementfourn->num_payment = $this->end_to_end_id ?: $this->ref;
|
|
$paiementfourn->note_private = $langs->trans("PaymentCreatedByBankImport").' - '.$this->name.' - '.count($invoices).' '.$langs->trans("Invoices");
|
|
|
|
$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) {
|
|
// If we had a cash discount, close the invoices with discount code
|
|
if ($hasDiscount) {
|
|
foreach ($invoices as $inv) {
|
|
$invObj = new FactureFournisseur($this->db);
|
|
if ($invObj->fetch($inv['id']) > 0) {
|
|
// Check if invoice still has remaining amount
|
|
$remaining = $invObj->getRemainToPay();
|
|
if ($remaining > 0 && $remaining <= $discountAmount) {
|
|
// Close with discount code
|
|
$invObj->setPaid($user, 'discount_vat', $langs->trans("CashDiscount").' ('.price($remaining).' '.$this->currency.')');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->fk_paiementfourn = $paymentId;
|
|
$this->fk_bank = $bankLineId;
|
|
$this->fk_societe = $socid;
|
|
$this->status = self::STATUS_MATCHED;
|
|
$this->fk_user_match = $user->id;
|
|
$this->date_match = dol_now();
|
|
// Store first invoice as reference (or could store all in note)
|
|
$this->fk_facture_fourn = $invoices[0]['id'];
|
|
$discountNote = $hasDiscount ? ' (Skonto: '.price($discountAmount).' '.$this->currency.')' : '';
|
|
$this->note_private = ($this->note_private ? $this->note_private."\n" : '').'Multi-invoice payment: '.implode(', ', array_column($invoices, 'ref')).$discountNote;
|
|
$this->update($user);
|
|
} else {
|
|
$this->error = 'Failed to add payment to bank';
|
|
$error++;
|
|
}
|
|
}
|
|
} else {
|
|
// 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';
|
|
|
|
$paiement = new Paiement($this->db);
|
|
$paiement->datepaye = $this->date_trans;
|
|
$paiement->amounts = $amounts;
|
|
$paiement->multicurrency_amounts = $multicurrency_amounts;
|
|
$paiement->paiementid = $paiementTypeId;
|
|
$paiement->paiementcode = 'VIR';
|
|
$paiement->num_payment = $this->end_to_end_id ?: $this->ref;
|
|
$paiement->note_private = $langs->trans("PaymentCreatedByBankImport").' - '.$this->name.' - '.count($invoices).' '.$langs->trans("Invoices");
|
|
|
|
$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) {
|
|
// If we had a cash discount, close the invoices with discount code
|
|
if ($hasDiscount) {
|
|
foreach ($invoices as $inv) {
|
|
$invObj = new Facture($this->db);
|
|
if ($invObj->fetch($inv['id']) > 0) {
|
|
// Check if invoice still has remaining amount
|
|
$remaining = $invObj->getRemainToPay();
|
|
if ($remaining > 0 && $remaining <= $discountAmount) {
|
|
// Close with discount code
|
|
$invObj->setPaid($user, 'discount_vat', $langs->trans("CashDiscount").' ('.price($remaining).' '.$this->currency.')');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->fk_paiement = $paymentId;
|
|
$this->fk_bank = $bankLineId;
|
|
$this->fk_societe = $socid;
|
|
$this->status = self::STATUS_MATCHED;
|
|
$this->fk_user_match = $user->id;
|
|
$this->date_match = dol_now();
|
|
$this->fk_facture = $invoices[0]['id'];
|
|
$discountNote = $hasDiscount ? ' (Skonto: '.price($discountAmount).' '.$this->currency.')' : '';
|
|
$this->note_private = ($this->note_private ? $this->note_private."\n" : '').'Multi-invoice payment: '.implode(', ', array_column($invoices, 'ref')).$discountNote;
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* Link transaction to an existing payment (for already paid invoices)
|
|
* Finds the existing payment for the invoice and creates the bank entry.
|
|
* If no payment exists (invoice marked paid without payment record), creates both payment and bank entry.
|
|
*
|
|
* @param User $user User performing the action
|
|
* @param string $invoiceType Invoice type ('facture' or 'facture_fourn')
|
|
* @param int $invoiceId Invoice ID
|
|
* @param int $bankAccountId Dolibarr bank account ID
|
|
* @return int >0 if OK (bank line ID), <0 if error
|
|
*/
|
|
public function linkExistingPayment($user, $invoiceType, $invoiceId, $bankAccountId)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
$error = 0;
|
|
$bankLineId = 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';
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
|
|
if ($invoiceType == 'facture_fourn') {
|
|
// Supplier invoice
|
|
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 = 'Invoice not found';
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
|
|
// Find the payment(s) for this invoice
|
|
$sql = "SELECT pfp.fk_paiementfourn as payment_id, pfp.amount";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."paiementfourn_facturefourn as pfp";
|
|
$sql .= " WHERE pfp.fk_facturefourn = ".((int) $invoiceId);
|
|
$sql .= " ORDER BY pfp.rowid DESC";
|
|
$sql .= " LIMIT 1";
|
|
|
|
$resql = $this->db->query($sql);
|
|
$paymentExists = ($resql && $this->db->num_rows($resql) > 0);
|
|
|
|
if ($paymentExists) {
|
|
// Payment exists - link bank entry to it
|
|
$obj = $this->db->fetch_object($resql);
|
|
$paymentId = (int) $obj->payment_id;
|
|
|
|
$paiementfourn = new PaiementFourn($this->db);
|
|
if ($paiementfourn->fetch($paymentId) <= 0) {
|
|
$this->error = 'Payment not found';
|
|
$this->db->rollback();
|
|
return -3;
|
|
}
|
|
|
|
// Check if payment already has a bank entry
|
|
if (!empty($paiementfourn->bank_line) && $paiementfourn->bank_line > 0) {
|
|
// Just link transaction to existing bank entry
|
|
$this->fk_paiementfourn = $paymentId;
|
|
$this->fk_bank = $paiementfourn->bank_line;
|
|
$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();
|
|
|
|
$result = $this->update($user);
|
|
if ($result <= 0) {
|
|
$this->error = 'Failed to update transaction: '.$this->error;
|
|
$this->db->rollback();
|
|
return -6;
|
|
}
|
|
|
|
$this->db->commit();
|
|
return $paiementfourn->bank_line > 0 ? $paiementfourn->bank_line : 1;
|
|
}
|
|
|
|
// Create bank entry for existing payment
|
|
$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++;
|
|
}
|
|
} else {
|
|
// No payment exists - create new payment AND bank entry
|
|
// This handles invoices marked as paid without actual payment record
|
|
$amounts = array($invoiceId => $invoice->total_ttc);
|
|
$multicurrency_amounts = array($invoiceId => $invoice->total_ttc);
|
|
|
|
$paiementfourn = new PaiementFourn($this->db);
|
|
$paiementfourn->datepaye = $this->date_trans;
|
|
$paiementfourn->amounts = $amounts;
|
|
$paiementfourn->multicurrency_amounts = $multicurrency_amounts;
|
|
$paiementfourn->paiementid = $paiementTypeId;
|
|
$paiementfourn->paiementcode = 'VIR';
|
|
$paiementfourn->num_payment = $this->end_to_end_id ?: $this->ref;
|
|
$paiementfourn->note_private = $langs->trans("PaymentCreatedByBankImport").' - '.$this->name;
|
|
|
|
$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++;
|
|
}
|
|
}
|
|
}
|
|
} elseif ($invoiceType == 'facture') {
|
|
// Customer invoice
|
|
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';
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
|
|
// Find the payment(s) for this invoice
|
|
$sql = "SELECT pfp.fk_paiement as payment_id, pfp.amount";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."paiement_facture as pfp";
|
|
$sql .= " WHERE pfp.fk_facture = ".((int) $invoiceId);
|
|
$sql .= " ORDER BY pfp.rowid DESC";
|
|
$sql .= " LIMIT 1";
|
|
|
|
$resql = $this->db->query($sql);
|
|
$paymentExists = ($resql && $this->db->num_rows($resql) > 0);
|
|
|
|
if ($paymentExists) {
|
|
// Payment exists - link bank entry to it
|
|
$obj = $this->db->fetch_object($resql);
|
|
$paymentId = (int) $obj->payment_id;
|
|
|
|
$paiement = new Paiement($this->db);
|
|
if ($paiement->fetch($paymentId) <= 0) {
|
|
$this->error = 'Payment not found';
|
|
$this->db->rollback();
|
|
return -3;
|
|
}
|
|
|
|
// Check if payment already has a bank entry
|
|
if (!empty($paiement->bank_line) && $paiement->bank_line > 0) {
|
|
// Just link transaction to existing bank entry
|
|
$this->fk_paiement = $paymentId;
|
|
$this->fk_bank = $paiement->bank_line;
|
|
$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);
|
|
|
|
$this->db->commit();
|
|
return $paiement->bank_line;
|
|
}
|
|
|
|
// Create bank entry for existing payment
|
|
$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++;
|
|
}
|
|
} else {
|
|
// No payment exists - create new payment AND bank entry
|
|
$amounts = array($invoiceId => $invoice->total_ttc);
|
|
$multicurrency_amounts = array($invoiceId => $invoice->total_ttc);
|
|
|
|
$paiement = new Paiement($this->db);
|
|
$paiement->datepaye = $this->date_trans;
|
|
$paiement->amounts = $amounts;
|
|
$paiement->multicurrency_amounts = $multicurrency_amounts;
|
|
$paiement->paiementid = $paiementTypeId;
|
|
$paiement->paiementcode = 'VIR';
|
|
$paiement->num_payment = $this->end_to_end_id ?: $this->ref;
|
|
$paiement->note_private = $langs->trans("PaymentCreatedByBankImport").' - '.$this->name;
|
|
|
|
$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++;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$this->error = 'Unknown invoice type: '.$invoiceType;
|
|
$this->db->rollback();
|
|
return -4;
|
|
}
|
|
|
|
if ($error) {
|
|
$this->db->rollback();
|
|
return -5;
|
|
}
|
|
|
|
$this->db->commit();
|
|
return $bankLineId;
|
|
}
|
|
|
|
/**
|
|
* Link multiple already paid invoices to this bank transaction
|
|
* Finds existing payments for these invoices and links them to a single bank entry
|
|
*
|
|
* @param User $user User making the link
|
|
* @param array $invoices Array of ['type' => 'facture'|'facture_fourn', 'id' => int]
|
|
* @param int $bankAccountId Dolibarr bank account ID
|
|
* @return int <0 if KO, >0 bank line ID if OK
|
|
*/
|
|
public function linkMultipleExistingPayments($user, $invoices, $bankAccountId)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
if (empty($invoices)) {
|
|
$this->error = 'No invoices provided';
|
|
return -1;
|
|
}
|
|
|
|
$error = 0;
|
|
$bankLineId = 0;
|
|
$this->db->begin();
|
|
|
|
// Determine invoice type (all must be same type)
|
|
$firstType = $invoices[0]['type'];
|
|
$isSupplier = ($firstType == 'facture_fourn');
|
|
|
|
// Collect all payment IDs and check they exist
|
|
$paymentIds = array();
|
|
$socid = 0;
|
|
$invoiceRefs = array();
|
|
|
|
foreach ($invoices as $inv) {
|
|
$invoiceId = (int) $inv['id'];
|
|
|
|
if ($isSupplier) {
|
|
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
|
$invoice = new FactureFournisseur($this->db);
|
|
if ($invoice->fetch($invoiceId) <= 0) {
|
|
$this->error = 'Invoice not found: '.$invoiceId;
|
|
$this->db->rollback();
|
|
return -2;
|
|
}
|
|
$invoiceRefs[] = $invoice->ref;
|
|
if ($socid == 0) {
|
|
$socid = $invoice->socid;
|
|
}
|
|
|
|
// Find payment for this invoice
|
|
$sql = "SELECT pfp.fk_paiementfourn as payment_id";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."paiementfourn_facturefourn as pfp";
|
|
$sql .= " WHERE pfp.fk_facturefourn = ".((int) $invoiceId);
|
|
$sql .= " ORDER BY pfp.rowid DESC LIMIT 1";
|
|
$resql = $this->db->query($sql);
|
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
$paymentIds[$obj->payment_id] = $obj->payment_id;
|
|
}
|
|
} else {
|
|
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
|
|
$invoice = new Facture($this->db);
|
|
if ($invoice->fetch($invoiceId) <= 0) {
|
|
$this->error = 'Invoice not found: '.$invoiceId;
|
|
$this->db->rollback();
|
|
return -2;
|
|
}
|
|
$invoiceRefs[] = $invoice->ref;
|
|
if ($socid == 0) {
|
|
$socid = $invoice->socid;
|
|
}
|
|
|
|
// Find payment for this invoice
|
|
$sql = "SELECT pfp.fk_paiement as payment_id";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."paiement_facture as pfp";
|
|
$sql .= " WHERE pfp.fk_facture = ".((int) $invoiceId);
|
|
$sql .= " ORDER BY pfp.rowid DESC LIMIT 1";
|
|
$resql = $this->db->query($sql);
|
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
$paymentIds[$obj->payment_id] = $obj->payment_id;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look up payment type ID for 'VIR' (bank transfer) - needed if we create new payment
|
|
$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 no payments found, we need to create new payment(s)
|
|
if (empty($paymentIds)) {
|
|
// No existing payment - create one for all invoices
|
|
if ($isSupplier) {
|
|
require_once DOL_DOCUMENT_ROOT.'/fourn/class/paiementfourn.class.php';
|
|
|
|
// Build amounts array from invoices
|
|
$amounts = array();
|
|
$multicurrency_amounts = array();
|
|
foreach ($invoices as $inv) {
|
|
$invoice = new FactureFournisseur($this->db);
|
|
if ($invoice->fetch($inv['id']) > 0) {
|
|
$amounts[$inv['id']] = $invoice->total_ttc;
|
|
$multicurrency_amounts[$inv['id']] = $invoice->total_ttc;
|
|
}
|
|
}
|
|
|
|
$paiementfourn = new PaiementFourn($this->db);
|
|
$paiementfourn->datepaye = $this->date_trans;
|
|
$paiementfourn->amounts = $amounts;
|
|
$paiementfourn->multicurrency_amounts = $multicurrency_amounts;
|
|
$paiementfourn->paiementid = $paiementTypeId;
|
|
$paiementfourn->paiementcode = 'VIR';
|
|
$paiementfourn->num_payment = $this->end_to_end_id ?: $this->ref;
|
|
$paiementfourn->note_private = $langs->trans("PaymentCreatedByBankImport").' - '.$this->name;
|
|
|
|
$paymentId = $paiementfourn->create($user, 1);
|
|
if ($paymentId < 0) {
|
|
$this->error = $paiementfourn->error;
|
|
$this->errors = $paiementfourn->errors;
|
|
$this->db->rollback();
|
|
return -3;
|
|
}
|
|
|
|
$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 = $invoices[0]['id'];
|
|
$this->fk_societe = $socid;
|
|
$this->status = self::STATUS_MATCHED;
|
|
$this->fk_user_match = $user->id;
|
|
$this->date_match = dol_now();
|
|
$this->note_private = ($this->note_private ? $this->note_private."\n" : '').'Multi-invoice link: '.implode(', ', $invoiceRefs);
|
|
$this->update($user);
|
|
|
|
$this->db->commit();
|
|
return $bankLineId;
|
|
} else {
|
|
$this->error = 'Failed to add payment to bank';
|
|
$this->db->rollback();
|
|
return -5;
|
|
}
|
|
} else {
|
|
require_once DOL_DOCUMENT_ROOT.'/compta/paiement/class/paiement.class.php';
|
|
|
|
// Build amounts array from invoices
|
|
$amounts = array();
|
|
$multicurrency_amounts = array();
|
|
foreach ($invoices as $inv) {
|
|
$invoice = new Facture($this->db);
|
|
if ($invoice->fetch($inv['id']) > 0) {
|
|
$amounts[$inv['id']] = $invoice->total_ttc;
|
|
$multicurrency_amounts[$inv['id']] = $invoice->total_ttc;
|
|
}
|
|
}
|
|
|
|
$paiement = new Paiement($this->db);
|
|
$paiement->datepaye = $this->date_trans;
|
|
$paiement->amounts = $amounts;
|
|
$paiement->multicurrency_amounts = $multicurrency_amounts;
|
|
$paiement->paiementid = $paiementTypeId;
|
|
$paiement->paiementcode = 'VIR';
|
|
$paiement->num_payment = $this->end_to_end_id ?: $this->ref;
|
|
$paiement->note_private = $langs->trans("PaymentCreatedByBankImport").' - '.$this->name;
|
|
|
|
$paymentId = $paiement->create($user, 1);
|
|
if ($paymentId < 0) {
|
|
$this->error = $paiement->error;
|
|
$this->errors = $paiement->errors;
|
|
$this->db->rollback();
|
|
return -3;
|
|
}
|
|
|
|
$bankLineId = $paiement->addPaymentToBank(
|
|
$user,
|
|
'payment',
|
|
'(CustomerInvoicePayment)',
|
|
$bankAccountId,
|
|
$this->name,
|
|
''
|
|
);
|
|
|
|
if ($bankLineId > 0) {
|
|
$this->fk_paiement = $paymentId;
|
|
$this->fk_bank = $bankLineId;
|
|
$this->fk_facture = $invoices[0]['id'];
|
|
$this->fk_societe = $socid;
|
|
$this->status = self::STATUS_MATCHED;
|
|
$this->fk_user_match = $user->id;
|
|
$this->date_match = dol_now();
|
|
$this->note_private = ($this->note_private ? $this->note_private."\n" : '').'Multi-invoice link: '.implode(', ', $invoiceRefs);
|
|
$this->update($user);
|
|
|
|
$this->db->commit();
|
|
return $bankLineId;
|
|
} else {
|
|
$this->error = 'Failed to add payment to bank';
|
|
$this->db->rollback();
|
|
return -5;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use the first payment to link to bank
|
|
$firstPaymentId = reset($paymentIds);
|
|
|
|
if ($isSupplier) {
|
|
require_once DOL_DOCUMENT_ROOT.'/fourn/class/paiementfourn.class.php';
|
|
$paiementfourn = new PaiementFourn($this->db);
|
|
if ($paiementfourn->fetch($firstPaymentId) <= 0) {
|
|
$this->error = 'Payment not found';
|
|
$this->db->rollback();
|
|
return -4;
|
|
}
|
|
|
|
// Check if payment already has a bank entry
|
|
if (!empty($paiementfourn->bank_line) && $paiementfourn->bank_line > 0) {
|
|
// Payment already has a bank entry in Dolibarr
|
|
// Just link this imported transaction to the existing payment WITHOUT creating new bank entry
|
|
$bankLineId = $paiementfourn->bank_line;
|
|
|
|
$this->fk_paiementfourn = $firstPaymentId;
|
|
$this->fk_bank = $bankLineId;
|
|
$this->fk_facture_fourn = $invoices[0]['id'];
|
|
$this->fk_societe = $socid;
|
|
$this->status = self::STATUS_MATCHED;
|
|
$this->fk_user_match = $user->id;
|
|
$this->date_match = dol_now();
|
|
$this->note_private = ($this->note_private ? $this->note_private."\n" : '').'Multi-invoice link: '.implode(', ', $invoiceRefs);
|
|
|
|
$result = $this->update($user);
|
|
if ($result <= 0) {
|
|
$this->error = 'Failed to update transaction: '.$this->error;
|
|
$this->db->rollback();
|
|
return -6;
|
|
}
|
|
|
|
$this->db->commit();
|
|
return $bankLineId > 0 ? $bankLineId : 1;
|
|
} else {
|
|
// Create bank entry for existing payment
|
|
$bankLineId = $paiementfourn->addPaymentToBank(
|
|
$user,
|
|
'payment_supplier',
|
|
'(SupplierInvoicePayment)',
|
|
$bankAccountId,
|
|
$this->name,
|
|
''
|
|
);
|
|
|
|
if ($bankLineId > 0) {
|
|
$this->fk_paiementfourn = $firstPaymentId;
|
|
$this->fk_bank = $bankLineId;
|
|
$this->fk_facture_fourn = $invoices[0]['id'];
|
|
$this->fk_societe = $socid;
|
|
$this->status = self::STATUS_MATCHED;
|
|
$this->fk_user_match = $user->id;
|
|
$this->date_match = dol_now();
|
|
$this->note_private = ($this->note_private ? $this->note_private."\n" : '').'Multi-invoice link: '.implode(', ', $invoiceRefs);
|
|
$this->update($user);
|
|
} else {
|
|
$this->error = 'Failed to add payment to bank';
|
|
$error++;
|
|
}
|
|
}
|
|
} else {
|
|
require_once DOL_DOCUMENT_ROOT.'/compta/paiement/class/paiement.class.php';
|
|
$paiement = new Paiement($this->db);
|
|
if ($paiement->fetch($firstPaymentId) <= 0) {
|
|
$this->error = 'Payment not found';
|
|
$this->db->rollback();
|
|
return -4;
|
|
}
|
|
|
|
// Check if payment already has a bank entry
|
|
if (!empty($paiement->bank_line) && $paiement->bank_line > 0) {
|
|
// Payment already has a bank entry in Dolibarr
|
|
// Just link this imported transaction to the existing payment WITHOUT creating new bank entry
|
|
$bankLineId = $paiement->bank_line;
|
|
|
|
$this->fk_paiement = $firstPaymentId;
|
|
$this->fk_bank = $bankLineId;
|
|
$this->fk_facture = $invoices[0]['id'];
|
|
$this->fk_societe = $socid;
|
|
$this->status = self::STATUS_MATCHED;
|
|
$this->fk_user_match = $user->id;
|
|
$this->date_match = dol_now();
|
|
$this->note_private = ($this->note_private ? $this->note_private."\n" : '').'Multi-invoice link: '.implode(', ', $invoiceRefs);
|
|
|
|
$result = $this->update($user);
|
|
if ($result <= 0) {
|
|
$this->error = 'Failed to update transaction: '.$this->error;
|
|
$this->db->rollback();
|
|
return -6;
|
|
}
|
|
|
|
$this->db->commit();
|
|
return $bankLineId > 0 ? $bankLineId : 1;
|
|
} else {
|
|
// Create bank entry for existing payment
|
|
$bankLineId = $paiement->addPaymentToBank(
|
|
$user,
|
|
'payment',
|
|
'(CustomerInvoicePayment)',
|
|
$bankAccountId,
|
|
$this->name,
|
|
''
|
|
);
|
|
|
|
if ($bankLineId > 0) {
|
|
$this->fk_paiement = $firstPaymentId;
|
|
$this->fk_bank = $bankLineId;
|
|
$this->fk_facture = $invoices[0]['id'];
|
|
$this->fk_societe = $socid;
|
|
$this->status = self::STATUS_MATCHED;
|
|
$this->fk_user_match = $user->id;
|
|
$this->date_match = dol_now();
|
|
$this->note_private = ($this->note_private ? $this->note_private."\n" : '').'Multi-invoice link: '.implode(', ', $invoiceRefs);
|
|
$this->update($user);
|
|
} else {
|
|
$this->error = 'Failed to add payment to bank';
|
|
$error++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($error) {
|
|
$this->db->rollback();
|
|
return -5;
|
|
}
|
|
|
|
$this->db->commit();
|
|
return $bankLineId > 0 ? $bankLineId : 1;
|
|
}
|
|
|
|
/**
|
|
* Unlink payment from this transaction
|
|
* Resets all links and sets status back to NEW
|
|
* Note: This does NOT delete the payment in Dolibarr, just removes the link
|
|
*
|
|
* @param User $user User making the change
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function unlinkPayment($user)
|
|
{
|
|
$this->db->begin();
|
|
|
|
// Reset all links
|
|
$this->fk_bank = null;
|
|
$this->fk_facture = null;
|
|
$this->fk_facture_fourn = null;
|
|
$this->fk_paiement = null;
|
|
$this->fk_paiementfourn = null;
|
|
$this->fk_societe = null;
|
|
$this->fk_salary = null;
|
|
$this->fk_don = null;
|
|
$this->fk_loan = null;
|
|
|
|
// Reset status to NEW
|
|
$this->status = self::STATUS_NEW;
|
|
$this->fk_user_match = null;
|
|
$this->date_match = null;
|
|
|
|
$result = $this->update($user);
|
|
|
|
if ($result > 0) {
|
|
$this->db->commit();
|
|
return 1;
|
|
} else {
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Link transaction to a Dolibarr object
|
|
*
|
|
* @param string $type Object type (facture, facture_fourn, societe, etc.)
|
|
* @param int $id Object ID
|
|
* @param User $user User making the link
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function linkTo($type, $id, $user)
|
|
{
|
|
switch ($type) {
|
|
case 'facture':
|
|
$this->fk_facture = $id;
|
|
break;
|
|
case 'facture_fourn':
|
|
$this->fk_facture_fourn = $id;
|
|
break;
|
|
case 'societe':
|
|
$this->fk_societe = $id;
|
|
break;
|
|
case 'bank':
|
|
$this->fk_bank = $id;
|
|
break;
|
|
case 'paiement':
|
|
$this->fk_paiement = $id;
|
|
break;
|
|
case 'paiementfourn':
|
|
$this->fk_paiementfourn = $id;
|
|
break;
|
|
case 'salary':
|
|
$this->fk_salary = $id;
|
|
break;
|
|
case 'don':
|
|
$this->fk_don = $id;
|
|
break;
|
|
case 'loan':
|
|
$this->fk_loan = $id;
|
|
break;
|
|
default:
|
|
$this->error = 'Unknown link type: '.$type;
|
|
return -1;
|
|
}
|
|
|
|
$this->status = self::STATUS_MATCHED;
|
|
return $this->update($user);
|
|
}
|
|
}
|