diff --git a/src/Extension.php b/src/Extension.php new file mode 100644 index 0000000..2f79f70 --- /dev/null +++ b/src/Extension.php @@ -0,0 +1,69 @@ +content; + } + + public function setContent($content): self + { + $this->content = $content; + + return $this; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function setAttributes(array $attributes): self + { + $this->attributes = $attributes; + + return $this; + } + + public function xmlSerialize(Writer $writer): void + { + $writer->write([ + [ + 'name' => Schema::EXT . 'UBLExtension', + 'value' => [ + Schema::EXT . 'ExtensionContent' => $this->content + ], + 'attributes' => $this->attributes + ] + ]); + } + + public static function xmlDeserialize(Reader $reader) + { + $keyValues = keyValue($reader); + $mixedContent = mixedContent($reader); + $collection = new ArrayCollection($mixedContent); + + $extensionContentTag = ReaderHelper::getTag(Schema::EXT . 'ExtensionContent', $collection); + + return (new static()) + ->setContent($keyValues[Schema::EXT . 'ExtensionContent'] ?? null) + ->setAttributes(isset($extensionContentTag, $extensionContentTag['attributes']) ? $extensionContentTag['attributes'] : []) + ; + } +} \ No newline at end of file diff --git a/src/Generator.php b/src/Generator.php index 477c16e..7ecb951 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -8,7 +8,7 @@ class Generator { public static $currencyID; - public static function invoice(Invoice $invoice, $currencyId = 'EUR') + public static function invoice(Invoice $invoice, $currencyId = 'EUR', array $additionalNamespaces = []) { self::$currencyID = $currencyId; @@ -20,6 +20,14 @@ public static function invoice(Invoice $invoice, $currencyId = 'EUR') 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2' => 'cac' ]; + if ($invoice->getExtensions()) { + $xmlService->namespaceMap['urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2'] = 'ext'; + } + + foreach ($additionalNamespaces as $namespace => $prefix) { + $xmlService->namespaceMap[$namespace] = $prefix; + } + return $xmlService->write($invoice->xmlTagName, [ $invoice ]); diff --git a/src/Invoice.php b/src/Invoice.php index 141d9d9..ab8cdcf 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -50,6 +50,8 @@ class Invoice implements XmlSerializable, XmlDeserializable private $despatchDocumentReference; private $receiptDocumentReference; private $originatorDocumentReference; + /** @var Extension[] $extensions */ + private array $extensions = []; /** * @return string @@ -675,6 +677,36 @@ public function setOriginatorDocumentReference( return $this; } + /** + * @param Extension $extension + * @return static + */ + public function addExtension(Extension $extension): self + { + $this->extensions[] = $extension; + + return $this; + } + + /** + * @param Extension[] $extensions + * @return static + */ + public function setExtensions(array $extensions): self + { + $this->extensions = $extensions; + + return $this; + } + + /** + * @return Extension[] + */ + public function getExtensions(): array + { + return $this->extensions; + } + /** * The validate function that is called during xml writing to valid the data of the object. * @@ -729,6 +761,13 @@ public function xmlSerialize(Writer $writer): void { $this->validate(); + if ($this->extensions) { + $writer->write([ + 'name' => Schema::EXT . 'UBLExtensions', + 'value' => $this->extensions + ]); + } + $writer->write([ Schema::CBC . "UBLVersionID" => $this->UBLVersionID, Schema::CBC . "CustomizationID" => $this->customizationID, @@ -963,6 +1002,12 @@ protected static function deserializedTag(array $mixedContent) ); return (new static()) + ->setExtensions( + ReaderHelper::getArrayValue( + Schema::EXT . "UBLExtensions", + $collection, + ), + ) ->setUBLVersionId( ReaderHelper::getTagValue( Schema::CBC . "UBLVersionID", diff --git a/src/Reader.php b/src/Reader.php index 2ec38e4..5417974 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -67,6 +67,7 @@ public static function ubl($currencyId = 'EUR'): Service Schema::CAC. 'TaxScheme' => fn ($reader) => TaxScheme::xmlDeserialize($reader), Schema::CAC. 'TaxSubtotal' => fn ($reader) => TaxSubTotal::xmlDeserialize($reader), Schema::CAC. 'TaxTotal' => fn ($reader) => TaxTotal::xmlDeserialize($reader), + Schema::EXT. 'UBLExtension' => fn ($reader) => Extension::xmlDeserialize($reader), ]; return $xmlService; diff --git a/src/Schema.php b/src/Schema.php index f394c92..8f21424 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -8,4 +8,5 @@ class Schema public const CREDITNOTE = '{urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2}'; public const CBC = '{urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2}'; public const CAC = '{urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2}'; + public const EXT = '{urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2}'; } diff --git a/tests/Write/ExtensionTest.php b/tests/Write/ExtensionTest.php new file mode 100644 index 0000000..9e87252 --- /dev/null +++ b/tests/Write/ExtensionTest.php @@ -0,0 +1,207 @@ +setIdentificationCode('BE'); + + $address = (new \NumNum\UBL\Address()) + ->setStreetName('Korenmarkt') + ->setBuildingNumber(1) + ->setCityName('Gent') + ->setPostalZone('9000') + ->setCountry($country); + + $supplierCompany = (new \NumNum\UBL\Party()) + ->setName('Supplier Company Name') + ->setPhysicalLocation($address) + ->setPostalAddress($address); + + $clientContact = (new \NumNum\UBL\Contact()) + ->setName('Client name') + ->setElectronicMail('email@client.com') + ->setTelephone('0032 472 123 456') + ->setTelefax('0032 9 1234 567'); + + $clientCompany = (new \NumNum\UBL\Party()) + ->setName('My client') + ->setPostalAddress($address) + ->setContact($clientContact); + + $legalMonetaryTotal = (new \NumNum\UBL\LegalMonetaryTotal()) + ->setPayableAmount(10 + 2) + ->setAllowanceTotalAmount(0); + + $taxScheme = (new \NumNum\UBL\TaxScheme()) + ->setId(0); + + $productItem = (new \NumNum\UBL\Item()) + ->setName('Product Name') + ->setDescription('Product Description') + ->setSellersItemIdentification('SELLERID'); + + $price = (new \NumNum\UBL\Price()) + ->setBaseQuantity(1) + ->setUnitCode(\NumNum\UBL\UnitCode::UNIT) + ->setPriceAmount(10); + + $lineTaxTotal = (new \NumNum\UBL\TaxTotal()) + ->setTaxAmount(2.5); + + $invoicePeriod = (new \NumNum\UBL\InvoicePeriod()) + ->setStartDate(new \DateTime()); + + $invoiceLines = []; + + $orderLineReference = (new \NumNum\UBL\OrderLineReference()) + ->setLineId('#ABC123'); + + $invoiceLines[] = (new \NumNum\UBL\InvoiceLine()) + ->setId(0) + ->setItem($productItem) + ->setInvoicePeriod($invoicePeriod) + ->setPrice($price) + ->setTaxTotal($lineTaxTotal) + ->setInvoicedQuantity(1) + ->setOrderLineReference($orderLineReference); + + $invoiceLines[] = (new \NumNum\UBL\InvoiceLine()) + ->setId(0) + ->setItem($productItem) + ->setInvoicePeriod($invoicePeriod) + ->setPrice($price) + ->setAccountingCost('Product 123') + ->setTaxTotal($lineTaxTotal) + ->setInvoicedQuantity(1) + ->setOrderLineReference($orderLineReference); + + $invoiceLines[] = (new \NumNum\UBL\InvoiceLine()) + ->setId(0) + ->setItem($productItem) + ->setInvoicePeriod($invoicePeriod) + ->setPrice($price) + ->setAccountingCostCode('Product 123') + ->setTaxTotal($lineTaxTotal) + ->setInvoicedQuantity(1) + ->setOrderLineReference($orderLineReference); + + $taxCategory = (new \NumNum\UBL\TaxCategory()) + ->setId(0) + ->setName('VAT25%') + ->setPercent(.25) + ->setTaxScheme($taxScheme); + + $taxSubTotal = (new \NumNum\UBL\TaxSubTotal()) + ->setTaxableAmount(10) + ->setTaxAmount(2.5) + ->setTaxCategory($taxCategory); + + $taxTotal = (new \NumNum\UBL\TaxTotal()) + ->addTaxSubTotal($taxSubTotal) + ->setTaxAmount(2.5); + + $accountingSupplierParty = (new \NumNum\UBL\AccountingParty()) + ->setParty($supplierCompany); + + $accountingCustomerParty = (new \NumNum\UBL\AccountingParty()) + ->setSupplierAssignedAccountId('10001') + ->setParty($clientCompany); + + $extension = (new \NumNum\UBL\Extension()) + ->setContent([ + 'hrextac:HRFISK20Data' => [ + 'hrextac:HRTaxTotal' => [ + [ + 'name' => 'cbc:TaxAmount', + 'value' => 2.5, + 'attributes' => [ + 'currencyID' => 'EUR' + ] + ], + 'hrextac:HRTaxSubtotal' => [ + [ + [ + 'name' => 'cbc:TaxableAmount', + 'value' => 10, + 'attributes' => [ + 'currencyID' => 'EUR' + ] + ], + [ + 'name' => 'cbc:TaxAmount', + 'value' => 2.5, + 'attributes' => [ + 'currencyID' => 'EUR' + ] + ], + 'hrextac:HRTaxCategory' => [ + 'cbc:ID' => 'S', + 'cbc:Name' => 'HR:PDV25', + 'cbc:Percent' => '25', + 'hrextac:HRTaxScheme' => [ + 'cbc:ID' => 'VAT' + ] + ] + ] + ] + ], + 'hrextac:HRLegalMonetaryTotal' => [ + [ + 'name' => 'cbc:TaxExclusiveAmount', + 'value' => 10, + 'attributes' => [ + 'currencyID' => 'EUR' + ] + ], + [ + 'name' => 'hrextac:OutOfScopeOfVATAmount', + 'value' => 0, + 'attributes' => [ + 'currencyID' => 'EUR' + ] + ] + ] + ] + ]) + ->setAttributes([ + 'xmlns:cct' => 'urn:un:unece:uncefact:data:specification:CoreComponentTypeSchemaModule:2', + 'xmlns:p3' => 'urn:oasis:names:specification:ubl:schema:xsd:UnqualifiedDataTypes-2', + 'xmlns:xsi' => 'https://www.w3.org/2001/XMLSchema-instance' + ]); + + $invoice = (new \NumNum\UBL\Invoice()) + ->setId(1234) + ->setCopyIndicator(false) + ->setIssueDate(new \DateTime()) + ->setAccountingSupplierParty($accountingSupplierParty) + ->setAccountingCustomerParty($accountingCustomerParty) + ->setInvoiceLines($invoiceLines) + ->setLegalMonetaryTotal($legalMonetaryTotal) + ->setTaxTotal($taxTotal) + ->addExtension($extension); + + $generator = new \NumNum\UBL\Generator(); + $outputXMLString = $generator->invoice($invoice, 'EUR', [ + 'urn:mfin.gov.hr:schema:xsd:HRExtensionAggregateComponents-1' => 'hrextac' + ]); + + $dom = new \DOMDocument(); + $dom->loadXML($outputXMLString); + + $dom->save('./tests/ExtensionTest.xml'); + + $this->assertEquals(true, $dom->schemaValidate($this->schema)); + } +}