From 35d73582b0f3900b8d07e2586e2027c64cd31ef8 Mon Sep 17 00:00:00 2001 From: sparkyx Date: Fri, 7 Nov 2025 18:22:35 +0100 Subject: [PATCH] E-INVOICE extract all documents from XML and store in DB --- .../document_reference_type.class.php | 90 +++++++++++++++++ include/XMLDocument/invoiceubl21.class.php | 10 +- .../XMLDocument/xmlinvoice_reader.class.php | 63 ++++++++---- include/class/acc_document.class.php | 99 ++++++++----------- include/constant.php | 4 +- .../database/jrn_sup_document_sql.class.php | 71 +++++++++++++ sql/upgrade.sql | 47 +++++---- 7 files changed, 288 insertions(+), 96 deletions(-) create mode 100644 include/XMLDocument/document_reference_type.class.php create mode 100644 include/database/jrn_sup_document_sql.class.php diff --git a/include/XMLDocument/document_reference_type.class.php b/include/XMLDocument/document_reference_type.class.php new file mode 100644 index 000000000..855cdde2f --- /dev/null +++ b/include/XMLDocument/document_reference_type.class.php @@ -0,0 +1,90 @@ +filecontent + . ", mimecode=" . $this->mimecode + . ", filename=" . $this->filename + . "]"; + } +} + +class Document_Reference +{ + protected $id; + protected $description; + protected Binary_Object $binary_object; + function __toString(): string + { + return "Document_Reference[id=" . $this->id + . ", description=" . $this->description + . ", binary_object=" . $this->binary_object + . "]"; + } + public function getId() + { + return $this->id; + } + + public function getDescription() + { + return $this->description; + } + + public function getBinary_object(): Binary_Object + { + return $this->binary_object; + } + + public function setId($id) + { + $this->id = $id; + return $this; + } + + public function setDescription($description) + { + $this->description = $description; + return $this; + } + + public function setBinary_object(Binary_Object $binary_object) + { + $this->binary_object = $binary_object; + return $this; + } + + + +} \ No newline at end of file diff --git a/include/XMLDocument/invoiceubl21.class.php b/include/XMLDocument/invoiceubl21.class.php index d97629f20..7a5bd226f 100644 --- a/include/XMLDocument/invoiceubl21.class.php +++ b/include/XMLDocument/invoiceubl21.class.php @@ -86,6 +86,14 @@ class InvoiceUBL21 extends XMLInvoice { /** * check that PEPPOL ID is valid */ + if ( isset($company['COMPANY_UBL_ID'])) + { + if ( strpos($company['COMPANY_UBL_ID'],':') == 0 ) + { + $a_error[]="COMPANY_UBL_ID"; + } + + } return $a_error; } /** @@ -107,7 +115,7 @@ class InvoiceUBL21 extends XMLInvoice { $a_error[]=$value; } } - if ( $this->data['customer'][ATTR_DEF_PEPPOLID] != "") + if ( $this->data['customer']["endpoint_id"] != "") { // check if peppol id has the form 9999:9999... list($scheme_id,$peppol)=explode(":", $this->data['customer'][$value]); diff --git a/include/XMLDocument/xmlinvoice_reader.class.php b/include/XMLDocument/xmlinvoice_reader.class.php index 7d723841e..d7b714777 100644 --- a/include/XMLDocument/xmlinvoice_reader.class.php +++ b/include/XMLDocument/xmlinvoice_reader.class.php @@ -56,6 +56,9 @@ class XMLInvoice_Reader { $this->domDocument = $domDocument; $this->xpath = new \DOMXPath($this->domDocument); + $this->xpath->registerNamespace("ns4", 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'); + $this->xpath->registerNamespace("cac", 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'); + $this->xpath->registerNamespace("cbc", 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'); } /** @@ -136,35 +139,53 @@ class XMLInvoice_Reader /** * @brief returns the embedded document in an array (keys : filecontent (BYTES),mimecode , filename) * or false if there is no document - * @return bool|array keys : filecontent (BYTES),mimecode , filename) - * or false if there is no document + * @return bool|array of array : filecontent (BYTES),mimecode , filename) * @throws \Exception if there are several documents * */ public function get_embedded_document() { - if (($node = $this->get_node("//cac:AdditionalDocumentReference")) == false) - { - return false; - } - if ($node->length > 1) - { - throw new \Exception("XR110 Too many documents found", 110); - } - ///@var DOMNodeList $data //cac:AdditionalDocumentReference[1]/cac:Attachment[1]/cbc:EmbeddedDocumentBinaryObject[1] - if ( - ($data = $this->get_node("//cac:AdditionalDocumentReference[1]/cac:Attachment[1]/cbc:EmbeddedDocumentBinaryObject[1]")) == null + $a_document_reference=array(); + foreach (array( + "AdditionalDocumentReference", + "ReceiptDocumentReference", + "StatementDocumentReference", + "OriginatorDocumentReference", + "ContractDocumentReference" ) + as $document) { + $document=$this->domDocument->getElementsByTagName($document); + if ( $document->length == 0 ) { + continue; + } + for ($e=0;$e < $document->count();$e++) + { + $item=$document->item($e); + + if ( $item->nodeType == XML_ELEMENT_NODE ) + { + $id=$item->getElementsByTagName("ID")[0]?->nodeValue; + $description=$item->getElementsByTagName("DocumentDescription")[0]?->nodeValue; + $embedded_document=$item->getElementsByTagName("EmbeddedDocumentBinaryObject"); + if ( $embedded_document->length == 0) + { + continue; + } + $binary_object=new Binary_Object; + $binary_object->filecontent=base64_decode($embedded_document[0]->nodeValue); + $binary_object->mimecode=$embedded_document[0]->getAttribute("mimeCode"); + $binary_object->filename=$embedded_document[0]->getAttribute("filename"); + $document_reference=new Document_Reference(); + $document_reference->setId($id) + ->setDescription($description) + ->setBinary_object($binary_object); + $a_document_reference[]=clone $document_reference; + } + } - throw new \Exception("X116 Document corrupted", 116); } - $filecontent = base64_decode($data->item(0)->nodeValue); - $mimecode = $data->item(0)->getAttribute("mimeCode"); - $filename = $data->item(0)->getAttribute("filename"); - return array("filecontent" => $filecontent, - "mimecode" => $mimecode, - "filename" => $filename); + return $a_document_reference; } /** @@ -192,6 +213,7 @@ class XMLInvoice_Reader $result['postcode'] = $this->get_node_value('//cac:AccountingCustomerParty[1]/cac:Party[1]/cac:PostalAddress[1]/cbc:PostalZone[1]'); $result['country_code'] = $this->get_node_value("//cac:AccountingCustomerParty[1]/cac:Party[1]/cac:PostalAddress[1]/cac:Country[1]/cbc:IdentificationCode[1]"); $result['company_id'] = $this->get_node_value("//cac:AccountingCustomerParty[1]/cac:Party[1]/cac:PartyTaxScheme[1]/cbc:CompanyID[1]"); + $result['scheme']=$this->get_node("//cac:AccountingCustomerParty[1]/cac:Party[1]/cbc:EndpointID[1]")[0]->getAttribute("schemeID"); return $result; } @@ -209,6 +231,7 @@ class XMLInvoice_Reader $result['postcode'] = $this->get_node_value("//cac:AccountingSupplierParty[1]/cac:Party[1]/cac:PostalAddress[1]/cbc:PostalZone[1]"); $result['country_code'] = $this->get_node_value("//cac:AccountingSupplierParty[1]/cac:Party[1]/cac:PostalAddress[1]/cac:Country[1]/cbc:IdentificationCode[1]"); $result['company_id'] = $this->get_node_value("//cac:AccountingSupplierParty[1]/cac:Party[1]/cac:PartyTaxScheme[1]/cbc:CompanyID[1]"); + $result['scheme']=$this->get_node("//cac:AccountingSupplierParty[1]/cac:Party[1]/cbc:EndpointID[1]")[0]->getAttribute("schemeID"); return $result; } diff --git a/include/class/acc_document.class.php b/include/class/acc_document.class.php index f9ff6ab0d..111df5d5d 100644 --- a/include/class/acc_document.class.php +++ b/include/class/acc_document.class.php @@ -320,70 +320,55 @@ class Acc_Document extends Document { //,mimecode mimetype and filename (string) try { - $embedded_file=$xmlreader->get_embedded_document(); - if ($embedded_file == false) - { - $embedded_file=array(); - // create a PDF with standard information - $pdf=$xmlreader->to_pdf($this->db); - $file_oid=$this->db->lo_write($pdf->Output("S")); - $embedded_file['filename']="invoice.pdf"; - $embedded_file['mimecode']="application/pdf"; - } - else - { - $file_oid=$this->db->lo_write($embedded_file['filecontent']); - if ( $file_oid == false ) - { - // create a PDF with standard information - $pdf=$xmlreader->to_pdf($this->db); - $file_oid=$this->db->lo_write($pdf->Output("S")); - } - } + + // create a PDF with standard information + $pdf=$xmlreader->to_pdf($this->db); + $file_oid=$this->db->lo_write($pdf->Output("S")); + //@var $file_oid OID of the large object saved in DB + $this->d_name="invoice.pdf"; + $this->d_description="Auto generated invoice"; + $this->d_lob=$file_oid; + $this->d_mimetype="application/pdf"; + // save extracted document into DB + $this->db->exec_sql("update jrn set jr_pj=$1 , jr_pj_name=$2, + jr_pj_type=$3 where jr_id=$4", + array( + $this->d_lob + , $this->d_name + , $this->d_description + , $this->d_id + ) + ); + // save all the documents into the DB + // @var embedded_file (array of Noalyss\XML\Document_Reference) + $embedded_file=$xmlreader->get_embedded_document(); + $nb_file = count($embedded_file); + for ($i=0;$i <$nb_file ; $i++) + { + // other documents save in jrn_sup_document + // @var $file (Binary File from XML) + $binary=$embedded_file[$i]->getBinary_object(); + + //@var $sup_oid : oid of the supplemental + $sup_oid=$this->db->lo_write($binary->filecontent); + + $jrn_sup_document=new Jrn_Sup_Document_SQL($this->db); + $jrn_sup_document->jr_id=$this->d_id; + $jrn_sup_document->js_mimetype=$binary->mimecode; + $jrn_sup_document->js_filename=$binary->filename; + $jrn_sup_document->js_lob=$sup_oid; + $jrn_sup_document->js_description=$embedded_file[$i]->getDescription(); + $jrn_sup_document->js_cbc_id=$embedded_file[$i]->getId(); + $jrn_sup_document->save(); + } - $this->d_name=$embedded_file['filename']; - $this->d_description=$embedded_file['filename']; - $this->d_lob=$file_oid; - $this->d_mimetype=$embedded_file['mimecode']; - // save extracted document into DB - $this->db->exec_sql("update jrn set jr_pj=$1 , jr_pj_name=$2, - jr_pj_type=$3 where jr_id=$4", - array( - $this->d_lob - , $this->d_name - , $this->d_description - , $this->d_id - ) - ); - return $file_oid; } catch (\Exception $e ) { \record_log($e); - // if exception is not too many document or document corrupted - // then rethrow the exception - if ( !in_array(e->getCode(),[110,116]) ) - { - throw new \Exception("X281 ",281,$e); - } + throw new \Exception("X281 ",281,$e); } - } - - // save new document - $this->db->exec_sql("update jrn set jr_pj=$1 , jr_pj_name=$2, - jr_pj_type=$3 where jr_id=$4", - array( - $oid - , $_FILES['pj']['name'] - , $_FILES['pj']['type'] - , $this->d_id - ) - ); - $this->d_name=$_FILES['pj']['name']; - $this->d_description=$_FILES['pj']['name']; - $this->d_lob=$oid; - $this->d_mimetype=$_FILES['pj']['type']; $this->db->commit(); return $oid; diff --git a/include/constant.php b/include/constant.php index e9d5c9053..0f37c02b8 100644 --- a/include/constant.php +++ b/include/constant.php @@ -441,7 +441,9 @@ function noalyss_class_autoloader($class) 'noalyss\xmldocument\xmlinvoice_reader'=>'XMLDocument/xmlinvoice_reader.class.php', 'noalyss\mail_parameter'=>'lib/mail_parameter.class.php', 'noalyss\smtpmail'=>'lib/smtpmail.class.php', - 'noalyss\iban_number'=>'lib/iban_number.class.php' + 'noalyss\iban_number'=>'lib/iban_number.class.php', + 'noalyss\xmldocument\document_reference'=>'XMLDocument/document_reference_type.class.php', + 'noalyss\xmldocument\binary_object'=>'XMLDocument/document_reference_type.class.php' ); if (isset ($aClass[$class])) { require_once NOALYSS_INCLUDE . "/" . $aClass[$class]; diff --git a/include/database/jrn_sup_document_sql.class.php b/include/database/jrn_sup_document_sql.class.php new file mode 100644 index 000000000..69c41cec7 --- /dev/null +++ b/include/database/jrn_sup_document_sql.class.php @@ -0,0 +1,71 @@ +table = "public.jrn_sup_document"; + $this->primary_key = "js_id"; + /* + * List of columns + */ + $this->name = array( + "js_id" => "js_id" + , "js_mimetype" => "js_mimetype" + , "js_lob" => "js_lob" + , "jr_id" => "jr_id" + ,'js_filename'=>'js_filename' + ,'js_description'=>'js_description' + ,'js_cbc_id'=>'js_cbc_id' + ); + + /* + * Type of columns + */ + $this->type = array( + "js_id" => "numeric" + , "js_mimetype" => "text" + , "js_lob" => "oid" + , "jr_id" => "numeric" + ,'js_filename'=>'text' + ,'js_description'=>'text' + ,'js_cbc_id'=>'text' + ); + + $this->default = array("js_id" => "auto"); + + $this->date_format = "DD.MM.YYYY"; + parent::__construct($p_cn, $p_id); + } +} diff --git a/sql/upgrade.sql b/sql/upgrade.sql index e004a0162..ccc8c7fe5 100644 --- a/sql/upgrade.sql +++ b/sql/upgrade.sql @@ -233,7 +233,7 @@ insert into parameter_extra(pe_code,pe_label) values ('COMPANY_LEGAL_REGISTRATIO insert into parameter_extra(pe_code,pe_label) values ('COMPANY_LEGAL_ENTITY','Forme légal de la société (SRL,ASBL,AISBL,...') on conflict do nothing; insert into parameter_extra(pe_code,pe_label) values ('COMPANY_BANK_IBAN','Compte en banque (IBAN)') on conflict do nothing; insert into parameter_extra(pe_code,pe_label) values ('COMPANY_BANK_BIC','BIC Bank Identification Code') on conflict do nothing; -insert into parameter_extra(pe_code,pe_label) values ('COMPANY_UBL_ID','ID PEPPOL') on conflict do nothing; +insert into parameter_extra(pe_code,pe_label) values ('COMPANY_UBL_ID','Identifiant PEPPOL') on conflict do nothing; insert into parameter_extra(pe_code,pe_label) values ('SIREN','n° SIREN') on conflict do nothing; insert into parameter_extra(pe_code,pe_label) values ('SIRET','n° SIRET') on conflict do nothing; @@ -438,24 +438,14 @@ insert into jnt_fic_attr (fd_id,ad_id,jnt_order) -- CREATE OR REPLACE FUNCTION comptaproc.update_peppol () RETURNS int2 - /*- SETOF menu_tree : (TABLE FUNCTION) menu_tree is a type of a rowtype create type menu_tree as (code text,description text); - - int2, text,varchar... - - trigger : if called from a trigger - - void : nothing - - boolean - */ AS + AS $BODY$ declare -/* - * Section for variables - */ -n_card_id int; -vat_number text; -record_card RECORD; -result_fct int; -country_code text; + n_card_id int; + vat_number text; + record_card RECORD; + result_fct int; begin -select pi_value into country_code from parameter_internal where pi_id ='COUNTRY_CODE'; result_fct := 0; @@ -478,4 +468,27 @@ end; $BODY$ LANGUAGE plpgsql; -select comptaproc.update_peppol(); \ No newline at end of file +select comptaproc.update_peppol(); + + +CREATE TABLE public.jrn_sup_document ( + js_id int8 GENERATED BY DEFAULT AS IDENTITY( INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1 NO CYCLE) NOT NULL, -- PK + js_mimetype text NOT NULL, -- Mimetype + js_lob oid NOT NULL, -- OID + jr_id int8 NOT NULL, -- FK to jrn + js_filename text not null, + js_description text , + js_cbc_id text, + CONSTRAINT jrn_sup_document_pkey PRIMARY KEY (js_id), + CONSTRAINT jrn_sup_document_jrn_fk FOREIGN KEY (jr_id) REFERENCES public.jrn(jr_id) ON DELETE SET NULL ON UPDATE CASCADE +); +COMMENT ON TABLE public.jrn_sup_document IS 'Supplement document for operation'; + +-- Column comments + +COMMENT ON COLUMN public.jrn_sup_document.js_id IS 'PK'; +COMMENT ON COLUMN public.jrn_sup_document.js_mimetype IS 'Mimetype'; +COMMENT ON COLUMN public.jrn_sup_document.js_lob IS 'OID'; +COMMENT ON COLUMN public.jrn_sup_document.jr_id IS 'FK to jrn'; +COMMENT ON COLUMN public.jrn_sup_document.js_description IS 'Description of document'; +COMMENT ON COLUMN public.jrn_sup_document.js_cbc_id IS 'ID in XML'; \ No newline at end of file