dolibarr/htdocs/accountancy/class/lettering.class.php
Alexandre SPANGARO c06ddd5a2f Spaces
2026-06-11 06:34:46 +02:00

1308 lines
49 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/* Copyright (C) 2004-2005 Rodolphe Quiedeville <rodolphe@quiedeville.org>
* Copyright (C) 2013 Olivier Geffroy <jeff@jeffinfo.com>
* Copyright (C) 2013-2026 Alexandre Spangaro <alexandre@inovea-conseil.com>
* Copyright (C) 2018-2026 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
*
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* \file htdocs/accountancy/class/lettering.class.php
* \ingroup Accountancy (Double entries)
* \brief File of class for lettering
*/
include_once DOL_DOCUMENT_ROOT."/accountancy/class/bookkeeping.class.php";
include_once DOL_DOCUMENT_ROOT."/societe/class/societe.class.php";
include_once DOL_DOCUMENT_ROOT."/core/lib/date.lib.php";
/**
* Class Lettering
*/
class Lettering extends BookKeeping
{
/**
* @var array<string,array{payment_table:string,payment_table_fk_bank:string,doc_payment_table:string,doc_payment_table_fk_payment:string,doc_payment_table_fk_doc:string,linked_info:array<array{table:string,fk_doc:string,fk_link:string,prefix:string,fk_line_link?:string,table_link_line?:string,fk_table_link_line?:string,fk_table_link_line_parent?:string,is_fk_link_is_also_fk_doc?:bool}>}>
*/
public static $doc_type_infos = array(
'customer_invoice' => array(
'payment_table' => 'paiement',
'payment_table_fk_bank' => 'fk_bank',
'doc_payment_table' => 'paiement_facture',
'doc_payment_table_fk_payment' => 'fk_paiement',
'doc_payment_table_fk_doc' => 'fk_facture',
'linked_info' => array(
array(
'table' => 'paiement_facture',
'fk_doc' => 'fk_facture',
'fk_link' => 'fk_paiement',
'prefix' => 'p',
),
array(
'table' => 'societe_remise_except',
'fk_doc' => 'fk_facture_source',
'fk_link' => 'fk_facture',
'fk_line_link' => 'fk_facture_line',
'table_link_line' => 'facturedet',
'fk_table_link_line' => 'rowid',
'fk_table_link_line_parent' => 'fk_facture',
'prefix' => 'a',
'is_fk_link_is_also_fk_doc' => true,
),
),
),
'supplier_invoice' => array(
'payment_table' => 'paiementfourn',
'payment_table_fk_bank' => 'fk_bank',
'doc_payment_table' => 'paiementfourn_facturefourn',
'doc_payment_table_fk_payment' => 'fk_paiementfourn',
'doc_payment_table_fk_doc' => 'fk_facturefourn',
'linked_info' => array(
array(
'table' => 'paiementfourn_facturefourn',
'fk_doc' => 'fk_facturefourn',
'fk_link' => 'fk_paiementfourn',
'prefix' => 'p',
),
array(
'table' => 'societe_remise_except',
'fk_doc' => 'fk_invoice_supplier_source',
'fk_link' => 'fk_invoice_supplier',
'fk_line_link' => 'fk_invoice_supplier_line',
'table_link_line' => 'facture_fourn_det',
'fk_table_link_line' => 'rowid',
'fk_table_link_line_parent' => 'fk_facture_fourn',
'prefix' => 'a',
'is_fk_link_is_also_fk_doc' => true,
),
),
),
);
/**
* letteringThirdparty
*
* @param int $socid Thirdparty id
* @return int 1 OK, <0 error
*/
public function letteringThirdparty($socid)
{
global $conf;
$error = 0;
$object = new Societe($this->db);
$object->id = $socid;
$object->fetch($socid);
if ($object->code_compta_client == '411CUSTCODE') {
$object->code_compta_client = '';
}
if ($object->code_compta_fournisseur == '401SUPPCODE') {
$object->code_compta_fournisseur = '';
}
/**
* Support for complex lettering with debit, credit transfer
*/
$sql = "SELECT DISTINCT bk.rowid, bk.doc_date, bk.doc_type, bk.doc_ref, bk.subledger_account, ";
$sql .= " bk.numero_compte , bk.label_compte, bk.debit , bk.credit, bk.montant ";
$sql .= " , bk.sens , bk.code_journal , bk.piece_num, bk.date_lettering, bu.url_id , bu.type ";
$sql .= " FROM ".MAIN_DB_PREFIX."accounting_bookkeeping as bk";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."bank_url as bu ON(bk.fk_doc = bu.fk_bank AND bu.type IN ('payment', 'payment_supplier') ) ";
$sql .= " WHERE ( ";
if ($object->code_compta_client != "") {
$sql .= " bk.subledger_account = '".$this->db->escape($object->code_compta_client)."' ";
}
if ($object->code_compta_client != "" && $object->code_compta_fournisseur != "") {
$sql .= " OR ";
}
if ($object->code_compta_fournisseur != "") {
$sql .= " bk.subledger_account = '".$this->db->escape($object->code_compta_fournisseur)."' ";
}
$sql .= " ) AND (bk.date_lettering ='' OR bk.date_lettering IS NULL) ";
$sql .= " AND (bk.lettering_code != '' OR bk.lettering_code IS NULL) ";
$sql .= ' AND bk.date_validated IS NULL ';
$sql .= $this->db->order('bk.doc_date', 'DESC');
// echo $sql;
//
$resql = $this->db->query($sql);
if ($resql) {
$num = $this->db->num_rows($resql);
while ($obj = $this->db->fetch_object($resql)) {
$ids = array();
$ids_fact = array();
if ($obj->type == 'payment_supplier') {
$sql = 'SELECT DISTINCT bk.rowid, facf.ref, facf.ref_supplier, payf.fk_bank, facf.rowid as fact_id';
$sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn facf ";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."paiementfourn_facturefourn as payfacf ON payfacf.fk_facturefourn=facf.rowid";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."paiementfourn as payf ON payfacf.fk_paiementfourn=payf.rowid";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."accounting_bookkeeping as bk ON (bk.fk_doc = payf.fk_bank AND bk.code_journal='".$this->db->escape($obj->code_journal)."')";
$sql .= " WHERE payfacf.fk_paiementfourn = '".$this->db->escape($obj->url_id)."' ";
$sql .= " AND facf.entity = " . (int) $conf->entity;
$sql .= " AND code_journal IN (SELECT code FROM ".MAIN_DB_PREFIX."accounting_journal WHERE nature=4 AND entity=" . (int) $conf->entity . ") ";
$sql .= " AND ( ";
if ($object->code_compta_client != "") {
$sql .= " bk.subledger_account = '".$this->db->escape($object->code_compta_client)."' ";
}
if ($object->code_compta_client != "" && $object->code_compta_fournisseur != "") {
$sql .= " OR ";
}
if ($object->code_compta_fournisseur != "") {
$sql .= " bk.subledger_account = '".$this->db->escape($object->code_compta_fournisseur)."' ";
}
$sql .= " ) ";
$resql2 = $this->db->query($sql);
if ($resql2) {
while ($obj2 = $this->db->fetch_object($resql2)) {
$ids[$obj2->rowid] = $obj2->rowid;
$ids_fact[] = $obj2->fact_id;
}
$this->db->free($resql2);
} else {
$this->errors[] = $this->db->lasterror;
return -1;
}
if (count($ids_fact)) {
$sql = 'SELECT bk.rowid, facf.ref, facf.ref_supplier ';
$sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn facf ";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."accounting_bookkeeping as bk ON( bk.fk_doc = facf.rowid AND facf.rowid IN (".$this->db->sanitize(implode(',', $ids_fact))."))";
$sql .= " WHERE bk.code_journal IN (SELECT code FROM ".MAIN_DB_PREFIX."accounting_journal WHERE nature=3 AND entity=". (int) $conf->entity.") ";
$sql .= " AND facf.entity = " . (int) $conf->entity;
$sql .= " AND ( ";
if ($object->code_compta_client != "") {
$sql .= " bk.subledger_account = '".$this->db->escape($object->code_compta_client)."' ";
}
if ($object->code_compta_client != "" && $object->code_compta_fournisseur != "") {
$sql .= " OR ";
}
if ($object->code_compta_fournisseur != "") {
$sql .= " bk.subledger_account = '".$this->db->escape($object->code_compta_fournisseur)."' ";
}
$sql .= ") ";
$resql2 = $this->db->query($sql);
if ($resql2) {
while ($obj2 = $this->db->fetch_object($resql2)) {
$ids[$obj2->rowid] = $obj2->rowid;
}
$this->db->free($resql2);
} else {
$this->errors[] = $this->db->lasterror;
return -1;
}
}
} elseif ($obj->type == 'payment') {
$sql = 'SELECT DISTINCT bk.rowid, fac.ref, fac.ref, pay.fk_bank, fac.rowid as fact_id';
$sql .= " FROM ".MAIN_DB_PREFIX."facture fac ";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."paiement_facture as payfac ON payfac.fk_facture=fac.rowid";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."paiement as pay ON payfac.fk_paiement=pay.rowid";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."accounting_bookkeeping as bk ON (bk.fk_doc = pay.fk_bank AND bk.code_journal='".$this->db->escape($obj->code_journal)."')";
$sql .= " WHERE payfac.fk_paiement = '".$this->db->escape($obj->url_id)."' ";
$sql .= " AND bk.code_journal IN (SELECT code FROM ".MAIN_DB_PREFIX."accounting_journal WHERE nature=4 AND entity=". (int) $conf->entity.") ";
$sql .= " AND fac.entity IN (".getEntity('invoice', 0).")"; // We don't share object for accountancy
$sql .= " AND ( ";
if ($object->code_compta_client != "") {
$sql .= " bk.subledger_account = '".$this->db->escape($object->code_compta_client)."' ";
}
if ($object->code_compta_client != "" && $object->code_compta_fournisseur != "") {
$sql .= " OR ";
}
if ($object->code_compta_fournisseur != "") {
$sql .= " bk.subledger_account = '".$this->db->escape($object->code_compta_fournisseur)."' ";
}
$sql .= " )";
$resql2 = $this->db->query($sql);
if ($resql2) {
while ($obj2 = $this->db->fetch_object($resql2)) {
$ids[$obj2->rowid] = $obj2->rowid;
$ids_fact[] = $obj2->fact_id;
}
} else {
$this->errors[] = $this->db->lasterror;
return -1;
}
if (count($ids_fact)) {
$sql = 'SELECT bk.rowid, fac.ref, fac.ref_supplier ';
$sql .= " FROM ".MAIN_DB_PREFIX."facture fac ";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."accounting_bookkeeping as bk ON( bk.fk_doc = fac.rowid AND fac.rowid IN (".$this->db->sanitize(implode(',', $ids_fact))."))";
$sql .= " WHERE code_journal IN (SELECT code FROM ".MAIN_DB_PREFIX."accounting_journal WHERE nature=2 AND entity=". (int) $conf->entity.") ";
$sql .= " AND fac.entity IN (".getEntity('invoice', 0).")"; // We don't share object for accountancy
$sql .= " AND ( ";
if ($object->code_compta_client != "") {
$sql .= " bk.subledger_account = '".$this->db->escape($object->code_compta_client)."' ";
}
if ($object->code_compta_client != "" && $object->code_compta_fournisseur != "") {
$sql .= " OR ";
}
if ($object->code_compta_fournisseur != "") {
$sql .= " bk.subledger_account = '".$this->db->escape($object->code_compta_fournisseur)."' ";
}
$sql .= " ) ";
$resql2 = $this->db->query($sql);
if ($resql2) {
while ($obj2 = $this->db->fetch_object($resql2)) {
$ids[$obj2->rowid] = $obj2->rowid;
}
$this->db->free($resql2);
} else {
$this->errors[] = $this->db->lasterror;
return -1;
}
}
}
if (count($ids) > 1) {
$result = $this->updateLettering($ids);
}
}
$this->db->free($resql);
}
if ($error) {
foreach ($this->errors as $errmsg) {
dol_syslog(__METHOD__.' '.$errmsg, LOG_ERR);
$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
}
return -1 * $error;
} else {
return 1;
}
}
/**
* @param int[] $ids ids array
* @param int $notrigger no trigger
* @param bool $partial Partial lettering
* @return int
*/
public function updateLettering($ids = array(), $notrigger = 0, $partial = false)
{
global $langs;
$now = dol_now();
$error = 0;
$affected_rows = 0;
// Generate a string with n char 'A' (for manual/auto reconcile) or 'a' (for partial reconcile) where n is ACCOUNTING_LETTERING_NBLETTERS (So 'AA'/'aa', 'AAA'/'aaa', ...) @phan-suppress-next-line PhanParamSuspiciousOrder
$letter = str_pad("", getDolGlobalInt('ACCOUNTING_LETTERING_NBLETTERS', 3), $partial ? 'a' : 'A');
$this->db->begin();
// Check partial / normal lettering case
$idlist = implode(',', array_map('intval', $ids));
$sql = "SELECT ab.lettering_code, GROUP_CONCAT(DISTINCT ab.rowid SEPARATOR ',') AS bookkeeping_ids";
$sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab";
$sql .= " WHERE ab.rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " GROUP BY ab.lettering_code";
$sql .= " ORDER BY ab.lettering_code DESC";
dol_syslog(__METHOD__ . " - Check partial / normal lettering case", LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
if (empty($obj->lettering_code)) {
continue;
}
// Remove normal lettering code if set partial lettering
if ($partial && preg_match('/^[A-Z]+$/', $obj->lettering_code)) {
if (!empty($obj->bookkeeping_ids)) {
$ids = array_diff($ids, explode(',', $obj->bookkeeping_ids));
}
} elseif (!$partial && preg_match('/^[a-z]+$/', $obj->lettering_code)) {
// Delete partial lettering code if set normal lettering
$sql2 = "UPDATE " . MAIN_DB_PREFIX . "accounting_bookkeeping SET";
$sql2 .= " matching_general = 0";
$sql2 .= ", lettering_code = NULL";
$sql2 .= ", date_lettering = NULL";
$sql2 .= " WHERE entity IN (" . getEntity('accountancy') . ")";
$sql2 .= " AND lettering_code = '" . $this->db->escape($obj->lettering_code) . "'";
dol_syslog(__METHOD__ . " - Remove partial lettering", LOG_DEBUG);
$resql2 = $this->db->query($sql2);
if (!$resql2) {
$this->errors[] = 'Error' . $this->db->lasterror();
$error++;
break;
}
}
}
$this->db->free($resql);
} else {
$this->errors[] = 'Error' . $this->db->lasterror();
$error++;
}
if (!$error && !empty($ids)) {
// Get next code
$idlist = implode(',', array_map('intval', $ids));
$sql = "SELECT DISTINCT ab2.lettering_code";
$sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab";
$sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab2 ON ab2.subledger_account = ab.subledger_account";
$sql .= " WHERE ab.rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " AND ab2.lettering_code != ''";
$sql .= " AND ab2.matching_general = 0";
$sql .= " ORDER BY ab2.lettering_code DESC";
dol_syslog(__METHOD__ . " - Get next code", LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
if (!empty($obj->lettering_code) &&
(($partial && preg_match('/^[a-z]+$/', $obj->lettering_code)) ||
(!$partial && preg_match('/^[A-Z]+$/', $obj->lettering_code)))
) {
$letter = $obj->lettering_code;
$letter++;
break;
}
}
$this->db->free($resql);
} else {
$this->errors[] = 'Error' . $this->db->lasterror();
$error++;
}
// Test amount integrity
if (!$error && !$partial) {
$idlist = implode(',', array_map('intval', $ids));
$sql = "SELECT SUM(ABS(debit)) as deb, SUM(ABS(credit)) as cred";
$sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping";
$sql .= " WHERE rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " AND lettering_code IS NULL";
$sql .= " AND subledger_account != ''";
dol_syslog(__METHOD__ . " - Test amount integrity", LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
if ($obj = $this->db->fetch_object($resql)) {
if (!(round(abs($obj->deb), 2) === round(abs($obj->cred), 2))) {
$this->errors[] = $langs->trans('ErrorMatchingUnbalanced', price(price2num(abs($obj->deb), 'MT')), price(price2num(abs($obj->cred), 'MT')));
$error++;
}
}
$this->db->free($resql);
} else {
$this->errors[] = 'SQL Error message: ' . $this->db->lasterror();
$error++;
}
}
// Update lettering code
if (!$error) {
$idlist = implode(',', array_map('intval', $ids));
$sql = "UPDATE " . MAIN_DB_PREFIX . "accounting_bookkeeping SET";
$sql .= " lettering_code='" . $this->db->escape($letter) . "'";
$sql .= ", date_lettering = '" . $this->db->idate($now) . "'"; // todo correct date it's false
$sql .= ", matching_general = 0";
$sql .= " WHERE rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " AND lettering_code IS NULL";
$sql .= " AND subledger_account != ''";
dol_syslog(__METHOD__ . " - Update lettering code", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error " . $this->db->lasterror();
} else {
$affected_rows = $this->db->affected_rows($resql);
}
}
}
// Commit or rollback
if ($error) {
$this->db->rollback();
foreach ($this->errors as $errmsg) {
dol_syslog(get_class($this) . "::update " . $errmsg, LOG_ERR);
$this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
}
return -1 * $error;
} else {
$this->db->commit();
return $affected_rows;
}
}
/**
* @param int[] $ids ids array
* @param int $notrigger no trigger
* @return int Nb of affectd rows or <0 if error
*/
public function deleteLettering($ids, $notrigger = 0)
{
$error = 0;
$idlist = implode(',', array_map('intval', $ids));
$sql = "UPDATE ".MAIN_DB_PREFIX."accounting_bookkeeping SET";
$sql .= " matching_general = 0";
$sql .= ", lettering_code = NULL";
$sql .= ", date_lettering = NULL";
$sql .= " WHERE rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " AND subledger_account != ''";
dol_syslog(get_class($this)."::update", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
// Commit or rollback
if ($error) {
foreach ($this->errors as $errmsg) {
dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
$this->error .= ($this->error ? ', '.$errmsg : $errmsg);
}
return -1 * $error;
} else {
return $this->db->affected_rows($resql);
}
}
/**
* Lettering bookkeeping lines all types
*
* @param int[] $bookkeeping_ids Lettering specific list of bookkeeping id
* @param bool $unlettering Do unlettering
* @return int Return integer <0 if error (nb lettered = result -1), 0 if noting to lettering, >0 if OK (nb lettered)
*/
public function bookkeepingLetteringAll($bookkeeping_ids, $unlettering = false)
{
dol_syslog(__METHOD__ . " - ", LOG_DEBUG);
$error = 0;
$errors = array();
$nb_lettering = 0;
$result = $this->bookkeepingLettering($bookkeeping_ids, $unlettering);
if ($result < 0) {
$error++;
$errors = array_merge($errors, $this->errors);
$nb_lettering += abs($result) - 2;
} else {
$nb_lettering += $result;
}
if ($error) {
$this->errors = $errors;
return -2 - $nb_lettering;
} else {
return $nb_lettering;
}
}
/**
* Update the matching on general account
*
* @param int[] $ids ids array
* @param bool $partial Partial lettering
* @return int
*/
public function updateGeneralMatching(array $ids = [], $partial = false)
{
global $conf, $langs;
$now = dol_now();
$error = 0;
$affected_rows = 0;
// Generate a string with n char 'A' (for manual/auto reconcile) or 'a' (for partial reconcile) where n is ACCOUNTING_LETTERING_NBLETTERS (So 'AA'/'aa', 'AAA'/'aaa', ...) @phan-suppress-next-line PhanParamSuspiciousOrder
$letter = str_pad("", getDolGlobalInt('ACCOUNTING_LETTERING_NBLETTERS', 3), $partial ? 'a' : 'A');
// Check for unreconcilable accounts
$pcgId = getDolGlobalInt('CHARTOFACCOUNTS');
$idlist = implode(',', array_map('intval', $ids));
$sql = "SELECT DISTINCT numero_compte";
$sql .= " FROM " . $this->db->prefix() . "accounting_bookkeeping AS ab";
$sql .= " LEFT JOIN (";
$sql .= " SELECT aa.rowid, aa.account_number";
$sql .= " FROM " . $this->db->prefix() . "accounting_account AS aa";
$sql .= " INNER JOIN " . $this->db->prefix() . "accounting_system AS asys ON asys.pcg_version = aa.fk_pcg_version";
$sql .= " WHERE asys.rowid = ".(int) $pcgId." AND aa.reconcilable";
$sql .= " ) AS reconciliable_accounts ON reconciliable_accounts.account_number = ab.numero_compte";
$sql .= " WHERE ab.rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " AND reconciliable_accounts.rowid IS NULL";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$this->errors[] = $langs->trans(
'ErrorAccountNotReconcilable',
$obj->numero_compte,
dol_buildpath('accountancy/admin/account.php', 1) . '?search_account=' . $obj->numero_compte,
$langs->transnoentitiesnoconv('Chartofaccounts')
);
return -1;
}
$this->db->free($resql);
} else {
$this->errors[] = 'Error' . $this->db->lasterror();
return -1;
}
// Get fiscal year & verify all selected rows belong to the same fiscal year
$idlist = implode(',', array_map('intval', $ids));
$sql = "SELECT fy.rowid, fy.date_start, fy.date_end, COUNT(DISTINCT fy.rowid) AS nb_fiscalyears";
$sql .= " FROM " . $this->db->prefix() . "accounting_bookkeeping AS ab";
$sql .= " INNER JOIN " . $this->db->prefix() . "accounting_fiscalyear AS fy";
$sql .= " ON ab.doc_date BETWEEN fy.date_start AND fy.date_end";
$sql .= " AND fy.entity = " . (int) $conf->entity;
$sql .= " WHERE ab.rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " GROUP BY fy.rowid, fy.date_start, fy.date_end";
dol_syslog(__METHOD__ . " - Get fiscal year", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$this->errors[] = 'Error ' . $this->db->lasterror();
return -1;
}
$fiscalYearRows = [];
while ($obj = $this->db->fetch_object($resql)) {
$fiscalYearRows[] = $obj;
}
$this->db->free($resql);
// No fiscal year found for any of the selected rows
if (empty($fiscalYearRows)) {
$this->errors[] = $langs->trans('ErrorFiscalYearNotFound');
return -1;
}
// Cross-fiscal-year matching is forbidden
if (count($fiscalYearRows) > 1) {
$periods = array_map(
/**
* @param object $row
* @return string
*/
function ($row) {
return dol_print_date($this->db->jdate($row->date_start), 'day') . ' – ' . dol_print_date($this->db->jdate($row->date_end), 'day');
},
$fiscalYearRows
);
$this->errors[] = $langs->transnoentitiesnoconv('ErrorMatchingCrossFiscalYear', implode(', ', $periods));
return -1;
}
$fiscalYear = $fiscalYearRows[0];
$fiscalYearStart = $this->db->jdate($fiscalYear->date_start);
$fiscalYearEnd = $this->db->jdate($fiscalYear->date_end);
$this->db->begin();
// Check partial / normal lettering case
$idlist = implode(',', array_map('intval', $ids));
$sql = "SELECT ab.lettering_code, GROUP_CONCAT(DISTINCT ab.rowid SEPARATOR ',') AS bookkeeping_ids";
$sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab";
$sql .= " WHERE ab.rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " GROUP BY ab.lettering_code";
$sql .= " ORDER BY ab.lettering_code DESC";
dol_syslog(__METHOD__ . " - Check partial / normal lettering case", LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
if (empty($obj->lettering_code)) {
continue;
}
// Remove normal lettering code if set partial lettering
if ($partial && preg_match('/^[A-Z]+$/', $obj->lettering_code)) {
if (!empty($obj->bookkeeping_ids)) {
$ids = array_diff($ids, explode(',', $obj->bookkeeping_ids));
}
} elseif (!$partial && preg_match('/^[a-z]+$/', $obj->lettering_code)) {
// Delete partial lettering code if set normal lettering
$sql2 = "UPDATE " . MAIN_DB_PREFIX . "accounting_bookkeeping SET";
$sql2 .= " matching_general = 1";
$sql2 .= ", lettering_code = NULL";
$sql2 .= ", date_lettering = NULL";
$sql2 .= " WHERE entity IN (" . getEntity('accountancy') . ")";
$sql2 .= " AND lettering_code = '" . $this->db->escape($obj->lettering_code) . "'";
dol_syslog(__METHOD__ . " - Remove partial lettering", LOG_DEBUG);
$resql2 = $this->db->query($sql2);
if (!$resql2) {
$this->errors[] = 'Error' . $this->db->lasterror();
$error++;
break;
}
}
}
$this->db->free($resql);
} else {
$this->errors[] = 'Error' . $this->db->lasterror();
$error++;
}
if (!$error && !empty($ids)) {
// Get next code
$idlist = implode(',', array_map('intval', $ids));
$sql = "SELECT DISTINCT ab2.lettering_code";
$sql .= " FROM " . $this->db->prefix() . "accounting_bookkeeping AS ab";
$sql .= " LEFT JOIN " . $this->db->prefix() . "accounting_bookkeeping AS ab2 ON ab2.numero_compte = ab.numero_compte";
$sql .= " WHERE ab.rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " AND ab2.lettering_code != ''";
$sql .= " AND ab2.matching_general = 1";
$sql .= " AND ab2.doc_date BETWEEN '" . $this->db->idate($fiscalYearStart) . "' AND '" . $this->db->idate($fiscalYearEnd) . "'";
$sql .= " ORDER BY ab2.lettering_code DESC";
dol_syslog(__METHOD__ . " - Get next code", LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
if (!empty($obj->lettering_code) &&
(($partial && preg_match('/^[a-z]+$/', $obj->lettering_code)) ||
(!$partial && preg_match('/^[A-Z]+$/', $obj->lettering_code)))
) {
$letter = $obj->lettering_code;
$letter++;
break;
}
}
$this->db->free($resql);
} else {
$this->errors[] = 'Error' . $this->db->lasterror();
$error++;
}
// Test amount integrity
if (!$error && !$partial) {
$idlist = implode(',', array_map('intval', $ids));
$sql = "SELECT SUM(ABS(debit)) as deb, SUM(ABS(credit)) as cred";
$sql .= " FROM " . $this->db->prefix() . "accounting_bookkeeping";
$sql .= " WHERE rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " AND lettering_code IS NULL";
dol_syslog(__METHOD__ . " - Test amount integrity", LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql) {
if ($obj = $this->db->fetch_object($resql)) {
if (!(round(abs($obj->deb), 2) === round(abs($obj->cred), 2))) {
$this->errors[] = $langs->trans('ErrorMatchingUnbalanced', price(price2num(abs($obj->deb), 'MT')), price(price2num(abs($obj->cred), 'MT')));
$error++;
}
}
$this->db->free($resql);
} else {
$this->errors[] = 'SQL Error message: ' . $this->db->lasterror();
$error++;
}
}
// Update matching code
if (!$error) {
$idlist = implode(',', array_map('intval', $ids));
$sql = "UPDATE " . $this->db->prefix() . "accounting_bookkeeping SET";
$sql .= " lettering_code='" . $this->db->escape($letter) . "'";
$sql .= ", date_lettering = '" . $this->db->idate($now) . "'";
$sql .= ", matching_general = 1";
$sql .= " WHERE rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " AND lettering_code IS NULL";
dol_syslog(__METHOD__ . " - Update general lettering code", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error " . $this->db->lasterror();
} else {
$affected_rows = $this->db->affected_rows($resql);
}
}
}
// Commit or rollback
if ($error) {
$this->db->rollback();
foreach ($this->errors as $errmsg) {
dol_syslog(get_class($this) . "::update " . $errmsg, LOG_ERR);
$this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
}
return -1 * $error;
} else {
$this->db->commit();
return $affected_rows;
}
}
/**
* Delete matching code on general account
* Deletes all lines sharing the same lettering_code+numero_compte in the same fiscal year
*
* @param int[] $ids ids array
* @return int Nb of affected rows or -1 if error
*/
public function deleteGeneralMatching(array $ids = [])
{
global $conf;
$error = 0;
// Step 1: collect all (lettering_code, numero_compte, fiscal year) from selected ids
$idlist = implode(',', array_map('intval', $ids));
$sql = "SELECT DISTINCT ab.lettering_code, ab.numero_compte, fy.date_start, fy.date_end";
$sql .= " FROM " . $this->db->prefix() . "accounting_bookkeeping AS ab";
$sql .= " INNER JOIN " . $this->db->prefix() . "accounting_fiscalyear AS fy";
$sql .= " ON ab.doc_date BETWEEN fy.date_start AND fy.date_end";
$sql .= " AND fy.entity = " . (int) $conf->entity;
$sql .= " WHERE ab.rowid IN (" . $this->db->sanitize($idlist) . ")";
$sql .= " AND ab.matching_general = 1";
$sql .= " AND ab.lettering_code IS NOT NULL AND ab.lettering_code != ''";
dol_syslog(__METHOD__ . " - Collect lettering groups to delete", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$this->errors[] = 'Error ' . $this->db->lasterror();
return -1;
}
$groups = [];
while ($obj = $this->db->fetch_object($resql)) {
$groups[] = [
'code' => $obj->lettering_code,
'compte' => $obj->numero_compte,
'date_start' => $obj->date_start,
'date_end' => $obj->date_end,
];
}
$this->db->free($resql);
if (empty($groups)) {
return 0; // Nothing to delete (lines not matched or already unmatched)
}
// Step 2: for each group, delete all lines sharing the same code+account+fiscal year
$affected = 0;
foreach ($groups as $group) {
$sql = "UPDATE " . $this->db->prefix() . "accounting_bookkeeping SET";
$sql .= " lettering_code = NULL";
$sql .= ", date_lettering = NULL";
$sql .= ", matching_general = 0";
$sql .= " WHERE numero_compte = '" . $this->db->escape($group['compte']) . "'";
$sql .= " AND lettering_code = '" . $this->db->escape($group['code']) . "'";
$sql .= " AND matching_general = 1";
$sql .= " AND doc_date BETWEEN '" . $this->db->escape($group['date_start']) . "' AND '" . $this->db->escape($group['date_end']) . "'";
dol_syslog(__METHOD__ . " - Delete gl matching group " . $group['code'] . "/" . $group['compte'], LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = 'Error ' . $this->db->lasterror();
break;
}
$affected += $this->db->affected_rows($resql);
}
if ($error) {
foreach ($this->errors as $errmsg) {
dol_syslog(get_class($this) . "::deleteGeneralMatching " . $errmsg, LOG_ERR);
$this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
}
return -1 * $error;
}
return $affected;
}
/**
* Lettering bookkeeping lines
*
* @param int[] $bookkeeping_ids Lettering specific list of bookkeeping id
* @param bool $unlettering Do unlettering
* @return int Return integer <0 if error (nb lettered = result -1), 0 if noting to lettering, >0 if OK (nb lettered)
*/
public function bookkeepingLettering($bookkeeping_ids, $unlettering = false)
{
global $langs;
$this->errors = array();
// Clean parameters
$bookkeeping_ids = is_array($bookkeeping_ids) ? $bookkeeping_ids : array();
$error = 0;
$nb_lettering = 0;
$grouped_lines = $this->getLinkedLines($bookkeeping_ids);
if (!is_array($grouped_lines)) {
return -2;
}
foreach ($grouped_lines as $lines) {
$group_error = 0;
$total = 0;
$do_it = !$unlettering;
$lettering_code = null;
$piece_num_lines = array();
$bookkeeping_lines = array();
foreach ($lines as $line_infos) {
$bookkeeping_lines[$line_infos['id']] = $line_infos['id'];
$piece_num_lines[$line_infos['piece_num']] = $line_infos['piece_num'];
$total += ($line_infos['credit'] > 0 ? $line_infos['credit'] : -$line_infos['debit']);
// Check lettering code
if ($unlettering) {
if (isset($lettering_code) && $lettering_code != $line_infos['lettering_code']) {
$this->errors[] = $langs->trans('AccountancyErrorMismatchLetteringCode');
$group_error++;
break;
}
if (!isset($lettering_code)) {
$lettering_code = (string) $line_infos['lettering_code'];
}
if (!empty($line_infos['lettering_code'])) {
$do_it = true;
}
} elseif (!empty($line_infos['lettering_code'])) {
$do_it = false;
}
}
// Check balance amount
if (!$group_error && !$unlettering && price2num($total) != 0) {
$this->errors[] = $langs->trans('AccountancyErrorMismatchBalanceAmount', $total);
$group_error++;
}
// Matching/Unmatching the group of bookkeeping lines
if (!$group_error && $do_it) {
if ($unlettering) {
$result = $this->deleteLettering($bookkeeping_lines);
} else {
$result = $this->updateLettering($bookkeeping_lines);
}
if ($result < 0) {
$group_error++;
} elseif ($result > 0) {
$nb_lettering++;
}
}
if ($group_error) {
$this->errors[] = $langs->trans('AccountancyErrorLetteringBookkeeping', implode(', ', $piece_num_lines));
$error++;
}
}
if ($error) {
return -2 - $nb_lettering;
} else {
return $nb_lettering;
}
}
/**
* Lettering bookkeeping lines
*
* @param int[] $bookkeeping_ids Lettering specific list of bookkeeping id
* @param bool $only_has_subledger_account Get only lines who have subledger account
* @return int<-1,-1>|array<array<int,array{id:int,piece_num:int,debit:int|float,credit:int|float,lettering_code:string}>> Return integer <0 if error otherwise all linked lines by block
*/
public function getLinkedLines($bookkeeping_ids, $only_has_subledger_account = true)
{
global $conf, $langs;
$this->errors = array();
// Clean parameters
$bookkeeping_ids = is_array($bookkeeping_ids) ? $bookkeeping_ids : array();
// Get all bookkeeping lines
$sql = "SELECT DISTINCT ab.doc_type, ab.fk_doc";
$sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab";
$sql .= " WHERE ab.entity IN (" . getEntity('accountancy') . ")";
$sql .= " AND ab.fk_doc > 0";
if (!empty($bookkeeping_ids)) {
// Get all bookkeeping lines of piece number
$sql .= " AND EXISTS (";
$sql .= " SELECT rowid";
$sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS pn";
$sql .= " WHERE pn.entity IN (" . getEntity('accountancy') . ")";
$sql .= " AND pn.rowid IN (" . $this->db->sanitize(implode(',', $bookkeeping_ids)) . ")";
$sql .= " AND pn.piece_num = ab.piece_num";
$sql .= " )";
}
if ($only_has_subledger_account) {
$sql .= " AND ab.subledger_account != ''";
}
dol_syslog(__METHOD__ . " - Get all bookkeeping lines", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$this->errors[] = "Error " . $this->db->lasterror();
return -1;
}
$bookkeeping_lines_by_type = array();
while ($obj = $this->db->fetch_object($resql)) {
$bookkeeping_lines_by_type[$obj->doc_type][$obj->fk_doc] = $obj->fk_doc;
}
$this->db->free($resql);
if (empty($bookkeeping_lines_by_type)) {
return array();
}
if (!empty($bookkeeping_lines_by_type['bank'])) {
$new_bookkeeping_lines_by_type = $this->getDocTypeAndFkDocFromBankLines($bookkeeping_lines_by_type['bank']);
if (!is_array($new_bookkeeping_lines_by_type)) {
return -1;
}
foreach ($new_bookkeeping_lines_by_type as $doc_type => $fk_docs) {
foreach ($fk_docs as $fk_doc) {
$bookkeeping_lines_by_type[$doc_type][$fk_doc] = $fk_doc;
}
}
}
$grouped_lines = array();
foreach (self::$doc_type_infos as $doc_type => $doc_type_info) {
if (empty($bookkeeping_lines_by_type[$doc_type]) || !is_array($bookkeeping_lines_by_type[$doc_type])) {
continue;
}
// Get all document ids grouped
$doc_grouped = $this->getLinkedDocumentByGroup($bookkeeping_lines_by_type[$doc_type], $doc_type);
if (!is_array($doc_grouped)) {
return -1;
}
// Group all lines by document/piece number
foreach ($doc_grouped as $doc_ids) {
$bank_ids = $this->getBankLinesFromFkDocAndDocType($doc_ids, $doc_type);
if (!is_array($bank_ids)) {
return -1;
}
// Get all bookkeeping lines linked
$sql = "SELECT DISTINCT ab.rowid, ab.piece_num, ab.debit, ab.credit, ab.lettering_code";
$sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab";
$sql .= " WHERE ab.entity IN (" . getEntity('accountancy') . ")";
$sql .= " AND (";
if (!empty($bank_ids)) {
$sql .= " EXISTS (";
$sql .= " SELECT bpn.rowid";
$sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS bpn";
$sql .= " WHERE bpn.entity IN (" . getEntity('accountancy') . ")";
$sql .= " AND bpn.doc_type = 'bank'";
$sql .= " AND bpn.fk_doc IN (" . $this->db->sanitize(implode(',', $bank_ids)) . ")";
$sql .= " AND bpn.piece_num = ab.piece_num";
$sql .= " ) OR ";
}
$sql .= " EXISTS (";
$sql .= " SELECT dpn.rowid";
$sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS dpn";
$sql .= " WHERE dpn.entity IN (" . getEntity('accountancy') . ")";
$sql .= " AND dpn.doc_type = '" . $this->db->escape($doc_type) . "'";
$sql .= " AND dpn.fk_doc IN (" . $this->db->sanitize(implode(',', $doc_ids)) . ")";
$sql .= " AND dpn.piece_num = ab.piece_num";
$sql .= " )";
$sql .= ")";
if ($only_has_subledger_account) {
$sql .= " AND ab.subledger_account != ''";
}
dol_syslog(__METHOD__ . " - Get all bookkeeping lines linked", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$this->errors[] = "Error " . $this->db->lasterror();
return -1;
}
$group = array();
while ($obj = $this->db->fetch_object($resql)) {
$group[$obj->rowid] = array(
'id' => $obj->rowid,
'piece_num' => $obj->piece_num,
'debit' => $obj->debit,
'credit' => $obj->credit,
'lettering_code' => $obj->lettering_code,
);
}
$this->db->free($resql);
if (!empty($group)) {
$grouped_lines[] = $group;
}
}
}
return $grouped_lines;
}
/**
* Get all fk_doc by doc_type from list of bank ids
*
* @param int[] $bank_ids List of bank ids
* @return array<string,array<int,int>>|int Return integer <0 if error otherwise all fk_doc by doc_type
*/
public function getDocTypeAndFkDocFromBankLines($bank_ids)
{
dol_syslog(__METHOD__ . " - bank_ids=".json_encode($bank_ids), LOG_DEBUG);
// Clean parameters
$bank_ids = is_array($bank_ids) ? $bank_ids : array();
if (empty($bank_ids)) {
return array();
}
$bookkeeping_lines_by_type = array();
foreach (self::$doc_type_infos as $doc_type => $doc_type_info) {
// Get all fk_doc by doc_type from bank ids
$sql = "SELECT DISTINCT dp." . $this->db->sanitize($doc_type_info['doc_payment_table_fk_doc']) . " AS fk_doc";
$sql .= " FROM " . MAIN_DB_PREFIX . $this->db->sanitize($doc_type_info['payment_table']) . " AS p";
$sql .= " LEFT JOIN " . MAIN_DB_PREFIX . $this->db->sanitize($doc_type_info['doc_payment_table']) . " AS dp ON dp." . $this->db->sanitize($doc_type_info['doc_payment_table_fk_payment']) . " = p.rowid";
$sql .= " WHERE p." . $this->db->sanitize($doc_type_info['payment_table_fk_bank']) . " IN (" . $this->db->sanitize(implode(',', $bank_ids)) . ")";
$sql .= " AND dp." . $this->db->sanitize($doc_type_info['doc_payment_table_fk_doc']) . " > 0";
dol_syslog(__METHOD__ . " - Get all fk_doc by doc_type from list of bank ids for '" . $doc_type . "'", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$this->errors[] = "Error " . $this->db->lasterror();
return -1;
}
while ($obj = $this->db->fetch_object($resql)) {
$bookkeeping_lines_by_type[$doc_type][$obj->fk_doc] = $obj->fk_doc;
}
$this->db->free($resql);
}
return $bookkeeping_lines_by_type;
}
/**
* Get all bank ids from list of document ids of a type
*
* @param int[] $document_ids List of document id
* @param string $doc_type Type of document ('customer_invoice' or 'supplier_invoice', ...)
* @return array<int,int>|int<-1,-1> Return integer <0 if error otherwise all all bank ids from list of document ids of a type
*/
public function getBankLinesFromFkDocAndDocType($document_ids, $doc_type)
{
global $langs;
dol_syslog(__METHOD__ . " - bank_ids=".json_encode($document_ids) . ", doc_type=$doc_type", LOG_DEBUG);
// Clean parameters
$document_ids = is_array($document_ids) ? $document_ids : array();
//remove empty entries
$document_ids = array_filter($document_ids);
$doc_type = trim($doc_type);
if (empty($document_ids)) {
return array();
}
if (!is_array(self::$doc_type_infos[$doc_type])) {
$langs->load('errors');
$this->errors[] = $langs->trans('ErrorBadParameters');
return -1;
}
$doc_type_info = self::$doc_type_infos[$doc_type];
$bank_ids = array();
// Get all fk_doc by doc_type from bank ids
$sql = "SELECT DISTINCT p." . $this->db->sanitize($doc_type_info['payment_table_fk_bank']) . " AS fk_doc";
$sql .= " FROM " . MAIN_DB_PREFIX . $this->db->sanitize($doc_type_info['payment_table']) . " AS p";
$sql .= " LEFT JOIN " . MAIN_DB_PREFIX . $this->db->sanitize($doc_type_info['doc_payment_table']) . " AS dp ON dp." . $this->db->sanitize($doc_type_info['doc_payment_table_fk_payment']) . " = p.rowid";
// Implode used on array of int @phan-suppress-next-line PhanTypeMismatchArgumentInternal
$sql .= " WHERE dp." . $this->db->sanitize($doc_type_info['doc_payment_table_fk_doc']) . " IN (" . $this->db->sanitize(implode(',', $document_ids)) . ")";
$sql .= " AND p." . $this->db->sanitize($doc_type_info['payment_table_fk_bank']) . " > 0";
dol_syslog(__METHOD__ . " - Get all bank ids from list of document ids of a type '" . $doc_type . "'", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$this->errors[] = "Error " . $this->db->lasterror();
return -1;
}
while ($obj = $this->db->fetch_object($resql)) {
$bank_ids[$obj->fk_doc] = $obj->fk_doc;
}
$this->db->free($resql);
return $bank_ids;
}
/**
* Get all linked document ids by group and type
*
* @param int[] $document_ids List of document id
* @param string $doc_type Type of document ('customer_invoice' or 'supplier_invoice', ...)
* @return array<int,array<int,int>>|int<-1,-1> Return integer <0 if error otherwise all linked document ids by group and type [ [ 'doc_type' => [ doc_id, ... ], ... ], ... ]
*/
public function getLinkedDocumentByGroup($document_ids, $doc_type)
{
global $langs;
// Clean parameters
$document_ids = is_array($document_ids) ? $document_ids : array();
$doc_type = trim($doc_type);
//remove empty entries
$document_ids = array_filter($document_ids);
if (empty($document_ids)) {
return array();
}
if (!is_array(self::$doc_type_infos[$doc_type])) {
$langs->load('errors');
$this->errors[] = $langs->trans('ErrorBadParameters');
return -1;
}
$doc_type_info = self::$doc_type_infos[$doc_type];
// Get document lines
$current_document_ids = array();
$link_by_element = array();
$element_by_link = array();
foreach ($doc_type_info['linked_info'] as $linked_info) {
if (empty($linked_info['fk_line_link'])) {
$sql = "SELECT DISTINCT tl2.".$this->db->sanitize($linked_info['fk_link'])." AS fk_link, tl2.".$this->db->sanitize($linked_info['fk_doc'])." AS fk_doc";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->db->sanitize($linked_info['table'])." AS tl";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$this->db->sanitize($linked_info['table'])." AS tl2 ON tl2.".$this->db->sanitize($linked_info['fk_link'])." = tl.".$this->db->sanitize($linked_info['fk_link']);
$sql .= " WHERE tl.".$this->db->sanitize($linked_info['fk_doc'])." IN (".$this->db->sanitize(implode(',', $document_ids)).")";
} else {
$sql = "SELECT DISTINCT tl2.fk_link, tl2.fk_doc";
$sql .= " FROM (";
// @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
$sql .= " SELECT DISTINCT " . $this->db->ifsql("tll.".$this->db->sanitize($linked_info['fk_table_link_line_parent'])." IS NOT NULL", "tll.".$this->db->sanitize($linked_info['fk_table_link_line_parent']), "tl.".$this->db->sanitize($linked_info['fk_link']))." AS fk_link, tl.".$this->db->sanitize($linked_info['fk_doc'])." AS fk_doc";
$sql .= " FROM " . MAIN_DB_PREFIX .$this->db->sanitize($linked_info['table'])." AS tl";
// @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
$sql .= " LEFT JOIN " . MAIN_DB_PREFIX . $this->db->sanitize($linked_info['table_link_line']) . " AS tll ON tll.".$this->db->sanitize($linked_info['fk_table_link_line']) . " = tl.".$this->db->sanitize($linked_info['fk_line_link']);
$sql .= ") AS tl";
$sql .= " LEFT JOIN (";
// @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
$sql .= " SELECT DISTINCT " . $this->db->ifsql("tll.".$this->db->sanitize($linked_info['fk_table_link_line_parent'])." IS NOT NULL", "tll.".$this->db->sanitize($linked_info['fk_table_link_line_parent']), "tl.".$this->db->sanitize($linked_info['fk_link']))." AS fk_link, tl.".$this->db->sanitize($linked_info['fk_doc'])." AS fk_doc";
$sql .= " FROM " . MAIN_DB_PREFIX .$this->db->sanitize($linked_info['table'])." AS tl";
// @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
$sql .= " LEFT JOIN " . MAIN_DB_PREFIX . $this->db->sanitize($linked_info['table_link_line']) . " AS tll ON tll.".$this->db->sanitize($linked_info['fk_table_link_line']) . " = tl.".$this->db->sanitize($linked_info['fk_line_link']);
$sql .= ") AS tl2 ON tl2.fk_link = tl.fk_link";
$sql .= " WHERE tl.fk_doc IN (" . $this->db->sanitize(implode(',', $document_ids)) . ")";
$sql .= " AND tl2.fk_doc IS NOT NULL";
}
dol_syslog(__METHOD__ . " - Get document lines", LOG_DEBUG);
$resql = $this->db->query($sql);
if (!$resql) {
$this->errors[] = "Error " . $this->db->lasterror();
return -1;
}
$is_fk_link_is_also_fk_doc = !empty($linked_info['is_fk_link_is_also_fk_doc']);
while ($obj = $this->db->fetch_object($resql)) {
$current_document_ids[$obj->fk_doc] = $obj->fk_doc;
$link_key = (string) $linked_info['prefix'] . (string) $obj->fk_link;
$element_by_link[$link_key][$obj->fk_doc] = $obj->fk_doc;
$link_by_element[(int) $obj->fk_doc][$link_key] = $link_key;
if ($is_fk_link_is_also_fk_doc) {
$element_by_link[$link_key][$obj->fk_link] = $obj->fk_link;
$link_by_element[(int) $obj->fk_link][$link_key] = $link_key;
}
}
$this->db->free($resql);
}
if (count(array_diff($document_ids, $current_document_ids))) {
return $this->getLinkedDocumentByGroup($current_document_ids, $doc_type);
}
return $this->getGroupElements($link_by_element, $element_by_link);
}
/**
* Get element ids grouped by link or element in common
*
* @param array<int,array<string,int|string>> $link_by_element List of payment ids by link key
* @param array<string,array<int,int>> $element_by_link List of element ids by link key
* @param string $link_key Link key (used for recursive function)
* @param array<int,int> $current_group Current group (used for recursive function)
* @return array<int,array<int,int>> List of element ids grouped by link or element in common
*/
public function getGroupElements(&$link_by_element, &$element_by_link, $link_key = '', &$current_group = array())
{
$grouped_elements = array();
if (!empty($link_key) && !isset($element_by_link[$link_key])) {
// Return if specific link key not found
return $grouped_elements;
}
if (empty($link_key)) {
// Save list when is the first step of the recursive recursive function
$save_link_by_element = $link_by_element;
$save_element_by_link = $element_by_link;
} else {
// To satisfy static analysis (phpstan)
$save_link_by_element = null;
$save_element_by_link = null;
}
do {
// Get current element id, get this payment id list and delete the entry
$current_link_key = !empty($link_key) ? $link_key : array_keys($element_by_link)[0];
$element_ids = $element_by_link[$current_link_key];
unset($element_by_link[$current_link_key]);
foreach ($element_ids as $element_id) {
// Continue if element id in not found
if (!isset($link_by_element[$element_id])) {
continue;
}
// Set the element in the current group
$current_group[$element_id] = $element_id;
// Get current link keys, get this element id list and delete the entry
$link_keys = $link_by_element[$element_id];
unset($link_by_element[$element_id]);
// Set element id on the current group for each link key of the element
foreach ($link_keys as $key) {
$this->getGroupElements($link_by_element, $element_by_link, (string) $key, $current_group);
}
}
if (empty($link_key)) {
// Save current group and reset the current group when is the begin of recursive function
$grouped_elements[] = $current_group;
$current_group = array();
}
} while (!empty($element_by_link) && empty($link_key));
if (empty($link_key)) {
// Restore list when is the begin of recursive function
$link_by_element = $save_link_by_element; // @phan-suppress-current-line PhanPossiblyUndeclaredVariable
$element_by_link = $save_element_by_link; // @phan-suppress-current-line PhanPossiblyUndeclaredVariable
}
return $grouped_elements;
}
}