<?php
/* Copyright (C) 2017       ATM Consulting      <contact@atm-consulting.fr>
 * Copyright (C) 2017-2020  Laurent Destailleur <eldy@destailleur.fr>
 * Copyright (C) 2022 		charlene benke		<charlene@patas-monkey.com>
 * Copyright (C) 2024-2025	MDW						<mdeweerd@users.noreply.github.com>
 * Copyright (C) 2024-2025  Frédéric France			<frederic.france@free.fr>
 *
 * 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/>.
 *
 * See https://medium.com/@lhartikk/a-blockchain-in-200-lines-of-code-963cc1cc0e54
 */


/**
 *	Class to manage Blocked Log
 */
class BlockedLog
{
	/**
	 * @var DoliDB	Database handler
	 */
	public $db;

	/**
	 * Id of the log
	 * @var int
	 */
	public $id;

	/**
	 * Entity
	 * @var int
	 */
	public $entity;

	/**
	 * Picto
	 * @var string
	 */
	public $picto = 'blockedlog';

	/**
	 * @var string Error message
	 */
	public $error = '';

	/**
	 * @var string[] Error codes (or messages)
	 */
	public $errors = array();

	/**
	 * Unique fingerprint of the log
	 * @var string
	 */
	public $signature = '';

	/**
	 * @var float|string|null
	 */
	public $amounts = null;

	/**
	 * @var float|string|null
	 */
	public $amounts_taxexcl = null;

	/**
	 * trigger action
	 * @var string
	 */
	public $action = '';

	/**
	 * Module source
	 * @var string
	 */
	public $module_source = '';

	/**
	 * @var string $linktype. Example 'paymentofinvoice'
	 */
	public $linktype = '';

	/**
	 * @var string $linktoref
	 */
	public $linktoref = '';

	/**
	 * Object element
	 * @var string
	 */
	public $element = '';

	/**
	 * Object id
	 * @var int
	 */
	public $fk_object = 0;

	/**
	 * Log certified by remote authority or not
	 * @var boolean
	 */
	public $certified = false;

	/**
	 * Author
	 * @var int
	 */
	public $fk_user = 0;

	/**
	 * @var int|string
	 */
	public $date_creation;

	/**
	 * @var int|string
	 */
	public $date_modification;

	/**
	 * @var int
	 */
	public $date_object = 0;

	/**
	 * @var string
	 */
	public $ref_object = '';

	/**
	 * @var ?stdClass
	 */
	public $object_data = null;

	/**
	 * @var string	Version of application
	 */
	public $object_version = '';

	/**
	 * @var string	Version of format of line ('', 'V1', ...).
	 */
	public $object_format = '';

	/**
	 * @var string
	 */
	public $user_fullname = '';

	/**
	 * @var string
	 */
	public $debuginfo;

	/**
	 * Array of tracked event codes
	 * @var array<string,string|mixed>
	 */
	public $trackedevents = array();

	/**
	 * Array of tracked modules (key => label)
	 * @var array<int|string,string>
	 */
	public $trackedmodules = array();



	/**
	 *      Constructor
	 *
	 *      @param		DoliDB		$db      Database handler
	 */
	public function __construct(DoliDB $db)
	{
		$this->db = $db;
	}


