diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..05214d8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing +Thanks for taking your time to contribute to this project! +This document will get you on the right track to help improve eInvoicing. + +## How to Get Started +Use the following sites to get more information about the European electronic invocing specification: + +- [EU e-Invoicing core concepts](https://josemmo.github.io/einvoicing/getting-started/eu-einvoicing-concepts/) +- [Compliance with the European standard on eInvoicing](https://ec.europa.eu/cefdigital/wiki/x/ggTvB) +- [Obtaining a copy of the European standard on eInvoicing](https://ec.europa.eu/cefdigital/wiki/x/kgLvB) +- [UBL Invoice fields](https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/tree/) +- [CEF eInvoicing Validator](https://www.itb.ec.europa.eu/invoice/upload) + +## PR Requirements +Before opening a Pull Request, please make sure your code meets the following requirements: + +### 1. Uses `develop` as the base branch +The main repository branch is only for stable releases. + +### 2. Passes static analysis inspection +``` +vendor/bin/phan --testdox +``` + +### 3. Passes all tests +``` +vendor/bin/simple-phpunit +``` + +### 4. Complies with EN 16931 +Although the most popular European Invoicing CIUS is [PEPPOL BIS Billing 3.0](https://docs.peppol.eu/poacc/billing/3.0/), +the real deal is the "European Standard for Electronic invocing" or EN 16931. + +This means that, while most users will use the [Peppol](src/Presets/Peppol.php) preset for reading and writing invoices, +there are other CIUS/extensions from various member states and business sectors which might not be an exact match to PEPPOL. + +Because the one thing all these specifications have in common is EN 16931, fields and methods you add to the library +must have the same names as they do in the European Standard. diff --git a/src/Invoice.php b/src/Invoice.php index cee62cb..d83da5f 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -10,6 +10,7 @@ use Einvoicing\Traits\BuyerAccountingReferenceTrait; use Einvoicing\Traits\InvoiceValidationTrait; use Einvoicing\Traits\PeriodTrait; +use Einvoicing\Traits\PrecedingInvoiceReferencesTrait; use InvalidArgumentException; use OutOfBoundsException; use function array_splice; @@ -34,6 +35,7 @@ class Invoice { protected $buyerReference = null; protected $purchaseOrderReference = null; protected $salesOrderReference = null; + protected $tenderOrLotReference = null; protected $paidAmount = 0; protected $roundingAmount = 0; protected $seller = null; @@ -48,6 +50,7 @@ class Invoice { use BuyerAccountingReferenceTrait; use PeriodTrait; use InvoiceValidationTrait; + use PrecedingInvoiceReferencesTrait; /** * Invoice constructor @@ -331,6 +334,26 @@ public function setSalesOrderReference(?string $salesOrderReference): self { } + /** + * Get tender or lot reference + * @return string|null Tender or lot reference + */ + public function getTenderOrLotReference(): ?string { + return $this->tenderOrLotReference; + } + + + /** + * Set tender or lot reference + * @param string|null $tenderOrLotReference Tender or lot reference + * @return self Invoice instance + */ + public function setTenderOrLotReference(?string $tenderOrLotReference): self { + $this->tenderOrLotReference = $tenderOrLotReference; + return $this; + } + + /** * Get invoice prepaid amount * NOTE: may be rounded according to the CIUS specification diff --git a/src/InvoiceReference.php b/src/InvoiceReference.php new file mode 100644 index 0000000..782df4b --- /dev/null +++ b/src/InvoiceReference.php @@ -0,0 +1,59 @@ +setValue($value); + $this->setIssueDate($issueDate); + } + + + /** + * Get value + * @return string Value + */ + public function getValue(): string { + return $this->value; + } + + + /** + * Set value + * @param string $value Value + * @return self Invoice reference instance + */ + public function setValue(string $value): self { + $this->value = $value; + return $this; + } + + + /** + * Get issue date + * @return DateTime|null Issue date + */ + public function getIssueDate(): ?DateTime { + return $this->issueDate; + } + + + /** + * Set issue date + * @param DateTime|null $issueDate Issue date + * @return self Invoice reference instance + */ + public function setIssueDate(?DateTime $issueDate): self { + $this->issueDate = $issueDate; + return $this; + } +} diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index 0ec0df5..ce33d32 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -9,6 +9,7 @@ use Einvoicing\Identifier; use Einvoicing\Invoice; use Einvoicing\InvoiceLine; +use Einvoicing\InvoiceReference; use Einvoicing\Party; use Einvoicing\Payments\Card; use Einvoicing\Payments\Mandate; @@ -142,6 +143,26 @@ public function import(string $document): Invoice { $invoice->setSalesOrderReference($salesOrderReferenceNode->asText()); } + // BG-3: Preceding invoice references + foreach ($xml->getAll("{{$cac}}BillingReference/{{$cac}}InvoiceDocumentReference") as $node) { + $invoiceReferenceValueNode = $node->get("{{$cbc}}ID"); + if ($invoiceReferenceValueNode === null) { + continue; + } + $invoiceReference = new InvoiceReference($invoiceReferenceValueNode->asText()); + $invoiceReferenceIssueDateNode = $node->get("{{$cbc}}IssueDate"); + if ($invoiceReferenceIssueDateNode !== null) { + $invoiceReference->setIssueDate(new DateTime($invoiceReferenceIssueDateNode->asText())); + } + $invoice->addPrecedingInvoiceReference($invoiceReference); + } + + // BT-17: Tender or lot reference + $tenderOrLotReferenceNode = $xml->get("{{$cac}}OriginatorDocumentReference/{{$cbc}}ID"); + if ($tenderOrLotReferenceNode !== null) { + $invoice->setTenderOrLotReference($tenderOrLotReferenceNode->asText()); + } + // BG-24: Attachment nodes foreach ($xml->getAll("{{$cac}}AdditionalDocumentReference") as $node) { $invoice->addAttachment($this->parseAttachmentNode($node)); diff --git a/src/Traits/PrecedingInvoiceReferencesTrait.php b/src/Traits/PrecedingInvoiceReferencesTrait.php new file mode 100644 index 0000000..3f17cb6 --- /dev/null +++ b/src/Traits/PrecedingInvoiceReferencesTrait.php @@ -0,0 +1,53 @@ +precedingInvoiceReferences; + } + + + /** + * Add preceding invoice reference + * @param InvoiceReference $reference Preceding invoice reference + * @return self This instance + */ + public function addPrecedingInvoiceReference(InvoiceReference $reference): self { + $this->precedingInvoiceReferences[] = $reference; + return $this; + } + + + /** + * Remove preceding invoice reference + * @param int $index Preceding invoice reference index + * @return self This instance + * @throws OutOfBoundsException if preceding invoice reference index is out of bounds + */ + public function removePrecedingInvoiceReference(int $index): self { + if ($index < 0 || $index >= count($this->precedingInvoiceReferences)) { + throw new OutOfBoundsException('Could not find preceding invoice reference by index'); + } + array_splice($this->precedingInvoiceReferences, $index, 1); + return $this; + } + + + /** + * Clear all preceding invoice references + * @return self This instance + */ + public function clearPrecedingInvoiceReferences(): self { + $this->precedingInvoiceReferences = []; + return $this; + } +} diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 8ee3db4..fb31768 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -96,6 +96,22 @@ public function export(Invoice $invoice): string { // Order reference node $this->addOrderReferenceNode($xml, $invoice); + // BG-3: Preceding invoice reference + foreach ($invoice->getPrecedingInvoiceReferences() as $invoiceReference) { + $invoiceDocumentReferenceNode = $xml->add('cac:BillingReference')->add('cac:InvoiceDocumentReference'); + $invoiceDocumentReferenceNode->add('cbc:ID', $invoiceReference->getValue()); + $invoiceReferenceIssueDate = $invoiceReference->getIssueDate(); + if ($invoiceReferenceIssueDate !== null) { + $invoiceDocumentReferenceNode->add('cbc:IssueDate', $invoiceReferenceIssueDate->format('Y-m-d')); + } + } + + // BT-17: Tender or lot reference + $tenderOrLotReference = $invoice->getTenderOrLotReference(); + if ($tenderOrLotReference !== null) { + $xml->add('cac:OriginatorDocumentReference')->add('cbc:ID', $tenderOrLotReference); + } + // BG-24: Attachments node foreach ($invoice->getAttachments() as $attachment) { $this->addAttachmentNode($xml, $attachment); diff --git a/tests/Integration/peppol-base.xml b/tests/Integration/peppol-base.xml index c52bf23..14a6ed0 100644 --- a/tests/Integration/peppol-base.xml +++ b/tests/Integration/peppol-base.xml @@ -15,6 +15,20 @@ 854777 + + + INV-122 + 2021-09-21 + + + + + INV-123 + + + + PPID-123 + INV-123 130 diff --git a/tests/Readers/UblReaderTest.php b/tests/Readers/UblReaderTest.php index 38435c6..e97de37 100644 --- a/tests/Readers/UblReaderTest.php +++ b/tests/Readers/UblReaderTest.php @@ -27,6 +27,7 @@ public function testCanReadInvoice(): void { $this->assertEquals(1656.25, $totals->payableAmount); $this->assertEquals('S', $totals->vatBreakdown[0]->category); $this->assertEquals(25, $totals->vatBreakdown[0]->rate); + $this->assertEquals('INV-123', $invoice->getPrecedingInvoiceReferences()[0]->getValue()); $this->assertEquals('This is a sample string', $invoice->getAttachments()[0]->getContents()); } } diff --git a/tests/Readers/peppol-example.xml b/tests/Readers/peppol-example.xml index e9de3c0..d56efac 100644 --- a/tests/Readers/peppol-example.xml +++ b/tests/Readers/peppol-example.xml @@ -12,6 +12,11 @@ EUR 4025:123:4343 0150abc + + + INV-123 + + ABC-123 Invoice ABC-123 diff --git a/tests/Writers/UblWriterTest.php b/tests/Writers/UblWriterTest.php index 14783f7..31a73ee 100644 --- a/tests/Writers/UblWriterTest.php +++ b/tests/Writers/UblWriterTest.php @@ -7,6 +7,7 @@ use Einvoicing\Identifier; use Einvoicing\Invoice; use Einvoicing\InvoiceLine; +use Einvoicing\InvoiceReference; use Einvoicing\Party; use Einvoicing\Presets\Peppol; use Einvoicing\Writers\UblWriter; @@ -70,6 +71,8 @@ private function getSampleInvoice(): Invoice { ->setIssueDate(new DateTime('-3 days')) ->setDueDate(new DateTime('+30 days')) ->setBuyerReference('REF-0172637') + ->addPrecedingInvoiceReference(new InvoiceReference('INV-123')) + ->setTenderOrLotReference('PPID-123') ->setSeller($seller) ->setBuyer($buyer) ->addLine($complexLine)