From 36183ceda01d92f15316545b3b2e006255e2ee49 Mon Sep 17 00:00:00 2001 From: sparkyx Date: Mon, 8 Sep 2025 20:22:31 +0200 Subject: [PATCH] E-INVOICE : add the PDF Into the UBL21 Save the XML Invoice into jrn.jr_document_xml --- include/XMLDocument/InvoiceUBL21.php | 74 ++++++++--- include/XMLDocument/XMLInvoice.php | 27 ++++ include/class/acc_document.class.php | 122 +++++++++++++++--- include/class/acc_ledger_sale.class.php | 12 +- include/class/acc_operation.class.php | 1 + include/class/document.class.php | 4 +- include/compta_ven.inc.php | 68 +++++++++- include/constant.php | 5 +- include/lib/ac_common.php | 17 +++ sql/upgrade.sql | 4 + unit-test/include/class/Acc_DocumentTest.php | 125 +++++++++++++++++++ 11 files changed, 406 insertions(+), 53 deletions(-) create mode 100644 unit-test/include/class/Acc_DocumentTest.php diff --git a/include/XMLDocument/InvoiceUBL21.php b/include/XMLDocument/InvoiceUBL21.php index af8edaf62..b607bb1e0 100644 --- a/include/XMLDocument/InvoiceUBL21.php +++ b/include/XMLDocument/InvoiceUBL21.php @@ -23,12 +23,14 @@ namespace Noalyss\XMLDocument; /** * @file - * @brief answer to an inplace object + * @brief UBL2.1 Belgique + * - $pdf_filename PDF file to insert into XML, it is the file on the filesystem */ /** * @class * @brief UBL2.1 Belgique - * @note Doit contenir le PDF + * @note Doit contenir le PDF. + * - $pdf_filename PDF file to insert into XML, it is the file on the filesystem @code @@ -53,12 +55,45 @@ class InvoiceUBL21 extends XMLInvoice { , 'MY_CITY' , 'MY_TVA' ]; - protected $pdf_filename; //!< PDF file to insert into XML + protected $pdf_filename; //!< PDF file to insert into XML, it is the file on the filesystem public function get_pdf_filename() { return $this->pdf_filename; } - + /** + * @brief returns the text of an error + */ + function get_message_error($a_code) + { + + } + /** + * @brief check that all the data are correct + * @returns null : no errors, string separated with comma of error code + * @see get_message_error + * @see InvoiceUBL21::get_message_error() + */ + function verify() + { + ///@var $a_error : array of error_code see check_company_error + $a_error=[]; + // verify all VAT + $x=parent::verify(); + // verify that all needed data in PARAMETER are valid + $this->check_company_data($a_error); + $this->check_customer_data($this->data['customer']['customer_id'], $a_error); + return $a_error; + + } + /** + * @brief set the PDF + * @param $pdf_filename (string) full path to the PDF + * @return $this + * @throws \Exception if the filename doesn't exist + */ public function set_pdf_filename($pdf_filename) { + if ( !file_exists($pdf_filename)) { + throw new \Exception("AD65 $pdf_filename doesn't not exist"); + } $this->pdf_filename = $pdf_filename; return $this; } @@ -87,17 +122,18 @@ class InvoiceUBL21 extends XMLInvoice { */ function check_customer_data($customer_id,&$a_error){ $card=new \Fiche($this->cn,$customer_id); - $a_needed=[ATTR_DEF_NAME=>_("Nom") - ,ATTR_DEF_ADRESS=>_("Adresse") - ,ATTR_DEF_POSTCODE=>_("Code postal") - ,ATTR_DEF_CITY=>_("Localité") - ,ATTR_DEF_COUNTRY_CODE=>_("Code pays") - ,ATTR_DEF_NUMTVA=>_("Numéro de TVA") + $a_needed=[ATTR_DEF_NAME=>'CUST_NAME' + ,ATTR_DEF_ADRESS=>'CUST_ADDR' + ,ATTR_DEF_POSTCODE=>'CUST_POSTCD' + ,ATTR_DEF_CITY=>'CUST_CITY' + ,ATTR_DEF_COUNTRY_CODE=>'CUST_CDCOUNTRY' + ,ATTR_DEF_NUMTVA=>'CUST_VAT' + ,ATTR_DEF_PEPPOLID=>'CUST_PEPPOLID' ]; foreach ($a_needed as $item=>$value) { if (\noalyss_trim($card->get_attribute($item))=="") { - printf (_("ATTENTION donnée manquante dans la fiche client [%s]"),$value); + $a_error[]=$value; } } if (count($a_error) == 0) { @@ -140,8 +176,7 @@ class InvoiceUBL21 extends XMLInvoice { $acc_tva=\Acc_TVA::build($this->cn,$result['operation'][$i]['vat_id'] ); $percent = bcmul($acc_tva->tva_rate,100); // subtotal for VAT - var_dump($VAT_SubTotal); - $n = \Noalyss\Invoicing\Utility::find_idx($VAT_SubTotal,'percent',$percent); + $n = find_idx($VAT_SubTotal,'percent',$percent); if ($n == -1 ) { $n=$idx_subtotal; $VAT_SubTotal[$idx_subtotal]=array(); @@ -417,7 +452,7 @@ class InvoiceUBL21 extends XMLInvoice { $item->appendChild($this->createElement("cbc:Name", $card->get_attribute(ATTR_DEF_NAME))); $classifiedTaxCat=$this->createElement("cac:ClassifiedTaxCategory"); ///@todo cbc:ID S = standard rate et que se passe-t'il pour l'autoliquidation ??? - /// Il faut ajouter dans TVA_RATE , un code pour la TVA, + /// Il faut ajouter dans TVA_RATE , un code pour la TVA, voir TVA_RATE.TVA_PEPPOL_CODE $classifiedTaxCat->appendChild($this->createElement("cbc:ID", "S")); $classifiedTaxCat->appendChild($this->createElement("cbc:Percent", $row['vat_percent'])); $tax_scheme=$this->createElement('cac:TaxScheme'); @@ -465,15 +500,16 @@ class InvoiceUBL21 extends XMLInvoice { function build_Invoice():\DOMElement { if ( $this->pdf_filename == "") return null; - $result=$this->createElement("AdditionalDocumentReference"); /** $pdf_filename = 'chemin/vers/votre/fichier.pdf';*/ - + if ( $this->pdf_filename == null ) { + return null; + } // Lire le fichier PDF - // $pdfContent = file_get_contents($pdfPath); + $pdfContent = file_get_contents( $this->pdf_filename ); // Encoder le PDF en base64 - // $base64Pdf = base64_encode($pdfContent); - + $base64Pdf = base64_encode($pdfContent); + $result=$this->createElement("AdditionalDocumentReference",$base64Pdf); return $result; } /** diff --git a/include/XMLDocument/XMLInvoice.php b/include/XMLDocument/XMLInvoice.php index 0a86c5fdb..b8668b3ae 100644 --- a/include/XMLDocument/XMLInvoice.php +++ b/include/XMLDocument/XMLInvoice.php @@ -255,4 +255,31 @@ abstract class XMLInvoice extends \DOMDocument * @return string : XML or PDF format */ abstract function create_invoice($operation_id) ; + + /** + * @brief thanks MY_INVOICE_FORMAT , create the corresponding object + * - UBL21BEL => InvoiceUBL21 + * - FacturX => FACTURXFR + * @returns null MY_INVOICE_FORMAT is BASIC + */ + static function build_xmlinvoice(\Database $conx) { + global $g_parameter; + if ($g_parameter->MY_INVOICE_FORMAT == 'UBL21BEL') { + return new \Noalyss\XMLDocument\InvoiceUBL21($conx); + } + if ($g_parameter->MY_INVOICE_FORMAT == 'FACTURXFR') { + return new \Noalyss\XMLDocument\FacturX($conx); + } + return null; + } + + /** + * @brief check that all the data are correct + * @returns int 0 : no errors, int separated value + * @see InvoiceUBL21::get_message_error() + */ + function verify() + { + + } } diff --git a/include/class/acc_document.class.php b/include/class/acc_document.class.php index 2f5dca1a9..bf7f51399 100644 --- a/include/class/acc_document.class.php +++ b/include/class/acc_document.class.php @@ -29,10 +29,106 @@ /** * @class * @brief Document used in accountancy : invoice , credit note, ... It is - * a specialization of Document used in Follow-UP + * a specialization of Document used in Follow-UP. + * property : + * - d_name name Receipt number + - d_description Comment of the operation + - d_mimetype mimetype of the document + - d_filename filename */ class Acc_Document extends Document { + ///@var $document_xml (oid) XML Document e-invoice + private $document_xml; + + public function get_document_xml() { + return $this->document_xml; + } + + public function set_document_xml($document_xml) { + $this->document_xml = $document_xml; + return $this; + } + + /** + * @brief constructor + * @param $cn \Database + * @param $jr_id (int) JRN.JRID will be in d_id + */ + function __construct($cn, $jr_id=0) + { + $this->db=$cn; + $this->set_id($jr_id); + // counter for MARCH_NEXT + $this->counter=0; + + } + /** + * @brief set_id fill up d_filename, d_mimetype,d_lob,d_description,jr_pj_number + */ + function set_id($jr_id) { + $this->d_id=$jr_id; + if ( $jr_id == 0 ){ + return $this; + } + $row = $this->db->get_row("select jr_comment + ,jr_pj + ,jr_pj_name + ,jr_pj_type + ,jr_pj_number + ,jr_document_xml + from jrn + where + jr_id=$1", [$this->d_id]); + if ( empty ($row)) { + return $this; + } + $this->d_name=$row['jr_pj_number']; + $this->d_description=$row['jr_comment']; + $this->d_mimetype=$row['jr_pj_type']; + $this->d_filename=$row['jr_pj_name']; + $this->d_lob=$row['jr_pj']; + $this->document_xml=$row['jr_document_xml']; + return $this; + } + /** + * @brief save the file into DB, will create a large object if there + * is no document to replace. It will change the d_filename, d_mimetype + * + * @param $d_filename (string) full path to the file to load into DB + * + * @returns false if d_id = 0 or the file doesn't exist, true for success + */ + function update($filename) { + if ($this->d_id == 0) return false; + if ( ! file_exists($filename)) return false; + $this->db->start(); + $this->d_mimetype= mime_content_type($filename); + $this->d_filename= basename($filename); + if ( $this->d_lob == "") { + $this->db->lo_unlink($this->d_lob); + } + + $this->d_lob=$this->db->lo_import($filename); + $this->db->exec_sql( + "update jrn set jr_pj=$1,jr_pj_name=$2,jr_pj_type=$3 + where jr_id=$4", + [$this->d_lob,$this->d_filename,$this->d_mimetype,$this->d_id] + ); + $this->db->commit(); + return true; + } + /** + * @brief save the Large Object $oid in the column JRN.JR_DOCUMENT_XML + * @param $oid( OID) PostgreSQL Object ID + */ + function update_document_xml($oid){ + $this->db->exec_sql("update jrn set jr_document_xml=$1 where + jr_id=$2",[ + $oid, + $this->d_id + ]); + } /** * @brief create the invoice and saved it as attachment to the * operation, @@ -87,9 +183,10 @@ class Acc_Document extends Document { $this->ag_id = 0; $p_array['e_pj'] = $this->db->get_value("select jr_pj_number from jrn where jr_internal=$1", [$internal]); $filename = ""; + // generate the document and set d_lob,d_mimetype, $this->generate($p_array, $p_array['e_pj']); - // Move the document to accountancy (table JRN) + // Move the document to accountancy (table JRN), $this->moveDocumentACC($internal); // Update the comment with invoice number, if the comment is empty @@ -101,25 +198,16 @@ class Acc_Document extends Document { /** * @brief export the file to the file system and complet $this->d_mimetype, d_filename and - * @param $internal is the internal code JRN.JR_INTERNAL - * @param $destination_file string path - * @return bool false for failure and true for success + * @return bool false for failure and string (the full path_name) for success */ - function export_file($internal, $destination_file) { - - - $row = $this->db->get_row("select jr_pj,jr_pj_name,jr_pj_type - from jrn - where - jr_internal=$1", [$internal]); - if ($row == null) { - \record_log("ACD117. not row found for $internal"); + function export_file($destination_file) { + + if (empty($this->d_filename)) { return false; } - $row = Database::fetch_array($ret, 0); - + $this->db->start(); - if ($this->db->lo_export($row['jr_pj'], $tmp) == false) { + if ($this->db->lo_export($this->d_lob, $destination_file) == false) { record_log("ACD122. cannot export"); $this->db->commit(); return false; diff --git a/include/class/acc_ledger_sale.class.php b/include/class/acc_ledger_sale.class.php index 2f832ef26..ab2f86256 100644 --- a/include/class/acc_ledger_sale.class.php +++ b/include/class/acc_ledger_sale.class.php @@ -698,17 +698,7 @@ class Acc_Ledger_Sale extends Acc_Ledger { where j_id in (select j_id from jrnx where j_grpt=$2)' , array($internal, $seq)); - /* Save the attachment or generate doc */ - if (isset($_FILES['pj'])) { - if (noalyss_strlentrim($_FILES['pj']['name']) != 0) - $this->db->save_receipt($seq); - else - /* Generate an invoice and save it into the database */ - if (isset($_POST['gen_invoice'])) { - $file = $this->create_document($internal, $p_array); - $this->doc=HtmlInput::show_receipt_document($this->jr_id,h($file)); - } - } + //---------------------------------------- // Save the payer //---------------------------------------- diff --git a/include/class/acc_operation.class.php b/include/class/acc_operation.class.php index c5e806437..55ce0161c 100644 --- a/include/class/acc_operation.class.php +++ b/include/class/acc_operation.class.php @@ -1363,4 +1363,5 @@ class Acc_Fin extends Acc_Detail return $array; } + } diff --git a/include/class/document.class.php b/include/class/document.class.php index d59586208..9dc82c7a4 100644 --- a/include/class/document.class.php +++ b/include/class/document.class.php @@ -1891,14 +1891,14 @@ class Document function export_file($p_destination_file) { if ($this->d_id==0) { - return; + return false; } $this->db->start(); $ret=$this->db->exec_sql( "select d_id,d_lob,d_filename,d_mimetype from document where d_id=$1", [$this->d_id]); if (Database::num_row($ret)==0) { - return; + return false; } $row=Database::fetch_array($ret, 0); //the document is saved into file $tmp diff --git a/include/compta_ven.inc.php b/include/compta_ven.inc.php index b4dc5217c..bec99f3f5 100644 --- a/include/compta_ven.inc.php +++ b/include/compta_ven.inc.php @@ -124,7 +124,69 @@ if ( isset($_POST['record']) ) $Ledger=new Acc_Ledger_Sale($cn,$_POST['p_jrn']); try { $internal=$Ledger->insert($_POST); - + + // var $receipt (string) contains the name of the file name of + // the invoice (document created), if empty there + // is no invoice + + $receipt=''; + /* Save the attachment or generate doc */ + if (isset($_FILES['pj'])) { + if (noalyss_strlentrim($_FILES['pj']['name']) != 0) + $cn->save_receipt($seq); + + else + /* Generate an invoice and save it into the database */ + if (isset($_POST['gen_invoice'])) + { + $file = $Ledger->create_document($internal, $_POST); + $receipt= HtmlInput::show_receipt_document($Ledger->jr_id + ,h($file)); + $acc_document=new Acc_Document($cn,$Ledger->jr_id); + //----------------------------------------------------------------- + // Generate a XLM invoice + // if a document has been created create the XML file + //----------------------------------------------------------------- + if ($g_parameter->MY_INVOICE_FORMAT != 'BASIC' && ! empty($acc_document->d_filename )) + { + + $xmldocument= \Noalyss\XMLDocument\XMLInvoice::build_xmlinvoice($cn); + $xmldocument->build_data($Ledger->jr_id); + $code_error = $xmldocument->verify() ; + if ( ! empty( $code_error ) ) { + echo "Impossible de générer facture : code error "; + echo $xmldocument->get_message_error($code_error); + } + $pdf_filename=$acc_document->transform2pdf(); + + // save PDF In db + $acc_document->update($pdf_filename); + + // make the PDF + $xmldocument->set_pdf_filename($pdf_filename); + + // make the XML + PDF + $xml=$xmldocument->make_xml($Ledger->jr_id); + if (DEBUGNOALYSS > 1) { + $uniq= tempnam($_ENV['TMP'], "e-invoice"); + file_put_contents($uniq, $xml); + echo \Noalyss\Dbg::echo_file("file save $uniq"); + } + // save XML string into the DB + $oid=$cn->lo_write($xml); + echo \Noalyss\Dbg::echo_var(1, "oid is $oid"); + if ($oid == false) { + throw new Exception ('CV177 : cannot import e-invoice'); + } + $acc_document->update_document_xml($oid); + + $receipt= HtmlInput::show_receipt_document($Ledger->jr_id,$acc_document->d_filename); + + } + } + } + + } catch (\Exception $e) { if ( $e->getCode()==EXC_BALANCE) @@ -148,10 +210,10 @@ if ( isset($_POST['record']) ) echo $Ledger->confirm($_POST,true); /* Show link for Invoice */ - if (isset ($Ledger->doc) ) + if ($receipt != "") { echo '

'._('Document').'

'; - echo $Ledger->doc; + echo $receipt; } diff --git a/include/constant.php b/include/constant.php index 3c3464076..d5245d262 100644 --- a/include/constant.php +++ b/include/constant.php @@ -427,7 +427,10 @@ function noalyss_class_autoloader($class) 'noalyss\file_cache'=>"lib/file_cache.class.php", "pdfland"=>"class/pdf_land.class.php", "noalyss\widget\widget"=>"widget/widget.php", - "noalyss\otp"=>"lib/otp.class.php" + "noalyss\otp"=>"lib/otp.class.php", + 'noalyss\xmldocument\xmlinvoice'=>'XMLDocument/XMLInvoice.php', + 'noalyss\xmldocument\facturx'=>'XMLDocument/FacturX.php', + 'noalyss\xmldocument\invoiceubl21'=>'XMLDocument/InvoiceUBL21.php' ); if (isset ($aClass[$class])) { require_once NOALYSS_INCLUDE . "/" . $aClass[$class]; diff --git a/include/lib/ac_common.php b/include/lib/ac_common.php index 809af585b..a471ddbe1 100644 --- a/include/lib/ac_common.php +++ b/include/lib/ac_common.php @@ -1877,3 +1877,20 @@ function guidv4($data = null) { // Output the 36 character UUID. return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } +/** + * @brief retrieve the index for the key percent, returns -1 if nothing found + * @param $array (array) SubTotal + * @param $key (string) name of the key + * @param $value (string) value to look for + * @return int + */ +function find_idx($array,$key,$value) { + if ( count($array) == 0 ) { return -1; } + $nb_array=count($array); + for($i=0;$i <$nb_array;$i++) { + if ($array[$i][$key] == $value) { + return $i; + } + } + return -1; +} \ No newline at end of file diff --git a/sql/upgrade.sql b/sql/upgrade.sql index f84d648f5..78b34bdd9 100644 --- a/sql/upgrade.sql +++ b/sql/upgrade.sql @@ -292,3 +292,7 @@ insert into quantity_code_ref values insert into "parameter" values ('MY_INVOICE_FORMAT','BASIC'); + +alter table jrn add jr_document_xml oid; + +comment on column jrn.jr_document_xml is 'OID of the XML files (e-invoice)'; \ No newline at end of file diff --git a/unit-test/include/class/Acc_DocumentTest.php b/unit-test/include/class/Acc_DocumentTest.php new file mode 100644 index 000000000..d6993e460 --- /dev/null +++ b/unit-test/include/class/Acc_DocumentTest.php @@ -0,0 +1,125 @@ + + * + */ + +/** + * @file + * @brief noalyss + */ +use PHPUnit\Framework\TestCase; + +require DIRTEST . '/global.php'; + +/** + * @testdox Class xxx : used for ... + * @backupGlobals enabled + * @coversDefaultClass + */ +class Acc_DocumentTest extends TestCase { + + /** + * @var Fiche + */ + protected $object; + protected $connection; + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test method is executed. + */ + protected function setUp(): void { + + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test method is executed. + */ + protected function tearDown(): void { + /** + * example + * if ( ! is_object($this->object->fiche_def)) return; + * include_once DIRTEST.'/global.php'; + * $g_connection=Dossier::connect(); + * $sql=new ArrayObject(); + * $sql->append("delete from fiche_detail where f_id in (select f_id from fiche where fd_id =\$1 )"); + * $sql->append("delete from fiche where f_id not in (select f_id from fiche_detail where \$1=\$1)"); + * $sql->append("delete from jnt_fic_attr where fd_id = \$1 "); + * $sql->append("delete from fiche_def where fd_id = \$1"); + * foreach ($sql as $s) { + * $g_connection->exec_sql($s,[$this->object->fiche_def->id]); + * } + */ + } + + /** + * the setUpBeforeClass() template methods is called before the first test of the test case + * class is run + */ + public static function setUpBeforeClass(): void { + // include 'global.php'; + } + + /** + * @brief Data simulation + + * @return array + */ + function dataExample() { + return array( + ['0A', 4] + , ['0B', 6] + , [6, 6] + , ['NONE', -1] + , [14, -1] + , [" ", -1] + , [null, -1] + ); + } + + /** + * tearDownAfterClass() template methods is calleafter the last test of the test case class is run, + * + */ + static function tearDownAfterClass(): void { + // include 'global.php'; + } + +// +// public function dataExample() +// { +// return array([1], [2], [3]); +// } + + /** + * @testdox description of the test + * @covers Acc_Balance::summary_add + * @depend Acc_Balance::summary_init + * @backupGlobals enabled + * @dataProvider dataExample + * @global $g_connection + */ + function testFunction() { + + } +}