E-INVOICE : add the PDF Into the UBL21

Save the XML Invoice into jrn.jr_document_xml
This commit is contained in:
sparkyx 2025-09-08 20:22:31 +02:00
parent 40839add43
commit 36183ceda0
11 changed files with 406 additions and 53 deletions

View file

@ -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
<cac:Attachment>
<cbc:EmbeddedDocumentBinaryObject mimeCode="application/pdf" filename="facture.pdf" encodingCode="Base64">
@ -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;
}
/**

View file

@ -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()
{
}
}

View file

@ -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;

View file

@ -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
//----------------------------------------

View file

@ -1363,4 +1363,5 @@ class Acc_Fin extends Acc_Detail
return $array;
}
}

View file

@ -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

View file

@ -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 '<h2 class="h-section">'._('Document').' </h2>';
echo $Ledger->doc;
echo $receipt;
}

View file

@ -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];

View file

@ -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;
}

View file

@ -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)';

View file

@ -0,0 +1,125 @@
<?php
/*
*
* 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 2
* 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, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*
* Author : Dany De Bontridder danydb@noalyss.eu
* Copyright (C) 2025 Dany De Bontridder <dany@alchimerys.be>
*
*/
/**
* @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() {
}
}