576 lines
21 KiB
PHP
576 lines
21 KiB
PHP
<?php
|
|
namespace Noalyss\XMLDocument;
|
|
|
|
//use Noalyss\Utility;
|
|
/*
|
|
* This file is part of NOALYSS.
|
|
*
|
|
* NOALYSS 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.
|
|
*
|
|
* NOALYSS 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 NOALYSS; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
// Copyright Author Dany De Bontridder danydb@aevalys.eu 22/10/23
|
|
|
|
/**
|
|
* @file
|
|
* @brief mother for e-invoice : ubl2.1 , Factur-X
|
|
*/
|
|
/**
|
|
* @class XMLInvoice
|
|
* @brief Mother class for e-invoice
|
|
* - $data is
|
|
* * @code
|
|
(
|
|
[id] => 25.822
|
|
[issue_date] => 2025-06-10
|
|
[due_date] =>
|
|
[supplier] => Array
|
|
(
|
|
[name] => My company sprl
|
|
[street] => Allée des Zoulons
|
|
[postalzone] => 1080
|
|
[city] => Molenbeek Saint Jean
|
|
[country] => BE
|
|
[supplier_vat_id] => BE012345678
|
|
[registration_name] => My Company sprl
|
|
)
|
|
|
|
[customer] => Array
|
|
(
|
|
[name] => This asbl
|
|
[street] =>
|
|
[postalzone] =>
|
|
[city] =>
|
|
[country] =>
|
|
[customer_vat_id] => numéro TVA
|
|
[registration_name] => This asbl
|
|
)
|
|
|
|
[currency] => 0
|
|
[info] => Array
|
|
[order] = order reference
|
|
[communication] = communication added to the invoice
|
|
[operation] => Array
|
|
(
|
|
[0] => Array
|
|
(
|
|
[card_id] => 568
|
|
[quantity] => 1.0000
|
|
[price] => 10.0000
|
|
[vat] => 2.1000
|
|
[vat_id] => 1
|
|
[vat_reversed] => 0.0000
|
|
[code_quantity]=> EA
|
|
[vat_code]=> Code VAT for PEPPOL (S,K,...)
|
|
)
|
|
|
|
[1] => Array
|
|
(
|
|
[card_id] => 164
|
|
[quantity] => 5.0000
|
|
[price] => 83.4500
|
|
[vat] => 17.5200
|
|
[vat_id] => 1
|
|
[vat_reversed] => 0.0000
|
|
[code_quantity]=> EA
|
|
[vat_code]=> Code VAT for PEPPOL (S,K,...)
|
|
)
|
|
|
|
[2] => Array
|
|
(
|
|
[card_id] => 483
|
|
[quantity] => 1.0000
|
|
[price] => 72.0000
|
|
[vat] => 15.1200
|
|
[vat_id] => 5
|
|
[vat_reversed] => 15.1200
|
|
[code_quantity]=> EA
|
|
[vat_code]=> Code VAT for PEPPOL (S,K,...)
|
|
)
|
|
|
|
)
|
|
|
|
*
|
|
*
|
|
)
|
|
|
|
|
|
* @endcode
|
|
*
|
|
*/
|
|
abstract class XMLInvoice extends \DOMDocument
|
|
{
|
|
protected $cn; //!< Database conx , current folder
|
|
protected $data; //! $data (Array) data retrieve from DB
|
|
protected $jr_id; //! $jr_id (int) is JRN.JR_ID
|
|
function __construct(\Database $conx)
|
|
{
|
|
parent::__construct("1.0", "UTF-8");
|
|
$this->cn=$conx;
|
|
$this->data=[];
|
|
}
|
|
/**
|
|
* @brief return data
|
|
* @return array
|
|
*/
|
|
public function get_data() {
|
|
return $this->data;
|
|
}
|
|
/**
|
|
* @brief returns data
|
|
* @param $data (array)
|
|
* @return XMLInvoice
|
|
*/
|
|
public function set_data($data): XMLInvoice {
|
|
$this->data = $data;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @brief get Database Connexion
|
|
* @param $cn (\Database)
|
|
* @return XMLInvoice
|
|
*/
|
|
|
|
public function get_db_conx():\Database {
|
|
return $this->cn;
|
|
}
|
|
/**
|
|
* @brief set Database Connexion
|
|
* @param $cn (\Database)
|
|
* @return XMLInvoice
|
|
*/
|
|
public function set_db_conx(\Database $cn): XMLInvoice {
|
|
$this->cn = $cn;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @brief transform an operation ($jr_id) into an array, which contains
|
|
* needed information for making an e-invoice
|
|
* @param $jr_id (int) operation JRN.JR_ID
|
|
* @return array with all info7
|
|
|
|
*
|
|
*/
|
|
function build_data($jr_id):array
|
|
{
|
|
global $g_parameter;
|
|
$this->jr_id=$jr_id;
|
|
$operation = new \Acc_Sold($this->cn,$jr_id);
|
|
|
|
$operation->get();
|
|
$result=array();
|
|
$result["id"]= $operation->det->jr_pj_number;
|
|
$result["issue_date"]=$operation->det->jr_date;
|
|
$result["due_date"]=($operation->det->jr_ech=="")?$operation->det->jr_date:$operation->det->jr_ech;
|
|
// supplier
|
|
$result['supplier']=$this->fill_supplier();
|
|
|
|
|
|
//customer
|
|
$result['customer']=$this->fill_customer($operation->det->array[0]['qs_client']);
|
|
|
|
// currency
|
|
$result['currency']=$this->cn->get_value("select cr_code_iso from currency where id=$1"
|
|
,array($operation->det->currency_id));
|
|
|
|
// document description
|
|
$result['description']=$operation->det->jr_comment;
|
|
|
|
// note if any
|
|
|
|
$result['note']= $result['description'];
|
|
$a_note = $this->cn->get_array("select n_id,n_text from jrn_note where jr_id=$1",[$operation->det->jr_id]);
|
|
$nb_note=count($a_note);
|
|
for ($e=0;$e<$nb_note;$e++)
|
|
{
|
|
|
|
$result['note'].= html_entity_decode($a_note[$e]['n_text']??"");
|
|
}
|
|
// goods and services
|
|
$result['operation']=array();
|
|
|
|
$nb_operation= count($operation->det->array);
|
|
for ($i=0;$i < $nb_operation;$i++) {
|
|
$result['operation'][$i]['card_id']=$operation->det->array[$i]['qs_fiche'];
|
|
$result['operation'][$i]['quantity']=$operation->det->array[$i]['qs_quantite'];
|
|
$card=new \Fiche($this->cn,$operation->det->array[$i]['qs_fiche']);
|
|
$result['operation'][$i]['qcode']=$card->get_attribute(ATTR_DEF_QUICKCODE);
|
|
$result['operation'][$i]['name']=$card->get_attribute(ATTR_DEF_NAME);
|
|
$result['operation'][$i]['description']=($operation->det->array[$i]['j_text']=="")?$card->get_attribute(9):$operation->det->array[$i]['j_text'];
|
|
|
|
// get the type of unity, if not found then it will be EA
|
|
$x= $card->get_attribute(ATTR_DEF_QUANTITY_TYPE,0);
|
|
$result['operation'][$i]['code_quantity']=($x===false||$x=="")?"EA":$x;
|
|
|
|
// $operation->det->currency_id == 0 default currency of the folder
|
|
if ($operation->det->currency_id == 0 ) {
|
|
$result['operation'][$i]['price']=$operation->det->array[$i]['qs_price'];
|
|
$result['operation'][$i]['price_unit']=$operation->det->array[$i]['qs_unit'];
|
|
$result['operation'][$i]['vat']=$operation->det->array[$i]['qs_vat'];
|
|
} else {
|
|
$result['operation'][$i]['price']=$operation->det->array[$i]['oc_amount'];
|
|
$result['operation'][$i]['price_unit']=bcdiv(
|
|
$operation->det->array[$i]['oc_amount'],
|
|
$operation->det->array[$i]['qs_quantite'],
|
|
2);
|
|
$result['operation'][$i]['vat']=$operation->det->array[$i]['oc_vat_amount'];
|
|
|
|
}
|
|
$result['operation'][$i]['vat_id']=$operation->det->array[$i]['qs_vat_code'];
|
|
// // tva code for PEPPOL
|
|
$x=$this->cn->get_row("select tva_peppol_code,tva_rate from tva_rate where tva_id=$1"
|
|
,[ $result['operation'][$i]['vat_id']]);
|
|
$result['operation'][$i]['vat_code']=($x['tva_peppol_code']=="")?"S":$x['tva_peppol_code'];
|
|
$result['operation'][$i]['vat_rate']=$x['tva_rate'];
|
|
|
|
$result['operation'][$i]['vat_reversed']=$operation->det->array[$i]['qs_vat_sided'];
|
|
}
|
|
//------------------------------------------------
|
|
// retrieve order and comment
|
|
//------------------------------------------------
|
|
$a_row=$this->cn->get_array("select id_type,ji_value from jrn_info where jr_id=$1"
|
|
,[$jr_id]);
|
|
$nb_row = count($a_row);
|
|
$result['info']=[];
|
|
$result['info']['order']='NA';
|
|
$result['info']['communication']='';
|
|
for($i=0;$i<$nb_row;$i++) {
|
|
switch ($a_row[$i]['id_type']) {
|
|
case 'BON_COMMANDE':
|
|
$result['info']['order']=$a_row[$i]['ji_value'];
|
|
break;
|
|
case 'OTHER':
|
|
$result['info']['communication']=$a_row[$i]['ji_value'];
|
|
break;
|
|
|
|
}
|
|
}
|
|
$result['info']['communication']=($result['info']['communication']=="")?$result['id']:$result['info']['communication'];
|
|
$result['document']=$this->fill_document($jr_id);
|
|
|
|
/**
|
|
* Compute totals VAT and AMOUNT
|
|
*/
|
|
$nb_operation = count($result['operation']);
|
|
|
|
/// block cac:LegalMonetaryTotal
|
|
$result['LineExtensionAmount']=0;
|
|
$result['TaxExclusiveAmount']=0;
|
|
$result['TaxInclusiveAmount']=0;
|
|
$result['PayableAmount']=0;
|
|
|
|
// block cac:TaxTotal
|
|
$result['TaxableAmount']=0;
|
|
$result['TaxAmount']=0;
|
|
|
|
// array for TaxSubtotal
|
|
$VAT_SubTotal=array();
|
|
$idx_subtotal=0;
|
|
bcscale(2);
|
|
// for each operation
|
|
$VAT_SubTotal=array();
|
|
for ($i=0;$i < $nb_operation;$i++) {
|
|
$acc_tva=\Acc_TVA::build($this->cn,$result['operation'][$i]['vat_id'] );
|
|
$percent = bcmul($acc_tva->tva_rate,100,2);
|
|
$idx=sprintf("%s - %s",$percent,$result['operation'][$i]['vat_code'] );
|
|
// subtotal for VAT
|
|
$n = find_idx($VAT_SubTotal,'idx',$idx);
|
|
if ($n == -1 ) {
|
|
$n=$idx_subtotal;
|
|
$VAT_SubTotal[$idx_subtotal]=array();
|
|
$VAT_SubTotal[$idx_subtotal]['idx']=$idx;
|
|
$VAT_SubTotal[$idx_subtotal]['vat_code']=$result['operation'][$i]['vat_code'] ;
|
|
$VAT_SubTotal[$idx_subtotal]['percent']=$percent;
|
|
$VAT_SubTotal[$idx_subtotal]['vatex']=$acc_tva->vx_code;
|
|
$VAT_SubTotal[$idx_subtotal]['amount']=$VAT_SubTotal[$idx_subtotal]['vat']=0;
|
|
|
|
$idx_subtotal++;
|
|
}
|
|
/**
|
|
* @todo Pour les intracomm , quel taux utilisé ? 0 ou 21%
|
|
*/
|
|
$VAT_SubTotal[$n]['amount']=bcadd($VAT_SubTotal[$n]['amount'],$result['operation'][$i]['price']);
|
|
$VAT_SubTotal[$n]['vat']=bcadd($VAT_SubTotal[$n]['vat'],$result['operation'][$i]['vat']);
|
|
$VAT_SubTotal[$n]['vat']=bcsub($VAT_SubTotal[$n]['vat'],$result['operation'][$i]['vat_reversed']);
|
|
$result['TaxableAmount']=bcadd( $result['TaxableAmount'],$result['operation'][$i]['price']);
|
|
$result['TaxAmount']=bcadd( $result['TaxAmount'],$result['operation'][$i]['vat']);
|
|
$result['TaxAmount']=bcsub( $result['TaxAmount'],$result['operation'][$i]['vat_reversed']);
|
|
$result['operation'][$i]['vat_percent']=$percent;
|
|
}
|
|
$result['subTotalVAT']=$VAT_SubTotal;
|
|
$result['LineExtensionAmount']= $result['TaxableAmount'];
|
|
$result['TaxExclusiveAmount']= $result['TaxableAmount'];
|
|
$result['TaxInclusiveAmount']=bcadd( $result['TaxableAmount'],$result['TaxAmount']);
|
|
$result['PayableAmount']=bcadd( $result['TaxableAmount'],$result['TaxAmount']);
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief make an array of parameter_extra where pe_code as key and pe_value
|
|
* as value
|
|
* @return array keys : pe_code,pe_value
|
|
*/
|
|
function load_noalyss_parameter()
|
|
{
|
|
$r=$this->cn->get_array('
|
|
select pe_code, pe_value from parameter_extra
|
|
union all
|
|
select pr_id,pr_value
|
|
from parameter');
|
|
|
|
return array_column($r,"pe_value","pe_code");
|
|
}
|
|
/**
|
|
* @brief create an XML invoice based on JRN.JR_ID operation.
|
|
* The PDF file could be added afterward.
|
|
* @parameter $jr_id (int) operation JRN.JR_ID operation
|
|
*/
|
|
abstract function make_xml($jr_id);
|
|
/**
|
|
* @brief check that mandatory info are saved in the DB for company (seller)
|
|
*/
|
|
abstract function check_company_data() ;
|
|
/**
|
|
* @brief check that mandatory info are saved in the DB for customer
|
|
*/
|
|
abstract function check_customer_data() ;
|
|
/**
|
|
* @brief create the invoice in the right format, with PDF if any
|
|
* @param $operation_id (int) JRN.JR_ID
|
|
* @return string : XML or PDF format
|
|
*/
|
|
abstract function create_invoice($operation_id) ;
|
|
|
|
/**
|
|
* @brief display_error display a warning with all error
|
|
*/
|
|
public function display_error()
|
|
{
|
|
$a_error=$this->verify();
|
|
include NOALYSS_TEMPLATE."/xmlinvoice-display_error.php";
|
|
|
|
}
|
|
/**
|
|
* @brief check that the VAT is using a PEPPOL Code
|
|
*/
|
|
abstract function check_VAT();
|
|
|
|
/**
|
|
* @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 empty arry : no errors, array with error code
|
|
* @see get_message_error
|
|
*/
|
|
public function verify()
|
|
{
|
|
// verify all VAT
|
|
///@var $a_error : array of error_code see check_company_error
|
|
$a_error = array();
|
|
$a_error['general'] = [];
|
|
$a_error['operation']=[];
|
|
|
|
// verify that all needed data in PARAMETER are valid
|
|
$a_error['company'] = $this->check_company_data();
|
|
$a_error['customer'] = $this->check_customer_data();
|
|
|
|
return $a_error;
|
|
}
|
|
|
|
/**
|
|
* @brief retrieve data from customer and return it into an array
|
|
* @param $card_id (int) FICHE.F_ID
|
|
* @return array keys :
|
|
* - name
|
|
* - ,street
|
|
* - ,postalzone
|
|
* - ,city
|
|
* - ,country
|
|
* - ,customer_vat_id => VAT Number
|
|
* - , registration_name,
|
|
* - card_id
|
|
* - endpoint_id
|
|
* @note :
|
|
* endpoint_id:
|
|
normally it this the VAT number (BE included) and scheme 9925
|
|
or the scheme 00208 VAT number without BE (enterprise number)
|
|
|
|
*/
|
|
function fill_customer($card_id):array
|
|
{
|
|
$customer =new \Fiche($this->cn,$card_id);
|
|
$result=array();
|
|
$result['card_id']=$card_id;
|
|
$result['name']=$customer->get_attribute(ATTR_DEF_NAME,0);
|
|
$result['street']=$customer->get_attribute(ATTR_DEF_ADRESS,0);
|
|
$result['postalzone']=$customer->get_attribute(ATTR_DEF_POSTCODE,0);
|
|
$result['city']=$customer->get_attribute(ATTR_DEF_CITY,0);
|
|
|
|
|
|
// official ID , like VAT
|
|
$result['customer_vat_id']=str_replace([" ",".","-","/"],"" ,$customer->get_attribute(ATTR_DEF_NUMTVA,0));
|
|
// official name of the company
|
|
$result['registration_name']=$customer->get_attribute(ATTR_DEF_NAME,0);
|
|
// find country_code of this card
|
|
$result['country']=$customer->get_attribute(ATTR_DEF_COUNTRY_CODE,0);
|
|
if ( $result['country'] == "")
|
|
{
|
|
$result['country']=substr($result['customer_vat_id'],0,2);
|
|
}
|
|
$result['endpoint_id']=$customer->get_attribute(ATTR_DEF_PEPPOLID,0);
|
|
|
|
|
|
return $result;
|
|
}
|
|
/**
|
|
* @brief complete $this->data from $g_parameter (global variable) for
|
|
* Noalyss_Folder_Parameter
|
|
* @return array keys :
|
|
* - name
|
|
* - ,street
|
|
* - ,postalzone
|
|
* - ,city
|
|
* - ,country
|
|
* - supplier_vat_id => VAT Number avec BE !
|
|
* - registration_name,
|
|
*
|
|
*/
|
|
function fill_supplier():array
|
|
{
|
|
$a_parameter=$this->load_noalyss_parameter();
|
|
$result=array();
|
|
$result['name']=$a_parameter['MY_NAME'];
|
|
$result['street']=$a_parameter['MY_STREET'];
|
|
$result['postalzone']=$a_parameter['MY_POSTCODE'];
|
|
$result['city']=$a_parameter['MY_CITY'];
|
|
$result['country']=$a_parameter['MY_COUNTRY'];
|
|
// official name of the company
|
|
$result['registration_name']=$a_parameter['MY_NAME'];
|
|
// official ID , like VAT
|
|
$result['supplier_vat_id']=str_replace([" ",".","-","/"],"" ,$a_parameter['MY_TVA']);
|
|
/**
|
|
* @TODO vérifier qu'il contient bien BE
|
|
*/
|
|
|
|
$result['COUNTRY_CODE']=$a_parameter['MY_COUNTRY_CODE']?? substr($result['supplier_vat_id'], 0, 2);
|
|
$result['COMPANY_LEGAL_REGISTRATION']=$a_parameter['COMPANY_LEGAL_REGISTRATION']??"";
|
|
$result['COMPANY_LEGAL_ENTITY']=$a_parameter['COMPANY_LEGAL_ENTITY']??"";
|
|
$result['INVOICE_CONTACT_NAME']=$a_parameter['INVOICE_CONTACT_NAME']??"";
|
|
$result['INVOICE_EMAIL_COMPANY']=$a_parameter['INVOICE_EMAIL_COMPANY']??"";
|
|
$result['COMPANY_PEPPOL_ID']=$a_parameter['COMPANY_PEPPOL_ID']??"";
|
|
return $result;
|
|
}
|
|
/**
|
|
* @brief build operation from array
|
|
* key :
|
|
* - [e_march0] => Quick code of the item
|
|
- [e_march0_label] => Label of item
|
|
- [e_march0_price] => Unit Price
|
|
- [e_quant0] => Quantity
|
|
- [htva_march0] => Price w/0 VAT
|
|
- [e_march0_tva_id] => Code VAT
|
|
- [e_march0_tva_amount] => Amount VAT
|
|
- [tva_march0] => Amount VAT (duplicate -> to remove)
|
|
- [tvac_march0] => Total Amount Tax included
|
|
* @param type $a_array
|
|
* @return type
|
|
*/
|
|
function fill_operation_from_array($a_array)
|
|
{
|
|
$result=array();
|
|
$http=new \HttpInput();
|
|
$http->set_array($a_array);
|
|
|
|
$nb_item=$http->get_value("nb_item");
|
|
for ($i=0;$i<$nb_item;$i++)
|
|
{
|
|
if ( $http->get_value("e_march{$i}_tva_id") == "")
|
|
{
|
|
continue;
|
|
}
|
|
$operation=array();
|
|
$card=\Fiche::from_qcode($this->cn,trim($http->get_value("e_march{$i}")));
|
|
$operation['card_id']=$card->id;
|
|
$operation['quantity']=$http->get_value("e_quant{$i}");
|
|
$operation['price']=$http->get_value("e_march{$i}_price");
|
|
$operation['vat']=$http->get_value("tvac_march{$i}");
|
|
$tva= \Acc_Tva::build($this->cn, $http->get_value("e_march{$i}_tva_id"));
|
|
$operation['vat_id']=$tva->tva_id;
|
|
$operation['vat_reversed']=($tva->tva_both_side==1)?$operation['vat']:0;
|
|
$operation['vat_code']=$tva->tva_peppol_code;
|
|
|
|
$operation['code_quantity']=$card->get_attribute(ATTR_DEF_QUANTITY_TYPE,0);
|
|
$operation['code_quantity']=($operation['code_quantity']=="")?"EA":$operation['code_quantity'];
|
|
$result[$i]=$operation;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
/**
|
|
* @brief retrieve additionnal documents but only PDF , not other files
|
|
* @param $jr_id (int) JRN.JR_DEF_ID
|
|
* @return array : empty or keys= (filename,description, log id) from table from JRN_SUP_DOCUMENT
|
|
*/
|
|
public function fill_document($jr_id)
|
|
{
|
|
$result=[];
|
|
$a_document=$this->cn->get_array("select js_id,js_filename,js_description,js_lob from jrn_sup_document where js_mimetype='application/pdf' AND jr_id=$1",
|
|
[$jr_id]);
|
|
if ($a_document == null) return array();
|
|
|
|
foreach ($a_document as $document)
|
|
{
|
|
$result[]=array("filename"=>$document['js_filename']
|
|
,"description"=>$document['js_description']
|
|
,'oid'=>$document['js_lob']
|
|
,'id'=>$document['js_id']
|
|
);
|
|
}
|
|
return $result;
|
|
|
|
}
|
|
|
|
}
|