<?php
/* Copyright (c) 2025       Eric Seigne                 <eric.seigne@cap-rel.fr>
 * Copyright (C) 2025       Laurent Destailleur         <eldy@users.sourceforge.net>
 * Copyright (C) 2025       Mohamed DAOUD               <mdaoud@dolicloud.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    einvoicing/class/protocols/FacturXProtocol.class.php
 * \ingroup einvoicing
 * \brief   Factur-X Protocol integration class
 */

//use custom\facturx\Fidry\FileSystem\FS;
use horstoeko\zugferd\codelists\ZugferdCountryCodes;
use horstoeko\zugferd\codelists\ZugferdCurrencyCodes;
use horstoeko\zugferd\codelists\ZugferdInvoiceType;
use horstoeko\zugferd\codelists\ZugferdReferenceCodeQualifiers;
use horstoeko\zugferd\codelists\ZugferdUnitCodes;
use horstoeko\zugferd\codelists\ZugferdVatCategoryCodes;
use horstoeko\zugferd\codelists\ZugferdVatTypeCodes;
use horstoeko\zugferd\ZugferdDocumentBuilder;
use horstoeko\zugferd\ZugferdProfiles;
use horstoeko\zugferd\ZugferdSettings;
use horstoeko\zugferd\ZugferdDocumentPdfBuilder;
use horstoeko\zugferd\ZugferdDocumentValidator;
use horstoeko\zugferd\ZugferdDocumentPdfReader;
use horstoeko\zugferd\ZugferdDocumentPdfReaderExt;

require __DIR__ . "/../../vendor/autoload.php";

dol_include_once('einvoicing/class/protocols/CIIProtocol.class.php');
dol_include_once('einvoicing/class/protocols/CommonProtocol.class.php');
dol_include_once('einvoicing/class/utils/XmlPatcher.class.php');
dol_include_once('einvoicing/class/utils/CtcFrPdfMerger.class.php');


/**
 * FacturX Protocol Class
 *
 * This class handles the FacturX protocol implementation for generating
 * and managing electronic invoices according to the FacturX standard.
 * This also throw an error if data is not correct.
 *
 * This implementation is based on FacturX plugin developed by CAP REL.
 * It has been adapted and integrated into the EInvoicing module to provide
 * electronic invoicing capabilities compliant with the French Factur-X standard.
 *
 * @author  Eric Seigne <eric.seigne@cap-rel.fr>
 * 			Modified by mdaoud
 * @see     https://inligit.fr/cap-rel/dolibarr/plugin-facturx plugin repository
 */
class FacturXProtocol extends CIIProtocol
{
	use CommonProtocol;

	/** @const string Invoice file extension (without the dot, example 'xml') */
	protected const INVOICE_FILE_EXTENSION = 'pdf';

	/** @const string Generated invoice file name */
	protected const GENERATED_INVOICE_XML_FILE_NAME = 'factur-x.xml';

	/** @const string The profile used to generate XML */
	protected const BUILD_XML_PROFILE = 'EXTENDED';

	/** @const string Path of the line unit price nodes, relative to the line item node */
	private const NODE_LINE_ITEM = '/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:IncludedSupplyChainTradeLineItem';

	/**
	 * Tell the lib how many decimals the norm allows on the nodes where its default of 2 is wrong.
	 *
	 * The lib serializes every AmountType and QuantityType with 2 decimals unless a node is
	 * declared here. That default is right for the amounts, but not for the unit prices
	 * (BT-146/BT-148, up to 6 decimals) nor for the invoiced quantity (BT-129, up to 4): rounding
	 * those to 2 loses the accuracy the receiver needs to recalculate the line amount.
	 *
	 * @return	void
	 */
	protected static function applyNormDecimalPlaces()
	{
		ZugferdSettings::addSpecialDecimalPlacesMap(
			self::NODE_LINE_ITEM . '/ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount',
			self::getUnitPriceDecimals()
		);
		ZugferdSettings::addSpecialDecimalPlacesMap(
			self::NODE_LINE_ITEM . '/ram:SpecifiedLineTradeAgreement/ram:GrossPriceProductTradePrice/ram:ChargeAmount',
			self::getUnitPriceDecimals()
		);
		ZugferdSettings::addSpecialDecimalPlacesMap(
			self::NODE_LINE_ITEM . '/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity',
			self::MAX_DECIMALS_QUANTITY
		);
	}

