[Contenu du PDF encodé en Base64] @endcode * */ class InvoiceUBL21 extends XMLInvoice { const EXTRA_PARAMETER = ["INVOICE_EMAIL_COMPANY" , 'INVOICE_CONTACT_NAME' , 'COMPANY_LEGAL_ENTITY' , 'COMPANY_LEGAL_REGISTRATION' , 'COMPANY_BANK_IBAN' , 'COMPANY_BANK_BIC' , 'MY_COUNTRY_CODE' , 'MY_NAME' , 'MY_STREET' , 'MY_CITY' , 'MY_TVA' , 'INVOICE_EMAIL_COMPANY' ,'COMPANY_PEPPOL_ID' ]; 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 display_error display a warning with all error */ function display_error() { $a_error=$this->verify(); include NOALYSS_TEMPLATE."/xmlinvoice-display_error.php"; } /** * @brief check that the VAT is using a PEPPOL Code */ function check_VAT() { $a_error=array(); $nb_operation=count($this->data['operation']); for ($i=0;$i <$nb_operation;$i++) { if ( $this->data['operation'][$i]['vat_code'] == "" ) { $card=new \Fiche( $this->cn ,$this->data['operation'][$i]['card_id'] ); $tva= \Acc_Tva::build($this->cn, $this->data['operation'][$i]['vat_id']); $a_error[]=sprintf(_("%s : %s code TVA pour PEPPOL non configuré code TVA [ %s %s ]") , $i , $card->get_quick_code() ,$tva->tva_id ,$tva->tva_code ); } elseif (! in_array($this->data['operation'][$i]['vat_code'],array("S","Z"))) { $card=new \Fiche( $this->cn ,$this->data['operation'][$i]['card_id'] ); $tva= \Acc_Tva::build($this->cn, $this->data['operation'][$i]['vat_id']); if ( $tva->vx_code == "") { $a_error[]=sprintf(_("%s : %s code Exemption pour PEPPOL non configuré code TVA [ %s %s ]") , $i , $card->get_quick_code() ,$tva->tva_id ,$tva->tva_code ); } } } return $a_error; } /** * @brief check that mandatory info are saved in the DB for company (seller) * @param $a_error (array) array of errors, empty if nothing found */ function check_company_data() { // var $a_error (array) contains the errors for the company $a_error=array(); $company = $this->load_noalyss_parameter(); foreach (InvoiceUBL21::EXTRA_PARAMETER as $item) { if (!isset($company[$item]) || trim($company[$item]) == '') { $a_error[]=$item; } } /** * check that PEPPOL ID is valid */ if ( isset($company['COMPANY_PEPPOL_ID'])) { if ( strpos($company['COMPANY_PEPPOL_ID'],':') == 0 ) { $a_error[]="COMPANY_PEPPOL_ID"; } } if ( isset ($company['MY_TVA']) && preg_match('/[a-z]{2}/i',$company['MY_TVA']) == false ) { $a_error[]="MY_TVA"; } return $a_error; } /** * @brief check that mandatory info are saved in the DB for customer */ function check_customer_data(){ $a_error=array(); $a_needed=[ATTR_DEF_NAME=>'name' ,ATTR_DEF_ADRESS=>'street' ,ATTR_DEF_POSTCODE=>'postalzone' ,ATTR_DEF_CITY=>'city' ,ATTR_DEF_COUNTRY_CODE=>'country' ,ATTR_DEF_NUMTVA=>'customer_vat_id' ,ATTR_DEF_PEPPOLID=>'endpoint_id' ]; foreach ($a_needed as $item=>$value) { if ( trim($this->data['customer'][$value])=="") { $a_error[]=$value; } } // check if peppol id has the form 9999:9999... if (preg_match('/[0-9]+:[[:alnum:]]+/',$this->data['customer']["endpoint_id"]) == 0) { $a_error[]='endpoint_id'; } if ( $this->data['customer']["customer_vat_id"] != "" && preg_match('/[a-z]{2}/i',$this->data['customer']["customer_vat_id"] ) == false) { $a_error[]='customer_vat_id'; } return $a_error; } /** * @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 * @param type $jr_id * @see XMLInvoice::build_data */ function build_data($jr_id): array { $this->data=parent::build_data($jr_id); return $this->data; } /** * @brief Information customer */ function build_customer() { $customer=$this->createElement('cac:AccountingCustomerParty'); $customer_party=$customer->appendChild($this->createElement('cac:Party')); list($scheme_id,$peppol)=explode( ":",trim($this->data['customer']['endpoint_id'])); $customer_party->appendChild($this->createElement('cbc:EndpointID',$peppol)) ->setAttribute('schemeID', $scheme_id); $party_name=$this->createElement('cac:PartyName'); $party_name->appendChild($this->createElement("cbc:Name", htmlspecialchars($this->data['customer']['name'],ENT_XML1,'UTF-8'))); $customer_party->appendChild($party_name); $postal_address=$customer_party->appendChild($this->createElement('cac:PostalAddress')); $postal_address->appendChild($this->createElement("cbc:StreetName", htmlspecialchars($this->data['customer']['street'],ENT_XML1,'UTF-8'))); $postal_address->appendChild($this->createElement("cbc:CityName", htmlspecialchars($this->data['customer']['city'],ENT_XML1,'UTF-8'))); $postal_address->appendChild($this->createElement("cbc:PostalZone", $this->data['customer']['postalzone'])); ///@todo customer = countryCode doit être dans les paramètres (voir upgrade.sql) $country_code =$this->data['customer']['country']; $country=$postal_address->appendChild($this->createElement("cac:Country")); $country->appendChild($this->createElement('cbc:IdentificationCode',$country_code)); $postal_address->appendChild($country); // Tax Schem $tax=$this->createElement('cac:PartyTaxScheme'); $tax->appendChild($this->createElement('cbc:CompanyID',$this->data["customer"]['customer_vat_id'])); $tax_scheme=$this->createElement('cac:TaxScheme'); $tax_scheme->appendChild($this->createElement('cbc:ID',"VAT")); $tax->appendChild($tax_scheme); // LegalEntity $ple=$this->createElement('cac:PartyLegalEntity'); ///@todo customer = name doit être fiche $ple->appendChild($this->createElement("cbc:RegistrationName", htmlspecialchars($this->data['customer']['name'],ENT_XML1,'UTF-8'))); ///@todo customer_vat_id = numéro de TVA doit être dans fiche $ple->appendChild($this->createElement("cbc:CompanyID", $this->data['customer']['customer_vat_id']??"ERROR")); // assemble supplier $customer_party->appendChild($tax); $customer_party->appendChild($ple); $customer->appendChild($customer_party); return $customer; } /** * @brief Delivery Date is mandatory for INTRACOMM , so by default * we set the invoice date 2026-01-06 */ function build_deliveryDate() { $delivery=$this->createElement("cac:Delivery"); $delivery->appendChild($this->createElement("cbc:ActualDeliveryDate",$this->data['issue_date'])); //(cac:Delivery/cac:DeliveryLocation/cac:Address/cac:Country/cbc:IdentificationCode// $loc= $this->createElement("cac:DeliveryLocation"); $addr=$this->createElement('cac:Address'); $country=$this->createElement("cac:Country"); $country->appendChild($this->createElement("cbc:IdentificationCode", $this->data['customer']['country'])); $addr->appendChild($country); $loc->appendChild($addr); $delivery->appendChild($loc); return $delivery; } /** /** * @brief Build XML Block for payment * @code 30 Invoice 2019000005 BE54000000000097 BPOTBEB1 * @endcode */ function build_paymentInfo() { $company = $this->load_noalyss_parameter(); $payment=$this->createElement("cac:PaymentMeans"); $payment->appendChild($this->createElement('cbc:PaymentMeansCode',30)); ///@note cbc:PaymentID est la communication lors du paiement $payment->appendChild($this->createElement('cbc:PaymentID', htmlspecialchars($this->data["info"]['communication'],ENT_XML1,'UTF-8'))); $f=$this->createElement ('cac:PayeeFinancialAccount'); ///@todo customer = IBAN doit être dans les paramètres (voir upgrade.sql) $f->appendChild($this->createElement("cbc:ID",$company['COMPANY_BANK_IBAN'])); $g=$this->createElement("cac:FinancialInstitutionBranch"); $g->appendChild($this->createElement("cbc:ID", $company['COMPANY_BANK_BIC'])); $f->appendChild($g); $payment->appendChild($f); return $payment; } /** * @brief Information supplier */ function build_supplier() { $company = $this->load_noalyss_parameter(); $supplier=$this->createElement('cac:AccountingSupplierParty'); $supplier_party=$supplier->appendChild($this->createElement('cac:Party')); list($scheme_id,$peppol)=explode( ":",trim($company['COMPANY_PEPPOL_ID'])); $supplier_party->appendChild($this->createElement('cbc:EndpointID',$peppol)) ->setAttribute('schemeID', $scheme_id); //$supplier_party->appendChild($this->createElement('cbc:EndpointID',$this->data["supplier"]['supplier_vat_id']))->setAttribute('schemeID', 9925); $party_name=$this->createElement('cac:PartyName'); $party_name->appendChild($this->createElement('cbc:Name', htmlspecialchars($this->data['supplier']['name'],ENT_XML1,'UTF-8'))); $supplier_party->appendChild($party_name); $postal_address=$supplier_party->appendChild($this->createElement('cac:PostalAddress')); $postal_address->appendChild($this->createElement("cbc:StreetName", htmlspecialchars($this->data['supplier']['street'],ENT_XML1,'UTF-8'))); $postal_address->appendChild($this->createElement("cbc:CityName", htmlspecialchars ($this->data['supplier']['city'],ENT_XML1,'UTF-8'))); $postal_address->appendChild($this->createElement("cbc:PostalZone", $this->data['supplier']['postalzone'])); $country_code = $company['MY_COUNTRY_CODE']; $country=$postal_address->appendChild($this->createElement("cac:Country")); $country->appendChild($this->createElement('cbc:IdentificationCode',$country_code??"ERROR")); $postal_address->appendChild($country); // Tax Schem $tax=$this->createElement('cac:PartyTaxScheme'); $tax->appendChild($this->createElement('cbc:CompanyID',$this->data["supplier"]['supplier_vat_id'])); $tax_scheme=$this->createElement('cac:TaxScheme'); $tax_scheme->appendChild($this->createElement('cbc:ID',"VAT")); $tax->appendChild($tax_scheme); // LegalEntity $ple=$this->createElement('cac:PartyLegalEntity'); $ple->appendChild($this->createElement("cbc:RegistrationName", htmlspecialchars($company['COMPANY_LEGAL_REGISTRATION']??"ERROR",ENT_XML1,'UTF-8'))); $ple->appendChild($this->createElement("cbc:CompanyID", $this->data['supplier']['supplier_vat_id']??"ERROR")); $ple->appendChild($this->createElement("cbc:CompanyLegalForm", $company['COMPANY_LEGAL_ENTITY']??"ERROR")); $contact=$this->createElement('cac:Contact'); $contact->appendChild($this->createElement("cbc:Name",$company['INVOICE_CONTACT_NAME']??"ERROR")); $contact->appendChild($this->createElement("cbc:ElectronicMail",$company['INVOICE_EMAIL_COMPANY']??"ERROR")); // assemble supplier $supplier_party->appendChild($tax); $supplier_party->appendChild($ple); $supplier_party->appendChild($contact); $supplier->appendChild($supplier_party); return $supplier; } /** * @brief TaxTotal Block * @code 516 2400 504 S 21 VAT 200 12 S 6 if ID !=Z and ID != S VAT * @endcode */ function build_taxTotal() { $taxTotal=$this->createElement("cac:TaxTotal"); $taxTotal->appendChild($this->createElement('cbc:TaxAmount',sprintf("%.2f",$this->data['TaxAmount']))) ->setAttribute("currencyID",$this->data['currency']); // for subTotal $subTotal=$this->data['subTotalVAT']; $nb_sub=count($subTotal); for ($i=0;$i<$nb_sub;$i++) { $subTotalXML=$this->createElement("cac:TaxSubtotal"); $subTotalXML->appendChild($this->createElement('cbc:TaxableAmount',sprintf("%.2f",$subTotal[$i]['amount']))) ->setAttribute("currencyID", $this->data['currency']); $subTotalXML->appendChild($this->createElement('cbc:TaxAmount',sprintf("%.2f",$subTotal[$i]['vat']))) ->setAttribute("currencyID",$this->data['currency']); $taxCategory=$this->createElement("cac:TaxCategory"); $taxCategory->appendChild($this->createElement("cbc:ID",$subTotal[$i]['vat_code'])); $taxCategory->appendChild($this->createElement("cbc:Percent",sprintf("%.2f",$subTotal[$i]['percent']))); if ($subTotal[$i]['vat_code'] != "S" && $subTotal[$i]['vat_code'] != "Z") { // if cbc:ID not S and not Z then exemption VAT code is needed $taxCategory->appendChild($this->createElement("cbc:TaxExemptionReasonCode",$subTotal[$i]['vatex'])); } $taxScheme=$this->createElement("cac:TaxScheme"); $taxScheme->appendChild($this->createElement("cbc:ID", "VAT")); $taxCategory->appendChild($taxScheme); $subTotalXML->appendChild($taxCategory); $taxTotal->appendChild($subTotalXML); } return $taxTotal; } /** * @brief legalMonetaryTotal * @code 2600 2600 3116 3116 * @endcode */ function build_legalMonetaryTotal() { $result=$this->createElement('cac:LegalMonetaryTotal' ); $result->appendChild($this->createElement("cbc:LineExtensionAmount",sprintf("%.2f",$this->data['LineExtensionAmount']))) ->setAttribute("currencyID",$this->data['currency']); $result->appendChild($this->createElement("cbc:TaxExclusiveAmount",sprintf("%.2f",$this->data['TaxExclusiveAmount']))) ->setAttribute("currencyID",$this->data['currency']); $result->appendChild($this->createElement("cbc:TaxInclusiveAmount",sprintf("%.2f",$this->data['TaxInclusiveAmount']))) ->setAttribute("currencyID", $this->data['currency'] ); $result->appendChild($this->createElement("cbc:PayableAmount",sprintf("%.2f",$this->data['PayableAmount']))) ->setAttribute("currencyID", $this->data['currency'] ); return $result; } /** * @brief cac:InvoiceLine * @code 2 10 200 Good X S 6 VAT 20 * @endcode * @param $i (int) idx f $this->data->operation[$i] * */ function build_invoiceLine($i) { $result=$this->createElement('cac:InvoiceLine'); $row=$this->data["operation"][$i]; $amount=sprintf("%.2f",$row['price']); $result->appendChild($this->createElement("cbc:ID", $i)); $amount=sprintf("%.2f",$row['price']); if ( $row['quantity'] > 0 && $amount < 0 ) $row['quantity'] =bcsub(0,$row['quantity'] ,4); $result->appendChild( $this->createElement("cbc:InvoicedQuantity", sprintf("%.2f",$row['quantity']))) ->setAttribute("unitCode", $row["code_quantity"]); $result->appendChild($this->createElement("cbc:LineExtensionAmount", $amount)) ->setAttribute("currencyID",$this->data['currency']); // ITEM $item=$this->createElement("cac:Item"); $item->appendChild($this->createElement("cbc:Description",htmlspecialchars($row['description'],ENT_XML1,'UTF-8'))); $item->appendChild($this->createElement("cbc:Name", $row['qcode'])); $classifiedTaxCat=$this->createElement("cac:ClassifiedTaxCategory"); //cbc:ID S = standard rate /// see TVA_RATE.TVA_PEPPOL_CODE & C0TVA $classifiedTaxCat->appendChild($this->createElement("cbc:ID", $row['vat_code'])); $classifiedTaxCat->appendChild($this->createElement("cbc:Percent", sprintf("%.2f",$row['vat_percent']))); $tax_scheme=$this->createElement('cac:TaxScheme'); $tax_scheme->appendChild($this->createElement("cbc:ID", "VAT")); $classifiedTaxCat->appendChild($tax_scheme); $item->appendChild($classifiedTaxCat); $result->appendChild($item); $price=$result->appendChild($this->createElement("cac:Price")); $price->appendChild($this->createElement("cbc:PriceAmount",sprintf("%.4f",abs($row['price_unit'])))) ->setAttribute("currencyID",$this->data['currency']); $result->appendChild($price); return $result; } /** * @brief Insert a PDF in the XML * the document type is not needed for BELGIUM */ /** * @code P01 Facture PDF $base64Pdf REF_ODT_001 Fichier OpenDocument [base64-encodage du fichier] @endcode * @return \DOMElement */ function build_Invoice():\DOMElement { if ( $this->pdf_filename == "") return null; /** $pdf_filename = 'chemin/vers/votre/fichier.pdf';*/ static $i=0; $i++; if ( $this->pdf_filename == null ) { return null; } // Lire le fichier PDF $pdfContent = file_get_contents( $this->pdf_filename ); $result=$this->createElement("cac:AdditionalDocumentReference"); $id=$this->createElement("cbc:ID",$i); $document_description=$this->createElement("cbc:DocumentDescription" , htmlspecialchars($this->data['description'],ENT_XML1,'UTF-8')); // PDF in base64 $base64Pdf = base64_encode($pdfContent); $embeddedDocument=$this->createElement("cbc:EmbeddedDocumentBinaryObject",$base64Pdf); $embeddedDocument->setAttribute("mimeCode", "application/pdf"); $embeddedDocument->setAttribute("filename", "facture.pdf"); $attachment=$this->createElement("cac:Attachment"); $attachment->appendChild($embeddedDocument); $result->appendChild($id); $result->appendChild($document_description); $result->appendChild($attachment); return $result; } /** * @brief Insert a PDF in the XML * the document type is not needed for BELGIUM */ /** * @code P01 Facture PDF $base64Pdf REF_ODT_001 Fichier OpenDocument [base64-encodage du fichier] @endcode * @return \DOMElement */ function include_document($i):\DOMElement { $pdfContent=$this->cn->lo_read($this->data['document'][$i]['oid']); // Lire le fichier PDF $result=$this->createElement("cac:AdditionalDocumentReference"); $id=$this->createElement("cbc:ID",sprintf("SD%d",$i)); $d=( $this->data['document'][$i]['description'] == "")?"NONE":$this->data['document'][$i]['description']; $document_description=$this->createElement("cbc:DocumentDescription" ,htmlspecialchars($d ,ENT_XML1,'UTF-8')); // PDF in base64 $base64Pdf = base64_encode($pdfContent); $embeddedDocument=$this->createElement("cbc:EmbeddedDocumentBinaryObject",$base64Pdf); $embeddedDocument->setAttribute("mimeCode", "application/pdf"); $embeddedDocument->setAttribute("filename", $this->data['document'][$i]['filename']); $attachment=$this->createElement("cac:Attachment"); $attachment->appendChild($embeddedDocument); $result->appendChild($id); $result->appendChild($document_description); $result->appendChild($attachment); return $result; } /** * @brief create an XML invoice(UBL2.1) based on JRN.JR_ID operation * @parameter $jr_id (int) operation JRN.JR_ID operation * @return XML String */ function make_xml($jr_id) { $this->data = $this->build_data($jr_id); // var $company (array) all the parameters of the company $company = $this->load_noalyss_parameter(); // HEADER $root=$this->createElementNS('urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',"Invoice",); $root->setAttributeNS('http://www.w3.org/2000/xmlns/',"xmlns:cac", "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"); $root->setAttributeNS('http://www.w3.org/2000/xmlns/',"xmlns:cbc", "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"); $root->appendChild($this->createElement('cbc:CustomizationID',"urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0")); $root->appendChild($this->createElement('cbc:ProfileID',"urn:fdc:peppol.eu:2017:poacc:billing:01:1.0")); $root->appendChild($this->createElement('cbc:ID',$this->data['id'])); $root->appendChild($this->createElement('cbc:IssueDate',$this->data['issue_date'])); if ($this->data ['due_date'] == '') { $this->data ['due_date']=$this->data['issue_date']; } $root->appendChild($this->createElement('cbc:DueDate',$this->data['due_date'])); $root->appendChild($this->createElement('cbc:InvoiceTypeCode',380)); /** * add note if any */ if ( $this->data['note'] != "") { $root->appendChild($this->createElement("cbc:Note", htmlspecialchars($this->data['note'],ENT_XML1,'UTF-8'))); } $root->appendChild($this->createElement('cbc:DocumentCurrencyCode',$this->data['currency'])); $root->appendChild($this->createElement('cbc:BuyerReference', htmlspecialchars($this->data['info']['order'],ENT_XML1,'UTF-8'))); /** * insert PDF in the XML */ $x = $this->build_Invoice(); if ($x != null ) { $root->appendChild($x); } /** * insert all Additionnal documents if any */ $nb_document=count($this->data['document']); for ($z=0;$z <$nb_document;$z++) { $x= $this->include_document($z); $root->appendChild($x); } // add the supplier $root->appendChild($this->build_supplier()); // add the customer $root->appendChild($this->build_customer()); // add delivery date $root->appendChild($this->build_deliveryDate()); // add the payment if there is a bank account if ( $company['COMPANY_BANK_IBAN'] != "") $root->appendChild($this->build_paymentInfo()); // Add cac:TaxTotal $root->appendChild($this->build_taxTotal()); // Add cac:LegalMonetatyTotal $root->appendChild($this->build_legalMonetaryTotal()); // add all the invoiceline // operation $nb_operation=count($this->data["operation"]); for ($i=0;$i < $nb_operation ; $i++) { $root->appendChild($this->build_invoiceLine($i)); } $this->append($root); $this->formatOutput=true; return $this->saveXML(); } /** * @brief create the invoice in the right format, with PDF if any * @param $operation_id (int) JRN.JR_ID * @return string XML Invoice */ function create_invoice($operation_id) { return $this->make_xml($operation_id); } }