	/**
	 * Load list of tracked events into $this->trackedevents.
	 *
	 * @return int<1,1>		Always 1
	 */
	public function loadTrackedEvents()
	{
		global $langs;

		$this->trackedevents = array();
		$this->trackedmodules = array();

		$sep = 0;

		$this->trackedmodules[0] = 'None';
		if (isModEnabled('takepos')) {
			$this->trackedmodules['takepos'] = 'TakePOS';
		}

		// Customer Invoice/Facture / Payment (For most VAT antifraud laws)
		if (isModEnabled('invoice')) {
			$sep++;
			$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----- '.$langs->trans("Invoices").' | '.$langs->trans("Payments").'</span>', 'disabled' => 1);

			$this->trackedevents['BILL_VALIDATE']           = array('id' => 'BILL_VALIDATE', 'label' => 'logBILL_VALIDATE', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('logBILL_VALIDATE'));
			//$this->trackedevents['BILL_UPDATE']           = array('id' => 'BILL_VALIDATE', 'label' => 'logBILL_UPDATE', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('logBILL_UPDATE'));
			$this->trackedevents['BILL_SENTBYMAIL']         = array('id' => 'BILL_SENTBYMAIL', 'label' => 'logBILL_SENTBYMAIL', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('logBILL_SENTBYMAIL'));
			$this->trackedevents['DOC_DOWNLOAD']            = array('id' => 'DOC_DOWNLOAD', 'label' => 'BlockedLogBillDownload', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('BlockedLogBillDownload'));
			$this->trackedevents['DOC_PREVIEW']             = array('id' => 'DOC_PREVIEW', 'label' => 'BlockedLogBillPreview', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('BlockedLogBillPreview'));
			$this->trackedevents['PAYMENT_CUSTOMER_CREATE'] = array('id' => 'PAYMENT_CUSTOMER_CREATE', 'label' => 'logPAYMENT_CUSTOMER_CREATE', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('logPAYMENT_CUSTOMER_CREATE'));
			$this->trackedevents['PAYMENT_CUSTOMER_DELETE'] = array('id' => 'PAYMENT_CUSTOMER_DELETE', 'label' => 'logPAYMENT_CUSTOMER_DELETE', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('logPAYMENT_CUSTOMER_DELETE'));
		}

		/* Supplier
		// Supplier Invoice / Payment
		if (isModEnabled("fournisseur")) {
			$this->trackedevents['BILL_SUPPLIER_VALIDATE']='BlockedLogSupplierBillValidate';
			$this->trackedevents['BILL_SUPPLIER_DELETE']='BlockedLogSupplierBillDelete';
			$this->trackedevents['BILL_SUPPLIER_SENTBYMAIL']='BlockedLogSupplierBillSentByEmail'; // Trigger key does not exists, we want just into array to list it as done
			$this->trackedevents['SUPPLIER_DOC_DOWNLOAD']='BlockedLogSupplierBillDownload';		// Trigger key does not exists, we want just into array to list it as done
			$this->trackedevents['SUPPLIER_DOC_PREVIEW']='BlockedLogSupplierBillPreview';		// Trigger key does not exists, we want just into array to list it as done
			$this->trackedevents['PAYMENT_SUPPLIER_CREATE']='BlockedLogSupplierBillPaymentCreate';
			$this->trackedevents['PAYMENT_SUPPLIER_DELETE']='BlockedLogsupplierBillPaymentCreate';
		}
		 */

		// Donation
		if (isModEnabled('don') && getDolGlobalString('BLOCKEDLOG_ENABLE_DONATION')) {	// For countries that need unalterable logs for donations
			if (!empty($this->trackedevents)) {
				$sep++;
				$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">-----  '.$langs->trans("Donations").' | '.$langs->trans("Payments").'</span>', 'disabled' => 1);
			}

			$this->trackedevents['DON_VALIDATE'] = array('id' => 'DON_VALIDATE', 'label' => 'logDON_VALIDATE', 'labelhtml' => img_picto('', 'donation', 'class="pictofixedwidth").').$langs->trans('logDON_VALIDATE'));
			$this->trackedevents['DON_DELETE'] = array('id' => 'DON_DELETE', 'label' => 'logDON_DELETE', 'labelhtml' => img_picto('', 'donation', 'class="pictofixedwidth").').$langs->trans('logDON_DELETE'));
			//$this->trackedevents['DON_SENTBYMAIL'] = array('id' => 'BILL_VALIDATE', img_picto('', 'don', 'class="pictofixedwidth").').$langs->trans('labelhtml' => 'logDON_SENTBYMAIL');
			$this->trackedevents['DONATION_PAYMENT_CREATE'] = array('id' => 'DONATION_PAYMENT_CREATE', 'label' => 'logDONATION_PAYMENT_CREATE', 'labelhtml' => img_picto('', 'donation', 'class="pictofixedwidth").').$langs->trans('logDONATION_PAYMENT_CREATE'));
			$this->trackedevents['DONATION_PAYMENT_DELETE'] = array('id' => 'DONATION_PAYMENT_DELETE', 'label' => 'logDONATION_PAYMENT_DELETE', 'labelhtml' => img_picto('', 'donation', 'class="pictofixedwidth").').$langs->trans('logDONATION_PAYMENT_DELETE'));
		}

		/*
		// Salary
		if (isModEnabled('salary')) {
			$this->trackedevents['PAYMENT_SALARY_CREATE'] = 'BlockedLogSalaryPaymentCreate';
			$this->trackedevents['PAYMENT_SALARY_MODIFY'] = 'BlockedLogSalaryPaymentCreate';
			$this->trackedevents['PAYMENT_SALARY_DELETE'] = 'BlockedLogSalaryPaymentCreate';
		}
		 */

		// Members
		if (isModEnabled('member') && getDolGlobalString('BLOCKEDLOG_ENABLE_MEMBER')) {	// For countries that need unalterable logs for membership management
			if (!empty($this->trackedevents)) {
				$sep++;
				$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----- '.$langs->trans("MenuMembers").'</span>', 'disabled' => 1);
			}

			$this->trackedevents['MEMBER_SUBSCRIPTION_CREATE'] = array('id' => 'MEMBER_SUBSCRIPTION_CREATE', 'label' => 'logMEMBER_SUBSCRIPTION_CREATE', 'labelhtml' => img_picto('', 'member', 'class="pictofixedwidth").').$langs->trans('logMEMBER_SUBSCRIPTION_CREATE'));
			$this->trackedevents['MEMBER_SUBSCRIPTION_MODIFY'] = array('id' => 'MEMBER_SUBSCRIPTION_MODIFY', 'label' => 'logMEMBER_SUBSCRIPTION_MODIFY', 'labelhtml' => img_picto('', 'member', 'class="pictofixedwidth").').$langs->trans('logMEMBER_SUBSCRIPTION_MODIFY'));
			$this->trackedevents['MEMBER_SUBSCRIPTION_DELETE'] = array('id' => 'MEMBER_SUBSCRIPTION_DELETE', 'label' => 'logMEMBER_SUBSCRIPTION_DELETE', 'labelhtml' => img_picto('', 'member', 'class="pictofixedwidth").').$langs->trans('logMEMBER_SUBSCRIPTION_DELETE'));
		}

		// Bank
		/*
		if (isModEnabled("bank")) {
			if (!empty($this->trackedevents)) {
				$sep++;
				$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">-----  '.$langs->trans("VariousPayment").'</span>', 'disabled' => 1);
			}

			$this->trackedevents['PAYMENT_VARIOUS_CREATE'] = array('id' => 'PAYMENT_VARIOUS_CREATE', 'label' => 'logPAYMENT_VARIOUS_CREATE', 'labelhtml' => img_picto('', 'bank', 'class="pictofixedwidth").').$langs->trans('logPAYMENT_VARIOUS_CREATE'));
			$this->trackedevents['PAYMENT_VARIOUS_MODIFY'] = array('id' => 'PAYMENT_VARIOUS_MODIFY', 'label' => 'logPAYMENT_VARIOUS_MODIFY', 'labelhtml' => img_picto('', 'bank', 'class="pictofixedwidth").').$langs->trans('logPAYMENT_VARIOUS_MODIFY'));
			$this->trackedevents['PAYMENT_VARIOUS_DELETE'] = array('id' => 'PAYMENT_VARIOUS_DELETE', 'label' => 'logPAYMENT_VARIOUS_DELETE', 'labelhtml' => img_picto('', 'bank', 'class="pictofixedwidth").').$langs->trans('logPAYMENT_VARIOUS_DELETE'));
		}
		*/

		// Cash register closing
		// $conf->global->BANK_ENABLE_POS_CASHCONTROL must be set to 1 by all external POS modules
		$moduleposenabled = (isModEnabled('cashdesk') || isModEnabled('takepos') || getDolGlobalString('BANK_ENABLE_POS_CASHCONTROL'));
		if ($moduleposenabled) {
			if (!empty($this->trackedevents)) {
				$sep++;
				$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">-----   '.$langs->trans("CashControl").'</span>', 'disabled' => 1);
			}
			if (getDolGlobalString('BLOCKEDLOG_ADD_OLD_CASHCONTROL_VALIDATE')) {
				$this->trackedevents['CASHCONTROL_VALIDATE'] = array('id' => 'CASHCONTROL_VALIDATE', 'label' => 'logCASHCONTROL_VALIDATE', 'labelhtml' => img_picto('', 'pos', 'class="pictofixedwidth").').$langs->trans('logCASHCONTROL_VALIDATE'));
			}
			$this->trackedevents['CASHCONTROL_CLOSE'] = array('id' => 'CASHCONTROL_CLOSE', 'label' => 'logCASHCONTROL_CLOSE', 'labelhtml' => img_picto('', 'pos', 'class="pictofixedwidth").').$langs->trans('logCASHCONTROL_CLOSE'));
		}

		// Add more action to track from a conf variable. For the case we want to track other actions into the unalterable log.
		// For example: STOCK_MOVEMENT, ...
		if (getDolGlobalString('BLOCKEDLOG_ADD_ACTIONS_SUPPORTED')) {
			if (!empty($this->trackedevents)) {
				$sep++;
				$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----------</span>', 'disabled' => 1);
			}

			$tmparrayofmoresupportedevents = explode(',', getDolGlobalString('BLOCKEDLOG_ADD_ACTIONS_SUPPORTED'));
			foreach ($tmparrayofmoresupportedevents as $val) {
				$this->trackedevents[$val] = array('id' => $val, 'label' => 'log'.$val, 'labelhtml' => img_picto('', 'generic', 'class="pictofixedwidth").').$langs->trans('log'.$val));
			}
		}

		if (!empty($this->trackedevents)) {
			$sep++;
			$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----- '.$langs->trans("Other").'</span>', 'disabled' => 1);
		}
		$this->trackedevents['BLOCKEDLOG_EXPORT'] = array('id' => 'BLOCKEDLOG_EXPORT', 'label' => 'logBLOCKEDLOG_EXPORT', 'labelhtml' => img_picto('', $this->picto, 'class="pictofixedwidth").').$langs->trans('logBLOCKEDLOG_EXPORT'));

		return 1;
	}