	/**
	 * Generate the XML content for a given invoice according to the Factur-X standard.
	 * This also make a lot of check
	 *
	 * This method converts the provided invoice data into a structured XML file
	 * compliant with the Factur-X specification (hybrid PDF + XML format).
	 *
	 * @param 	CommonInvoice	$invoice 		Invoice object containing all necessary data.
	 * @param	?Translate		$outputlangs	Output language
	 * @return 	string 							XML representation of the invoice.
	 */
	public function generateXML($invoice, $outputlangs = null)
	{
		global $conf, $user, $langs, $mysoc, $db;	// Used by the include

		if (!getDolGlobalInt('EINVOICING_USE_EXTERNAL_FACTURX_BUILDER')) {
			return parent::generateXML($invoice, $outputlangs);
		} else {
			// deprecated, use the native $CII->buildXML method instead !

			// Call page to generate the invoice
			include dol_buildpath('einvoicing/lib/buildinvoicelines.inc.php');
			/**
			 * From include:
			 * @var Facture 			$object			The `$invoice` object used in entry on inc file, but completed.
			 * @var array{
			 *   documentno: string,
			 *   documenttypecode: null|string,
			 *   documentdate: DateTimeInterface,
			 *   invoiceCurrency: string|list<string>,
			 *   taxCurrency: null,
			 *   documentname: null,
			 *   documentlanguage: string,
			 *   effectiveSpecifiedPeriod: 'NA',
			 *   documentDeliveryDate: DateTimeInterface,
			 *   invoicingPeriodStart: null,
			 *   invoicingPeriodEnd: null,
			 *   businessProcessId: string,
			 *   isTestDocument: bool,
			 *   documentNotePublic: string,
			 *   documentNotePMT: string,
			 *   documentNotePMD: string,
			 *   documentNoteAAB: string,
		 *   documentNoteTXD: string,
		 *   vatDueDateTypeCode: string,
			 *   documentNotes: array,
			 *   sellername: string,
			 *   sellerids: string,
			 *   sellerlineone: string,
			 *   sellerlinetwo: string,
			 *   sellerlinethree: string,
			 *   sellerpostcode: string,
			 *   sellercity: string,
			 *   sellercountry: string,
			 *   sellersubdivision: null,
			 *   sellercontactpersonname: string,
			 *   sellercontactdepartmentname: null,
			 *   sellercontactphoneno: string,
			 *   sellercontactfaxno: string,
			 *   sellercontactemailaddr: string,
			 *   sellerCommunicationUriScheme: string,
			 *   sellerCommunicationUri: string,
			 *   sellerGlobalIds: list<array{schemeID: string, value: string}>,
			 *   sellerTaxRegistrations: list<array{type: string, value: string}>,
			 *   sellervatnumber: string,
			 *   sellerLegalOrgId: string,
			 *   sellerLegalOrgScheme: string,
			 *   sellerTradingName: string,
			 *   buyername: string,
			 *   buyerids: string,
			 *   buyerlineone: string,
			 *   buyerlinetwo: string,
			 *   buyerlinethree: string,
			 *   buyerpostcode: string,
			 *   buyercity: string,
			 *   buyercountry: string,
			 *   buyersubdivision: null,
			 *   buyervatnumber: string,
			 *   buyerGlobalIds: list<array{schemeID: string, value: string}>,
			 *   buyerLegalOrgId: string,
			 *   buyerLegalOrgScheme: string,
			 *   buyerTradingName: string,
			 *   buyerReference: null|string,
			 *   buyerCommunicationUriScheme: string,
			 *   buyerCommunicationUri: string,
			 *   buyercontactpersonname: null,
			 *   buyercontactemailaddr: null,
			 *   buyercontactphoneno: null,
			 *   grandTotalAmount: float|int,
			 *   duePayableAmount: float|int,
			 *   lineTotalAmount: float|int,
			 *   chargeTotalAmount: float,
			 *   allowanceTotalAmount: float|int,
			 *   taxBasisTotalAmount: float|int,
			 *   taxTotalAmount: float|int,
			 *   roundingAmount: null,
			 *   totalPrepaidAmount: float|int,
			 *   iban_id: int,
			 *   iban: string,
			 *   bic: string,
			 *   accountName: string,
			 *   accountRef: string,
			 *   accountLabel: string,
			 *   paymentDueDate: DateTimeInterface,
			 *   paymentTermsText: string,
			 *   headerAllowancesCharges: array,
			 *   invoiceRefDocs: array|list<array{ref: string|int, date: \DateTimeInterface, type: string}>,
			 *   orderReference: string,
			 *   contractReference: null|string,
			 *   despatchAdviceRef: null,
			 *   taxBreakdown: array|list<array<string, array>>,
			 *   _chorus: bool,
			 *   _depositlines: array|list<array{lineId: int, invoiceRef: string, invoiceDate: DateTimeInterface}>,
			 *   _globalDiscounts: array|list<array{value: float, reason: string, taxRate: float, categoryVAT: string}>,
			 *   _customerOrderReferenceList: string[],
			 *   _project: \Project|null,
			 *   paymentMeansCode?: int,
			 *   paymentMeansText?: string,
			 *   _shipFromContactBill?: array{address: null|string, zip: null|string, town: null|string, country: string},
			 *   _shipFromContactShip?: array{name: string, address: null|string, zip: null|string, town: null|string, country: string}
			 * }	$invoiceData
			 * @var array<int, array{
			 *   lineid: int,
			 *   linestatuscode: 'NA',
			 *   linestatusreasoncode: 'NA',
			 *   lineNote: null,
			 *   prodname: string,
			 *   proddesc: string,
			 *   prodsellerid: string,
			 *   prodbuyerid: null|string,
			 *   prodglobalidtype: null|string,
			 *   prodglobalid: null|string,
			 *   prodmultilangs: array,
			 *   prodClassificationCode: null|string,
			 *   prodClassificationScheme: null|string,
			 *   prodOriginCountry: null|string,
			 *   netpriceamount: float,
			 *   netpricebasisquantity: null|float,
			 *   netpricebasisquantityunitcode: null|string,
			 *   billedquantity: float,
			 *   billedquantityunitcode: string,
			 *   chargeFreeQuantity: null|float,
			 *   chargeFreeQuantityunitcode: null|string,
			 *   packageQuantity: null|float,
			 *   packageQuantityunitcode: null|string,
			 *   lineTotalAmount: float|string,
			 *   totalAllowanceChargeAmount: null|float,
			 *   categoryCode: string,
			 *   typeCode: 'VAT',
			 *   rateApplicablePercent: string,
			 *   tva_tx: float|string,
			 *   vat_src_code: string,
			 *   ExemptionReason: string,
			 *   ExemptionReasonCode: string,
			 *   calculatedAmount: null|float,
			 *   lineAllowances: array,
			 *   lineGrossPriceAllowances: array,
			 *   lineremisepercent: 'NA'|float,
			 *   linePeriodStart: ?\DateTimeInterface,
			 *   linePeriodEnd: ?\DateTimeInterface,
			 *   additionalRefDocs: array,
			 *   isDepositLine: bool,
			 *   depositInvoiceRef: null|string,
			 *   depositInvoiceDate: ?DateTimeInterface,
			 *   parentDocumentNo: null|string,
			 *   is_deposit: int<0,1>,
			 *   fk_remise: null|int,
			 *   discountPercent: float,
			 *   grosspriceamount: null|float,
			 *   grosspricebasisquantity: null|float,
			 *   grosspricebasisquantityunitcode: null|string
			 * }> $linesData
			 * @var string 				$outputlang		Value of $outputlangs->defaultlang
			 * @var Account				$account
			 * @var EInvoicing			$einvoicing
			 * @var string 				$schemeUri		Buyer scheme uri
			 * @var string 				$uri			Buyer uri
			 */
			'
			@phan-var-force Facture 			$object			The $invoice object used in entry on inc file, but completed.
			@phan-var-force array{documentno:string,documenttypecode:null|string,documentdate:DateTimeInterface,invoiceCurrency:string|array<string>,taxCurrency:null,documentname:null,documentlanguage:string,effectiveSpecifiedPeriod:\'NA\',documentDeliveryDate:DateTimeInterface,invoicingPeriodStart:null,invoicingPeriodEnd:null,businessProcessId:string,isTestDocument:bool,documentNotePublic:string,documentNotePMT:string,documentNotePMD:string,documentNoteAAB:string,documentNoteTXD:string,documentNotes:array,vatDueDateTypeCode:string,sellername:string,sellerids:string,sellerlineone:string,sellerlinetwo:string,sellerlinethree:string,sellerpostcode:string,sellercity:string,sellercountry:string,sellersubdivision:null,sellercontactpersonname:string,sellercontactdepartmentname:null,sellercontactphoneno:string,sellercontactfaxno:string,sellercontactemailaddr:string,sellerCommunicationUriScheme:string,sellerCommunicationUri:string,sellerGlobalIds:array<array{schemeID:string,value:string}>,sellerTaxRegistrations:array<array{type:string,value:string}>,sellervatnumber:string,sellerLegalOrgId:string,sellerLegalOrgScheme:string,sellerTradingName:string,buyername:string,buyerids:string,buyerlineone:string,buyerlinetwo:string,buyerlinethree:string,buyerpostcode:string,buyercity:string,buyercountry:string,buyersubdivision:null,buyervatnumber:string,buyerGlobalIds:array<array{schemeID:string,value:string}>,buyerLegalOrgId:string,buyerLegalOrgScheme:string,buyerTradingName:string,buyerReference:null|string,buyerCommunicationUriScheme:string,buyerCommunicationUri:string,buyercontactpersonname:null,buyercontactemailaddr:null,buyercontactphoneno:null,grandTotalAmount:float|int,duePayableAmount:float|int,lineTotalAmount:float|int,chargeTotalAmount:float,allowanceTotalAmount:float|int,taxBasisTotalAmount:float|int,taxTotalAmount:float|int,roundingAmount:null,totalPrepaidAmount:float|int,iban_id:int,iban:string,bic:string,accountName:string,accountRef:string,accountLabel:string,paymentDueDate:DateTimeInterface,paymentTermsText:string,headerAllowancesCharges:array,invoiceRefDocs:array|array<array{ref:string|int,date:DateTimeInterface,type:string}>,orderReference:string,contractReference:null|string,despatchAdviceRef:null,taxBreakdown:array|array<array<string,array>>,_chorus:bool,_depositlines:array|array<array{lineId:int,invoiceRef:string,invoiceDate:DateTimeInterface}>,_globalDiscounts:array|array<array{value:float,reason:string,taxRate:float,categoryVAT:string}>,_customerOrderReferenceList:string[],_project:Project|null,paymentMeansCode?:int,paymentMeansText?:string,_shipFromContactBill?:array{address:null|string,zip:null|string,town:null|string,country:string},_shipFromContactShip?:array{name:string,address:null|string,zip:null|string,town:null|string,country:string}} $invoiceData
			@phan-var-force array<int,array{lineid:int,linestatuscode:\'NA\',linestatusreasoncode:\'NA\',lineNote:null,prodname:string,proddesc:string,prodsellerid:string,prodbuyerid:null|string,prodglobalidtype:null|string,prodglobalid:null|string,prodmultilangs:array,prodClassificationCode:null|string,prodClassificationScheme:null|string,prodOriginCountry:null|string,netpriceamount:float,netpricebasisquantity:null|float,netpricebasisquantityunitcode:null|string,billedquantity:float,billedquantityunitcode:string,chargeFreeQuantity:null|float,chargeFreeQuantityunitcode:null|string,packageQuantity:null|float,packageQuantityunitcode:null|string,lineTotalAmount:float|string,totalAllowanceChargeAmount:null|float,categoryCode:string,typeCode:\'VAT\',rateApplicablePercent:string,tva_tx:float|string,vat_src_code:string,ExemptionReason:string,ExemptionReasonCode:string,calculatedAmount:null|float,lineAllowances:array,lineGrossPriceAllowances:array,lineremisepercent:\'NA\'|float,linePeriodStart:?DateTimeInterface,linePeriodEnd:?DateTimeInterface,additionalRefDocs:array,isDepositLine:bool,depositInvoiceRef:null|string,depositInvoiceDate:?DateTimeInterface,parentDocumentNo:null|string,is_deposit:int<0,1>,fk_remise:null|int,discountPercent:float,grosspriceamount:null|float,grosspricebasisquantity:null|float,grosspricebasisquantityunitcode:null|string}> $linesData
			@phan-var-force string 				$outputlang		Value of $outputlangs->defaultlang
			@phan-var-force Account				$account
			@phan-var-force EInvoicing			$einvoicing
			@phan-var-force string 				$schemeUri		Buyer scheme uri
			@phan-var-force string 				$uri			Buyer uri
			';

			// =====================================================================
			// Use horstoeko lib to build the XML
			// =====================================================================

			// The lib serializes every amount and quantity with 2 decimals, which is the accuracy of an
			// amount, not of a unit price: it would truncate the unit price and make the receiver
			// recalculate a wrong line amount. Ask for the accuracy the norm allows on these nodes.
			self::applyNormDecimalPlaces();

			// Initialize ZugferdDocumentBuilder (FacturX XML)
			dol_syslog(get_class($this) . '::executeHooks create new XML document based on PROFILE_EN16931 (CIUS-FR)');
			$profile = getDolGlobalString('EINVOICING_PROFILE');
			switch ($profile) {
				case 'EN16931':
					$used_profile = ZugferdProfiles::PROFILE_EXTENDED;
					$facturxpdf = ZugferdDocumentBuilder::createNew($used_profile);
					// no break
				default:
					$used_profile = ZugferdProfiles::PROFILE_EXTENDED;
					$facturxpdf = ZugferdDocumentBuilder::createNew($used_profile);
			}
			dol_syslog(get_class($this) . '::executeHooks create new XML document based on ' . $used_profile);

			// Get the type of invoice in FacturX nomenclature
			$objecttype = $invoiceData['documenttypecode'];
			if ($objecttype == null) {
				throw new Exception('BADINVOICETYPE: The type for invoice id ' . $object->id . ' is not yet supported.');
			}

			//  Build XML Document Header (Seller, Buyer, Dates)
			$facturxpdf
				->setDocumentInformation(
					$invoiceData['documentno'],
					$invoiceData['documenttypecode'],
					$invoiceData['documentdate'],
					$invoiceData['invoiceCurrency'],
					$object->ref_customer,
					$outputlang
				)
				->addDocumentNote($invoiceData['documentNotePublic'])
				->addDocumentNote($invoiceData['documentNotePMT'], null, "PMT")
				->addDocumentNote($invoiceData['documentNotePMD'], null, "PMD")
				->addDocumentNote($invoiceData['documentNoteAAB'], null, "AAB")
				->addDocumentNote($invoiceData['documentNoteTXD'], null, "TXD")

				// ---------------- Seller ----------------
				->setDocumentSeller($invoiceData['sellername'], $invoiceData['sellerids'])
				->addDocumentSellerTaxRegistration("VA", $invoiceData['sellervatnumber'])
				->setDocumentSellerLegalOrganisation(
					$invoiceData['sellerLegalOrgId'],
					$invoiceData['sellerLegalOrgScheme'],
					$invoiceData['sellerTradingName']
				)
				->addDocumentSellerGlobalId($invoiceData['sellerGlobalIds'][0]['value'], $invoiceData['sellerGlobalIds'][0]['schemeID'])
				->setDocumentSellerCommunication(
					$invoiceData['sellerCommunicationUriScheme'],
					$invoiceData['sellerCommunicationUri']
				)
				->setDocumentSellerAddress(
					$invoiceData['sellerlineone'],
					$invoiceData['sellerlinetwo'],
					$invoiceData['sellerlinethree'],
					$invoiceData['sellerpostcode'],
					$invoiceData['sellercity'],
					$invoiceData['sellercountry']
				)

				// ---------------- Buyer ----------------
				->setDocumentBuyer(
					$invoiceData['buyername'],
					$invoiceData['buyerids']
				)
				->setDocumentBuyerAddress(
					$invoiceData['buyerlineone'],
					$invoiceData['buyerlinetwo'],
					$invoiceData['buyerlinethree'],
					$invoiceData['buyerpostcode'],
					$invoiceData['buyercity'],
					$invoiceData['buyercountry']
				)
				->addDocumentBuyerTaxRegistration("VA", $invoiceData['buyervatnumber'])
				->setDocumentBuyerLegalOrganisation(
					$invoiceData['buyerLegalOrgId'],
					$invoiceData['buyerLegalOrgScheme'],
					$invoiceData['buyerTradingName']
				)
				->addDocumentBuyerGlobalId($invoiceData['buyerGlobalIds'][0]['value'], $invoiceData['buyerGlobalIds'][0]['schemeID'])
				->setDocumentBuyerCommunication(
					$schemeUri,
					$uri
				);

			// If specimen, we set the test flag
			if ($invoiceData['isTestDocument']) {
				$facturxpdf->setIsTestDocument();
			}

			// Add delivery date for section ApplicableHeaderTradeDelivery
			$facturxpdf->setDocumentSupplyChainEvent($invoiceData['documentDeliveryDate']);

			// Add data of project if invoice is into a project
			if ($invoiceData['_project'] instanceof Project) {
				$facturxpdf->setDocumentProcuringProject((string) $invoiceData['_project']->ref, $invoiceData['_project']->title);
			}

			// Add additional referenced documents (Order references) - Disabled for Chorus
			if (!$invoiceData['_chorus']) {
				foreach ($invoiceData['_customerOrderReferenceList'] as $customerOrderRef) {
					if ($customerOrderRef != $invoiceData['orderReference']) {
						$facturxpdf->addDocumentAdditionalReferencedDocument($customerOrderRef, "130");
					}
				}
			}

			// Set Trade Contact details (sale representative). No fax: EN16931 has no business term
			// for it and the CII syntax binding forbids ram:FaxUniversalCommunication (CII-SR-236),
			// same as for the buyer contact below.
			$facturxpdf->setDocumentSellerContact(
				$invoiceData['sellercontactpersonname'],
				"",
				$invoiceData['sellercontactphoneno'],
				"",
				$invoiceData['sellercontactemailaddr']
			);

			// Set Buyer Trade Contact details (e.g. invoice billing contact). Only emitted when a contact person is known.
			if (!empty($invoiceData['buyercontactpersonname'])) {
				$facturxpdf->setDocumentBuyerContact(
					$invoiceData['buyercontactpersonname'],
					"",
					$invoiceData['buyercontactphoneno'],
					"",
					$invoiceData['buyercontactemailaddr']
				);
			}
			// Set Buyer Reference (Service Code for Chorus)
			if (!empty($invoiceData['buyerReference'])) {
				$facturxpdf->setDocumentBuyerReference($invoiceData['buyerReference']);
			}

			// Contract reference
			if (!empty($invoiceData['contractReference'])) {
				$facturxpdf->setDocumentContractReferencedDocument($invoiceData['contractReference']);
			}

			// Commitment number / client ref
			if (!empty($invoiceData['orderReference'])) {
				$facturxpdf->setDocumentBuyerOrderReferencedDocument($invoiceData['orderReference']);
			}

			// Set Business Process ID
			$facturxpdf->setDocumentBusinessProcess($invoiceData['businessProcessId']);

			// Add reference to source invoice for credit notes (BT-25/BT-26)
			if (!empty($invoiceData['invoiceRefDocs'])) {
				foreach ($invoiceData['invoiceRefDocs'] as $refDoc) {
					$facturxpdf->setDocumentInvoiceReferencedDocument($refDoc['ref'], $refDoc['type'], $refDoc['date']);
				}
			}

			// --- Process Invoice Lines ---
			foreach ($linesData as $numligne => $lineData) {
				$facturxpdf
					->addNewPosition((string) $lineData['lineid'])
					->setDocumentPositionProductDetails($lineData['prodname'], $lineData['proddesc'], $lineData['prodsellerid'])
					->setDocumentPositionGrossPrice($lineData['grosspriceamount'])
					->setDocumentPositionNetPrice($lineData['netpriceamount'])
					->setDocumentPositionQuantity($lineData['billedquantity'], $lineData['billedquantityunitcode'])
					->setDocumentPositionLineSummation($lineData['lineTotalAmount']);

				// Add reference to original invoice for deposit lines
				if ($lineData['isDepositLine']) {
					$facturxpdf->setDocumentInvoiceReferencedDocument($lineData['depositInvoiceRef'], ZugferdInvoiceType::PREPAYMENTINVOICE, $lineData['depositInvoiceDate']);
				}

				// Set billing period for the line
				if ($lineData['linePeriodStart'] !== null && $lineData['linePeriodEnd'] !== null) {
					$facturxpdf->setDocumentPositionBillingPeriod($lineData['linePeriodStart'], $lineData['linePeriodEnd']);
				}

				// Handle negative amount lines as a line discount
				if ($lineData['grosspriceamount'] < 0) {
					dol_syslog("EInvoicing : there is negative line, convert as a global discount", \LOG_INFO);
					$facturxpdf->addDocumentPositionGrossPriceAllowanceCharge(abs($lineData['grosspriceamount']) * $lineData['billedquantity'], false, null, null, "Discount");
				}

				// VAT information (Line Tax)
				if ($lineData['rateApplicablePercent'] > 0) {
					$facturxpdf->addDocumentPositionTax($lineData['categoryCode'], 'VAT', (empty($lineData['rateApplicablePercent']) ? null : (float) $lineData['rateApplicablePercent']));
				} else {
					$facturxpdf->addDocumentPositionTax($lineData['categoryCode'], 'VAT', 0.00);
				}

				// Discount percentage on a line
				if ($lineData['lineremisepercent'] !== 'NA' && $lineData['lineremisepercent'] > 0) {
					$remise_amount = $lineData['lineTotalAmount'] - $lineData['grosspriceamount'] * $lineData['billedquantity'];
					dol_syslog("EInvoicing : there is a discount on that line : " . $lineData['lineremisepercent'] . ", amount is " . $remise_amount);
					$facturxpdf->addDocumentPositionAllowanceCharge(abs($remise_amount), false, $lineData['lineremisepercent'], $lineData['grosspriceamount'] * $lineData['billedquantity'], null, "Discount");
				}
			}

			// Final Document Summation and Payment Means

			// Multi VAT (Document Tax Summary)
			foreach ($invoiceData['taxBreakdown'] as $k => $v) {
				$code = ($k == 0) ? 'K' : 'S';
				// Last argument is BT-8 (VAT point date code), empty when the invoice carries goods only
				$facturxpdf->addDocumentTax($code, "VAT", $v['totalHT'], $v['totalTVA'], $k, null, null, null, null, null, $invoiceData['vatDueDateTypeCode'] ?: null);
			}

			// Set final summation details
			$facturxpdf
				->setDocumentSummation(
					$invoiceData['grandTotalAmount'],
					$invoiceData['duePayableAmount'],
					$invoiceData['lineTotalAmount'],
					$invoiceData['chargeTotalAmount'],
					$invoiceData['allowanceTotalAmount'],
					$invoiceData['taxBasisTotalAmount'],
					$invoiceData['taxTotalAmount'],
					null,
					$invoiceData['totalPrepaidAmount']
				)
				->addDocumentPaymentTerm(
					$invoiceData['paymentTermsText'],
					$invoiceData['paymentDueDate']
				)
				->addDocumentPaymentMean(
					(string) $invoiceData['paymentMeansCode'],
					$invoiceData['paymentMeansText'],
					null,
					null,
					null,
					null,
					$invoiceData['iban'],
					$invoiceData['accountName'],
					$einvoicing->removeSpaces($account->number),
					$invoiceData['bic']
				);

			// Validate the generated XML document
			dol_syslog(get_class($this) . '::executeHooks try to validate XML');

			$pdfCheck = new ZugferdDocumentValidator($facturxpdf);
			$res = $pdfCheck->validateDocument();
			if (count($res) > 0) {
				$allErrors = "";
				foreach ($res as $error) {
					if (is_array($error)) {
						$allErrors .= json_encode($error) . "\n";
					} else {
						$allErrors .= $error . "\n";
					}
				}
				$this->errors = $allErrors;
				dol_syslog(get_class($this) . '::executeHooks  (1) : ' . $allErrors, LOG_ERR);
			} else {
				dol_syslog(get_class($this) . '::executeHooks XML validation ok');
			}

			// Generate the XML file Factur-X
			$filename = dol_sanitizeFileName($invoice->ref);
			$filedir  = getMultidirOutputCompat($invoice, '', 1, 'temp');   // Example '/mydolibarr/documents/facture/temp/FAYYMM-XXXX'
			$xmlfile  = $filedir . '/' . $filename . '/factur-x3.xml';

			dol_mkdir(dirname($xmlfile));
			dol_delete_file($xmlfile);

			$facturxpdf->writeFile($xmlfile);

			// Patch the generated XML for better EXTENDED-CTC-FR compatibility
			if ($invoice->type == $invoice::TYPE_CREDIT_NOTE || !empty($invoiceData['_depositlines'])) {
				dol_syslog(get_class($this) . '::executeHooks Patch XML for better EXTENDED-CTC-FR compatibility');
				$patcher    = new XmlPatcher($facturxpdf);
				$patchedXml = $patcher->patchXmlString($xmlfile, $invoiceData['_depositlines']);
				file_put_contents($xmlfile, $patchedXml);
			}

			// Local EN 16931 business rules safety net on the final XML (warnings, or abort in strict mode),
			// same as the native builder branch above, so the external builder is covered too.
			$this->checkBusinessRules(file_get_contents($xmlfile));

			dolChmod($xmlfile);

			return $xmlfile;
		}
	}

	/**
	 * Generate a complete Factur-X invoice file by embedding the XML into a PDF.
	 *
	 * This function combines the invoice data with its corresponding XML
	 * to produce a final hybrid document ready for exchange or archiving.
	 *
	 * @param 	int|Object 	$invoice_id    	Invoice ID or Invoice Object to be processed.
	 * @param	?Translate	$outputlangs	Output language
	 * @param	string		$sourceFilePath	Full path of the source document produced by the doc generator (PDF, or ODT/ODS whose PDF rendition is reused). Empty = resolve from the output directory.
	 * @return 	-1|string       			-1 if ko, path if ok.
	 */
	public function generateInvoice($invoice_id, $outputlangs = null, $sourceFilePath = '')
	{
		// Global variables declaration (typical for Dolibarr environment)
		global $langs, $db;

		dol_syslog(get_class($this) . '::generateInvoice');

		if (empty($outputlangs) || ! ($outputlangs instanceof Translate)) {
			$outputlangs = $langs;
		}

		require_once DOL_DOCUMENT_ROOT . "/compta/facture/class/facture.class.php";
		require_once DOL_DOCUMENT_ROOT . '/core/lib/pdf.lib.php';
		require_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';

		if ($invoice_id instanceof Facture) {
			$invoice = $invoice_id;
			$invoice_id = $invoice->id;
		} else {
			$invoice = new Facture($db);
			$invoiceResult = $invoice->fetch((int) $invoice_id);

			if ($invoiceResult < 0) {
				dol_syslog(get_class($this) . "::generateInvoice failed to load invoice id=" . $invoice_id, LOG_ERR);
				$this->error = $langs->trans("ErrorLoadingInvoice");
				$this->errors[] = $this->error;
				return -1;
			}
		}

		// Generate XML
		try {
			$xmlfile = $this->generateXML($invoice, $outputlangs);
		} catch (Exception $e) {
			dol_syslog(get_class($this) . "::generateInvoice failed to generate XML for invoice id=" . $invoice_id . ". Error " . $e->getMessage(), LOG_ERR);
			$this->error = $langs->trans("ErrorGeneratingXML") . '. ' . $e->getMessage();
			$this->errors[] = $this->error;
			return -1;
		}

		if (empty($xmlfile) || !file_exists($xmlfile)) {
			dol_syslog(get_class($this) . "::generateInvoice failed to generate XML for invoice id=" . $invoice_id, LOG_ERR);
			$this->error = $langs->trans("ErrorGeneratingXML");
			$this->errors[] = $this->error;
			return -1;
		}


		// Load EInvoicing specific translations
		$langs->loadLangs(array("admin", "einvoicing@einvoicing"));

		$filename = dol_sanitizeFileName($invoice->ref);
		$filedir = getMultidirOutputCompat($invoice, '', 1);		// Example '/mydolibarr/documents/facture/FAYYMM-XXXX'

		// Resolve the source PDF into which the Factur-X XML will be embedded.
		// Priority:
		//   1. $sourceFilePath provided by the generation hook (afterPDFCreation / afterODTCreation).
		//      ODT/ODS models hand over the .odt path; the PDF rendition (MAIN_ODT_AS_PDF) shares the basename.
		//   2. the most recent <ref>*.pdf already present in the output dir (manual generation, ODT output
		//      like <ref>_Template.pdf for which last_main_doc is not maintained), excluding our own output.
		//   3. legacy <ref>.pdf, regenerated with the default PDF model if missing.
		$orig_pdf = '';
		$fromodt = false;
		if (!empty($sourceFilePath)) {
			if (preg_match('/\.(odt|ods)$/i', $sourceFilePath)) {
				$fromodt = true;
				$orig_pdf = preg_replace('/\.(odt|ods)$/i', '.' . self::INVOICE_FILE_EXTENSION, $sourceFilePath);
			} else {
				$orig_pdf = $sourceFilePath;
			}
		} else {
			$candidates = dol_dir_list($filedir, 'files', 0, '', '', 'date', SORT_DESC);
			foreach ($candidates as $cand) {
				if (!preg_match('/\.pdf$/i', $cand['name'])) {
					continue;
				}
				if (preg_match('/_facturx\.pdf$/i', $cand['name'])) {		// skip our own Factur-X output
					continue;
				}
				if (strpos($cand['name'], $filename) !== 0) {				// must belong to this invoice ref
					continue;
				}
				$orig_pdf = $cand['fullname'];								// list is sorted by date desc: newest first
				break;
			}
		}
		if (empty($orig_pdf)) {
			$orig_pdf = $filedir . '/' . $filename . '.' . self::INVOICE_FILE_EXTENSION;				// legacy default
		}

		// If the source PDF is missing, decide whether we can recover.
		if (!file_exists($orig_pdf)) {
			if ($fromodt && !getDolGlobalString('MAIN_ODT_AS_PDF')) {
				// ODT invoice template without a PDF rendition: there is no PDF carrier for Factur-X.
				dol_syslog(get_class($this) . "::generateInvoice ODT template without MAIN_ODT_AS_PDF, no PDF carrier for Factur-X, invoice id=" . $invoice_id, LOG_ERR);
				$this->error = $langs->trans("ErrorEInvoiceRequiresPdfEnableMainOdtAsPdf");
				$this->errors[] = $this->error;
				return -1;
			}
			// Source PDF deleted or never generated: regenerate it with the default PDF model before embedding.
			$modelname = getDolGlobalString('FACTURE_ADDON_PDF') ?: 'crabe';
			$resultpdf = $invoice->generateDocument($modelname, $langs);
			if ($resultpdf < 0) {
				dol_syslog(get_class($this) . "::generateInvoice failed to regenerate missing PDF for invoice id=" . $invoice_id, LOG_ERR);
				$this->error = $langs->trans("ErrorFailedToRegeneratePDF");
				$this->errors[] = $this->error;
				return -1;
			}
			$orig_pdf = $filedir . '/' . $filename . '.' . self::INVOICE_FILE_EXTENSION;				// generateDocument writes <ref>.pdf
		}

		// Make a copy of the original PDF file
		$pathfacturxpdf = $filedir . '/' . $filename . '_facturx.' . self::INVOICE_FILE_EXTENSION;	// The new name of the PDF including xml
		if (dol_copy($orig_pdf, $pathfacturxpdf)) {
			dol_syslog(get_class($this) . "::generateInvoice copied original PDF to " . $pathfacturxpdf);
		} else {
			dol_syslog(get_class($this) . "::generateInvoice failed to copy original PDF to " . $pathfacturxpdf, LOG_ERR);
			$this->error = $langs->trans("ErrorFailToCopyFile", $orig_pdf, $pathfacturxpdf);
			$this->errors[] = $this->error;
			return -1;
		}

		// Initial PDF File Pre-check ---
		$precheck = false;
		if (file_exists($pathfacturxpdf) && is_readable($pathfacturxpdf)) {
			$finfo = finfo_open(FILEINFO_MIME_TYPE);
			if (finfo_file($finfo, $pathfacturxpdf) == 'application/pdf') {
				$precheck = true;
			}
		}

		// Check if the source PDF is valid, log error and exit if not.
		if (!$precheck) {
			dol_syslog(get_class($this) . "::executeHooks orig pdf file does not exists, can't create facturX");
			$this->error = 'Orig pdf file does not exists, can t create facturX';
			$this->errors[] = $this->error;
			return -1;
		}

		clearstatcache(true);


		// Embed the XML file $xmlfile into the file $pathfacturxpdf (that was copied from $orig_pdf) using FPDI and overwrite it.
		// 2 methods are provided depending on the version of Dolibarr.
		// TODO A third method can be tried using the atgp/factur-x library.

		if (class_exists('FPDF', false) && is_subclass_of('FPDF', 'TCPDF')) {
			// Generate the PDF including the XML using the TCPDF library.
			// Bugged version that include the factur-x.xml file twice in the PDF. Only Acrobat Reader show there is 2 files, other PDF reader works correctly showing one file.
			// But it works with Esalink and is the only solution when Dolibarr < 24.0 because such version have a class FPDF provided by default in Dolibarr
			// that is in conflict with the class FPDF provided by the module einvoicing and the library horstoeko/zugferd.
			$pdf = pdf_getInstance();
			$pagecount = $pdf->setSourceFile($pathfacturxpdf);

			// import all pages of the original PDF
			for ($i = 1; $i <= $pagecount; $i++) {
				$tpl = $pdf->importPage($i);
				$pdf->addPage();
				$pdf->useTemplate($tpl);
			}

			// Embed the XML file as a file attachment in the PDF
			if (file_exists($xmlfile)) {
				$pdf->Annotation(10, 10, 5, 5, 'factur-x.xml', array(
					'Subtype' => 'FileAttachment',
					'Name' => 'PushPin',
					'FS' => $xmlfile
				));
			}

			// Restore metadata from original PDF.
			if (function_exists('pdfExtractMetadata')) {	// From Dolibarr v22
				// Now we get the metadata keywords from the $sourcefile PDF (by parsing the binary PDF file)
				$keywords = pdfExtractMetadata($pathfacturxpdf, 'Keywords');
				$subject = pdfExtractMetadata($pathfacturxpdf, 'Subject');
				$author = pdfExtractMetadata($pathfacturxpdf, 'Author');
				$creator = pdfExtractMetadata($pathfacturxpdf, 'Creator');

				if (!preg_match('/^ERROR/', $keywords)) {
					$pdf->setKeywords($keywords);
				}
				if (!preg_match('/^ERROR/', $subject)) {
					$pdf->setSubject($subject);
				}
				if (!preg_match('/^ERROR/', $author)) {
					$pdf->setAuthor($author);
				}
				if (!preg_match('/^ERROR/', $creator)) {
					$pdf->setCreator($creator);
				}
			}

			// Save the final PDF with the embedded XML
			$pdf->Output($pathfacturxpdf, 'F');
		} else {
			// Generate the PDF including the XML using the horstoeko/zugferd library.
			// This can works with Dolibarr 24.0+ only, because the previous version of Dolibarr was already including a FPDF class that is
			// in conflict with the one provided by horstoeko/zugferd library.
			if (!file_exists($orig_pdf)) {
				throw new \Exception("XML and/or PDF does not exist");
			}

			// Restore metadata from original PDF.
			// horstoeko/zugferd setters require non-null strings, so default to '' for Dolibarr
			// versions that do not ship pdfExtractMetadata() (v18 / v19); v22+ overwrites these
			// with the actual values parsed from the source PDF.
			$keywords = '';
			$subject = '';
			$author = '';
			$creator = '';
			if (function_exists('pdfExtractMetadata')) {	// From Dolibarr v22
				// Now we get the metadata keywords from the $sourcefile PDF (by parsing the binary PDF file)
				$keywords = (string) pdfExtractMetadata($orig_pdf, 'Keywords');
				$subject = (string) pdfExtractMetadata($orig_pdf, 'Subject');
				$author = (string) pdfExtractMetadata($orig_pdf, 'Author');
				$creator = (string) pdfExtractMetadata($orig_pdf, 'Creator');
			}

			// CtcFrPdfMerger behaves exactly like ZugferdDocumentPdfMerger, except that it can still
			// supply the attachment and XMP parameters when the guideline URN is one the library does
			// not know — which is the case of EXTENDED-CTC-FR.
			$merger = new CtcFrPdfMerger($xmlfile, $orig_pdf);

			$merger->setKeywordTemplate($keywords);
			$merger->setSubjectTemplate($subject);
			$merger->setAuthorTemplate($author);
			$merger->setAdditionalCreatorTool($creator);

			$merger->generateDocument();

			$merger->saveDocument($pathfacturxpdf);
		}


		// Clean up the temporary XML file
		if (file_exists($xmlfile) && !getDolGlobalString('EINVOICING_DEBUG_MODE')) {
			dol_delete_file($xmlfile);
			dol_syslog(get_class($this) . '::generateInvoice cleaned up temporary XML file: ' . $xmlfile);
		}

		// Add afterEinvoiceCreation hook
		global $action, $hookmanager;
		$hookmanager->initHooks(array('einvoicegeneration'));
		$parameters = array('protocol' => 'factur-x', 'file' => $orig_pdf, 'object' => $invoice, 'outputlangs' => $langs);
		$reshook = $hookmanager->executeHooks('afterEinvoiceCreation', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
		if ($reshook < 0) {
			$this->error = $hookmanager->error;
			$this->errors = $hookmanager->errors;
			return -1;
		}

		// Set status of einvoice
		$einvoicing = new EInvoicing($db);
		$result = $einvoicing->fetchLastknownInvoiceStatus($invoice->id, $invoice->ref);

		if (
			isset($result['code']) &&
			(in_array($result['code'], array($einvoicing::STATUS_UNKNOWN, $einvoicing::STATUS_NOT_GENERATED))
				|| !array_key_exists($result['code'], $einvoicing::STATUS_LABEL_KEYS))
		) {
			// Set status to e-einvoice generated
			$einvoicing->setEInvoiceStatus($invoice, $einvoicing::STATUS_GENERATED, 'Invoice status set to Generated by generateInvoice()');
		}

		// Warn if the generated file exceeds the configured size limit
		$this->checkFileSizeLimit($pathfacturxpdf);

		return $pathfacturxpdf;		// Name of generated Einvoice
	}


	/**
	 * Generate a sample Factur-X invoice for demonstration or testing purposes (for Dolibarr version < 24.0)
	 *
	 * This method creates a dummy invoice with representative data
	 * to illustrate the Factur-X structure without using real business information.
	 *
	 * @param	EInvoicing			$einvoicing			EInvoicing
	 * @param   Societe|null			$thirdpartySeller		Optional third party object to use for generating the sample invoice. If null, a dummy third party will be created.
	 * @param   Societe|null			$thirdpartyBuyer		Optional third party object to use for generating the sample invoice. If null, a dummy third party will be created.
	 * @param   array<string,mixed>		$options				More options
	 * @return 	array<string,string> 							Path or content of the generated sample invoice.
	 * @throws  Exception
	 */
	public function generateSampleInvoiceOld($einvoicing, $thirdpartySeller = null, $thirdpartyBuyer = null, $options = array())
	{
		global $conf, $langs, $mysoc;

		dol_mkdir($conf->einvoicing->dir_temp);

		$outputlangs = $langs;		// TODO Use the target language
		$outputlangs->load("einvoicing@einvoicing");

		require __DIR__ . "/ExampleHelpers.php";

		$existingPdfFilename = __DIR__ . "/../../doc/00_ZugferdDocumentPdfBuilder_PrintLayout.pdf";
		$newPdfFilename = $conf->einvoicing->dir_temp . "/INVTEST-".dol_print_date(dol_now(), '%y%m%d-%H%M%S').".pdf";
		//$AdditionalDocument = __DIR__ . "/../../assets/00_AdditionalDocument.csv";

		// First we create a new valid document in EN16931-Profile (== COMFORT-Profile)
		// See examples/01_ZugferdDocumentBuilder_EN16931.php for detailed explanations

		$documentBuilder = ZugferdDocumentBuilder::createNew(ZugferdProfiles::PROFILE_EN16931);

		$invoicetype = ZugferdInvoiceType::INVOICE;				// Type "Invoice" (BT-3)
		if (!empty($options['invoicetype'])) {
			if ($options['invoicetype'] == Facture::TYPE_CREDIT_NOTE) {
				$invoicetype = ZugferdInvoiceType::CREDITNOTE;
			}
		}

		$documentBuilder->setDocumentInformation(
			'INVTEST',                                     	// Invoice Number (BT-1)
			$invoicetype,                        			// Type "Invoice" (BT-3)
			DateTime::createFromFormat("Ymd", "20241231"),  // Invoice Date (BT-2)
			ZugferdCurrencyCodes::EURO                      // Invoice currency is EUR (Euro) (BT-5)
		);

		$sellername = $mysoc->name ?: "MyBigCompanyTest";
		$sellervat = $mysoc->tva_intra ?: "FRVAT123456";

		$mySchemeIdProf = "0002";
		//$sellerid = $einvoicing->getSellerCommunicationURI(0);
		$sellerid = idprof($mysoc);														// May be SIREN

		$mySchemeGlobalId = "0225";
		//$sellerglobalid = $einvoicing->getSellerCommunicationURI(0);
		$sellerglobalid = idprof($mysoc);

		if ($mySchemeIdProf == "0002" && strlen($sellerid) != 9) {	// If einvoice ID is French SIREN, we check it has 9 chars.
			throw new Exception('BADPROFID (generateSampleInvoiceOld): The professional ID ' . $sellerid . ' has type SIREN but length is not 9 characters. Fix this in your company or einvoice module setup page.');
		}

		$documentBuilder->addDocumentNote($sellername . PHP_EOL . 'Lieferantenstraße 20' . PHP_EOL . '80333 München' . PHP_EOL . 'Deutschland' . PHP_EOL . 'Geschäftsführer: Hans Muster' . PHP_EOL . 'Handelsregisternummer: H A 123' . PHP_EOL . PHP_EOL, null, 'REG');


		$documentBuilder->addDocumentNote(getDolGlobalString('EINVOICING_PMT') ?: $outputlangs->trans('NoInvoiceCollectionFees'), null, "PMT");
		$documentBuilder->addDocumentNote(getDolGlobalString('EINVOICING_PMD') ?: $outputlangs->trans('NoLatePaymentFees'), null, "PMD");
		$documentBuilder->addDocumentNote(getDolGlobalString('EINVOICING_AAB') ?: $outputlangs->trans('NoEarlyPaymentDiscount'), null, "AAB");


		$documentBuilder->setDocumentBillingPeriod(DateTime::createFromFormat("Ymd", "20250101"), DateTime::createFromFormat("Ymd", "20250131"), "01.01.2025 - 31.01.2025");

		$documentBuilder->addDocumentInvoiceSupportingDocumentWithUri('FA2401-000001', 'https://publiclinktoinvoice', 'LISIBLE');
		$documentBuilder->addDocumentInvoiceSupportingDocumentWithUri('SO2401-000001', 'https://linktoorder', 'BON_COMMANDE');
		//$documentBuilder->addDocumentInvoiceSupportingDocumentWithFile('REFDOC-2024/00001-2', $AdditionalDocument, 'Herkunftsnachweis Trennblätter');

		$documentBuilder->addDocumentTenderOrLotReferenceDocument('LOS 738625');
		$documentBuilder->addDocumentInvoicedObjectReferenceDocument('125', ZugferdReferenceCodeQualifiers::SALE_PERS_NUMB); // Sales person number

		$documentBuilder->setDocumentContractReferencedDocument('CO2401-000001');			// Ref of contract

		$documentBuilder->setDocumentProcuringProject('PR2401-000001', 'Project label');	// Ref of project

		$documentBuilder->addDocumentPaymentMeanToDirectDebit("DE12500105170648489890", "INV-TEST");
		$documentBuilder->addDocumentPaymentTerm('Wird von Konto DE12500105170648489890 abgebucht', DateTime::createFromFormat("Ymd", "20250131"), 'MANDATE-2024/000001');

		$documentBuilder->setDocumentSeller($sellername, $sellerid);
		$documentBuilder->setDocumentSellerLegalOrganisation($sellerid, $mySchemeIdProf, $sellername);	// Mandatory: 0002 = SIREN
		$documentBuilder->addDocumentSellerGlobalId($sellerglobalid, $mySchemeGlobalId);				// Optional : 0225 = SIREN. Can be a more international code like DUNS

		//$documentBuilder->setSpecifiedLegalOrganization();
		$documentBuilder->addDocumentSellerTaxNumber($sellervat);
		$documentBuilder->addDocumentSellerVATRegistrationNumber($sellervat);

		$documentBuilder->setDocumentSellerAddress("Lieferantenstraße 20", "", "", "80333", "München", ZugferdCountryCodes::GERMANY);
		$documentBuilder->setDocumentSellerContact("H. Müller", "", "+49-111-2222222", "+49-111-3333333", "hm@lieferant.de");

		//$documentBuilder->setDocumentSellerCommunication(ZugferdElectronicAddressScheme::UNECE3155_EM, 'sales@lieferant.de');
		$myUri             = $einvoicing->getSellerCommunicationURI(0);			// Example "315143296_1939"
		$mySchemeUri       = $this->getIEC6523Code($mysoc->country_code, 2);		// Example "0225"
		$documentBuilder->setDocumentSellerCommunication($mySchemeUri, $myUri);

		$documentBuilder->setDocumentBuyer("Kunden AG Mitte", "GE2020211");
		$documentBuilder->setDocumentBuyerAddress("Kundenstraße 15", "", "", "69876", "Frankfurt", ZugferdCountryCodes::GERMANY);
		$documentBuilder->setDocumentBuyerContact("H. Meier", "", "+49-333-4444444", "+49-333-5555555", "hm@kunde.de");

		//$documentBuilder->setDocumentBuyerCommunication(ZugferdElectronicAddressScheme::UNECE3155_EM, 'purchase@kunde.de');
		$buyerUri             = $einvoicing->getBuyerCommunicationURI(null);		// Example "315143296_1940"
		$buyerSchemeUri       = $this->getIEC6523Code($mysoc->country_code, 2);		// Example "0225"
		if (empty($buyerUri)) {
			$buyerUri = '315143296_1940';
		}
		$documentBuilder->setDocumentBuyerCommunication($buyerSchemeUri, $buyerUri);

		$documentBuilder->setDocumentPayee('Kunden AG Zahlungsdienstleistung');
		$documentBuilder->setDocumentBuyerOrderReferencedDocument("PO-2024-0003324");
		$documentBuilder->setDocumentSellerOrderReferencedDocument('SO-2024-000993337');

		// If there is a delivery address
		$documentBuilder->setDocumentShipTo("Kunden AG Ost");
		$documentBuilder->setDocumentShipToAddress("Lieferstraße 1", "", "", "04109", "Leipzig", ZugferdCountryCodes::GERMANY);
		$documentBuilder->setDocumentSupplyChainEvent(DateTime::createFromFormat("Ymd", "20250115"));

		$documentBuilder->addNewPosition("1");
		$documentBuilder->setDocumentPositionProductDetails("Trennblätter A4", "50er Pack", "TB100A4");
		$documentBuilder->setDocumentPositionNetPrice(9.9000);
		$documentBuilder->setDocumentPositionQuantity(20, ZugferdUnitCodes::REC20_PIECE);
		$vatrate = 20;
		if (!$this->checkIfVatRateIsValid((string) $vatrate, $mysoc->country_code)) {
			throw new Exception('BADVATRATE: The VAT rate ' . $vatrate . ' on line is not a valid string value for country ' . $mysoc->country_code . '.');
		}
		$documentBuilder->addDocumentPositionTax(ZugferdVatCategoryCodes::STAN_RATE, ZugferdVatTypeCodes::VALUE_ADDED_TAX, $vatrate);
		$documentBuilder->setDocumentPositionLineSummation(198.0);

		$documentBuilder->addNewPosition("2");
		$documentBuilder->setDocumentPositionProductDetails("Joghurt Banane", "B-Ware", "ARNR2");
		$documentBuilder->setDocumentPositionNetPrice(5.5000);
		$documentBuilder->setDocumentPositionQuantity(50, ZugferdUnitCodes::REC20_PIECE);
		$vatrate = 7;
		if (!$this->checkIfVatRateIsValid((string) $vatrate, $mysoc->country_code)) {
			throw new Exception('BADVATRATE: The VAT rate ' . $vatrate . ' on line is not a valid string value for country ' . $mysoc->country_code . '.');
		}
		$documentBuilder->addDocumentPositionTax(ZugferdVatCategoryCodes::STAN_RATE, ZugferdVatTypeCodes::VALUE_ADDED_TAX, $vatrate);
		$documentBuilder->setDocumentPositionLineSummation(275.0);

		$documentBuilder->addNewPosition("3");
		$documentBuilder->setDocumentPositionProductDetails("Joghurt Erdbeer", "", "ARNR3");
		$documentBuilder->setDocumentPositionNetPrice(4.0000);
		$documentBuilder->setDocumentPositionQuantity(100, ZugferdUnitCodes::REC20_PIECE);
		$vatrate = 7;
		if (!$this->checkIfVatRateIsValid((string) $vatrate, $mysoc->country_code)) {
			throw new Exception('BADVATRATE: The VAT rate ' . $vatrate . ' on line is not a valid string value for country ' . $mysoc->country_code . '.');
		}
		$documentBuilder->addDocumentPositionTax(ZugferdVatCategoryCodes::STAN_RATE, ZugferdVatTypeCodes::VALUE_ADDED_TAX, $vatrate);

		$documentBuilder->setDocumentPositionLineSummation(400.0);

		// Add total of vat per vat rate
		$documentBuilder->addDocumentTax(ZugferdVatCategoryCodes::STAN_RATE, ZugferdVatTypeCodes::VALUE_ADDED_TAX, 198.0, 39.60, 20.0);
		$documentBuilder->addDocumentTax(ZugferdVatCategoryCodes::STAN_RATE, ZugferdVatTypeCodes::VALUE_ADDED_TAX, 675.0, 47.25, 7.0);

		$documentBuilder->setDocumentSummation(959.85, 959.85, 873.00, 0.0, 0.0, 873.00, 86.85);


		// This is a test doc
		$documentBuilder->setIsTestDocument();


		// Next let's do the ZugferddocumentPdfBuilder it's job - let's attach the XML to the PDF. The attachment filename will be factur-x.xml
		// since we chose the profile EN16931 in the ZugferdDocumentBuilder (see above)
		// In the following there are multiple methods how you can build a conform PDF from an existing print layout

		// First method: Merge the generated XML from ZugferdDocumentBuilder with an existing print layout file to a new PDF file

		/*
		$zugferdDocumentPdfBuilder = ZugferdDocumentPdfBuilder::fromPdfFile($documentBuilder, $existingPdfFilename);
		$zugferdDocumentPdfBuilder->generateDocument();
		$zugferdDocumentPdfBuilder->saveDocument($newPdfFilename);
		*/

		// Second method: Merge the generated XML from ZugferdDocumentBuilder with an stream (string) which contains the PDF to a new PDF file

		// Note: We simulate the PDF stream (string) by calling file_get_contents.
		/*
		$pdfContent = file_get_contents($existingPdfFilename);

		$zugferdDocumentPdfBuilder = ZugferdDocumentPdfBuilder::fromPdfString($documentBuilder, $pdfContent);
		$zugferdDocumentPdfBuilder->generateDocument();
		$zugferdDocumentPdfBuilder->saveDocument($newPdfFilename);
		*/

		// There is not only the saveDocument method of the ZugferdDocumentPdfBuilder. It is also possible to receive the merged
		// content (PDF with embedded XML) as a stream (string)

		/*
		$mergedPdfContent = $zugferdDocumentPdfBuilder->downloadString();

		$pdfContent = file_get_contents($existingPdfFilename);

		$zugferdDocumentPdfBuilder = ZugferdDocumentPdfBuilder::fromPdfString($documentBuilder, $pdfContent);
		$zugferdDocumentPdfBuilder->generateDocument();
		$zugferdDocumentPdfBuilder->saveDocument($newPdfFilename);
		*/

		// And last but not least, it is also possible to add additional attachments to the merged PDF. These can be any files that can help the invoice
		// recipient with processing. For example, a time sheet as an Excel file would be conceivable.
		// The method attachAdditionalFileByRealFile has 3 parameters:
		// - The file to attach which must exist and must be readable
		// - (Optional) A name to display in the attachments of the PDF
		// - (Optional) The type of the relationship of the attachment. Valid values are defined in the class ZugferdDocumentPdfBuilderAbstract. The constants are starting with AF_
		// If you omit the last 2 parameters the following will happen:
		// - The displayname is calculated from the filename you specified
		// - The type of the relationship of the attachment will be AF_RELATIONSHIP_SUPPLEMENT (Supplement)

		/*
		$zugferdDocumentPdfBuilder = ZugferdDocumentPdfBuilder::fromPdfString($documentBuilder, $pdfContent);
		$zugferdDocumentPdfBuilder->attachAdditionalFileByRealFile($AdditionalDocument, "Some display Name", ZugferdDocumentPdfBuilderAbstract::AF_RELATIONSHIP_SUPPLEMENT);
		$zugferdDocumentPdfBuilder->generateDocument();
		$zugferdDocumentPdfBuilder->saveDocument($newPdfFilename);

		// You can also add an attachment to the PDF as an stream (string). The conditions are the same as above for the attachAdditionalFileByRealFile method
		// The only difference to attachAdditionalFileByRealFile is that the attachAdditionalFileByContent method accepts 4 parameters, whereby here (as with attachAdditionalFileByRealFile)
		// the last two can be omitted. You only need to specify a file name under which the file is to be embedded
		// Note: We simulate the attachment stream (string) by calling file_get_contents.

		$attachmentContent = file_get_contents($AdditionalDocument);

		$zugferdDocumentPdfBuilder = ZugferdDocumentPdfBuilder::fromPdfString($documentBuilder, $pdfContent);
		$zugferdDocumentPdfBuilder->attachAdditionalFileByContent($attachmentContent, 'additionalDocument.csv', "Some other display Name", ZugferdDocumentPdfBuilderAbstract::AF_RELATIONSHIP_SUPPLEMENT);
		$zugferdDocumentPdfBuilder->generateDocument();
		$zugferdDocumentPdfBuilder->saveDocument($newPdfFilename);
		*/

		// Set values for metadata-fields
		// We can change some meta information such as the title, the subject, the author and the keywords.  This library essentially provides 4 methods for this.
		// These methods use so-called templates. These methods are:

		// The 4 methods just mentioned accept a free text that can accept the following placeholders:
		// - %1$s .... contains the invoice number (is extracted from the XML data)
		// - %2$s .... contains the type of XML document, such as ‘Invoice’ (is extracted from the XML data)
		// - %3$s .... contains the name of the seller (extracted from the XML data)
		// - %4$s .... contains the invoice date (extracted from the XML data)
		// The following example generates...
		// - the author:  .... Issued by seller with name Lieferant GmbH
		// - the title    .... Lieferant GmbH : Invoice INV-TEST
		// - the subject  .... Invoice-Document, Issued by Lieferant GmbH
		// - the keywords .... INV-TEST, Invoice, Lieferant GmbH, 2024-12-31

		// This rebuild the PDF including the XML and added PDF metadata.
		$zugferdDocumentPdfBuilder = ZugferdDocumentPdfBuilder::fromPdfFile($documentBuilder, $existingPdfFilename);
		$zugferdDocumentPdfBuilder->setAuthorTemplate('Issued by %3$s');
		$zugferdDocumentPdfBuilder->setTitleTemplate('%3$s : %2$s SPECIMEN %1$s');
		$zugferdDocumentPdfBuilder->setSubjectTemplate('%2$s-Document, Issued by %3$s - Dolibarr ' . DOL_VERSION);
		$zugferdDocumentPdfBuilder->setKeywordTemplate('%1$s, %2$s, %3$s, %4$s, Dolibarr');
		// If you would like to brand the merged PDF with the name of you own solution you can call
		// the method setAdditionalCreatorTool. Before calling this method the creator of the PDF is identified as 'Factur-X library 1.x.x by HorstOeko'.
		// After calling this method you get 'MyERPSolution 1.0 / Factur-X PHP library 1.x.x by HorstOeko' as the creator
		$zugferdDocumentPdfBuilder->setAdditionalCreatorTool('Dolibarr generateSampleInvoiceOld - ' . DOL_VERSION);
		$zugferdDocumentPdfBuilder->generateDocument();
		$zugferdDocumentPdfBuilder->saveDocument($newPdfFilename);

		return array('path' => $newPdfFilename, 'ref' => 'INV-TEST');
	}


	/**
	 * Build the supplier invoice from a received Factur-X document written to a per-call working file.
	 * The temp-file lifecycle is owned by createSupplierInvoiceFromSource() (the public wrapper).
	 * The vendor synchronization runs in its own transaction, opened and closed here. The invoice
	 * import transaction is opened here too, right after, but closed by that same wrapper.
	 *
	 * @param  string			$file                 Raw Factur-X PDF content
	 * @param  string|null		$ReadableViewFile     Optional readable view (PDP-generated readable PDF)
	 * @param  string			$flowId               Source flow identifier
	 * @param  string			$tempFile             Unique working file for the received PDF
	 * @param  string			$tempFileReadableView Unique working file for the readable view
	 * @return array{res:int<-1,1>, message:string, action?:string|null}
	 */
	protected function doCreateSupplierInvoiceFromSource($file, $ReadableViewFile, $flowId, $tempFile, $tempFileReadableView)
	{
		global $conf, $db, $user;

		// Duplicate code with doCreateSupplierInvoiceFromSource in CIIProtocol.class.php
		// TODO Merge tis code with the one into CIIProtocol.class.php to avoid duplicate

		$einvoicing = new EInvoicing($db);
		$return_messages = array();

		if (file_put_contents($tempFile, $file) === false) {
			return ['res' => -1, 'message' => 'Failed to save EInvoice file to temporary location'];
		}

		if ($ReadableViewFile) {
			if (file_put_contents($tempFileReadableView, $ReadableViewFile) === false) {
				return ['res' => -1, 'message' => 'Failed to save readable view file to temporary location'];
			}
		}

		//return ['res' => 1, 'message' => 'bypass' ];

		// --- Create Supplier Invoice object
		require_once DOL_DOCUMENT_ROOT . '/fourn/class/fournisseur.facture.class.php';
		$supplierInvoice = new FactureFournisseur($db);


		// --- Read the Factur-X file
		$document = ZugferdDocumentPdfReader::readAndGuessFromFile($tempFile);
		$embeddedXml = ZugferdDocumentPdfReaderExt::getInvoiceDocumentContentFromFile($tempFile);

		$parsedHeader = [];
		$parsedLines = [];
		if (!getDolGlobalInt('EINVOICING_USE_EXTERNAL_FACTURX_READER')) { // The default is to use the same parser than the CII one.
			$parsedHeader = $this->parseInvoiceHeader($embeddedXml);
			$parsedLines  = $this->parseInvoiceLines($embeddedXml);
		} else {
			// Use a duplicate parser (for test or dev tests)
			$document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $invoiceCurrency, $taxCurrency, $documentname, $documentlanguage, $effectiveSpecifiedPeriod);

			$document->getDocumentSupplyChainEvent(
				$documentDeliveryDate
			);

			// Get seller information (supplier)
			$document->getDocumentSeller($sellername, $sellerids, $sellerdescription);

			// Get seller address
			$document->getDocumentSellerAddress(
				$sellerlineone,
				$sellerlinetwo,
				$sellerlinethree,
				$sellerpostcode,
				$sellercity,
				$sellercountry,
				$sellersubdivision
			);

			// Get seller contact
			$document->getDocumentSellerContact(
				$sellercontactpersonname,
				$sellercontactdepartmentname,
				$sellercontactphoneno,
				$sellercontactfaxno,
				$sellercontactemailaddr
			);

			$document->getDocumentSellerCommunication(
				$sellerCommunicationUriScheme,
				$sellerCommunicationUri
			);

			// Get document summation
			$document->getDocumentSummation($grandTotalAmount, $duePayableAmount, $lineTotalAmount, $chargeTotalAmount, $allowanceTotalAmount, $taxBasisTotalAmount, $taxTotalAmount, $roundingAmount, $totalPrepaidAmount);

			$document->getDocumentSellerGlobalId(
				$sellerGlobalIds
			);

			$document->getDocumentSellerTaxRegistration(
				$sellerTaxRegistations
			);

			// Get references to the previous invoices if any (for credit notes for example)
			$document->getDocumentInvoiceReferencedDocuments($invoiceRefDocs);

			// Debug: print all retrieved variables
			$parsedHeader = array(
				'documentno' => $documentno ?? null,
				'documenttypecode' => $documenttypecode ?? null,
				'documentdate' => isset($documentdate) && $documentdate instanceof DateTime ? $documentdate->format('Y-m-d') : ($documentdate ?? null),
				'invoiceCurrency' => $invoiceCurrency ?? null,
				'taxCurrency' => $taxCurrency ?? null,
				'documentname' => $documentname ?? null,
				'documentlanguage' => $documentlanguage ?? null,
				'effectiveSpecifiedPeriod' => $effectiveSpecifiedPeriod ?? null,
				'documentDeliveryDate' => isset($documentDeliveryDate) && $documentDeliveryDate instanceof DateTime ? $documentDeliveryDate->format('Y-m-d') : ($documentDeliveryDate ?? null),

				// Seller
				'sellername' => $sellername ?? null,
				'sellerids' => $sellerids ?? null,
				'sellerdescription' => $sellerdescription ?? null,

				// Seller Address
				'sellerlineone' => $sellerlineone ?? null,
				'sellerlinetwo' => $sellerlinetwo ?? null,
				'sellerlinethree' => $sellerlinethree ?? null,
				'sellerpostcode' => $sellerpostcode ?? null,
				'sellercity' => $sellercity ?? null,
				'sellercountry' => $sellercountry ?? null,
				'sellersubdivision' => $sellersubdivision ?? null,

				// Seller Contact
				'sellercontactpersonname' => $sellercontactpersonname ?? null,
				'sellercontactdepartmentname' => $sellercontactdepartmentname ?? null,
				'sellercontactphoneno' => $sellercontactphoneno ?? null,
				'sellercontactfaxno' => $sellercontactfaxno ?? null,
				'sellercontactemailaddr' => $sellercontactemailaddr ?? null,

				// Seller Communication (may be unset due to reader var name)
				'sellerCommunicationUriScheme' => $sellerCommunicationUriScheme ?? null,
				'sellerCommunicationUri' => $sellerCommunicationUri ?? null,

				// Summation
				'grandTotalAmount' => $grandTotalAmount ?? null,
				'duePayableAmount' => $duePayableAmount ?? null,
				'lineTotalAmount' => $lineTotalAmount ?? null,
				'chargeTotalAmount' => $chargeTotalAmount ?? null,
				'allowanceTotalAmount' => $allowanceTotalAmount ?? null,
				'taxBasisTotalAmount' => $taxBasisTotalAmount ?? null,
				'taxTotalAmount' => $taxTotalAmount ?? null,
				'roundingAmount' => $roundingAmount ?? null,
				'totalPrepaidAmount' => $totalPrepaidAmount ?? null,

				// Seller Global Ids and Tax Registrations (may be unset due to reader var name)
				'sellerGlobalIds' => $sellerGlobalIds ?? null,
				'sellerTaxRegistations' => $sellerTaxRegistations ?? null,

				// Invoice referenced documents
				'invoiceRefDocs' => $invoiceRefDocs ?? null,
			);


			// Read invoice lines
			$additionalRefDocs = [];
			if ($document->firstDocumentPosition()) {
				do {
					// Get line information
					$document->getDocumentPositionGenerals($lineid, $linestatuscode, $linestatusreasoncode);
					$document->getDocumentPositionProductDetails($prodname, $proddesc, $prodsellerid, $prodbuyerid, $prodglobalidtype, $prodglobalid);
					$document->getDocumentPositionGrossPrice($grosspriceamount, $grosspricebasisquantity, $grosspricebasisquantityunitcode);
					$document->getDocumentPositionNetPrice($netpriceamount, $netpricebasisquantity, $netpricebasisquantityunitcode);
					$document->getDocumentPositionLineSummation($lineTotalAmount, $totalAllowanceChargeAmount);
					$document->getDocumentPositionQuantity($billedquantity, $billedquantityunitcode, $chargeFreeQuantity, $chargeFreeQuantityunitcode, $packageQuantity, $packageQuantityunitcode);

					// Get AdditionalReferencedDocument at line level
					$patcher = new XmlPatcher(null, $embeddedXml);
					$additionalRefDocs[(string) $lineid] = $patcher->getLineAdditionalReferencedDocuments((string) $lineid);

					// Get tax information for the line
					//$vatRate = 0;
					if ($document->firstDocumentPositionTax()) {
						$document->getDocumentPositionTax($categoryCode, $typeCode, $rateApplicablePercent, $calculatedAmount, $exemptionReason, $exemptionReasonCode);
						//$vatRate = $rateApplicablePercent;
					}

					$parsedLines[] = array(
						'lineid' => $lineid ?? null,
						'linestatuscode' => $linestatuscode ?? null,
						'linestatusreasoncode' => $linestatusreasoncode ?? null,
						'prodname' => $prodname ?? null,
						'proddesc' => $proddesc ?? null,
						'prodsellerid' => $prodsellerid ?? null,
						'prodbuyerid' => $prodbuyerid ?? null,
						'prodglobalidtype' => $prodglobalidtype ?? null,
						'prodglobalid' => $prodglobalid ?? null,
						'grosspriceamount' => $grosspriceamount ?? null,
						'grosspricebasisquantity' => $grosspricebasisquantity ?? null,
						'grosspricebasisquantityunitcode' => $grosspricebasisquantityunitcode ?? null,
						'netpriceamount' => $netpriceamount ?? null,
						'netpricebasisquantity' => $netpricebasisquantity ?? null,
						'netpricebasisquantityunitcode' => $netpricebasisquantityunitcode ?? null,
						'lineTotalAmount' => $lineTotalAmount ?? null,
						'totalAllowanceChargeAmount' => $totalAllowanceChargeAmount ?? null,
						'billedquantity' => $billedquantity ?? null,
						'billedquantityunitcode' => $billedquantityunitcode ?? null,
						'chargeFreeQuantity' => $chargeFreeQuantity ?? null,
						'chargeFreeQuantityunitcode' => $chargeFreeQuantityunitcode ?? null,
						'packageQuantity' => $packageQuantity ?? null,
						'packageQuantityunitcode' => $packageQuantityunitcode ?? null,
						// Tax
						'categoryCode' => $categoryCode ?? null,
						'typeCode' => $typeCode ?? null,
						'rateApplicablePercent' => $rateApplicablePercent ?? null,
						'calculatedAmount' => $calculatedAmount ?? null,
						'ExemptionReason' => $exemptionReason ?? null,
						'ExemptionReasonCode' => $exemptionReasonCode ?? null,
						// Parent invoice ref
						'parentDocumentNo' => $parsedHeader['documentno'] ?? null,
						// Additional referenced documents at line level
						'additionalRefDocs' => $additionalRefDocs[(string) $lineid] ?? null,
					);


					dol_syslog(get_class($this) . '::createSupplierInvoiceFromSource parsedLines: ' . json_encode($parsedLines), LOG_DEBUG);
				} while ($document->nextDocumentPosition());
			}
		}

		dol_syslog(get_class($this) . '::createSupplierInvoiceFromSource parsedHeader: ' . json_encode($parsedHeader), LOG_DEBUG);
		dol_syslog(get_class($this) . '::createSupplierInvoiceFromSource parsedHeader: ' . json_encode($parsedHeader), LOG_DEBUG, 0, '_einvoicing');

		// Sync or create supplier based on seller info.
		// Done before the duplicate/ref-docs checks below so those checks can be scoped to this supplier
		// (ref_supplier is only unique per supplier, not globally - see issue about cross-supplier collisions).
		//
		// The vendor is reference data, not part of the invoice: it gets its own transaction, committed
		// before the import starts. A business error raised further down - a product that cannot be
		// auto-created, a referenced document missing - must not roll back the thirdparty the operator is
		// precisely being asked to complete: the "create the product" and "map the product" links returned
		// with that error carry its socid, so a rolled back vendor makes them point to a thirdparty that
		// never existed.
		$db->begin();

		$syncSocRes = $this->_syncOrCreateThirdpartyFromEInvoiceSeller($parsedHeader, 'dolibarr', $flowId);

		$socId = $syncSocRes['res'];
		$return_messages[] = $syncSocRes['message'];
		if ($socId < 0) {
			$db->rollback();
			return [
				'res' => -1,
				'message' => 'Thirdparty sync or creation error: ' . implode("<br>\n", $return_messages),
				'actioncode' => $syncSocRes['actioncode'] ?? '',
				'actionurl' => $syncSocRes['actionurl'] ?? '',
				'action' => $syncSocRes['action'] ?? null,
				'actiondata' => $syncSocRes['actiondata'] ?? null
			];
		}

		$db->commit();

		// From this point on, everything belongs to the invoice import (products, invoice, lines) and
		// stays atomic. This second transaction is closed (commit or rollback) by
		// createSupplierInvoiceFromSource(), the public wrapper.
		$db->begin();

		// Load supplier (thirdparty)
		require_once DOL_DOCUMENT_ROOT . '/fourn/class/fournisseur.class.php';
		$supplier = new Fournisseur($db);
		if ($supplier->fetch($socId) < 0) {
			return ['res' => -1, 'message' => 'Failed to load supplier id ' . $socId];
		}

		// Check if this invoice has already been imported for this supplier
		$sql = "SELECT rowid as id FROM " . MAIN_DB_PREFIX . "facture_fourn";
		$sql .= " WHERE ref_supplier = '" . $db->escape($parsedHeader['documentno']) . "'";
		$sql .= " AND fk_soc = " . ((int) $socId);
		$sql .= " AND entity IN (" . getEntity('facture_fourn') . ")";
		$resql = $db->query($sql);
		if ($resql) {
			if ($db->num_rows($resql) > 0) {
				$supplierInvoiceId = $db->fetch_object($resql)->id;
				$einvoicing->cleanUpTemporaryFiles(); // Clean up temp files to remove retrieved Einvoice file since invoice already exists

				// FIXME supplierinvoice already found but may be that documents are not linked (this is done later but only after creating invoice,
				// may be we should also do it in this case to fix inconsistent data).

				return ['res' => $supplierInvoiceId, 'message' => 'Supplier Invoice with reference ' . $parsedHeader['documentno'] . ' already exists'];
			}
		} else {
			return ['res' => -1, 'message' => 'Database error while checking existing supplier invoice: ' . $db->lasterror()];
		}

		// Check if all referenced documents in the invoice exist in Dolibarr for the same supplier, if not return with error since we need them for correct linking in the invoice
		if (!empty($parsedHeader['invoiceRefDocs']) && is_array($parsedHeader['invoiceRefDocs'])) {
			foreach ($parsedHeader['invoiceRefDocs'] as $invoiceRefDoc) {
				$refDoc = $invoiceRefDoc['IssuerAssignedID'] ?? null;
				$dateDoc = $invoiceRefDoc['FormattedIssueDateTime'] ?? null;
				$typeDoc = $invoiceRefDoc['TypeCode'] ?? null;

				$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "facture_fourn WHERE ref_supplier = '" . $db->escape($refDoc) . "' AND fk_soc = " . ((int) $socId) . " AND entity IN (" . getEntity('facture_fourn') . ") LIMIT 1";
				$resql = $db->query($sql);
				if ($db->num_rows($resql) != 1) {
					return ['res' => -1, 'message' => 'Document : ' . $refDoc . ' linked to document ' . $parsedHeader['documentno'] . ' not found in Dolibarr'];
				}
			}
		}

		// Set supplier reference
		$supplierInvoice->socid = $socId;
		$supplierInvoice->ref_supplier = $parsedHeader['documentno'] ?? '';

		// Set basic invoice information (type, date)
		$supplierInvoice->type = $this->getDolibarrInvoiceType($parsedHeader['documenttypecode'] ?? null);
		if ($supplierInvoice->type === '-1') {
			return ['res' => -1, 'message' => 'Unfounded dolibarr corresponding Invoice code for document type code: ' . ($parsedHeader['documenttypecode'] ?? 'NA')];
		}
		// documentdate is already formatted into 'Y-m-d' by the parser ZugFerd and CII
		$supplierInvoice->date = !empty($parsedHeader['documentdate']) ? dol_stringtotime($parsedHeader['documentdate']) : null;

		// For credit notes, link to the source invoice via fk_facture_source (BT-25)
		if ($supplierInvoice->type == FactureFournisseur::TYPE_CREDIT_NOTE && !empty($parsedHeader['invoiceRefDocs']) && is_array($parsedHeader['invoiceRefDocs'])) {
			$firstRefDoc = reset($parsedHeader['invoiceRefDocs']);
			$refSourceSupplier = !empty($firstRefDoc['IssuerAssignedID']) ? (string) $firstRefDoc['IssuerAssignedID'] : '';
			if ($refSourceSupplier !== '') {
				$sqlSource = "SELECT rowid FROM " . MAIN_DB_PREFIX . "facture_fourn WHERE ref_supplier = '" . $db->escape($refSourceSupplier) . "' AND fk_soc = " . ((int) $socId) . " AND entity IN (" . getEntity('facture_fourn') . ") LIMIT 1";
				$resqlSource = $db->query($sqlSource);
				if ($resqlSource) {
					$objSource = $db->fetch_object($resqlSource);
					if ($objSource) {
						$supplierInvoice->fk_facture_source = (int) $objSource->rowid;
						dol_syslog(get_class($this) . '::doCreateSupplierInvoiceFromSource Credit note linked to source invoice id=' . $supplierInvoice->fk_facture_source, LOG_DEBUG);
					} else {
						dol_syslog(get_class($this) . '::doCreateSupplierInvoiceFromSource Source invoice ref_supplier="' . $refSourceSupplier . '" not found for credit note ' . ($parsedHeader['documentno'] ?? ''), LOG_WARNING);
					}
				}
			}
		}

		// Set currency
		$supplierInvoice->multicurrency_code = (string) $parsedHeader['invoiceCurrency'];

		// Set import_key
		$supplierInvoice->import_key = AbstractPDPProvider::$EINVOICING_LAST_IMPORT_KEY;

		// Set payment due date, payment terms and payment method
		$paymentInfoRes = $this->_applyPaymentInfoToSupplierInvoice($supplierInvoice, $parsedHeader);
		if (!empty($paymentInfoRes['message'])) {
			dol_syslog(get_class($this) . '::doCreateSupplierInvoiceFromSource ' . $paymentInfoRes['message'], LOG_DEBUG, 0, '_einvoicing');
		}


		$remise_already_used_line_level_ids = array();
		$supplierPriceEntries = array(); // Collect product/price data to create supplier prices after invoice creation

		// Create document level discounts (allowances) as discounts in Dolibarr
		$globalDiscountIds = array();
		if (!empty($parsedHeader['headerAllowancesCharges'])) {
			$headerDiscountIds = $this->createHeaderDiscounts($parsedHeader['headerAllowancesCharges'], $socId, (string) $parsedHeader['documentno']);
			if (!empty($headerDiscountIds[-1])) {
				return ['res' => -1, 'message' => $headerDiscountIds[-1]];
			} else {
				$globalDiscountIds = $headerDiscountIds;
			}
		}

		//return ['res' => 1, 'message' => 'Not implemented yet' ];

		// Set invoice totals
		$supplierInvoice->total_ht = $parsedHeader['taxBasisTotalAmount'] ?? 0;
		$supplierInvoice->total_tva = $parsedHeader['taxTotalAmount'] ?? 0;
		$supplierInvoice->total_ttc = $parsedHeader['grandTotalAmount'] ?? 0;

		// Add a note about PDP import ( TODO: add a hook or extrafields to store import details)
		$supplierInvoice->note_private = "Imported from PDP";

		// TODO : save AAB, PMD, PMT notes (all notes are grouped into documentNotes)

		// Create the invoice
		$supplierInvoiceId = $supplierInvoice->create($user);

		if ($supplierInvoiceId < 0) {
			return ['res' => -1, 'message' => 'Invoice creation error: ' . $supplierInvoice->error];
		} else {
			// Link the invoice to its purchase order (commande fournisseur) when the order reference
			// (BT-13) matches a single order for the same supplier. Non-blocking. See issue #303.
			$orderLinkMessage = $this->_linkSupplierInvoiceToPurchaseOrder($supplierInvoice, $socId, $parsedHeader['orderReference'] ?? '');
			if ($orderLinkMessage !== '') {
				$return_messages[] = $orderLinkMessage;
			}


			// --------------------------------------------------
			// Create supplier invoice lines
			// --------------------------------------------------

			$res = $this->createSupplierInvoiceLinesFromSource($supplierInvoice, $parsedLines, $remise_already_used_line_level_ids, $supplierPriceEntries, $return_messages, $flowId);
			if ($res['res'] < 0) {
				return $res;
			}

			$create_deposit_line = 0;
			$fk_remise_for_deposit = 0;
			// --------------------------------------------------
			// Loop on linked documents at document level
			// --------------------------------------------------
			if (!empty($parsedHeader['invoiceRefDocs']) && is_array($parsedHeader['invoiceRefDocs'])) {
				foreach ($parsedHeader['invoiceRefDocs'] as $doc) {
					$refDoc = $doc['IssuerAssignedID'] ?? null;
					$dateDoc = $doc['FormattedIssueDateTime'] ?? null;
					$typeDoc = $doc['TypeCode'] ?? null;

					$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "facture_fourn WHERE ref_supplier = '" . $db->escape($refDoc) . "' AND fk_soc = " . ((int) $socId) . " AND entity IN (" . getEntity('facture_fourn') . ") LIMIT 1";
					$resql = $db->query($sql);
					if ($db->num_rows($resql) != 1) {
						return ['res' => -1, 'message' => 'Document : ' . $refDoc . ' linked to document ' . $parsedHeader['documentno'] . ' not found in Dolibarr'];
					}
					$linkedObjectId = $db->fetch_object($resql)->rowid;

					// Fetch Object
					$linkedObject = new FactureFournisseur($db);
					$resFetchLinkedObject = $linkedObject->fetch($linkedObjectId);
					if ($resFetchLinkedObject > 0) {
						// --------------------------------------------------
						// Deposit handling
						// --------------------------------------------------
						if ($linkedObject->type == FactureFournisseur::TYPE_DEPOSIT) {
							$create_deposit_line = 1;

							$depositDiscountRes = $this->getOrCreateDepositDiscount($linkedObject);
							if ($depositDiscountRes['res'] < 0) {
								return $depositDiscountRes;
							}
							$fk_remise_for_deposit = $depositDiscountRes['fkRemise'];

							// After creating the discount for the deposit, we create a line in the invoice to link it to the deposit
							if ($create_deposit_line && !empty($fk_remise_for_deposit)) {
								if (!in_array($fk_remise_for_deposit, $remise_already_used_line_level_ids)) { // If the discount for deposit is not already used at line level we link it to the invoice, otherwise it is already linked at line level so we skip to avoid duplicates
									$currentSupplierInvoice = new FactureFournisseur($db);
									$currentSupplierInvoice->fetch($supplierInvoiceId);
									$result = $currentSupplierInvoice->insert_discount($fk_remise_for_deposit);
									if ($result < 0) {
										return ['res' => -1, 'message' => 'Failed to link discount for deposit to supplier invoice: ' . $currentSupplierInvoice->error];
									} else {
										dol_syslog('Deposit line linked to supplier invoice with line id: ' . $result);
									}
								}
							}
						}

						// Other linked document handling can be implemented here based on the type of the linked document for example credit note etc...
					} else {
						return ['res' => -1, 'message' => 'Document : ' . $refDoc . ' linked to document ' . $parsedHeader['documentno'] . ' not found in Dolibarr'];
					}
				}
			}

			// Update thirdparty as a supplier if not already the case
			if ($supplier->fournisseur != 1) {
				$supplier->fournisseur = 1;
				$supplier->code_fournisseur = 'auto';
				$supplier->update($supplier->id, $user);
			}

			// Insert global discounts (allowances) as lines in this supplier invoice
			if (!empty($globalDiscountIds)) {
				foreach ($globalDiscountIds as $fk_remise_except) {
					$currentSupplierInvoice = new FactureFournisseur($db);
					$currentSupplierInvoice->fetch($supplierInvoiceId);
					$result = $currentSupplierInvoice->insert_discount($fk_remise_except);
					if ($result < 0) {
						return ['res' => -1, 'message' => 'Failed to insert global discount into supplier invoice: ' . $currentSupplierInvoice->error];
					} else {
						dol_syslog('Global discount inserted into supplier invoice with line id: ' . $result);
					}
				}
			}

			// Create or update supplier prices for imported products
			if (!empty($supplierPriceEntries)) {
				require_once DOL_DOCUMENT_ROOT . '/fourn/class/fournisseur.product.class.php';
				foreach ($supplierPriceEntries as $entry) {
					$productFourn = new ProductFournisseur($db);
					$productFourn->id = $entry['productId'];
					$result = $productFourn->update_buyprice(
						1,                    // qty min
						$entry['unitPrice'],  // prix unitaire HT
						$user,
						'HT',
						$supplier,
						0,                    // availability
						$entry['refFourn'],   // ref fournisseur
						$entry['tvaTx']
					);
					if ($result < 0) {
						dol_syslog(__METHOD__ . ' Failed to create supplier price for product id=' . $entry['productId'] . ': ' . $productFourn->error, LOG_WARNING);
					} else {
						dol_syslog(__METHOD__ . ' Supplier price created/updated for product id=' . $entry['productId'], LOG_DEBUG);
					}
				}
			}

			// Set import_key
			$sql = 'UPDATE ' . MAIN_DB_PREFIX . "facture_fourn SET import_key = '" . $db->escape($supplierInvoice->import_key) . "'";
			$sql .= " WHERE rowid = " . ((int) $supplierInvoiceId);
			$db->query($sql);

			// Add entry in einvoicing_extlinks table to mark that this supplier invoice is imported from PDP
			$einvoicing->insertOrUpdateExtLink($supplierInvoiceId, $supplierInvoice->element, $flowId);

			dol_syslog(__METHOD__ . ' New supplier invoice created or updated (ID: ' . $supplierInvoiceId . ')');

			$return_messages[] = 'Supplier Invoice created or updated with ID: ' . $supplierInvoiceId;


			// Save original invoice in supplier invoice attachments
			if ($tempFile && file_exists($tempFile)) {
				$res = $this->saveEInvoiceFileToSupplierInvoiceAttachment($supplierInvoice, $tempFile);

				if ($res['res'] < 0) {
					$return_messages[] = 'Failed to save Einvoice file as attachment: ' . $res['message'];
				} else {
					$return_messages[] = 'Einvoice file saved as attachment';
				}
			} else {
				dol_syslog("Temporary 'converted pdf file' not found for attachment", LOG_ERR);
			}


			// Save readable view file in supplier invoice attachments
			if ($ReadableViewFile && $tempFileReadableView && file_exists($tempFileReadableView)) {
				$res = $this->saveEInvoiceFileToSupplierInvoiceAttachment($supplierInvoice, $tempFileReadableView, getDolGlobalString('EINVOICING_PDP', 'PDP'));

				if ($res['res'] < 0) {
					$return_messages[] = 'Failed to save readable view file as attachment: ' . $res['message'];
				} else {
					$return_messages[] = 'Readable view file saved as attachment';
				}
			} else {
				dol_syslog("Temporary 'readable pdf file' not found for attachment", LOG_ERR);
			}

			// TODO : Save receivedFile in supplier invoice attachments
			return ['res' => $supplierInvoiceId, 'message' => implode("\n", $return_messages), 'xml_data' => $embeddedXml];
		}
	}

	/**
	 * Extract XML from an input file content and return it
	 *
	 * @param  string $fileContent Raw file content
	 * @return string The extracted XML content
	 */
	public function extractXmlFromFileContent(string $fileContent)
	{
		$extractedXml = ZugferdDocumentPdfReaderExt::getInvoiceDocumentContentFromContent($fileContent);
		return $extractedXml;
	}
}
