diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 9c5d4ba..92216e0 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -14,6 +14,7 @@ use Einvoicing\Payments\Payment; use Einvoicing\Payments\Transfer; use UXML\UXML; +use function in_array; class UblWriter extends AbstractWriter { const NS_INVOICE = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"; @@ -168,8 +169,16 @@ public function export(Invoice $invoice): string { // Invoice lines $lines = $invoice->getLines(); - foreach ($lines as $i=>$line) { - $this->addLineNode($xml, $line, $i+1, $invoice); // @phan-suppress-current-line PhanPartialTypeMismatchArgument + $lastGenId = 0; + $usedIds = []; + foreach ($lines as $line) { + $lineId = $line->getId(); + if ($lineId !== null) { + $usedIds[] = $lineId; + } + } + foreach ($lines as $line) { + $this->addLineNode($xml, $line, $invoice, $lastGenId, $usedIds); } return $xml->asXML(); @@ -745,16 +754,23 @@ private function addDocumentTotalsNode(UXML $parent, InvoiceTotals $totals) { /** * Add invoice line - * @param UXML $parent Parent XML element - * @param InvoiceLine $line Invoice line - * @param int $index Invoice line index - * @param Invoice $invoice Invoice instance + * @param UXML $parent Parent XML element + * @param InvoiceLine $line Invoice line + * @param Invoice $invoice Invoice instance + * @param int &$lastGenId Last used auto-generated ID + * @param string[] &$usedIds Used invoice line IDs */ - private function addLineNode(UXML $parent, InvoiceLine $line, int $index, Invoice $invoice) { + private function addLineNode(UXML $parent, InvoiceLine $line, Invoice $invoice, int &$lastGenId, array &$usedIds) { $xml = $parent->add('cac:InvoiceLine'); // BT-126: Invoice line identifier - $xml->add('cbc:ID', $line->getId() ?? (string) $index); + $lineId = $line->getId(); + if ($lineId === null) { + do { + $lineId = (string) ++$lastGenId; + } while (in_array($lineId, $usedIds)); + } + $xml->add('cbc:ID', $lineId); // BT-127: Invoice line note $note = $line->getNote(); diff --git a/tests/Integration/peppol-allowance.xml b/tests/Integration/peppol-allowance.xml index ccec97a..afd415e 100644 --- a/tests/Integration/peppol-allowance.xml +++ b/tests/Integration/peppol-allowance.xml @@ -275,7 +275,7 @@ - 3 + a-custom-identifier Testing note on line level 10 909 diff --git a/tests/Readers/UblReaderTest.php b/tests/Readers/UblReaderTest.php index e97de37..ae1716c 100644 --- a/tests/Readers/UblReaderTest.php +++ b/tests/Readers/UblReaderTest.php @@ -18,6 +18,11 @@ protected function setUp(): void { public function testCanReadInvoice(): void { $invoice = $this->reader->import(file_get_contents(self::DOCUMENT_PATH)); $invoice->validate(); + + $lines = $invoice->getLines(); + $this->assertEquals('1', $lines[0]->getId()); + $this->assertEquals('2', $lines[1]->getId()); + $totals = $invoice->getTotals(); $this->assertEquals(1300, $totals->netAmount); $this->assertEquals(1325, $totals->taxExclusiveAmount); diff --git a/tests/Writers/UblWriterTest.php b/tests/Writers/UblWriterTest.php index fdd9972..2623e10 100644 --- a/tests/Writers/UblWriterTest.php +++ b/tests/Writers/UblWriterTest.php @@ -12,10 +12,12 @@ use Einvoicing\Presets\Peppol; use Einvoicing\Writers\UblWriter; use PHPUnit\Framework\TestCase; +use UXML\UXML; use const CURLOPT_HTTPHEADER; use const CURLOPT_POSTFIELDS; use const CURLOPT_RETURNTRANSFER; use const CURLOPT_URL; +use function array_map; use function curl_close; use function curl_exec; use function curl_init; @@ -79,7 +81,7 @@ private function getSampleInvoice(): Invoice { ->addLine($complexLine) ->addLine((new InvoiceLine)->setName('Line #2')->setPrice(40, 2)->setVatRate(21)->setQuantity(4)) ->addLine((new InvoiceLine)->setName('Line #3')->setPrice(0.56)->setVatRate(10)->setQuantity(2)) - ->addLine((new InvoiceLine)->setId('5')->setName('Line #4')->setPrice(0.56)->setVatRate(10)->setQuantity(2)) + ->addLine((new InvoiceLine)->setName('Line #4')->setPrice(0.56)->setVatRate(10)->setQuantity(2)) ->addAllowance((new AllowanceOrCharge)->setReason('5% discount')->setAmount(5)->markAsPercentage()->setVatRate(21)) ->addAttachment((new Attachment)->setId(new Identifier('INV-123', 'ABT'))) ->addAttachment($externalAttachment) @@ -125,4 +127,27 @@ public function testCanGenerateValidInvoice(): void { $contents = $this->writer->export($invoice); $this->assertTrue($this->validateInvoice($contents, 'ubl')); } + + public function testCanHaveLinesWithForcedDuplicateIdentifiers(): void { + $invoice = $this->getSampleInvoice(); + $invoice->getLines()[1]->setId('DuplicateId'); + $invoice->getLines()[2]->setId('DuplicateId'); + $invoice->getLines()[3]->setId('DuplicateId'); + $xml = UXML::fromString($this->writer->export($invoice)); + $actualLineIds = array_map(function(UXML $item) { + return $item->asText(); + }, $xml->getAll('cac:InvoiceLine/cbc:ID')); + $this->assertEquals(['1', 'DuplicateId', 'DuplicateId', 'DuplicateId'], $actualLineIds); + } + + public function testCanAutogenerateInvoiceLineIdentifiers(): void { + $invoice = $this->getSampleInvoice(); + $invoice->getLines()[1]->setId('1'); + $invoice->getLines()[2]->setId('AnotherCustomId'); + $xml = UXML::fromString($this->writer->export($invoice)); + $actualLineIds = array_map(function(UXML $item) { + return $item->asText(); + }, $xml->getAll('cac:InvoiceLine/cbc:ID')); + $this->assertEquals(['2', '1', 'AnotherCustomId', '3'], $actualLineIds); + } }