Skip to content

Commit

Permalink
Fixed auto-generated line ID collision
Browse files Browse the repository at this point in the history
- Updated UblWriter
- Updated unit tests
  • Loading branch information
josemmo committed Nov 16, 2021
1 parent a1000ca commit 9fa01ba
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 10 deletions.
32 changes: 24 additions & 8 deletions src/Writers/UblWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion tests/Integration/peppol-allowance.xml
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>3</cbc:ID>
<cbc:ID>a-custom-identifier</cbc:ID>
<cbc:Note>Testing note on line level</cbc:Note>
<cbc:InvoicedQuantity unitCode="C62">10</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">909</cbc:LineExtensionAmount>
Expand Down
5 changes: 5 additions & 0 deletions tests/Readers/UblReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
27 changes: 26 additions & 1 deletion tests/Writers/UblWriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 9fa01ba

Please sign in to comment.