	/**
	 * Try to retrieve source object (it it still exists).
	 *
	 * @return string		URL string of source object
	 */
	public function getObjectLink()
	{
		global $langs;

		if ($this->element === 'facture') {
			require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';

			$object = new Facture($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		}
		if ($this->element === 'invoice_supplier') {
			require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';

			$object = new FactureFournisseur($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		} elseif ($this->element === 'payment') {
			require_once DOL_DOCUMENT_ROOT.'/compta/paiement/class/paiement.class.php';

			$object = new Paiement($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		} elseif ($this->element === 'payment_supplier') {
			require_once DOL_DOCUMENT_ROOT.'/fourn/class/paiementfourn.class.php';

			$object = new PaiementFourn($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		} elseif ($this->element === 'payment_donation') {
			require_once DOL_DOCUMENT_ROOT.'/don/class/paymentdonation.class.php';

			$object = new PaymentDonation($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		} elseif ($this->element === 'payment_various') {
			require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/paymentvarious.class.php';

			$object = new PaymentVarious($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		} elseif ($this->element === 'don' || $this->element === 'donation') {
			require_once DOL_DOCUMENT_ROOT.'/don/class/don.class.php';

			$object = new Don($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		} elseif ($this->element === 'subscription') {
			require_once DOL_DOCUMENT_ROOT.'/adherents/class/subscription.class.php';

			$object = new Subscription($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		} elseif ($this->element === 'cashcontrol') {
			require_once DOL_DOCUMENT_ROOT.'/compta/cashcontrol/class/cashcontrol.class.php';

			$object = new CashControl($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		} elseif ($this->element === 'stockmouvement') {
			require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';

			$object = new MouvementStock($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		} elseif ($this->element === 'project') {
			require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';

			$object = new Project($this->db);
			if ($object->fetch($this->fk_object) > 0) {
				return $object->getNomUrl(1);
			} else {
				$this->error = (string) (((int) $this->error) + 1);
			}
		} elseif ($this->action == 'BLOCKEDLOG_EXPORT') {
			return '<i class="opacitymedium">'.$langs->trans("logBLOCKEDLOG_EXPORT").'</i>';
		} elseif ($this->action == 'MODULE_SET') {
			return '<i class="opacitymedium">'.$langs->trans("BlockedLogEnabled").'</i>';
		} elseif ($this->action == 'MODULE_RESET') {
			if ($this->signature == '0000000000') {
				return '<i class="opacitymedium">'.$langs->trans("BlockedLogDisabled").'</i>';
			} else {
				return '<i class="opacitymedium">'.$langs->trans("BlockedLogDisabledBis").'</i>';
			}
		}

		return '<i class="opacitymedium">'.$langs->trans('ImpossibleToReloadObject', $this->element, $this->fk_object).'</i>';
	}

	/**
	 * Try to retrieve user author
	 *
	 * @return string
	 */
	public function getUser()
	{
		global $langs, $cachedUser;

		if (empty($cachedUser)) {
			$cachedUser = array();
		}

		if (empty($cachedUser[$this->fk_user])) {
			$u = new User($this->db);
			if ($u->fetch($this->fk_user) > 0) {
				$cachedUser[$this->fk_user] = $u;
			}
		}

		if (!empty($cachedUser[$this->fk_user])) {
			return $cachedUser[$this->fk_user]->getNomUrl(1);
		}

		return $langs->trans('ImpossibleToRetrieveUser', $this->fk_user);
	}

	/**
	 *	Populate properties of an unalterable log entry from object data.
	 *  This populates ->object_data but also other fields like ->action, ->module_source, ->amounts_taxexcl, ->amounts and ->linktoref and ->linktype
	 *  It also populates some debug info like ->element and ->fk_object
	 *
	 *	@param	CommonObject|stdClass		$object				Object to store
	 *	@param	string						$action				Action code ('BILL_VALIDATE', 'BILL_SENTBYMAIL', ...)
	 *	@param	float|int					$amounts			amounts (incl tax)
	 *	@param	?User						$fuser				User object (forced)
	 *	@param	float|int|null				$amounts_taxexcl	amounts (excl tax or null if not relevant)
	 *	@return	int<-1,-1>|int<1,1>								Return >0 if OK, <0 if KO
	 */
	public function setObjectData(&$object, $action, $amounts, $fuser = null, $amounts_taxexcl = null)
	{
		global $langs, $user, $mysoc;

		if (is_object($fuser)) {
			$user = $fuser;
		}

		// Generic fields

		// action
		$this->action = $action;
		// amount
		$this->amounts_taxexcl = $amounts_taxexcl;
		$this->amounts = $amounts;
		// date
		if ($object->element == 'payment' || $object->element == 'payment_supplier') {
			'@phan-var-force Paiement|PaiementFourn $object';
			$this->date_object = empty($object->datepaye) ? $object->date : $object->datepaye;
		} elseif ($object->element == 'payment_salary') {
			'@phan-var-force PaymentSalary $object';
			$this->date_object = $object->datev;
		} elseif ($object->element == 'payment_donation' || $object->element == 'payment_various') {
			'@phan-var-force PaymentDonation $object';
			$this->date_object = empty($object->datepaid) ? $object->datep : $object->datepaid;
		} elseif ($object->element == 'subscription') {
			'@phan-var-force Subscription $object';
			$this->date_object = $object->dateh;
		} elseif ($object->element == 'cashcontrol') {
			/** var CashControl $object */
			'@phan-var-force CashControl $object';
			$this->date_object = $object->date_creation;
		} elseif (property_exists($object, 'date')) {
			// Generic case
			$this->date_object = $object->date; // @phan-suppress-current-line PhanUndeclaredProperty
		} elseif (property_exists($object, 'datem')) {
			// Generic case (second chance, for example for stock movement)
			$this->date_object = $object->datem; // @phan-suppress-current-line PhanUndeclaredProperty
		}

		// In case of credit note, we add link to source invoice to have more tracking info when doing tracking later
		if ($object->element == 'invoice_supplier') {
			'@phan-var-force FactureFournisseur $object';
			if ($object->type == FactureFournisseur::TYPE_CREDIT_NOTE) {
				$invoice = new FactureFournisseur($this->db);
				$invoice->fetch($object->fk_facture_source);
				if ($invoice->id > 0) {
					$this->linktype = 'credit_note_of';
					$this->linktoref = $invoice->ref;
				}
				//$this->module_source = (string) $invoice->module_source;
			}
		}
		if ($object->element == 'facture') {
			'@phan-var-force Facture $object';
			if ($object->type == Facture::TYPE_CREDIT_NOTE) {
				$invoice = new Facture($this->db);
				$invoice->fetch($object->fk_facture_source);
				if ($invoice->id > 0) {
					$this->linktype = 'credit_note_of';
					$this->linktoref = $invoice->ref;
				}
				$this->module_source = (string) $invoice->module_source;
			}
		}

		// ref object
		$this->ref_object = ((!empty($object->newref)) ? $object->newref : $object->ref); // newref is set when validating a draft, ref is set in other cases
		// type of object
		$this->element = $object->element;
		// id of object
		$this->fk_object = $object->id;

		// Add thirdparty info if not yet done
		if (empty($object->thirdparty) && method_exists($object, 'fetch_thirdparty')) {
			$object->fetch_thirdparty();
		}


		// Set object_data
		$this->object_data = new stdClass();

		// Add fields to exclude (this has become useless because we now use a list fields to keep later).
		$arrayoffieldstoexclude = array(
			'table_element', 'fields',
			'ref_previous', 'ref_next',
			'origin', 'origin_id',
			'oldcopy', 'picto', 'error', 'errors',
			'model_pdf', 'modelpdf', 'last_main_doc', 'civility_id', 'contact', 'contact_id',
			'table_element_line', 'ismultientitymanaged', 'isextrafieldmanaged',
			'array_languages',
			'childtables',
			'contact_ids',
			'context',
			'element',
			'labelStatus',
			'labelStatusShort',
			'linkedObjectsIds',
			'linkedObjects',
			'fk_delivery_address',
			'projet',          // There is already ->fk_project
			'restrictiononfksoc',
			'specimen',
		);

		// Add more fields to exclude depending on object type
		if ($this->element == 'cashcontrol') {
			$arrayoffieldstoexclude = array_merge($arrayoffieldstoexclude, array(
				'name', 'lastname', 'firstname', 'region', 'region_id', 'region_code', 'state', 'state_id', 'state_code', 'country', 'country_id', 'country_code',
				'total_ht', 'total_tva', 'total_ttc', 'total_localtax1', 'total_localtax2',
				'barcode_type', 'barcode_type_code', 'barcode_type_label', 'barcode_type_coder', 'mode_reglement_id', 'cond_reglement_id', 'mode_reglement', 'cond_reglement', 'shipping_method_id',
				'extraparams', 'fk_incoterms', 'fk_user_creat', 'fk_user_valid', 'label_incoterms', 'location_incoterms', 'lines', 'nb', 'tms', 'comments', 'array_options', 'warnings',
				'opening', 'status', 'date_valid'
				)
			);
		}

		// For customer payment and supplier payment, the thirdparty can be added in payment detail
		$addthirdpartyatpaymentlevel = 0;
		if (!empty($object->thirdparty) && !in_array($this->element, array('payment', 'payment_supplier'))) {
			$addthirdpartyatpaymentlevel = 1;
		}

		if (!empty($object->thirdparty) && !$addthirdpartyatpaymentlevel) {
			$this->object_data->thirdparty = new stdClass();

			foreach ($object->thirdparty as $key => $value) {
				if (in_array($key, $arrayoffieldstoexclude)) {
					continue; // Discard some properties
				}
				// List of fields qualified
				if (!in_array($key, array(
				'name', 'name_alias', 'ref_ext', 'address', 'zip', 'town', 'state_code', 'country_code', 'idprof1', 'idprof2', 'idprof3', 'idprof4', 'idprof5', 'idprof6', 'phone', 'fax', 'email', 'barcode',
				'tva_intra', 'tva_assuj', 'localtax1_assuj', 'localtax2_assuj', 'managers', 'capital', 'typent_code', 'forme_juridique_code', 'code_client', 'code_fournisseur'
				))) {
					continue; // Discard if not into this dedicated list
				}

				$valuequalifiedforstorage = false;
				if (!is_object($value)) {
					if (empty($value) && in_array($key, array('country_code', 'idprof1', 'idprof2', 'tva_intra'))) {
						$valuequalifiedforstorage = true; // We accept '' value for some fields
						$value = (string) $value;
					}
					if (!is_null($value) && empty($value) && in_array($key, array('tva_assuj', 'localtax1_assuj', 'localtax2_assuj'))) {
						$valuequalifiedforstorage = true; // We accept zero value for amounts
					}
					if (!is_null($value) && (string) $value !== '') {
						$valuequalifiedforstorage = true;
					}
				}

				if ($valuequalifiedforstorage) {
					$this->object_data->thirdparty->$key = $value;
				}
			}
		}

		// Add my company info
		if (!empty($mysoc) && !in_array($object->element, array('cashcontrol'))) {
			$this->object_data->mycompany = new stdClass();

			foreach ($mysoc as $key => $value) {
				if (in_array($key, $arrayoffieldstoexclude)) {
					continue; // Discard some properties
				}
				// List of fields qualified to keep
				if (!in_array($key, array(
				'name', 'name_alias', 'ref_ext', 'address', 'zip', 'town', 'state_code', 'country_code', 'idprof1', 'idprof2', 'idprof3', 'idprof4', 'idprof5', 'idprof6', 'phone', 'fax', 'email', 'barcode',
				'tva_assuj', 'tva_intra', 'localtax1_assuj', 'localtax1_value', 'localtax2_assuj', 'localtax2_value', 'managers', 'capital', 'typent_code', 'forme_juridique_code', 'code_client', 'code_fournisseur'
				))) {
					continue; // Discard if not into this dedicated list
				}

				$valuequalifiedforstorage = false;
				if (!is_object($value)) {
					if (empty($value) && in_array($key, array('country_code', 'idprof1', 'idprof2', 'tva_intra'))) {
						$valuequalifiedforstorage = true; // We accept '' value for some fields
						$value = (string) $value;
					}
					if (!is_null($value) && empty($value) && in_array($key, array('tva_assuj', 'localtax1_assuj', 'localtax2_assuj'))) {
						$valuequalifiedforstorage = true; // We accept zero value for amounts
					}
					if (!is_null($value) && (string) $value !== '') {
						$valuequalifiedforstorage = true;
					}
				}

				if ($valuequalifiedforstorage) {
					$this->object_data->mycompany->$key = $value;
				}
			}
		}

		// Add user info
		if (!empty($user)) {
			$this->fk_user = $user->id;
			$this->user_fullname = $user->getFullName($langs);
		}

		// Field specific to object
		if ($this->element == 'facture') {
			'@phan-var-force Facture $object';
			$this->module_source = (string) $object->module_source;

			foreach ($object as $key => $value) {
				if (in_array($key, $arrayoffieldstoexclude)) {
					continue; // Discard some properties
				}
				// List of fields qualified
				if (!in_array($key, array(
					'ref', 'ref_client', 'ref_supplier', 'date', 'datef', 'datev', 'type',
					//'vat_src_code', 'tva_tx', 'localtax1_tx', 'localtax2_tx',  There is no rate at full doc level
					'total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2',
					'revenuestamp', 'datepointoftax', 'note_public',
					'lines',
					'module_source', 'pos_source', 'pos_print_counter', 'email_sent_counter'
				))) {
					continue; // Discarded if not into the dedicated list
				}
				if ($key == 'lines') {
					$lineid = 0;
					foreach ($value as $tmpline) {	// $tmpline is object FactureLine
						$lineid++;
						foreach ($tmpline as $keyline => $valueline) {
							if (!in_array($keyline, array(
								'ref', 'product_type', 'product_label',
								'qty', 'subprice',
								'vat_src_code', 'tva_tx', 'localtax1_tx', 'localtax2_tx',
								'total_ht', 'total_tva', 'total_ttc', 'total_localtax1', 'total_localtax2',
								'multicurrency_code', 'multicurrency_total_ht', 'multicurrency_total_tva', 'multicurrency_total_ttc',
								'info_bits', 'special_code',
							))) {
								continue; // Discard if not into a dedicated list
							}

							if (empty($this->object_data->invoiceline[$lineid]) || !is_object($this->object_data->invoiceline[$lineid])) {		// To avoid warning
								$this->object_data->invoiceline[$lineid] = new stdClass();
							}

							$valuequalifiedforstorage = false;
							if (!is_object($valueline)) {
								if (!is_null($valueline) && empty($valueline) && in_array($key, array('tva_tx', 'localtax1_tx', 'localtax2_tx', 'total_ht', 'total_tva', 'total_ttc', 'total_localtax1', 'total_localtax2'))) {
									$valuequalifiedforstorage = true; // We accept zero value for amounts
								}
								if (!is_null($valueline) && (string) $valueline !== '') {
									$valuequalifiedforstorage = true;
								}
							}
							if ($keyline == 'product_label' && empty($valueline)) {
								$valueline = dol_trunc(dolGetFirstLineOfText($tmpline->desc)); // Fallback on description if label is empty
								$valuequalifiedforstorage = true;
							}

							if ($valuequalifiedforstorage) {
								$this->object_data->invoiceline[$lineid]->$keyline = $valueline;
							}
						}
					}
				} else {
					$valuequalifiedforstorage = false;
					if (!is_object($value)) {
						if (empty($value) && in_array($key, array('pos_source', 'module_source'))) {
							$valuequalifiedforstorage = true; // We accept '' value for some fields
							$value = (string) $value;
						}
						if (!is_null($value) && empty($value) && in_array($key, array('total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2', 'pos_print_counter', 'email_sent_counter'))) {
							$valuequalifiedforstorage = true; // We accept zero value for amounts
						}
						if (!is_null($value) && (string) $value !== '') {
							$valuequalifiedforstorage = true;
						}
					}

					if ($valuequalifiedforstorage) {
						$this->object_data->$key = $value;
					}
				}
			}

			if (!empty($object->newref)) {
				$this->object_data->ref = $object->newref;
			}

			// Add data for action emails
			if ($action == 'BILL_SENTBYMAIL') {
				$this->object_data->action_email_sent = array(
					"email_from" => $object->context['email_from'],
					"email_to" => $object->context['email_to'],
					"email_msgid" => $object->context['email_msgid']
				);
			}
		} elseif ($this->element == 'invoice_supplier') {
			'@phan-var-force FactureFournisseur $object';
			foreach ($object as $key => $value) {
				if (in_array($key, $arrayoffieldstoexclude)) {
					continue; // Discard some properties
				}
				// List of fields qualified
				if (!in_array($key, array(
					'ref', 'ref_client', 'ref_supplier', 'date', 'datef', 'type', 'total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2', 'revenuestamp', 'datepointoftax', 'note_public'
				))) {
					continue; // Discard if not into a dedicated list
				}

				$valuequalifiedforstorage = false;
				if (!is_object($value)) {
					if (empty($value) && in_array($key, array('pos_source', 'module_source'))) {
						$valuequalifiedforstorage = true; // We accept '' value for some fields
						$value = (string) $value;
					}
					if (!is_null($value) && empty($value) && in_array($key, array('total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2', 'pos_print_counter', 'email_sent_counter'))) {
						$valuequalifiedforstorage = true; // We accept zero value for amounts
					}
					if (!is_null($value) && (string) $value !== '') {
						$valuequalifiedforstorage = true;
					}
				}

				if ($valuequalifiedforstorage) {
					$this->object_data->$key = $value;
				}
			}

			if (!empty($object->newref)) {
				$this->object_data->ref = $object->newref;
			}
		} elseif ($this->element == 'payment' || $this->element == 'payment_supplier' || $this->element == 'payment_donation' || $this->element == 'payment_various') {
			'@phan-var-force Paiement|PaiementFourn|PaymentDonation|PaymentVarious $object';
			$datepayment = $object->datepaye ? $object->datepaye : ($object->datepaid ? $object->datepaid : $object->datep);
			$paymenttypeid = $object->paiementid ? $object->paiementid : ($object->paymenttype ? $object->paymenttype : $object->type_payment);

			$this->object_data->ref = $object->ref;
			$this->object_data->date = $datepayment;
			$this->object_data->type_code = dol_getIdFromCode($this->db, $paymenttypeid, 'c_paiement', 'id', 'code');

			if (!empty($object->num_payment)) {
				$this->object_data->payment_num = $object->num_payment;
			}
			if (!empty($object->note_private)) {
				$this->object_data->note_private = $object->note_private;
			}
			//$this->object_data->fk_account = $object->fk_account;
			//var_dump($this->object_data);exit;

			$totalamount = 0;

			$this->linktype = $this->element;
			$this->linktoref = '';

			// Loop on each invoice payment amount (the payment_part)
			if (is_array($object->amounts) && !empty($object->amounts)) {
				// Loop on each invoice the payment is part of to set the linktoref and the module_source
				$originofpayment = null;
				$paymentpartnumber = 0;
				foreach ($object->amounts as $objid => $amount) {
					if (empty($amount)) {
						continue;
					}

					$totalamount += $amount;

					$tmpobject = null;
					if ($this->element == 'payment_supplier') {
						include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
						$tmpobject = new FactureFournisseur($this->db);
					} elseif ($this->element == 'payment') {
						include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
						$tmpobject = new Facture($this->db);
					} elseif ($this->element == 'payment_donation') {
						include_once DOL_DOCUMENT_ROOT.'/don/class/don.class.php';
						$tmpobject = new Don($this->db);
					} elseif ($this->element == 'payment_various') {
						include_once DOL_DOCUMENT_ROOT.'/compta/bank/class/paymentvarious.class.php';
						$tmpobject = new PaymentVarious($this->db);
					}

					if (!is_object($tmpobject)) {
						continue;
					}

					$result = $tmpobject->fetch($objid);

					if ($result <= 0) {
						$this->error = $tmpobject->error;
						$this->errors = $tmpobject->errors;
						dol_syslog("Failed to fetch object with id ".$objid, LOG_ERR);
						return -1;
					}

					$this->linktoref .= ($this->linktoref ? ',' : '').$tmpobject->ref;
					// Set the ->module_source of payment from origin object if relevant
					if (property_exists($tmpobject, 'module_source')) {
						if (is_null($originofpayment)) {
							$originofpayment = $tmpobject->module_source;
						} elseif ($originofpayment != $tmpobject->module_source) {
							$originofpayment = 'mix';	// the payment is on several invoices with different origins
						} else {
							$originofpayment = (string) $tmpobject->module_source;
						}
					}

					$paymentpart = new stdClass();
					$paymentpart->amount = $amount;

					// If we want to add thirdparty on each payment level
					// (seems not necessary as we have one thirdparty per payment on invoice level)
					if ($addthirdpartyatpaymentlevel) {
						$result = $tmpobject->fetch_thirdparty();
						if ($result == 0) {
							$this->error = 'Failed to fetch thirdparty for object with id '.$tmpobject->id;
							$this->errors[] = $this->error;
							dol_syslog("Failed to fetch thirdparty for object with id ".$tmpobject->id, LOG_ERR);
							return -1;
						} elseif ($result < 0) {
							$this->error = $tmpobject->error;
							$this->errors = $tmpobject->errors;
							return -1;
						}

						$paymentpart->thirdparty = new stdClass();
						foreach ($tmpobject->thirdparty as $key => $value) {
							if (in_array($key, $arrayoffieldstoexclude)) {
								continue; // Discard some properties
							}
							// List of thirdparty fields qualified
							if (!in_array($key, array(
							'name', 'name_alias', 'ref_ext', 'address', 'zip', 'town', 'state_code', 'country_code', 'idprof1', 'idprof2', 'idprof3', 'idprof4', 'idprof5', 'idprof6', 'phone', 'fax', 'email', 'barcode',
							'tva_intra', 'tva_assuj', 'localtax1_assuj', 'localtax1_value', 'localtax2_assuj', 'localtax2_value', 'managers', 'capital', 'typent_code', 'forme_juridique_code', 'code_client', 'code_fournisseur'
							))) {
								continue; // Discard if not into a dedicated list
							}
							if (!is_object($value) && !is_null($value) && $value !== '') {
								$paymentpart->thirdparty->$key = $value;
							}
						}
					}

					// Init object to avoid warnings
					if ($this->element == 'payment_donation') {
						$paymentpart->donation = new stdClass();
					} elseif ($this->element == 'payment_various') {
						$paymentpart->various = new stdClass();
					} else {
						$paymentpart->invoice = new stdClass();
					}

					if ($this->element != 'payment_various') {
						foreach ($tmpobject as $key => $value) {
							if (in_array($key, $arrayoffieldstoexclude)) {
								continue; // Discard some properties
							}
							// List of fields qualified
							if (!in_array($key, array(
							'ref', 'ref_client', 'ref_supplier', 'date', 'datef', 'type', 'total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2', 'revenuestamp', 'datepointoftax', 'note_public',
							'pos_source', 'module_source', 'pos_print_counter', 'email_sent_counter'
							))) {
								continue; // Discard if not into a dedicated list
							}

							$valuequalifiedforstorage = false;
							if (!is_object($value)) {
								if (empty($value) && in_array($key, array('pos_source', 'module_source'))) {
									$valuequalifiedforstorage = true; // We accept '' value for some fields
									$value = (string) $value;
								}
								if (!is_null($value) && empty($value) && in_array($key, array('total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2', 'pos_print_counter', 'email_sent_counter'))) {
									$valuequalifiedforstorage = true; // We accept zero value for amounts
								}
								if (!is_null($value) && (string) $value !== '') {
									$valuequalifiedforstorage = true;
								}
							}

							if ($valuequalifiedforstorage) {
								if ($this->element == 'payment_donation') {
									$paymentpart->donation->$key = $value;
								} elseif ($this->element == 'payment_various') {
									$paymentpart->various->$key = $value;
								} else {
									$paymentpart->invoice->$key = $value;
								}
							}
						}

						$paymentpartnumber++; // first payment will be 1
						$this->object_data->payment_part[$paymentpartnumber] = $paymentpart;
					}
				}

				$this->module_source = (string) $originofpayment;
			} elseif (!empty($object->amount)) {
				$totalamount = $object->amount;
			}

			$this->object_data->amount = $totalamount;

			if (!empty($object->newref)) {
				$this->object_data->ref = $object->newref;
			}
		} elseif ($this->element == 'payment_salary') {
			'@phan-var-force PaymentSalary $object';
			$this->object_data->amounts = array($object->amount);

			if (!empty($object->newref)) {
				$this->object_data->ref = $object->newref;
			}
		} elseif ($this->element == 'subscription') {
			'@phan-var-force Subscription $object';
			foreach ($object as $key => $value) {
				if (in_array($key, $arrayoffieldstoexclude)) {
					continue; // Discard some properties
				}
				if (!in_array($key, array(
					'id', 'datec', 'dateh', 'datef', 'fk_adherent', 'amount', 'import_key', 'statut', 'note'
				))) {
					continue; // Discard if not into a dedicated list
				}
				if (!is_object($value) && !is_null($value) && $value !== '') {
					$this->object_data->$key = $value;
				}
			}

			if (!empty($object->newref)) {
				$this->object_data->ref = $object->newref;
			}
		} elseif ($this->element == 'stockmouvement') {
			'@phan-var-force StockTransfer $object';
			foreach ($object as $key => $value) {
				if (in_array($key, $arrayoffieldstoexclude)) {
					continue; // Discard some properties
				}
				if (!is_object($value) && !is_null($value) && $value !== '') {
					$this->object_data->$key = $value;
				}
			}
		} else {
			if ($object->element == 'cashcontrol') {
				$this->module_source = (string) $object->posmodule;		// Module
				//$this->pos_source = (string) $object->posnumber;		// Terminal
			}

			// Generic case
			foreach ($object as $key => $value) {
				if (in_array($key, $arrayoffieldstoexclude)) {
					continue; // Discard some properties
				}
				if (!is_object($value) && !is_null($value) && $value !== '') {
					$this->object_data->$key = $value;
				}
			}

			if (!empty($object->newref)) {
				$this->object_data->ref = $object->newref;
			}
		}

		// A trick to be sure all the object_data is an associative array
		// json_encode and json_decode are not able to manage mixed object (with array/object, only full arrays or full objects)
		$this->object_data = json_decode(json_encode($this->object_data, JSON_FORCE_OBJECT), false);

		return 1;
	}

	/**
	 *	Get object from database
	 *
	 *	@param      int		$id       	Id of object to load
	 *	@return     int<-1,1>			>0 if OK, <0 if KO, 0 if not found
	 */
	public function fetch($id)
	{
		global $langs;

		if (empty($id)) {
			$this->error = 'BadParameter';
			return -1;
		}

		$sql = "SELECT b.rowid, b.date_creation, b.action, b.module_source, b.amounts_taxexcl, b.amounts, b.element, b.fk_object, b.entity,";
		$sql .= " b.certified, b.tms, b.fk_user, b.user_fullname, b.date_object, b.ref_object, b.linktoref, b.linktype, b.object_data, b.object_version, b.object_format, b.signature";
		$sql .= " FROM ".MAIN_DB_PREFIX."blockedlog as b";
		if ($id) {
			$sql .= " WHERE b.rowid = ".((int) $id);
		}

		$resql = $this->db->query($sql);
		if ($resql) {
			$obj = $this->db->fetch_object($resql);
			if ($obj) {
				$this->id 				= $obj->rowid;
				$this->entity 			= $obj->entity;

				$this->date_creation 	= $this->db->jdate($obj->date_creation);	// jdate(date_creation)is UTC
				$this->date_modification = $this->db->jdate($obj->tms);				// jdate(tms) is UTC

				$this->action 			= $obj->action;
				$this->module_source	= $obj->module_source;

				$this->amounts_taxexcl	= (is_null($obj->amounts_taxexcl) ? null : (float) $obj->amounts);
				$this->amounts			= (float) $obj->amounts;

				$this->fk_object = $obj->fk_object;
				$this->date_object = $this->db->jdate($obj->date_object);			// jdate(date_object) is UTC
				$this->ref_object = $obj->ref_object;
				$this->linktoref = $obj->linktoref;
				$this->linktype = $obj->linktype;

				$this->fk_user = $obj->fk_user;
				$this->user_fullname = $obj->user_fullname;

				$this->object_data = $this->dolDecodeBlockedData($obj->object_data);
				$this->object_version = $obj->object_version;
				$this->object_format = $obj->object_format;

				$this->element			= $obj->element;

				$this->signature		= $obj->signature;
				$this->certified		= ($obj->certified == 1);

				return 1;
			} else {
				$langs->load("errors");
				$this->error = $langs->trans("ErrorRecordNotFound");
				return 0;
			}
		} else {
			$this->error = $this->db->error();
			return -1;
		}
	}


	/**
	 * Encode data
	 *
	 * @param	?stdClass	$data	Data to serialize
	 * @param	int<0,1>	$mode	0=serialize, 1=json_encode
	 * @return 	string				Value serialized, an object (stdClass)
	 */
	public function dolEncodeBlockedData($data, $mode = 0)
	{
		$aaa = '';
		try {
			$aaa = json_encode($data);
		} catch (Exception $e) {
			// print $e->getErrs);
		}

		return $aaa;
	}


	/**
	 * Decode data
	 *
	 * @param	string	$data	Data to unserialize
	 * @param	int		$mode	0=unserialize, 1=json_decode
	 * @return 	Object			Value unserialized, an object (stdClass)
	 */
	public function dolDecodeBlockedData($data, $mode = 0)
	{
		$aaa = null;
		try {
			$aaa = (object) jsonOrUnserialize($data, false);
		} catch (Exception $e) {
			// print $e->getErrs);
		}

		return $aaa;
	}


	/**
	 *	Set block certified by an external authority
	 *
	 *	@return	boolean
	 */
	public function setCertified()
	{
		$res = $this->db->query("UPDATE ".MAIN_DB_PREFIX."blockedlog SET certified = 1 WHERE rowid = ".((int) $this->id));
		if (!$res) {
			return false;
		}

		return true;
	}

	/**
	 *	Create blocked log in database.
	 *
	 *	@param	User					$user      			Object user that create
	 *  @param	string					$forcesignature		Force signature (for example '0000000000' when we disabled the module, to force a non valid record, for test purpose for example)
	 *	@return	int<-3,-1>|int<1,1>							Return integer <0 if KO, >0 if OK
	 */
	public function create($user, $forcesignature = '')
	{
		global $conf, $langs, $mysoc;

		$langs->load('blockedlog');

		// Clean data
		$this->amounts = (float) $this->amounts;

		dol_syslog(get_class($this).'::create action='.$this->action.' fk_user='.$this->fk_user.' user_fullname='.$this->user_fullname, LOG_DEBUG);

		// Check parameters/properties
		if (!isset($this->amounts)) {	// amount can be 0 for some events (like when module is disabled)
			$langs->load("errors");
			$this->error = $langs->trans("ErrorBlockLogNeedAmountsValue");
			dol_syslog($this->error, LOG_WARNING);
			return -1;
		}

		if (empty($this->element)) {
			$langs->load("errors");
			$this->error = $langs->trans("ErrorBlockLogNeedElement");
			dol_syslog($this->error, LOG_WARNING);
			return -2;
		}

		if (empty($this->object_data)) {
			$langs->load("errors");
			$this->error = $langs->trans("ErrorBlockLogNeedObject");
			dol_syslog($this->error, LOG_WARNING);
			return -2;
		}

		if (empty($this->action)) {
			$langs->load("errors");
			$this->error = $langs->trans("ErrorBadParameterWhenCallingCreateOfBlockedLog");
			dol_syslog($this->error, LOG_WARNING);
			return -3;
		}
		if (empty($this->fk_user)) {
			$this->user_fullname = '(Anonymous)';
		}

		include_once DOL_DOCUMENT_ROOT.'/core/lib/security.lib.php';

		$this->db->begin();

		$this->date_creation = dol_now();

		$this->object_version = DOL_VERSION;
		// The object_format define the formatting rules into buildKeyForSignature and buildFirstPartOfKeyForSignature and buildFinalSignatureHash
		$this->object_format = 'V1';	// TODO Switch to V2 when v2 support is complete

		try {
			$previoushash = $this->getPreviousHash(1, 0); // This get last record and lock database until insert is done and transaction closed

			$concatenateddata = $this->buildKeyForSignature();	// All the information for the hash (meta data + data saved)

			$this->signature = $this->buildFinalSignatureHash($previoushash.$concatenateddata);	// Build the hmac signature

			// For debug info (we can clean this field later)
			if (getDolGlobalString('BLOCKEDLOG_ADD_DEBUG_INFO')) {
				$this->debuginfo = $this->buildFirstPartOfKeyForSignature();	// Not used
			}
		} catch (Exception $e) {
			$this->error = $e->getMessage();

			dol_syslog($this->error, LOG_ERR);

			$this->db->rollback();
			return -1;
		}

		if ($forcesignature) {
			$this->signature = $forcesignature;
		}
		//var_dump($concatenateddata);var_dump($previoushash);var_dump($this->signature);

		$sql = "INSERT INTO ".MAIN_DB_PREFIX."blockedlog (";
		$sql .= " date_creation,";
		$sql .= " action,";
		$sql .= " module_source,";
		$sql .= " amounts_taxexcl,";
		$sql .= " amounts,";
		$sql .= " signature,";
		$sql .= " element,";
		$sql .= " fk_object,";
		$sql .= " date_object,";
		$sql .= " ref_object,";
		$sql .= " linktoref,";
		$sql .= " linktype,";
		$sql .= " object_data,";
		$sql .= " object_version,";
		$sql .= " object_format,";
		$sql .= " certified,";
		$sql .= " fk_user,";
		$sql .= " user_fullname,";
		$sql .= " entity,";
		$sql .= " debuginfo";	// Only stored
		$sql .= ") VALUES (";
		$sql .= "'".$this->db->idate($this->date_creation)."',";
		$sql .= "'".$this->db->escape($this->action)."',";
		$sql .= "'".$this->db->escape((string) $this->module_source)."',";
		$sql .= (is_null($this->amounts_taxexcl) ? "null" : (float) $this->amounts_taxexcl).",";
		$sql .= (float) $this->amounts.",";
		$sql .= "'".$this->db->escape($this->signature)."',";
		$sql .= "'".$this->db->escape($this->element)."',";
		$sql .= (int) $this->fk_object.",";
		$sql .= "'".$this->db->idate($this->date_object)."',";
		$sql .= "'".$this->db->escape($this->ref_object)."',";
		$sql .= ($this->linktoref ? "'".$this->db->escape($this->linktoref)."'" : "null").",";
		$sql .= ($this->linktoref ? "'".$this->db->escape($this->linktype)."'" : "null").",";
		$sql .= "'".$this->db->escape($this->dolEncodeBlockedData($this->object_data))."',";
		$sql .= "'".$this->db->escape($this->object_version)."',";
		$sql .= "'".$this->db->escape($this->object_format)."',";
		$sql .= "0,";
		$sql .= $this->fk_user.",";
		$sql .= "'".$this->db->escape($this->user_fullname)."',";
		$sql .= ($this->entity ? $this->entity : $conf->entity).",";
		$sql .= "'".$this->db->escape($this->debuginfo)."'";
		$sql .= ")";

		/*
		$a = serialize($this->object_data); $a2 = unserialize($a); $a4 = print_r($a2, true);
		$b = json_encode($this->object_data); $b2 = json_decode($b); $b4 = print_r($b2, true);
		var_dump($a4 == print_r($this->object_data, true) ? 'a=a' : 'a not = a');
		var_dump($b4 == print_r($this->object_data, true) ? 'b=b' : 'b not = b');
		exit;
		*/

		$res = $this->db->query($sql);
		if ($res) {
			$id = $this->db->last_insert_id(MAIN_DB_PREFIX."blockedlog");

			if ($id > 0) {
				$this->id = $id;

				$this->db->commit();

				include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/blockedlog.lib.php';
				if (isALNERunningVersion(1) && $mysoc->country_code == 'FR') {
					// TODO Push last rowid + signature to remote dolibarr server
					/*
					$remoteurl = '';
					$param = '';
					$addheaders = array();
					$timeoutconnect = 0;
					$timeoutresponse = 0;

					$result = getURLContent($remoteurl, 'POSTALREADYFORMATED', $param, 1, $addheaders, 'https', 0, -1, $timeoutconnect, $timeoutresponse);
					*/
				}

				return $this->id;
			} else {
				$this->db->rollback();
				return -2;
			}
		} else {
			$this->error = $this->db->error();
			$this->db->rollback();
			return -1;
		}

		// The commit or rollback will release the lock so app can insert other record now
	}

	/**
	 *	Check if calculated signature still correct compared to the value in the chain
	 *
	 *	@param	string			$previoushash		If previous signature hash is known, we can provide it to avoid to make a search of it in database.
	 *  @param	int<0,2>		$returnarray		1=Return array of details, 2=Return array of details including keyforsignature, 0=Boolean
	 *	@return	boolean|array{checkresult:bool,calculatedsignature:string,previoushash:string,keyforsignature?:string}	Array or true if OK, false if KO
	 */
	public function checkSignature($previoushash = '', $returnarray = 0)
	{
		if (empty($previoushash)) {
			$previoushash = $this->getPreviousHash(0, $this->id);
		}

		$concatenateddata = '';
		$signature = '';

		// Recalculate the signature
		try {
			// Build the string for the signature
			$concatenateddata = $this->buildKeyForSignature();

			$signature = $this->buildFinalSignatureHash($previoushash.$concatenateddata);
		} catch (Exception $e) {
			$res = ($signature === $this->signature);
			$this->error = $e->getMessage();

			dol_syslog($this->error, LOG_ERR);

			if ($returnarray) {
				return array('checkresult' => $res, 'calculatedsignature' => $signature, 'previoushash' => $previoushash);
			} else {
				return false;
			}
		}

		$res = ($signature === $this->signature);

		if (!$res) {
			$this->error = 'Signature KO';
		}

		if ($returnarray) {
			if ($returnarray == 1) {
				unset($concatenateddata);
				return array('checkresult' => $res, 'calculatedsignature' => $signature, 'previoushash' => $previoushash);
			} else {	// Consume much memory ($concatenateddata is a large var)
				return array('checkresult' => $res, 'calculatedsignature' => $signature, 'previoushash' => $previoushash, 'keyforsignature' => $concatenateddata);
			}
		} else {
			unset($concatenateddata);
			return $res;
		}
	}

	/**
	 * Return first part of string for signature (clear data)
	 * Note: rowid of line not included as it is not a business data and this allow to make backup of a year
	 * and restore it into another database with different ids without comprimising checksums
	 *
	 * @return string		First part of key for signature
	 */
	private function buildFirstPartOfKeyForSignature()
	{
		// Note: $this->amounts can be '0', '1.1', '1.123';  // All 0 at end should have been removed already
		if ($this->object_format == '') {
			return $this->date_creation.'|'.$this->action.'|'.$this->amounts.'|'.$this->ref_object.'|'.$this->date_object.'|'.$this->user_fullname;
		} elseif ($this->object_format == 'V1') {	// Note: $this->amounts can be '0', '1.1', '1.123';  // All 0 at end should have been removed already
			return $this->date_creation.'|'.$this->action.'|'.$this->amounts.'|'.$this->ref_object.'|'.$this->date_object.'|'.$this->user_fullname;
		} elseif ($this->object_format == 'V2') {
			$s = $this->entity;
			$s .= '|'.$this->date_creation.'|'.$this->action.'|'.$this->module_source.'|'.$this->amounts_taxexcl.'|'.$this->amounts.'|'.$this->ref_object.'|'.$this->date_object.'|'.$this->user_fullname;
			$s .= '|'.(string) $this->linktoref;
			$s .= '|'.(string) $this->linktype;
			return $s;
		} else {
			throw new Exception('Error bad value "'.$this->object_format.'" for object_format');
		}
	}

	/**
	 * Return the string for signature (clear data).
	 *
	 * @return string		Key for signature
	 */
	public function buildKeyForSignature()
	{
		//print_r($this->object_data);
		if ($this->object_format == '') {
			return $this->buildFirstPartOfKeyForSignature().'|'.print_r($this->object_data, true);
		} elseif ($this->object_format == 'V1') {	// Note: $this->amounts can be '0', '1.1', '1.123';  // All 0 at end should have been removed already
			return $this->buildFirstPartOfKeyForSignature().'|'.json_encode($this->object_data, JSON_FORCE_OBJECT);
		} elseif ($this->object_format == 'V2') {
			return $this->buildFirstPartOfKeyForSignature().'|'.json_encode($this->object_data, JSON_FORCE_OBJECT);
		} else {
			throw new Exception('Error bad value "'.$this->object_format.'" for object_format');
		}
	}

	/**
	 * Return a hash that is the signature of a line (hash_hmac en SHA256 des données + clé secrète)
	 *
	 * @param 	string $clearstring		Data to sign
	 * @return 	string					Signature string
	 */
	private function buildFinalSignatureHash($clearstring)
	{
		if ($this->object_format == '') {
			return dol_hash($clearstring, '5');
		} elseif ($this->object_format == 'V1') {
			return dol_hash($clearstring, '5');
		} elseif ($this->object_format == 'V2') {
			// BLOCKEDLOG_HMAC_KEY is a HMAC key starting with 'BLOCKEDLOGHMAC....', but it is not stored as a clear data. It will be decrypted later.
			$hmac_encoded_secret_key = getDolGlobalString('BLOCKEDLOG_HMAC_KEY');
			if (empty($hmac_encoded_secret_key)) {
				throw new Exception('Error: BLOCKEDLOG_HMAC_KEY was not found. It should have been initialized to a value "BLOCKEDLOG_HMAC_...." during initialization of module BlockedLog or during migration of v23');
			}
			$hmac_secret_key = dolDecrypt($hmac_encoded_secret_key);
			if (!preg_match('/^BLOCKEDLOGHMAC/', $hmac_secret_key)) {
				throw new Exception('Error: Failed to decode the crypted value of the parameter BLOCKEDLOG_HMAC_KEY using the $dolibarr_main_crypt_key. A value was found but decoding failed. May be the database data were restored onto another environment and the coding/decoding key $dolibarr_main_dolcrypt_key or $dolibarr_main_instance_unique_id was not restored with the same value in conf.php file.');
			}

			return hash_hmac('sha256', $clearstring, $hmac_secret_key);
		} else {
			throw new Exception('Error bad value "'.$this->object_format.'" for object_format');
		}
	}

	/**
	 *	Get previous signature/hash in chain
	 *
	 *	@param int<0,1>	$withlock		1=With a lock
	 *	@param int		$beforeid		ID of a record
	 *  @return	string					Hash of previous record (if beforeid is defined) or hash of last record (if beforeid is 0)
	 */
	public function getPreviousHash($withlock = 0, $beforeid = 0)
	{
		global $conf;

		$previoussignature = '';

		// Fast search of previous record by searching with beforeid - 1. This is very fast and will work 99% of time.
		if ($beforeid) {
			$sql = "SELECT rowid, signature FROM ".MAIN_DB_PREFIX."blockedlog";
			$sql .= " WHERE entity = ".((int) $conf->entity);
			$sql .= " AND rowid = ".((int) $beforeid - 1);
			$sql .= ($withlock ? " FOR UPDATE " : "");

			$resql = $this->db->query($sql);
			if ($resql) {
				$obj = $this->db->fetch_object($resql);
				if ($obj) {
					$previoussignature = $obj->signature;
				}
			} else {
				dol_print_error($this->db);
				exit;
			}
		}

		if (empty($previoussignature)) {
			$sql = "SELECT rowid, signature FROM ".MAIN_DB_PREFIX."blockedlog";
			if ($beforeid) {
				$sql .= $this->db->hintindex('entity_rowid', 1);
			}
			$sql .= " WHERE entity = ".((int) $conf->entity);
			if ($beforeid) {
				$sql .= " AND rowid < ".(int) $beforeid;
			}
			$sql .= " ORDER BY rowid DESC LIMIT 1";
			$sql .= ($withlock ? " FOR UPDATE " : "");

			$resql = $this->db->query($sql);
			if ($resql) {
				$obj = $this->db->fetch_object($resql);
				if ($obj) {
					$previoussignature = $obj->signature;
				}
			} else {
				dol_print_error($this->db);		// can happen after a deadlock when too many requests do create into blocked log happen at the same time.
				http_response_code(503);
				exit;
			}
		}

		if (empty($previoussignature)) {
			// First signature line (line 0)
			$previoussignature = $this->getOrInitFirstSignature();
		}

		return $previoussignature;
	}

	/**
	 *	Return array of log objects (with criteria)
	 *
	 *	@param	string 					$element      			Element to search
	 *	@param	string|int				$fk_object				Id of object to search. Can be a UFS search criteria.
	 *	@param	int<0,max> 				$limit      			Max number of element, 0 for all
	 *	@param	string 					$sortfield     			Sort field
	 *	@param	string 					$sortorder     			Sort order
	 *	@param	int 					$search_fk_user 		Id of user(s)
	 *	@param	int 					$search_start   		Start time limit
	 *	@param	int 					$search_end     		End time limit
	 *  @param	string					$search_ref				Search ref
	 *  @param	string					$search_amount			Search amount
	 *  @param	string|string[]	        $search_code			Search code
	 *  @param	string			        $search_signature		Search signature
	 *  @param	string			        $search_module_source	Search on module source
	 *	@return	BlockedLog[]|int<-2,-1>							Array of object log or <0 if error
	 */
	public function getLog($element, $fk_object, $limit = 0, $sortfield = '', $sortorder = '', $search_fk_user = -1, $search_start = -1, $search_end = -1, $search_ref = '', $search_amount = '', $search_code = '', $search_signature = '', $search_module_source = '')
	{
		global $conf;
		//global $cachedlogs;

		/* $cachedlogs allow fastest search */
		//if (empty($cachedlogs)) $cachedlogs = array();

		if ($element == 'all') {
			$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog
			 WHERE entity = ".$conf->entity;
		} elseif ($element == 'not_certified') {
			$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog
			 WHERE entity = ".$conf->entity." AND certified = 0";
		} elseif ($element == 'just_certified') {
			$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog
			 WHERE entity = ".$conf->entity." AND certified = 1";
		} else {
			$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog
			 WHERE entity = ".$conf->entity." AND element = '".$this->db->escape($element)."'";
		}

		if ($fk_object) {
			$sql .= natural_search("rowid", (string) $fk_object, 1);
		}
		if ($search_fk_user > 0) {
			$sql .= natural_search("fk_user", (string) $search_fk_user, 2);
		}
		if ($search_start > 0) {
			$sql .= " AND date_creation >= '".$this->db->idate($search_start)."'";
		}
		if ($search_end > 0) {
			$sql .= " AND date_creation <= '".$this->db->idate($search_end)."'";
		}
		if ($search_ref != '') {
			$sql .= " AND (".natural_search("ref_object", $search_ref, 0, 1);
			$sql .= " OR ".natural_search("linktoref", $search_ref, 0, 1).")";
		}
		if ($search_amount != '') {
			$sql .= natural_search("amounts", $search_amount, 1);
		}
		if ($search_signature != '') {
			$sql .= natural_search("signature", $search_signature, 0);
		}
		if (is_array($search_code)) {
			if (!empty($search_code)) {
				$sql .= natural_search("action", implode(',', $search_code), 3);
			}
		} else {
			if ($search_code != '' && $search_code != '-1') {
				$sql .= natural_search("action", $search_code, 3);
			}
		}
		if (is_array($search_module_source)) {
			if (!empty($search_module_source)) {
				$sql .= " AND (";
				if (in_array('0', $search_module_source)) {
					$sql .= "module_source = ''";
					unset($search_module_source[0]);
					if (!empty($search_module_source)) {
						$sql .= " OR ";
					}
				}
				if (!empty($search_module_source)) {
					$sql .= natural_search("module_source", implode(',', $search_module_source), 3, 1);
				}
				$sql .= " OR module_source = 'mix'";	// When a payment was reocrd and payment was on an invoice with different origins (pos and not pos)
				$sql .= ")";
			}
		} else {
			if ($search_module_source != '' && $search_module_source != '-1') {
				$sql .= natural_search("module_source", $search_module_source, 3);
			}
		}

		$sql .= $this->db->order($sortfield, $sortorder);
		$sql .= $this->db->plimit($limit + 1); // We want more, because we will stop into loop later with error if we reach max

		$res = $this->db->query($sql);
		if ($res) {
			$results = array();

			$i = 0;
			while ($obj = $this->db->fetch_object($res)) {
				$i++;
				if ($i > $limit) {
					// Too many record, we will consume too much memory
					return -2;
				}

				//if (!isset($cachedlogs[$obj->rowid]))
				//{
				$b = new BlockedLog($this->db);
				$b->fetch($obj->rowid);
				//$b->loadTrackedEvents();
				//$cachedlogs[$obj->rowid] = $b;
				//}

				//$results[] = $cachedlogs[$obj->rowid];
				$results[] = $b;
			}

			return $results;
		}

		return -1;
	}

	/**
	 *	Return the signature (hash) of the "genesis-block" (Block 0).
	 *
	 *	@return	string					Signature of genesis-block for current conf->entity
	 */
	public function getOrInitFirstSignature()
	{
		global $db, $conf;

		if (!getDolGlobalString('BLOCKEDLOG_ENTITY_FINGERPRINT')) { // creation of a unique fingerprint
			require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
			require_once DOL_DOCUMENT_ROOT.'/core/lib/security.lib.php';
			require_once DOL_DOCUMENT_ROOT.'/core/lib/security2.lib.php';

			//$fingerprint = dol_hash(print_r($mysoc, true).getRandomPassword(true), '5');
			$fingerprint = bin2hex(random_bytes(32)); // 64 char hex

			dolibarr_set_const($db, 'BLOCKEDLOG_ENTITY_FINGERPRINT', $fingerprint, 'chaine', 0, 'Numeric Unique Fingerprint', $conf->entity);

			$conf->global->BLOCKEDLOG_ENTITY_FINGERPRINT = $fingerprint;
		}

		if (!getDolGlobalString('BLOCKEDLOG_LAST_RECORD_FINGERPRINT')) {
			dolibarr_set_const($db, 'BLOCKEDLOG_LAST_RECORD_FINGERPRINT', '0:none', 'chaine', 0, 'Last record fingerprint', $conf->entity);
		}

		return getDolGlobalString('BLOCKEDLOG_ENTITY_FINGERPRINT');
	}


	/**
	 * Check if module was already used or not for at least one recording.
	 *
	 * @param   int<0,1>	$ignoresystem       Ignore system events for the test
	 * @return  bool
	 */
	public function alreadyUsed($ignoresystem = 0)
	{
		include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/blockedlog.lib.php';
		return isBlockedLogUsed($ignoresystem);
	}


	/**
	 * Check if module can be enabled.
	 *
	 * @return  string			'' if ok, error message if not possible
	 */
	public function canBeEnabled()
	{
		global $dolibarr_main_force_https;

		include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/blockedlog.lib.php';

		if (isALNEQualifiedVersion(0, 1) && empty($dolibarr_main_force_https)) {
			return 'Error: The HTTPS must be forced by setting the $dolibarr_main_force_https into Dolibarr conf/conf.php file to allow the use of this module in France.';
		}

		return '';
	}


	/**
	 * Check if module can be disabled.
	 *
	 * @return  int<0,1>		0=Can't be disabled, 1=Can be disabled
	 */
	public function canBeDisabled()
	{
		global $mysoc;

		include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/blockedlog.lib.php';

		$canbedisabled = 1;
		if (isALNEQualifiedVersion() && $mysoc->country_code == 'FR') {
			$canbedisabled = 0;
		}

		return $canbedisabled;
	}
}
