From 16dd0950ae3a79648949e1f63d9f467e5efa28f0 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Fri, 22 Dec 2023 16:02:08 +0200 Subject: [PATCH 01/33] feat: allow multiple payment means --- .idea/.gitignore | 8 ++++ .idea/einvoicing.iml | 11 ++++++ .idea/modules.xml | 8 ++++ .idea/php.xml | 20 ++++++++++ .idea/vcs.xml | 6 +++ src/Invoice.php | 38 ++++++++++++++---- src/Payments/Payment.php | 21 ---------- src/Payments/PaymentTerms.php | 25 ++++++++++++ src/Presets/Peppol.php | 20 +++++++--- src/Readers/UblReader.php | 55 ++++++++++++++++++--------- src/Traits/InvoiceValidationTrait.php | 50 +++++++++++++++--------- src/Writers/UblWriter.php | 31 ++++++++++++--- tests/Writers/UblWriterTest.php | 2 +- 13 files changed, 218 insertions(+), 77 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/einvoicing.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/php.xml create mode 100644 .idea/vcs.xml create mode 100644 src/Payments/PaymentTerms.php diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/einvoicing.iml b/.idea/einvoicing.iml new file mode 100644 index 0000000..6a66c7f --- /dev/null +++ b/.idea/einvoicing.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5837958 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..0a7086f --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Invoice.php b/src/Invoice.php index 97fc946..996b930 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -4,6 +4,7 @@ use DateTime; use Einvoicing\Models\InvoiceTotals; use Einvoicing\Payments\Payment; +use Einvoicing\Payments\PaymentTerms; use Einvoicing\Presets\AbstractPreset; use Einvoicing\Traits\AllowanceOrChargeTrait; use Einvoicing\Traits\AttachmentsTrait; @@ -262,7 +263,8 @@ class Invoice { protected $buyer = null; protected $payee = null; protected $delivery = null; - protected $payment = null; + protected $payments = []; + protected $paymentTerms = null; protected $lines = []; use AllowanceOrChargeTrait; @@ -819,22 +821,42 @@ public function setDelivery(?Delivery $delivery): self { } + /** + * Get payment terms information + * @return Delivery|null PaymentTerms instance + */ + public function getPaymentTerms(): ?PaymentTerms { + return $this->paymentTerms; + } + + + /** + * Set payment terms information + * @param PaymentTerms|null $paymentTerms PaymentTerms instance + * @return self Invoice instance + */ + public function setPaymentTerms(?PaymentTerms $paymentTerms): self { + $this->paymentTerms = $paymentTerms; + return $this; + } + + /** * Get payment information - * @return Payment|null Payment instance + * @return Payment[] Payment instance */ - public function getPayment(): ?Payment { - return $this->payment; + public function getPayments(): array { + return $this->payments; } /** * Set payment information - * @param Payment|null $payment Payment instance - * @return self Invoice instance + * @param Payment $payment Payment instance + * @return self Invoice instance */ - public function setPayment(?Payment $payment): self { - $this->payment = $payment; + public function addPayment(Payment $payment): self { + $this->payments[] = $payment; return $this; } diff --git a/src/Payments/Payment.php b/src/Payments/Payment.php index 6caa26a..4358b34 100644 --- a/src/Payments/Payment.php +++ b/src/Payments/Payment.php @@ -9,7 +9,6 @@ class Payment { protected $id = null; protected $meansCode = null; protected $meansText = null; - protected $terms = null; protected $transfers = []; protected $card = null; protected $mandate = null; @@ -74,26 +73,6 @@ public function setMeansText(?string $meansText): self { } - /** - * Get payment terms - * @return string|null Payment terms - */ - public function getTerms(): ?string { - return $this->terms; - } - - - /** - * Set payment terms - * @param string|null $terms Payment terms - * @return self Payment instance - */ - public function setTerms(?string $terms): self { - $this->terms = $terms; - return $this; - } - - /** * Get payment transfers * @return Transfer[] Array of transfers diff --git a/src/Payments/PaymentTerms.php b/src/Payments/PaymentTerms.php new file mode 100644 index 0000000..4421b5c --- /dev/null +++ b/src/Payments/PaymentTerms.php @@ -0,0 +1,25 @@ +note; + } + + + /** + * Set note + * @param string|null $id note + * @return self PaymentTerms instance + */ + public function setNote(?string $id): self { + $this->note = $id; + return $this; + } +} diff --git a/src/Presets/Peppol.php b/src/Presets/Peppol.php index 8496ccc..c8be041 100644 --- a/src/Presets/Peppol.php +++ b/src/Presets/Peppol.php @@ -36,16 +36,24 @@ public function getRules(): array { return "A buyer reference or purchase order reference MUST be provided."; }; $res['PEPPOL-EN16931-R061'] = static function(Invoice $inv) { - if ($inv->getPayment() === null) return; - if ($inv->getPayment()->getMandate() === null) return; - if ($inv->getPayment()->getMandate()->getReference() === null) { - return "Mandate reference MUST be provided for direct debit"; + foreach ($inv->getPayments() as $payment) { + if ($payment === null) continue; + if ($payment->getMandate() === null) continue; + if ($payment->getMandate()->getReference() === null) { + return "Mandate reference MUST be provided for direct debit"; + } } + + return; }; $res['BG-17'] = static function(Invoice $inv) { - if ($inv->getPayment() !== null && count($inv->getPayment()->getTransfers()) > 1) { - return "An Invoice shall not have multiple credit transfers"; + foreach ($inv->getPayments() as $payment) { + if ($payment !== null && count($payment->getTransfers()) > 1) { + return "An Invoice shall not have multiple credit transfers"; + } } + + return; }; return $res; diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index 8078d46..869edbe 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -14,6 +14,7 @@ use Einvoicing\Payments\Card; use Einvoicing\Payments\Mandate; use Einvoicing\Payments\Payment; +use Einvoicing\Payments\PaymentTerms; use Einvoicing\Payments\Transfer; use Einvoicing\Traits\VatTrait; use Einvoicing\Writers\UblWriter; @@ -207,9 +208,17 @@ public function import(string $document): Invoice { $invoice->setDelivery($this->parseDeliveryNode($deliveryNode)); } + // Payment term node + $termsNode = $xml->get("{{$cac}}PaymentTerms"); + if ($termsNode !== null) { + $invoice->setPaymentTerms($this->parsePaymentTermsNode($termsNode)); + } + // Payment nodes - $payment = $this->parsePaymentNodes($xml); - $invoice->setPayment($payment); + foreach ($xml->getAll("{{$cac}}PaymentMeans") as $node) { + $payment = $this->parsePaymentNode($node); + $invoice->addPayment($payment); + } // Allowances and charges foreach ($xml->getAll("{{$cac}}AllowanceCharge") as $node) { @@ -489,26 +498,39 @@ private function parseDeliveryNode(UXML $xml): Delivery { return $delivery; } + /** + * Parse payment terms node + * @param UXML $xml XML node + * @return PaymentTerms Delivery instance + */ + private function parsePaymentTermsNode(UXML $xml): PaymentTerms { + $paymentTerms = new PaymentTerms(); + $cbc = UblWriter::NS_CBC; + + // BT-20: Payment terms + $noteNode = $xml->get("{{$cbc}}Note"); + if ($noteNode !== null) { + $paymentTerms->setNote($noteNode->asText()); + } + + return $paymentTerms; + } + /** * Parse payment nodes * @param UXML $xml XML node - * @return Payment|null Payment instance or NULL if not found + * @return Payment Payment instance or NULL if not found */ - private function parsePaymentNodes(UXML $xml): ?Payment { + private function parsePaymentNode(UXML $xml): Payment { $cac = UblWriter::NS_CAC; $cbc = UblWriter::NS_CBC; - // Get root nodes - $meansNode = $xml->get("{{$cac}}PaymentMeans"); - $termsNode = $xml->get("{{$cac}}PaymentTerms/{{$cbc}}Note"); - if ($meansNode === null && $termsNode === null) return null; - $payment = new Payment(); // BT-81: Payment means code // BT-82: Payment means name - $meansCodeNode = $xml->get("{{$cac}}PaymentMeans/{{$cbc}}PaymentMeansCode"); + $meansCodeNode = $xml->get("{{$cbc}}PaymentMeansCode"); if ($meansCodeNode !== null) { $payment->setMeansCode($meansCodeNode->asText()); if ($meansCodeNode->element()->hasAttribute('name')) { @@ -517,34 +539,29 @@ private function parsePaymentNodes(UXML $xml): ?Payment { } // BT-83: Payment ID - $paymentIdNode = $xml->get("{{$cac}}PaymentMeans/{{$cbc}}PaymentID"); + $paymentIdNode = $xml->get("{{$cbc}}PaymentID"); if ($paymentIdNode !== null) { $payment->setId($paymentIdNode->asText()); } // BG-18: Payment card - $cardNode = $xml->get("{{$cac}}PaymentMeans/{{$cac}}CardAccount"); + $cardNode = $xml->get("{{$cac}}CardAccount"); if ($cardNode !== null) { $payment->setCard($this->parsePaymentCardNode($cardNode)); } // BG-17: Payment transfers - $transferNodes = $xml->getAll("{{$cac}}PaymentMeans/{{$cac}}PayeeFinancialAccount"); + $transferNodes = $xml->getAll("{{$cac}}PayeeFinancialAccount"); foreach ($transferNodes as $transferNode) { $payment->addTransfer($this->parsePaymentTransferNode($transferNode)); } // BG-19: Payment mandate - $mandateNode = $xml->get("{{$cac}}PaymentMeans/{{$cac}}PaymentMandate"); + $mandateNode = $xml->get("{{$cac}}PaymentMandate"); if ($mandateNode !== null) { $payment->setMandate($this->parsePaymentMandateNode($mandateNode)); } - // BT-20: Payment terms - if ($termsNode !== null) { - $payment->setTerms($termsNode->asText()); - } - return $payment; } diff --git a/src/Traits/InvoiceValidationTrait.php b/src/Traits/InvoiceValidationTrait.php index 20e0da9..6fa6652 100644 --- a/src/Traits/InvoiceValidationTrait.php +++ b/src/Traits/InvoiceValidationTrait.php @@ -168,26 +168,38 @@ private function getDefaultRules(): array { } }; $res['BR-49'] = static function(Invoice $inv) { - if ($inv->getPayment() !== null && $inv->getPayment()->getMeansCode() === null) { - return "A Payment instruction (BG-16) shall specify the Payment means type code (BT-81)"; + foreach ($inv->getPayments() as $payment) { + if ($payment !== null && $payment->getMeansCode() === null) { + return "A Payment instruction (BG-16) shall specify the Payment means type code (BT-81)"; + } } + + return; }; $res['BR-50'] = static function(Invoice $inv) { - if ($inv->getPayment() === null) return; - foreach ($inv->getPayment()->getTransfers() as $transfer) { - if ($transfer->getAccountId() === null) { - return "A Payment account identifier (BT-84) shall be present if Credit transfer (BG-17) " . - "information is provided in the Invoice"; + foreach ($inv->getPayments() as $payment) { + if ($payment === null) return; + foreach ($payment->getTransfers() as $transfer) { + if ($transfer->getAccountId() === null) { + return "A Payment account identifier (BT-84) shall be present if Credit transfer (BG-17) " . + "information is provided in the Invoice"; + } } } + + return; }; $res['BR-51'] = static function(Invoice $inv) { - if ($inv->getPayment() === null) return; - if ($inv->getPayment()->getCard() === null) return; - if ($inv->getPayment()->getCard()->getPan() === null) { - return "The last 4 to 6 digits of the Payment card primary account number (BT-87) " . - "shall be present if Payment card information (BG-18) is provided in the Invoice"; + foreach ($inv->getPayments() as $payment) { + if ($payment === null) return; + if ($payment->getCard() === null) return; + if ($payment->getCard()->getPan() === null) { + return "The last 4 to 6 digits of the Payment card primary account number (BT-87) " . + "shall be present if Payment card information (BG-18) is provided in the Invoice"; + } } + + return; }; $res['BR-52'] = static function(Invoice $inv) { foreach ($inv->getAttachments() as $attachment) { @@ -197,12 +209,16 @@ private function getDefaultRules(): array { } }; $res['BR-61'] = static function(Invoice $inv) { - if ($inv->getPayment() === null) return; - if (!in_array($inv->getPayment()->getMeansCode(), ['30', '58'])) return; - if (empty($inv->getPayment()->getTransfers())) { - return "If the Payment means type code (BT-81) means SEPA credit transfer, Local credit transfer or " . - "Non-SEPA international credit transfer, the Payment account identifier (BT-84) shall be present"; + foreach ($inv->getPayments() as $payment) { + if ($payment === null) return; + if (!in_array($payment->getMeansCode(), ['30', '58'])) return; + if (empty($payment->getTransfers())) { + return "If the Payment means type code (BT-81) means SEPA credit transfer, Local credit transfer or " . + "Non-SEPA international credit transfer, the Payment account identifier (BT-84) shall be present"; + } } + + return; }; $res['BR-64'] = static function(Invoice $inv) { foreach ($inv->getLines() as $line) { diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 36f1fa5..114d1d0 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -13,6 +13,7 @@ use Einvoicing\Payments\Card; use Einvoicing\Payments\Mandate; use Einvoicing\Payments\Payment; +use Einvoicing\Payments\PaymentTerms; use Einvoicing\Payments\Transfer; use UXML\UXML; use function in_array; @@ -166,11 +167,16 @@ public function export(Invoice $invoice): string { } // Payment nodes - $payment = $invoice->getPayment(); - if ($payment !== null) { + foreach ($invoice->getPayments() as $payment) { $this->addPaymentNodes($xml, $payment, $isCreditNoteProfile ? $dueDate : null); } + // Payment Terms node + $paymentTerms = $invoice->getPaymentTerms(); + if ($paymentTerms !== null) { + $this->addPaymentTermsNodes($xml, $paymentTerms); + } + // Allowances and charges foreach ($invoice->getAllowances() as $item) { $this->addAllowanceOrCharge($xml, $item, false, $invoice, $totals, null); @@ -611,11 +617,26 @@ private function addPaymentNodes(UXML $parent, Payment $payment, ?DateTime $dueD if ($xml->isEmpty()) { $xml->remove(); } + } + + + /** + * Add payment terms nodes + * @param UXML $parent Invoice element + * @param PaymentTerms $paymentTerms Payment instance + */ + private function addPaymentTermsNodes(UXML $parent, PaymentTerms $paymentTerms) { + $xml = $parent->add('cac:PaymentTerms'); // BT-20: Payment terms - $terms = $payment->getTerms(); - if ($terms !== null) { - $parent->add('cac:PaymentTerms')->add('cbc:Note', $terms); + $note = $paymentTerms->getNote(); + if ($note !== null) { + $xml->add('cbc:Note', $note); + } + + // Remove PaymentTerms node if empty + if ($xml->isEmpty()) { + $xml->remove(); } } diff --git a/tests/Writers/UblWriterTest.php b/tests/Writers/UblWriterTest.php index d1ade72..b6eb408 100644 --- a/tests/Writers/UblWriterTest.php +++ b/tests/Writers/UblWriterTest.php @@ -134,7 +134,7 @@ public function testCanGenerateValidInvoice(): void { public function testCanGenerateValidCreditNote(): void { $invoice = $this->getSampleInvoice(); $invoice->setType(Invoice::TYPE_CREDIT_NOTE); - $invoice->setPayment((new Payment)->setMeansCode('10')->setMeansText('In cash')); + $invoice->addPayment((new Payment)->setMeansCode('10')->setMeansText('In cash')); $invoice->validate(); $contents = $this->writer->export($invoice); $this->assertTrue($this->validateInvoice($contents, 'credit')); From 50b1e2e167bf6a6af728b2e74a2874ff0535d150 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Fri, 22 Dec 2023 16:03:11 +0200 Subject: [PATCH 02/33] feat: allow multiple payment means --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dfd6caa..0679353 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor -composer.lock \ No newline at end of file +composer.lock +.idea \ No newline at end of file From 6b1230de68d38b17945c57bde216a1989d8f28d5 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Fri, 22 Dec 2023 16:03:38 +0200 Subject: [PATCH 03/33] feat: allow multiple payment means --- .idea/.gitignore | 8 -------- .idea/einvoicing.iml | 11 ----------- .idea/modules.xml | 8 -------- .idea/php.xml | 20 -------------------- .idea/vcs.xml | 6 ------ 5 files changed, 53 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/einvoicing.iml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/php.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/einvoicing.iml b/.idea/einvoicing.iml deleted file mode 100644 index 6a66c7f..0000000 --- a/.idea/einvoicing.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 5837958..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml deleted file mode 100644 index 0a7086f..0000000 --- a/.idea/php.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 537244c15603ca3d2669fa05bbd6a859a26fd054 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Wed, 17 Jan 2024 17:24:08 +0200 Subject: [PATCH 04/33] Update InvoiceTotals.php --- src/Models/InvoiceTotals.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Models/InvoiceTotals.php b/src/Models/InvoiceTotals.php index 1917db4..73a1589 100644 --- a/src/Models/InvoiceTotals.php +++ b/src/Models/InvoiceTotals.php @@ -99,7 +99,7 @@ static public function fromInvoice(Invoice $inv): InvoiceTotals { // Process all invoice lines foreach ($inv->getLines() as $line) { - $lineNetAmount = $inv->round($line->getNetAmount() ?? 0.0, 'line/netAmount'); + $lineNetAmount = $line->getNetAmount() ?? 0.0; $totals->netAmount += $lineNetAmount; self::updateVatMap($vatMap, $line, $lineNetAmount); } @@ -123,8 +123,9 @@ static public function fromInvoice(Invoice $inv): InvoiceTotals { // Calculate VAT amounts foreach ($vatMap as $item) { - $item->taxableAmount = $inv->round($item->taxableAmount, 'invoice/allowancesChargesAmount'); - $item->taxAmount = $inv->round($item->taxableAmount * ($item->rate / 100), 'invoice/vatAmount'); + $taxAmount = $inv->round($item->taxableAmount, 'line/taxAmount'); + $item->taxableAmount = $inv->round($item->taxableAmount, 'line/taxableAmount'); + $item->taxAmount = $inv->round($taxAmount * ($item->rate / 100), 'invoice/vatAmount'); $totals->vatAmount += $item->taxAmount; } $totals->vatAmount = $inv->round($totals->vatAmount, 'invoice/vatAmount'); From 94f2694ae1be6c43c8712e6b746688b57590e97b Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Wed, 17 Jan 2024 17:30:43 +0200 Subject: [PATCH 05/33] fix static analysis --- src/Invoice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Invoice.php b/src/Invoice.php index 996b930..cb79f63 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -823,7 +823,7 @@ public function setDelivery(?Delivery $delivery): self { /** * Get payment terms information - * @return Delivery|null PaymentTerms instance + * @return PaymentTerms|null PaymentTerms instance */ public function getPaymentTerms(): ?PaymentTerms { return $this->paymentTerms; From 659d553d5528b1bcd4833e45f75c1d1acd1a9a94 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 29 Jan 2024 12:04:39 +0200 Subject: [PATCH 06/33] fix ubl reader price --- src/Readers/UblReader.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index 869edbe..cb90906 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -829,11 +829,11 @@ private function parseInvoiceLine(UXML $xml, array &$taxExemptions): InvoiceLine $line->addClassificationIdentifier($this->parseIdentifierNode($classNode, 'listID')); } - // Price amount - $priceNode = $xml->get("{{$cac}}Price/{{$cbc}}PriceAmount"); - if ($priceNode !== null) { - $line->setPrice((float) $priceNode->asText()); - } +// // Price amount +// $priceNode = $xml->get("{{$cac}}Price/{{$cbc}}PriceAmount"); +// if ($priceNode !== null) { +// $line->setPrice((float) $priceNode->asText()); +// } // Base quantity $baseQuantityNode = $xml->get("{{$cac}}Price/{{$cbc}}BaseQuantity"); @@ -841,6 +841,12 @@ private function parseInvoiceLine(UXML $xml, array &$taxExemptions): InvoiceLine $line->setBaseQuantity((float) $baseQuantityNode->asText()); } + // Line Extension Amount + $lineExtensionAmountNode = $xml->get("{{$cbc}}LineExtensionAmount"); + if ($lineExtensionAmountNode !== null) { + $line->setPrice(((float) $lineExtensionAmountNode->asText() / $line->getQuantity()) * $line->getBaseQuantity()); + } + // VAT attributes $vatNode = $xml->get("{{$cac}}Item/{{$cac}}ClassifiedTaxCategory"); if ($vatNode !== null) { From b4a902165a84f6eb0a87aaabfe55282e30061839 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 29 Jan 2024 12:07:49 +0200 Subject: [PATCH 07/33] fix php stan --- src/Invoice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Invoice.php b/src/Invoice.php index 996b930..cb79f63 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -823,7 +823,7 @@ public function setDelivery(?Delivery $delivery): self { /** * Get payment terms information - * @return Delivery|null PaymentTerms instance + * @return PaymentTerms|null PaymentTerms instance */ public function getPaymentTerms(): ?PaymentTerms { return $this->paymentTerms; From 0611ddb5d860f16156b0c1249002e8c0a201b702 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 29 Jan 2024 13:04:11 +0200 Subject: [PATCH 08/33] fix price reader --- src/Readers/UblReader.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index cb90906..fdd9582 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -844,7 +844,18 @@ private function parseInvoiceLine(UXML $xml, array &$taxExemptions): InvoiceLine // Line Extension Amount $lineExtensionAmountNode = $xml->get("{{$cbc}}LineExtensionAmount"); if ($lineExtensionAmountNode !== null) { - $line->setPrice(((float) $lineExtensionAmountNode->asText() / $line->getQuantity()) * $line->getBaseQuantity()); + $basePrice = (float) $lineExtensionAmountNode->asText(); + + foreach ($xml->getAll("{{$cac}}AllowanceCharge") as $allowanceChargeNode) { + $chargeIndicatorNode = $allowanceChargeNode->get("{{$cbc}}ChargeIndicator"); + if ($chargeIndicatorNode !== null && $chargeIndicatorNode->asText() === "true") { + $basePrice -= (float) $allowanceChargeNode->get("{{$cbc}}Amount")->asText(); + } else { + $basePrice += (float) $allowanceChargeNode->get("{{$cbc}}Amount")->asText(); + } + } + + $line->setPrice(($basePrice / $line->getQuantity()) * $line->getBaseQuantity()); } // VAT attributes From 23ef35d83d13c63ba971794ca97b6c0f109700ba Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 29 Jan 2024 13:06:30 +0200 Subject: [PATCH 09/33] fix price reader --- src/Readers/UblReader.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index fdd9582..1259c65 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -847,11 +847,17 @@ private function parseInvoiceLine(UXML $xml, array &$taxExemptions): InvoiceLine $basePrice = (float) $lineExtensionAmountNode->asText(); foreach ($xml->getAll("{{$cac}}AllowanceCharge") as $allowanceChargeNode) { + $amountNode = $allowanceChargeNode->get("{{$cbc}}Amount"); + + if ($amountNode === null) { + continue; + } + $chargeIndicatorNode = $allowanceChargeNode->get("{{$cbc}}ChargeIndicator"); if ($chargeIndicatorNode !== null && $chargeIndicatorNode->asText() === "true") { - $basePrice -= (float) $allowanceChargeNode->get("{{$cbc}}Amount")->asText(); + $basePrice -= (float) $amountNode->asText(); } else { - $basePrice += (float) $allowanceChargeNode->get("{{$cbc}}Amount")->asText(); + $basePrice += (float) $amountNode->asText(); } } From 29c4f9a0efa8075fa358d2122582aab855869e7f Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 29 Jan 2024 13:50:50 +0200 Subject: [PATCH 10/33] fix: division by 0 --- src/Readers/UblReader.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index 1259c65..7e5e112 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -843,7 +843,9 @@ private function parseInvoiceLine(UXML $xml, array &$taxExemptions): InvoiceLine // Line Extension Amount $lineExtensionAmountNode = $xml->get("{{$cbc}}LineExtensionAmount"); - if ($lineExtensionAmountNode !== null) { + if ($lineExtensionAmountNode === null || $line->getQuantity() === 0.0) { + $line->setPrice(0.0); + } elseif ($lineExtensionAmountNode !== null) { $basePrice = (float) $lineExtensionAmountNode->asText(); foreach ($xml->getAll("{{$cac}}AllowanceCharge") as $allowanceChargeNode) { From aa6b32629509e25eb678e34e2dfa022b34aaae0a Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 4 Mar 2024 21:36:22 +0200 Subject: [PATCH 11/33] conditional-vat-number-tax-scheme --- .github/workflows/ci.yml | 2 +- src/Writers/UblWriter.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56447e6..eab1d6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] + php-version: ['8.2', '8.3'] include: - php-version: '8.2' deploy: ${{ github.ref == 'refs/heads/master' }} diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 114d1d0..55ec487 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -437,7 +437,9 @@ private function addSellerOrBuyerNode(UXML $parent, Party $party) { if ($vatNumber !== null) { $taxNode = $xml->add('cac:PartyTaxScheme'); $taxNode->add('cbc:CompanyID', $vatNumber); - $taxNode->add('cac:TaxScheme')->add('cbc:ID', 'VAT'); + if (! is_numeric(substr($vatNumber, 0, 2))) { + $taxNode->add('cac:TaxScheme')->add('cbc:ID', 'VAT'); + } } // Tax registration identifier From fbb31aa2e54a3c23c7cb1889e49e2d92845cb703 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 4 Mar 2024 21:37:36 +0200 Subject: [PATCH 12/33] conditional-vat-number-tax-scheme --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eab1d6f..e781bc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -on: [push, pull_request] +on: [ pull_request] jobs: ci: From 7abb3581e0bb49c1404c267aeba91019a85a2168 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 4 Mar 2024 22:09:38 +0200 Subject: [PATCH 13/33] rollback --- src/Writers/UblWriter.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 55ec487..1f9a7ec 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -436,10 +436,7 @@ private function addSellerOrBuyerNode(UXML $parent, Party $party) { $vatNumber = $party->getVatNumber(); if ($vatNumber !== null) { $taxNode = $xml->add('cac:PartyTaxScheme'); - $taxNode->add('cbc:CompanyID', $vatNumber); - if (! is_numeric(substr($vatNumber, 0, 2))) { - $taxNode->add('cac:TaxScheme')->add('cbc:ID', 'VAT'); - } + $taxNode->add('cac:TaxScheme')->add('cbc:ID', 'VAT'); } // Tax registration identifier From 597d1cbcb260454a5e8849cff9956762609d3aec Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 4 Mar 2024 22:10:14 +0200 Subject: [PATCH 14/33] rollback --- src/Writers/UblWriter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 1f9a7ec..114d1d0 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -436,6 +436,7 @@ private function addSellerOrBuyerNode(UXML $parent, Party $party) { $vatNumber = $party->getVatNumber(); if ($vatNumber !== null) { $taxNode = $xml->add('cac:PartyTaxScheme'); + $taxNode->add('cbc:CompanyID', $vatNumber); $taxNode->add('cac:TaxScheme')->add('cbc:ID', 'VAT'); } From 7ae67d085ef755c1affabac334f5ec4365c6ca8a Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Fri, 15 Mar 2024 09:59:28 +0200 Subject: [PATCH 15/33] add two ways for calculation totals --- src/Models/InvoiceTotals.php | 67 +++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/src/Models/InvoiceTotals.php b/src/Models/InvoiceTotals.php index 73a1589..6806b2e 100644 --- a/src/Models/InvoiceTotals.php +++ b/src/Models/InvoiceTotals.php @@ -1,95 +1,113 @@ getLines() as $line) { - $lineNetAmount = $line->getNetAmount() ?? 0.0; + $lineNetAmount = $inv->round($line->getNetAmount() ?? 0.0, 'line/netAmount'); $totals->netAmount += $lineNetAmount; - self::updateVatMap($vatMap, $line, $lineNetAmount); + self::updateVatMap($vatMap, $line, $line->getNetAmount() ?? 0.0); } $totals->netAmount = $inv->round($totals->netAmount, 'invoice/netAmount'); @@ -109,7 +127,7 @@ static public function fromInvoice(Invoice $inv): InvoiceTotals { foreach ($inv->getAllowances() as $item) { $allowanceAmount = $inv->round($item->getEffectiveAmount($totals->netAmount), 'line/allowanceChargeAmount'); $totals->allowancesAmount += $allowanceAmount; - self::updateVatMap($vatMap, $item, -$allowanceAmount); + self::updateVatMap($vatMap, $item, -$item->getEffectiveAmount($totals->netAmount)); } $totals->allowancesAmount = $inv->round($totals->allowancesAmount, 'invoice/allowancesChargesAmount'); @@ -117,15 +135,18 @@ static public function fromInvoice(Invoice $inv): InvoiceTotals { foreach ($inv->getCharges() as $item) { $chargeAmount = $inv->round($item->getEffectiveAmount($totals->netAmount), 'line/allowanceChargeAmount'); $totals->chargesAmount += $chargeAmount; - self::updateVatMap($vatMap, $item, $chargeAmount); + self::updateVatMap($vatMap, $item, $item->getEffectiveAmount($totals->netAmount)); } $totals->chargesAmount = $inv->round($totals->chargesAmount, 'invoice/allowancesChargesAmount'); // Calculate VAT amounts foreach ($vatMap as $item) { - $taxAmount = $inv->round($item->taxableAmount, 'line/taxAmount'); - $item->taxableAmount = $inv->round($item->taxableAmount, 'line/taxableAmount'); - $item->taxAmount = $inv->round($taxAmount * ($item->rate / 100), 'invoice/vatAmount'); + if (false) { + $item->taxableAmount = $inv->round($item->taxableAmount, 'line/taxableAmount'); + $item->taxAmount = $inv->round($item->taxableAmount * ($item->rate / 100), 'line/vatAmount'); + } else { + } + $totals->vatAmount += $item->taxAmount; } $totals->vatAmount = $inv->round($totals->vatAmount, 'invoice/vatAmount'); @@ -158,21 +179,22 @@ static public function fromInvoice(Invoice $inv): InvoiceTotals { return $totals; } - /** * Update VAT map - * @param VatBreakdown[string] &$vatMap VAT map reference - * @param VatTrait $item Item instance - * @param float|null $rate VAT rate - * @param float $addTaxableAmount Taxable amount to add + * + * @param VatBreakdown[string] &$vatMap VAT map reference + * @param VatTrait $item Item instance + * @param float|null $rate VAT rate + * @param float $addTaxableAmount Taxable amount to add */ - static private function updateVatMap(array &$vatMap, $item, float $addTaxableAmount) { + private static function updateVatMap(array &$vatMap, $item, float $addTaxableAmount) + { $category = $item->getVatCategory(); $rate = $item->getVatRate(); $key = "$category:$rate"; // Initialize VAT breakdown - if (!isset($vatMap[$key])) { + if (! isset($vatMap[$key])) { $vatMap[$key] = new VatBreakdown(); $vatMap[$key]->category = $category; $vatMap[$key]->rate = $rate; @@ -189,6 +211,11 @@ static private function updateVatMap(array &$vatMap, $item, float $addTaxableAmo } // Increase taxable amount - $vatMap[$key]->taxableAmount += $addTaxableAmount; + if (false) { + $vatMap[$key]->taxableAmount += $addTaxableAmount; + } else { + $vatMap[$key]->taxableAmount += round($addTaxableAmount, 2); + $vatMap[$key]->taxAmount += round($addTaxableAmount * ($rate / 100), 2); + } } } From ce2053de24482ce3092a14c8fef2c4031e107e21 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Fri, 15 Mar 2024 10:29:57 +0200 Subject: [PATCH 16/33] add two ways for calculation totals --- src/Invoice.php | 20 ++++++++++++++++++++ src/Models/InvoiceTotals.php | 13 +++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/Invoice.php b/src/Invoice.php index cb79f63..beb8f0d 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -245,6 +245,7 @@ class Invoice { protected $businessProcess = null; protected $number = null; protected $type = self::TYPE_COMMERCIAL_INVOICE; + protected $legacySum = true; protected $currency = "EUR"; // TODO: add constants protected $vatCurrency = null; protected $issueDate = null; @@ -390,6 +391,25 @@ public function setNumber(string $number): self { return $this; } + /** + * Get legacy sum + * @return bool legacy sum + */ + public function getLegacySum(): bool { + return $this->legacySum; + } + + + /** + * Set legacy sum + * @param bool $legacySum legacy sum + * @return self Invoice instance + */ + public function setLegacySum(bool $legacySum): self { + $this->legacySum = $legacySum; + return $this; + } + /** * Get invoice type code diff --git a/src/Models/InvoiceTotals.php b/src/Models/InvoiceTotals.php index 6806b2e..c786027 100644 --- a/src/Models/InvoiceTotals.php +++ b/src/Models/InvoiceTotals.php @@ -119,7 +119,7 @@ public static function fromInvoice(Invoice $inv): InvoiceTotals foreach ($inv->getLines() as $line) { $lineNetAmount = $inv->round($line->getNetAmount() ?? 0.0, 'line/netAmount'); $totals->netAmount += $lineNetAmount; - self::updateVatMap($vatMap, $line, $line->getNetAmount() ?? 0.0); + self::updateVatMap($inv, $vatMap, $line, $line->getNetAmount() ?? 0.0); } $totals->netAmount = $inv->round($totals->netAmount, 'invoice/netAmount'); @@ -127,7 +127,7 @@ public static function fromInvoice(Invoice $inv): InvoiceTotals foreach ($inv->getAllowances() as $item) { $allowanceAmount = $inv->round($item->getEffectiveAmount($totals->netAmount), 'line/allowanceChargeAmount'); $totals->allowancesAmount += $allowanceAmount; - self::updateVatMap($vatMap, $item, -$item->getEffectiveAmount($totals->netAmount)); + self::updateVatMap($inv, $vatMap, $item, -$item->getEffectiveAmount($totals->netAmount)); } $totals->allowancesAmount = $inv->round($totals->allowancesAmount, 'invoice/allowancesChargesAmount'); @@ -135,13 +135,13 @@ public static function fromInvoice(Invoice $inv): InvoiceTotals foreach ($inv->getCharges() as $item) { $chargeAmount = $inv->round($item->getEffectiveAmount($totals->netAmount), 'line/allowanceChargeAmount'); $totals->chargesAmount += $chargeAmount; - self::updateVatMap($vatMap, $item, $item->getEffectiveAmount($totals->netAmount)); + self::updateVatMap($inv, $vatMap, $item, $item->getEffectiveAmount($totals->netAmount)); } $totals->chargesAmount = $inv->round($totals->chargesAmount, 'invoice/allowancesChargesAmount'); // Calculate VAT amounts foreach ($vatMap as $item) { - if (false) { + if ($inv->getLegacySum()) { $item->taxableAmount = $inv->round($item->taxableAmount, 'line/taxableAmount'); $item->taxAmount = $inv->round($item->taxableAmount * ($item->rate / 100), 'line/vatAmount'); } else { @@ -182,12 +182,13 @@ public static function fromInvoice(Invoice $inv): InvoiceTotals /** * Update VAT map * + * @param Invoice $inv Invoice instance * @param VatBreakdown[string] &$vatMap VAT map reference * @param VatTrait $item Item instance * @param float|null $rate VAT rate * @param float $addTaxableAmount Taxable amount to add */ - private static function updateVatMap(array &$vatMap, $item, float $addTaxableAmount) + private static function updateVatMap(Invoice $inv, array &$vatMap, $item, float $addTaxableAmount) { $category = $item->getVatCategory(); $rate = $item->getVatRate(); @@ -211,7 +212,7 @@ private static function updateVatMap(array &$vatMap, $item, float $addTaxableAmo } // Increase taxable amount - if (false) { + if ($inv->getLegacySum()) { $vatMap[$key]->taxableAmount += $addTaxableAmount; } else { $vatMap[$key]->taxableAmount += round($addTaxableAmount, 2); From 9e70d0a269eb808b8b6ed9484a6c2b980b4d447b Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Fri, 15 Mar 2024 12:13:09 +0200 Subject: [PATCH 17/33] add two ways for calculation totals --- src/Models/InvoiceTotals.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Models/InvoiceTotals.php b/src/Models/InvoiceTotals.php index c786027..de0c31a 100644 --- a/src/Models/InvoiceTotals.php +++ b/src/Models/InvoiceTotals.php @@ -119,7 +119,11 @@ public static function fromInvoice(Invoice $inv): InvoiceTotals foreach ($inv->getLines() as $line) { $lineNetAmount = $inv->round($line->getNetAmount() ?? 0.0, 'line/netAmount'); $totals->netAmount += $lineNetAmount; - self::updateVatMap($inv, $vatMap, $line, $line->getNetAmount() ?? 0.0); + if ($inv->getLegacySum()) { + self::updateVatMap($inv, $vatMap, $line, $lineNetAmount ?? 0.0); + } else { + self::updateVatMap($inv, $vatMap, $line, $line->getNetAmount() ?? 0.0); + } } $totals->netAmount = $inv->round($totals->netAmount, 'invoice/netAmount'); @@ -127,7 +131,11 @@ public static function fromInvoice(Invoice $inv): InvoiceTotals foreach ($inv->getAllowances() as $item) { $allowanceAmount = $inv->round($item->getEffectiveAmount($totals->netAmount), 'line/allowanceChargeAmount'); $totals->allowancesAmount += $allowanceAmount; - self::updateVatMap($inv, $vatMap, $item, -$item->getEffectiveAmount($totals->netAmount)); + if ($inv->getLegacySum()) { + self::updateVatMap($inv, $vatMap, $item, -$allowanceAmount); + } else { + self::updateVatMap($inv, $vatMap, $item, -$item->getEffectiveAmount($totals->netAmount)); + } } $totals->allowancesAmount = $inv->round($totals->allowancesAmount, 'invoice/allowancesChargesAmount'); @@ -135,7 +143,11 @@ public static function fromInvoice(Invoice $inv): InvoiceTotals foreach ($inv->getCharges() as $item) { $chargeAmount = $inv->round($item->getEffectiveAmount($totals->netAmount), 'line/allowanceChargeAmount'); $totals->chargesAmount += $chargeAmount; - self::updateVatMap($inv, $vatMap, $item, $item->getEffectiveAmount($totals->netAmount)); + if ($inv->getLegacySum()) { + self::updateVatMap($inv, $vatMap, $item, $chargeAmount); + } else { + self::updateVatMap($inv, $vatMap, $item, $item->getEffectiveAmount($totals->netAmount)); + } } $totals->chargesAmount = $inv->round($totals->chargesAmount, 'invoice/allowancesChargesAmount'); From daab6ab0033d59a46a00f58a5a278b064128594e Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Fri, 22 Mar 2024 11:58:43 +0200 Subject: [PATCH 18/33] fix rounding (#8) * fix rounding * fix rounding * fix rounding * fix rounding * fix rounding * fix rounding --- src/Invoice.php | 15 +++++++++++++++ src/InvoiceLine.php | 4 ++-- src/Models/InvoiceTotals.php | 2 ++ src/Writers/UblWriter.php | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Invoice.php b/src/Invoice.php index beb8f0d..5eaadf7 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -267,6 +267,7 @@ class Invoice { protected $payments = []; protected $paymentTerms = null; protected $lines = []; + protected $totals = null; use AllowanceOrChargeTrait; use AttachmentsTrait; @@ -931,6 +932,20 @@ public function clearLines(): self { * @return InvoiceTotals Invoice totals */ public function getTotals(): InvoiceTotals { + if (! empty($this->totals)) { + return $this->totals; + } + return InvoiceTotals::fromInvoice($this); } + + + /** + * Set invoice total + * @return self Invoice instance + */ + public function setTotals(InvoiceTotals $invoiceTotals): self { + $this->totals = $invoiceTotals; + return $this; + } } diff --git a/src/InvoiceLine.php b/src/InvoiceLine.php index 37af8f3..0e31bde 100644 --- a/src/InvoiceLine.php +++ b/src/InvoiceLine.php @@ -21,7 +21,7 @@ class InvoiceLine { protected $quantity = 1; protected $unit = "C62"; // TODO: add constants protected $price = null; - protected $baseQuantity = 1; + protected $baseQuantity = 1.0; use AllowanceOrChargeTrait; use AttributesTrait; @@ -289,7 +289,7 @@ public function getBaseQuantity(): float { * @return self Invoice line instance */ public function setBaseQuantity(float $baseQuantity): self { - $this->baseQuantity = $baseQuantity; + $this->baseQuantity = $baseQuantity != 0.0 ? $baseQuantity : 1.0; return $this; } diff --git a/src/Models/InvoiceTotals.php b/src/Models/InvoiceTotals.php index de0c31a..58b2525 100644 --- a/src/Models/InvoiceTotals.php +++ b/src/Models/InvoiceTotals.php @@ -157,6 +157,8 @@ public static function fromInvoice(Invoice $inv): InvoiceTotals $item->taxableAmount = $inv->round($item->taxableAmount, 'line/taxableAmount'); $item->taxAmount = $inv->round($item->taxableAmount * ($item->rate / 100), 'line/vatAmount'); } else { + $item->taxableAmount = $inv->round($item->taxableAmount, 'line/taxableAmount'); + $item->taxAmount = $inv->round($item->taxAmount, 'line/vatAmount'); } $totals->vatAmount += $item->taxAmount; diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 114d1d0..3fbd635 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -854,7 +854,7 @@ private function addDocumentTotalsNode(UXML $parent, InvoiceTotals $totals) { $totalsMatrix['cbc:PayableAmount'] = $totals->payableAmount; // Create and append XML nodes - foreach ($totalsMatrix as $field=>$amount) { + foreach ($totalsMatrix as $field => $amount) { $this->addAmountNode($xml, $field, $amount, $totals->currency); } } From 5fc92716da04d8468f7a2ae26f5c47a1b6b828a9 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 25 Mar 2024 22:14:07 +0200 Subject: [PATCH 19/33] feat: add read-totals (#9) * feat: add read-totals * feat: add read-totals * feat: add read-totals * feat: add read-totals * feat: add read-totals --- src/Readers/AbstractReader.php | 3 +- src/Readers/UblReader.php | 78 +++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/Readers/AbstractReader.php b/src/Readers/AbstractReader.php index 6f6647e..252fd10 100644 --- a/src/Readers/AbstractReader.php +++ b/src/Readers/AbstractReader.php @@ -65,7 +65,8 @@ public function registerPreset(string $classname): self { /** * Import invoice * @param string $document Document contents + * @param bool $readTotals Read totals from XML contents * @return Invoice Invoice instance */ - abstract public function import(string $document): Invoice; + abstract public function import(string $document, bool $readTotals = false): Invoice; } diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index 7e5e112..d4f60b2 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -10,6 +10,8 @@ use Einvoicing\Invoice; use Einvoicing\InvoiceLine; use Einvoicing\InvoiceReference; +use Einvoicing\Models\InvoiceTotals; +use Einvoicing\Models\VatBreakdown; use Einvoicing\Party; use Einvoicing\Payments\Card; use Einvoicing\Payments\Mandate; @@ -28,8 +30,9 @@ class UblReader extends AbstractReader { * @inheritdoc * @throws InvalidArgumentException if failed to parse XML */ - public function import(string $document): Invoice { + public function import(string $document, bool $readTotals = false): Invoice { $invoice = new Invoice(); + $totals = new InvoiceTotals(); // Load XML document $xml = UXML::fromString($document); @@ -118,12 +121,14 @@ public function import(string $document): Invoice { $currencyNode = $xml->get("{{$cbc}}DocumentCurrencyCode"); if ($currencyNode !== null) { $invoice->setCurrency($currencyNode->asText()); + $totals->currency = $invoice->getCurrency(); } // BT-6: VAT accounting currency code $vatCurrencyNode = $xml->get("{{$cbc}}TaxCurrencyCode"); if ($vatCurrencyNode !== null) { $invoice->setVatCurrency($vatCurrencyNode->asText()); + $totals->vatCurrency = $invoice->getVatCurrency(); } // BT-19: Buyer accounting reference @@ -227,33 +232,102 @@ public function import(string $document): Invoice { // BT-111: Total VAT amount in accounting currency foreach ($xml->getAll("{{$cac}}TaxTotal") as $taxTotalNode) { - if ($taxTotalNode->get("{{$cac}}TaxSubtotal") !== null) { + $taxSubtotals = $taxTotalNode->getAll("{{$cac}}TaxSubtotal"); + if (! empty($taxSubtotals)) { + foreach ($taxSubtotals as $taxSubtotal) { + $vatBreakdown = new VatBreakdown(); + if (($taxableAmountNode = $taxSubtotal->get("{{$cbc}}TaxableAmount")) !== null) { + $vatBreakdown->taxableAmount = (float) $taxableAmountNode->asText(); + } + if (($taxAmountNode = $taxSubtotal->get("{{$cbc}}TaxAmount")) !== null) { + $vatBreakdown->taxAmount = (float) $taxAmountNode->asText(); + } + if (($categoryNode = $taxSubtotal->get("{{$cac}}TaxCategory/{{$cbc}}ID")) !== null) { + $vatBreakdown->category = $categoryNode->asText(); + } + if (($rateNode = $taxSubtotal->get("{{$cac}}TaxCategory/{{$cbc}}Percent")) !== null) { + $vatBreakdown->rate = (float) $rateNode->asText(); + } + if (($exemptionReasonNode = $taxSubtotal->get("{{$cac}}TaxCategory/{{$cbc}}TaxExemptionReason")) !== null) { + $vatBreakdown->exemptionReason = $exemptionReasonNode->asText(); + } + if (($exemptionReasonCodeNode = $taxSubtotal->get("{{$cac}}TaxCategory/{{$cbc}}TaxExemptionReasonCode")) !== null) { + $vatBreakdown->exemptionReasonCode = $exemptionReasonCodeNode->asText(); + } + + $totals->vatBreakdown[] = $vatBreakdown; + } // The other tax total node, then continue; } $taxAmountNode = $taxTotalNode->get("{{$cbc}}TaxAmount"); if ($taxAmountNode !== null) { $invoice->setCustomVatAmount((float) $taxAmountNode->asText()); + $totals->customVatAmount = (float) $taxAmountNode->asText(); } } + // BT-106: Line Extension Amount + $lineExtensionAmount = $xml->get("{{$cac}}LegalMonetaryTotal/{{$cbc}}LineExtensionAmount"); + if ($lineExtensionAmount !== null) { + $totals->netAmount = (float) $lineExtensionAmount->asText(); + } + + // BT-107: Allowance Total Amount + $allowanceTotalAmount = $xml->get("{{$cac}}LegalMonetaryTotal/{{$cbc}}AllowanceTotalAmount"); + if ($allowanceTotalAmount !== null) { + $totals->allowancesAmount = (float) $allowanceTotalAmount->asText(); + } + + // BT-108: Charge Total Amount + $chargeTotalAmount = $xml->get("{{$cac}}LegalMonetaryTotal/{{$cbc}}ChargeTotalAmount"); + if ($chargeTotalAmount !== null) { + $totals->chargesAmount = (float) $chargeTotalAmount->asText(); + } + + // BT-109: Tax Exclusive Amount + $taxExclusiveAmount = $xml->get("{{$cac}}LegalMonetaryTotal/{{$cbc}}TaxExclusiveAmount"); + if ($taxExclusiveAmount !== null) { + $totals->taxExclusiveAmount = (float) $taxExclusiveAmount->asText(); + } + + // BT-112: Tax Inclusive Amount + $taxInclusiveAmount = $xml->get("{{$cac}}LegalMonetaryTotal/{{$cbc}}TaxInclusiveAmount"); + if ($taxInclusiveAmount !== null) { + $totals->taxInclusiveAmount = (float) $taxInclusiveAmount->asText(); + } + + // BT-115: Payable Amount + $payableAmount = $xml->get("{{$cac}}LegalMonetaryTotal/{{$cbc}}PayableAmount"); + if ($payableAmount !== null) { + $totals->payableAmount = (float) $payableAmount->asText(); + } + // BT-113: Paid amount $paidAmountNode = $xml->get("{{$cac}}LegalMonetaryTotal/{{$cbc}}PrepaidAmount"); if ($paidAmountNode !== null) { $invoice->setPaidAmount((float) $paidAmountNode->asText()); + $totals->paidAmount = (float) $paidAmountNode->asText(); } // BT-114: Rounding amount $roundingAmountNode = $xml->get("{{$cac}}LegalMonetaryTotal/{{$cbc}}PayableRoundingAmount"); if ($roundingAmountNode !== null) { $invoice->setRoundingAmount((float) $roundingAmountNode->asText()); + $totals->roundingAmount = (float) $roundingAmountNode->asText(); } + $totals->vatAmount = $totals->taxInclusiveAmount - $totals->taxExclusiveAmount; + // Invoice lines foreach ($xml->getAll("{{$cac}}InvoiceLine | {{$cac}}CreditNoteLine") as $node) { $invoice->addLine($this->parseInvoiceLine($node, $taxExemptions)); } + if ($readTotals) { + $invoice->setTotals($totals); + } + return $invoice; } From 40adb1ca55bbfa38dcc4a2eb995d09265ecd0339 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 25 Mar 2024 22:15:47 +0200 Subject: [PATCH 20/33] Create dependabot.yml --- .github/dependabot.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d969fe2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# Basic set up for three package managers + +version: 2 +updates: + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + # Workflow files stored in the default location of `.github/workflows`. (You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`.) + directory: "/" + schedule: + interval: "weekly" + + # Maintain dependencies for Composer + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" From e069c813d5e0a1d138e6d9506f9efd832d1943a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 22:16:53 +0200 Subject: [PATCH 21/33] Bump actions/checkout from 3 to 4 (#10) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e781bc6..6544df1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: steps: # Download code from repository - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Setup PHP - name: Setup PHP From c5f1489e7c0ba9ec4ec9a00557686e3a1c359f87 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 25 Mar 2024 22:23:40 +0200 Subject: [PATCH 22/33] feat: add read-totals (#11) --- src/Readers/UblReader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index d4f60b2..258793e 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -317,7 +317,7 @@ public function import(string $document, bool $readTotals = false): Invoice { $totals->roundingAmount = (float) $roundingAmountNode->asText(); } - $totals->vatAmount = $totals->taxInclusiveAmount - $totals->taxExclusiveAmount; + $totals->vatAmount = $invoice->round($totals->taxInclusiveAmount - $totals->taxExclusiveAmount, 'invoice/vatAmount'); // Invoice lines foreach ($xml->getAll("{{$cac}}InvoiceLine | {{$cac}}CreditNoteLine") as $node) { From 0bdcafcd97076df7bd34d6e2b03a8a2b48c4e8eb Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Thu, 4 Apr 2024 12:43:59 +0300 Subject: [PATCH 23/33] fix: prepaid amount != 0 (#12) --- src/Writers/UblWriter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 3fbd635..218a076 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -845,7 +845,7 @@ private function addDocumentTotalsNode(UXML $parent, InvoiceTotals $totals) { if ($totals->chargesAmount > 0) { $totalsMatrix['cbc:ChargeTotalAmount'] = $totals->chargesAmount; } - if ($totals->paidAmount > 0) { + if ($totals->paidAmount != 0) { $totalsMatrix['cbc:PrepaidAmount'] = $totals->paidAmount; } if ($totals->roundingAmount != 0) { From 4e77ec5866e7145c8ceb0d225ee1c45c50e4f9d3 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 8 Jul 2024 15:47:58 +0300 Subject: [PATCH 24/33] feat: add project reference (#13) * feat: add project reference * feat: add project reference --- src/Invoice.php | 21 +++++++++++++++++++++ src/Readers/UblReader.php | 6 ++++++ src/Writers/UblWriter.php | 19 +++++++++++++++++++ tests/Integration/peppol-base.xml | 3 +++ 4 files changed, 49 insertions(+) diff --git a/src/Invoice.php b/src/Invoice.php index 5eaadf7..9a45696 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -255,6 +255,7 @@ class Invoice { protected $buyerReference = null; protected $purchaseOrderReference = null; protected $salesOrderReference = null; + protected $projectReference = null; protected $tenderOrLotReference = null; protected $contractReference = null; protected $paidAmount = 0; @@ -642,6 +643,26 @@ public function setPurchaseOrderReference(?string $purchaseOrderReference): self } + /** + * Get project reference + * @return string|null Project reference + */ + public function getProjectReference(): ?string { + return $this->projectReference; + } + + + /** + * Set project reference + * @param string|null $projectReference Project reference + * @return self Invoice instance + */ + public function setProjectReference(?string $projectReference): self { + $this->projectReference = $projectReference; + return $this; + } + + /** * Get sales order reference * @return string|null Sales order reference diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index 258793e..30288cb 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -146,6 +146,12 @@ public function import(string $document, bool $readTotals = false): Invoice { // BG-14: Invoice period $this->parsePeriodFields($xml, $invoice); + // BT-11: Project reference + $projectReferenceNode = $xml->get("{{$cac}}ProjectReference/{{$cbc}}ID"); + if ($projectReferenceNode !== null) { + $invoice->setProjectReference($projectReferenceNode->asText()); + } + // BT-13: Purchase order reference $purchaseOrderReferenceNode = $xml->get("{{$cac}}OrderReference/{{$cbc}}ID"); if ($purchaseOrderReferenceNode !== null) { diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 218a076..28e85cf 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -111,6 +111,9 @@ public function export(Invoice $invoice): string { // Order reference node $this->addOrderReferenceNode($xml, $invoice); + // Project reference node + $this->addProjectReferenceNode($xml, $invoice); + // BG-3: Preceding invoice reference foreach ($invoice->getPrecedingInvoiceReferences() as $invoiceReference) { $invoiceDocumentReferenceNode = $xml->add('cac:BillingReference')->add('cac:InvoiceDocumentReference'); @@ -286,6 +289,22 @@ private function addOrderReferenceNode(UXML $parent, Invoice $invoice) { } + /** + * Add project reference node + * @param UXML $parent Parent element + * @param Invoice $invoice Invoice instance + */ + private function addProjectReferenceNode(UXML $parent, Invoice $invoice) { + $projectReference = $invoice->getProjectReference(); + if ($projectReference === null) return; + + $projectReferenceNode = $parent->add('cac:ProjectReference'); + + // BT-11: Project reference + $projectReferenceNode->add('cbc:ID', $projectReference); + } + + /** * Add tender or lot reference node * @param UXML $parent Parent element diff --git a/tests/Integration/peppol-base.xml b/tests/Integration/peppol-base.xml index f4110b7..0f8eafb 100644 --- a/tests/Integration/peppol-base.xml +++ b/tests/Integration/peppol-base.xml @@ -15,6 +15,9 @@ 854777 + + 123projref + INV-122 From a97754848688a81d3705c3d3cfe19e0480034f55 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 8 Jul 2024 20:25:19 +0300 Subject: [PATCH 25/33] Add project reference: fix order (#14) * feat: add project reference * feat: add project reference * fix: order --- src/Writers/UblWriter.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 28e85cf..e490c9e 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -108,6 +108,9 @@ public function export(Invoice $invoice): string { // BG-14: Invoice period $this->addPeriodNode($xml, $invoice); + // Project reference node + $this->addProjectReferenceNode($xml, $invoice); + // Order reference node $this->addOrderReferenceNode($xml, $invoice); From 676318a69c720d27bf5627f2403a7391e5321e8e Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 8 Jul 2024 20:28:08 +0300 Subject: [PATCH 26/33] Add project reference (#15) * feat: add project reference * feat: add project reference * fix: order * fix: tests * fix: tests --- tests/Integration/peppol-base.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Integration/peppol-base.xml b/tests/Integration/peppol-base.xml index 0f8eafb..453ff17 100644 --- a/tests/Integration/peppol-base.xml +++ b/tests/Integration/peppol-base.xml @@ -12,12 +12,12 @@ EUR 4025:123:4343 0150abc - - 854777 - 123projref + + 854777 + INV-122 From 38a5b67beeb059eadea3b5c64e494a47edccc30e Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 8 Jul 2024 20:33:55 +0300 Subject: [PATCH 27/33] Add project reference (#16) * feat: add project reference * feat: add project reference * fix: order * fix: tests * fix: tests * fix: tests --- src/Writers/UblWriter.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index e490c9e..15440a9 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -114,9 +114,6 @@ public function export(Invoice $invoice): string { // Order reference node $this->addOrderReferenceNode($xml, $invoice); - // Project reference node - $this->addProjectReferenceNode($xml, $invoice); - // BG-3: Preceding invoice reference foreach ($invoice->getPrecedingInvoiceReferences() as $invoiceReference) { $invoiceDocumentReferenceNode = $xml->add('cac:BillingReference')->add('cac:InvoiceDocumentReference'); From 16d6e46718985ed0c049b7f05f9177c2f09254a8 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 8 Jul 2024 20:51:30 +0300 Subject: [PATCH 28/33] Add project reference: fix order (#17) * feat: add project reference * feat: add project reference * fix: order * fix: tests * fix: tests * fix: tests * fix: order --- src/Writers/UblWriter.php | 6 +++--- tests/Integration/peppol-base.xml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 15440a9..06ff491 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -108,9 +108,6 @@ public function export(Invoice $invoice): string { // BG-14: Invoice period $this->addPeriodNode($xml, $invoice); - // Project reference node - $this->addProjectReferenceNode($xml, $invoice); - // Order reference node $this->addOrderReferenceNode($xml, $invoice); @@ -145,6 +142,9 @@ public function export(Invoice $invoice): string { $this->addTenderOrLotReferenceNode($xml, $invoice); } + // Project reference node + $this->addProjectReferenceNode($xml, $invoice); + // Seller node $seller = $invoice->getSeller(); if ($seller !== null) { diff --git a/tests/Integration/peppol-base.xml b/tests/Integration/peppol-base.xml index 453ff17..711e6e8 100644 --- a/tests/Integration/peppol-base.xml +++ b/tests/Integration/peppol-base.xml @@ -12,9 +12,6 @@ EUR 4025:123:4343 0150abc - - 123projref - 854777 @@ -54,6 +51,9 @@ VGhlIGF0dGFjaG1lbnQgcmF3IGNvbnRlbnRz + + 123projref + 9482348239847239874 From 5d28865ae0eda86b15efd8b39da26a682d7eda33 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Thu, 5 Sep 2024 17:20:25 +0300 Subject: [PATCH 29/33] add-despatch-document-reference (#19) --- src/Invoice.php | 21 +++++++++++++++++++++ src/Readers/UblReader.php | 6 ++++++ src/Writers/UblWriter.php | 20 ++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/src/Invoice.php b/src/Invoice.php index 9a45696..45975ba 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -254,6 +254,7 @@ class Invoice { protected $notes = []; protected $buyerReference = null; protected $purchaseOrderReference = null; + protected $despatchDocumentReference = null; protected $salesOrderReference = null; protected $projectReference = null; protected $tenderOrLotReference = null; @@ -643,6 +644,26 @@ public function setPurchaseOrderReference(?string $purchaseOrderReference): self } + /** + * Get despatch document reference + * @return string|null despatch document reference + */ + public function getDespatchDocumentReference(): ?string { + return $this->despatchDocumentReference; + } + + + /** + * Set despatch document reference + * @param string|null $despatchDocumentReference despatch document reference + * @return self Invoice instance + */ + public function setDespatchDocumentReference(?string $despatchDocumentReference): self { + $this->despatchDocumentReference = $despatchDocumentReference; + return $this; + } + + /** * Get project reference * @return string|null Project reference diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index 30288cb..eac326d 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -164,6 +164,12 @@ public function import(string $document, bool $readTotals = false): Invoice { $invoice->setSalesOrderReference($salesOrderReferenceNode->asText()); } + // BT-16: Purchase order reference + $despatchDocumentReferenceNode = $xml->get("{{$cac}}DespatchDocumentReference/{{$cbc}}ID"); + if ($despatchDocumentReferenceNode !== null) { + $invoice->setDespatchDocumentReference($despatchDocumentReferenceNode->asText()); + } + // BG-3: Preceding invoice references foreach ($xml->getAll("{{$cac}}BillingReference/{{$cac}}InvoiceDocumentReference") as $node) { $invoiceReferenceValueNode = $node->get("{{$cbc}}ID"); diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 06ff491..ee9e5e7 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -121,6 +121,9 @@ public function export(Invoice $invoice): string { } } + // BT-16: Despatch Document Reference + $this->addDespatchDocumentReference($xml, $invoice); + // BT-17: Tender or lot reference (for invoice profile) if (!$isCreditNoteProfile) { $this->addTenderOrLotReferenceNode($xml, $invoice); @@ -289,6 +292,23 @@ private function addOrderReferenceNode(UXML $parent, Invoice $invoice) { } + /** + * Add order reference node + * @param UXML $parent Parent element + * @param Invoice $invoice Invoice instance + */ + private function addDespatchDocumentReference(UXML $parent, Invoice $invoice) { + $despatchDocumentReference = $invoice->getDespatchDocumentReference(); + + if ($despatchDocumentReference === null) return; + + $despatchDocumentReferenceNode = $parent->add('cac:DespatchDocumentReference'); + + // BT-16: Purchase order reference + $despatchDocumentReferenceNode->add('cbc:ID', $despatchDocumentReference); + } + + /** * Add project reference node * @param UXML $parent Parent element From 7003d1c79fdf2707d0a6bb1e96fe2f76456641a7 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 9 Dec 2024 13:45:02 +0200 Subject: [PATCH 30/33] fix-allowance-amount-read (#20) --- src/Readers/UblReader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index eac326d..c80ecb6 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -943,9 +943,9 @@ private function parseInvoiceLine(UXML $xml, array &$taxExemptions): InvoiceLine $chargeIndicatorNode = $allowanceChargeNode->get("{{$cbc}}ChargeIndicator"); if ($chargeIndicatorNode !== null && $chargeIndicatorNode->asText() === "true") { - $basePrice -= (float) $amountNode->asText(); + $basePrice -= abs((float) $amountNode->asText()); } else { - $basePrice += (float) $amountNode->asText(); + $basePrice += abs((float) $amountNode->asText()); } } From c585c4d1e300dc7392c5a85247819c279b639401 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 9 Dec 2024 15:51:42 +0200 Subject: [PATCH 31/33] add-base-amount-to-allowance-or-charge (#21) * add-base-amount-to-allowance-or-charge * fix phpstan * fix phpstan * fix phpstan * fix phpstan --- src/AllowanceOrCharge.php | 50 +++++++++++++++++++++++++++++---- src/InvoiceLine.php | 6 ++-- src/Models/InvoiceTotals.php | 8 +++--- src/Readers/UblReader.php | 11 ++++++-- src/Writers/UblWriter.php | 23 ++++++--------- tests/InvoiceLineTest.php | 6 +++- tests/InvoiceTest.php | 2 +- tests/Writers/UblWriterTest.php | 9 +++++- 8 files changed, 82 insertions(+), 33 deletions(-) diff --git a/src/AllowanceOrCharge.php b/src/AllowanceOrCharge.php index 2d21b72..153da68 100644 --- a/src/AllowanceOrCharge.php +++ b/src/AllowanceOrCharge.php @@ -7,6 +7,8 @@ class AllowanceOrCharge { protected $reasonCode = null; protected $reason = null; protected $amount = null; + protected $baseAmount = null; + protected $factorMultiplier = null; protected $isPercentage = false; use VatTrait; @@ -71,6 +73,46 @@ public function setAmount(float $amount): self { } + /** + * Get base amount + * @return float|null Allowance/charge base amount + */ + public function getBaseAmount(): ?float { + return $this->baseAmount; + } + + + /** + * Set base amount + * @param float $baseAmount Allowance/charge base amount + * @return self This instance + */ + public function setBaseAmount(float $baseAmount): self { + $this->baseAmount = $baseAmount; + return $this; + } + + + /** + * Get factor multiplier + * @return float|null Allowance/charge factor multiplier + */ + public function getFactorMultiplier(): ?float { + return $this->factorMultiplier; + } + + + /** + * Set factor multiplier + * @param float $factorMultiplier Allowance/charge factor multiplier + * @return self This instance + */ + public function setFactorMultiplier(float $factorMultiplier): self { + $this->factorMultiplier = $factorMultiplier; + return $this; + } + + /** * Is percentage * @return boolean Whether amount is a percentage or not @@ -102,15 +144,13 @@ public function markAsFixedAmount(): self { /** * Get effective amount relative to base amount - * @param float $baseAmount Base amount * @return float Effective amount */ - public function getEffectiveAmount(float $baseAmount): float { - $amount = $this->getAmount(); + public function getEffectiveAmount(): float { if ($this->isPercentage()) { - $amount = $baseAmount * ($amount / 100); + return $this->baseAmount * ($this->factorMultiplier / 100); } - return $amount; + return $this->getAmount(); } } diff --git a/src/InvoiceLine.php b/src/InvoiceLine.php index 0e31bde..608ef3c 100644 --- a/src/InvoiceLine.php +++ b/src/InvoiceLine.php @@ -312,9 +312,8 @@ public function getNetAmountBeforeAllowancesCharges(): ?float { */ public function getAllowancesAmount(): float { $allowancesAmount = 0; - $baseAmount = $this->getNetAmountBeforeAllowancesCharges() ?? 0.0; foreach ($this->getAllowances() as $item) { - $allowancesAmount += $item->getEffectiveAmount($baseAmount); + $allowancesAmount += $item->getEffectiveAmount(); } return $allowancesAmount; } @@ -326,9 +325,8 @@ public function getAllowancesAmount(): float { */ public function getChargesAmount(): float { $chargesAmount = 0; - $baseAmount = $this->getNetAmountBeforeAllowancesCharges() ?? 0.0; foreach ($this->getCharges() as $item) { - $chargesAmount += $item->getEffectiveAmount($baseAmount); + $chargesAmount += $item->getEffectiveAmount(); } return $chargesAmount; } diff --git a/src/Models/InvoiceTotals.php b/src/Models/InvoiceTotals.php index 58b2525..26077ca 100644 --- a/src/Models/InvoiceTotals.php +++ b/src/Models/InvoiceTotals.php @@ -129,24 +129,24 @@ public static function fromInvoice(Invoice $inv): InvoiceTotals // Process allowances foreach ($inv->getAllowances() as $item) { - $allowanceAmount = $inv->round($item->getEffectiveAmount($totals->netAmount), 'line/allowanceChargeAmount'); + $allowanceAmount = $inv->round($item->getEffectiveAmount(), 'line/allowanceChargeAmount'); $totals->allowancesAmount += $allowanceAmount; if ($inv->getLegacySum()) { self::updateVatMap($inv, $vatMap, $item, -$allowanceAmount); } else { - self::updateVatMap($inv, $vatMap, $item, -$item->getEffectiveAmount($totals->netAmount)); + self::updateVatMap($inv, $vatMap, $item, -$item->getEffectiveAmount()); } } $totals->allowancesAmount = $inv->round($totals->allowancesAmount, 'invoice/allowancesChargesAmount'); // Process charges foreach ($inv->getCharges() as $item) { - $chargeAmount = $inv->round($item->getEffectiveAmount($totals->netAmount), 'line/allowanceChargeAmount'); + $chargeAmount = $inv->round($item->getEffectiveAmount(), 'line/allowanceChargeAmount'); $totals->chargesAmount += $chargeAmount; if ($inv->getLegacySum()) { self::updateVatMap($inv, $vatMap, $item, $chargeAmount); } else { - self::updateVatMap($inv, $vatMap, $item, $item->getEffectiveAmount($totals->netAmount)); + self::updateVatMap($inv, $vatMap, $item, $item->getEffectiveAmount()); } } $totals->chargesAmount = $inv->round($totals->chargesAmount, 'invoice/allowancesChargesAmount'); diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index c80ecb6..8704a10 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -804,9 +804,16 @@ private function addAllowanceOrCharge($target, UXML $xml, array &$taxExemptions) // Amount $factorNode = $xml->get("{{$cbc}}MultiplierFactorNumeric"); $amountNode = $xml->get("{{$cbc}}Amount"); - if ($factorNode !== null) { + $baseAmountNode = $xml->get("{{$cbc}}BaseAmount"); + if ($factorNode !== null && $baseAmountNode !== null && $amountNode !== null) { $percent = (float) $factorNode->asText(); - $allowanceOrCharge->markAsPercentage()->setAmount($percent); + $baseAmount = (float) $baseAmountNode->asText(); + $amount = (float) $amountNode->asText(); + $allowanceOrCharge + ->markAsPercentage() + ->setBaseAmount($baseAmount) + ->setFactorMultiplier($percent) + ->setAmount($amount); } elseif ($amountNode !== null) { $amount = (float) $amountNode->asText(); $allowanceOrCharge->setAmount($amount); diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index ee9e5e7..d5bdaf0 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -185,10 +185,10 @@ public function export(Invoice $invoice): string { // Allowances and charges foreach ($invoice->getAllowances() as $item) { - $this->addAllowanceOrCharge($xml, $item, false, $invoice, $totals, null); + $this->addAllowanceOrCharge($xml, $item, false, $invoice, null); } foreach ($invoice->getCharges() as $item) { - $this->addAllowanceOrCharge($xml, $item, true, $invoice, $totals, null); + $this->addAllowanceOrCharge($xml, $item, true, $invoice, null); } // Invoice totals @@ -764,7 +764,6 @@ private function addPaymentMandateNode(UXML $parent, Mandate $mandate) { * @param AllowanceOrCharge $item Allowance or charge instance * @param boolean $isCharge Is charge (TRUE) or allowance (FALSE) * @param Invoice $invoice Invoice instance - * @param InvoiceTotals|null $totals Invoice totals or NULL in case at line level * @param InvoiceLine|null $line Invoice line or NULL in case of at document level */ private function addAllowanceOrCharge( @@ -772,7 +771,6 @@ private function addAllowanceOrCharge( AllowanceOrCharge $item, bool $isCharge, Invoice $invoice, - ?InvoiceTotals $totals, ?InvoiceLine $line ) { $atDocumentLevel = ($line === null); @@ -795,26 +793,21 @@ private function addAllowanceOrCharge( // Percentage if ($item->isPercentage()) { - $xml->add('cbc:MultiplierFactorNumeric', (string) $item->getAmount()); + $xml->add('cbc:MultiplierFactorNumeric', (string) $item->getFactorMultiplier()); } - // Amount - $baseAmount = $atDocumentLevel ? - $totals->netAmount : // @phan-suppress-current-line PhanPossiblyUndeclaredProperty - $line->getNetAmountBeforeAllowancesCharges() ?? 0.0; // @phan-suppress-current-line PhanPossiblyNonClassMethodCall $this->addAmountNode( $xml, 'cbc:Amount', - $invoice->round($item->getEffectiveAmount($baseAmount), 'line/allowanceChargeAmount'), + $invoice->round($item->getAmount(), 'line/allowanceChargeAmount'), $invoice->getCurrency() ); - // Base amount - if ($item->isPercentage()) { + if ($item->getBaseAmount()) { $this->addAmountNode( $xml, 'cbc:BaseAmount', - $invoice->round($baseAmount, 'line/netAmount'), + $invoice->round($item->getBaseAmount(), 'line/netAmount'), $invoice->getCurrency() ); } @@ -966,10 +959,10 @@ private function addLineNode( // Allowances and charges foreach ($line->getAllowances() as $item) { - $this->addAllowanceOrCharge($xml, $item, false, $invoice, null, $line); + $this->addAllowanceOrCharge($xml, $item, false, $invoice, $line); } foreach ($line->getCharges() as $item) { - $this->addAllowanceOrCharge($xml, $item, true, $invoice, null, $line); + $this->addAllowanceOrCharge($xml, $item, true, $invoice, $line); } // Initial item node diff --git a/tests/InvoiceLineTest.php b/tests/InvoiceLineTest.php index 391d9c3..3e25dfc 100644 --- a/tests/InvoiceLineTest.php +++ b/tests/InvoiceLineTest.php @@ -22,7 +22,11 @@ public function testCanSetPriceWithCustomBaseQuantity(): void { public function testTotalAmountsAreCalculatedCorrectly(): void { $allowance = (new AllowanceOrCharge)->setAmount(20.2); - $charge = (new AllowanceOrCharge)->setAmount(5)->markAsPercentage(); + $charge = (new AllowanceOrCharge) + ->setFactorMultiplier(5) + ->setBaseAmount(250) + ->setAmount(12.5) + ->markAsPercentage(); $this->line ->setPrice(50, 2) ->setQuantity(10) diff --git a/tests/InvoiceTest.php b/tests/InvoiceTest.php index 165a3db..08d1195 100644 --- a/tests/InvoiceTest.php +++ b/tests/InvoiceTest.php @@ -101,7 +101,7 @@ public function testDecimalMatrixIsUsed(): void { public function testTotalAmountsAreCalculatedCorrectly(): void { $allowance = (new AllowanceOrCharge)->setAmount(12.34); - $charge = (new AllowanceOrCharge)->setAmount(7.5)->markAsPercentage(); + $charge = (new AllowanceOrCharge)->setFactorMultiplier(7.5)->setBaseAmount(300.5)->markAsPercentage(); $firstLine = (new InvoiceLine)->setPrice(100)->setVatRate(10); $secondLine = (new InvoiceLine)->setPrice(200.5)->setVatRate(21); $this->invoice->clearLines() diff --git a/tests/Writers/UblWriterTest.php b/tests/Writers/UblWriterTest.php index b6eb408..4143dd4 100644 --- a/tests/Writers/UblWriterTest.php +++ b/tests/Writers/UblWriterTest.php @@ -84,7 +84,14 @@ private function getSampleInvoice(): Invoice { ->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)->setName('Line #4')->setPrice(0.56)->setVatRate(10)->setQuantity(2)) - ->addAllowance((new AllowanceOrCharge)->setReason('5% discount')->setAmount(5)->markAsPercentage()->setVatRate(21)) + ->addAllowance( + (new AllowanceOrCharge)->setReason('5% discount') + ->setFactorMultiplier(5) + ->setBaseAmount(149.0634) + ->markAsPercentage() + ->setAmount(7.45317) + ->setVatRate(21) + ) ->addAttachment((new Attachment)->setId(new Identifier('INV-123', 'ABT'))) ->addAttachment($externalAttachment) ->addAttachment($embeddedAttachment); From b5bbbfdc37ec4541c56dbcd74c681241413b663d Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Mon, 9 Dec 2024 23:23:57 +0200 Subject: [PATCH 32/33] add abs to allowance charge (#22) --- src/Readers/UblReader.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index 8704a10..d67c266 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -811,12 +811,12 @@ private function addAllowanceOrCharge($target, UXML $xml, array &$taxExemptions) $amount = (float) $amountNode->asText(); $allowanceOrCharge ->markAsPercentage() - ->setBaseAmount($baseAmount) - ->setFactorMultiplier($percent) - ->setAmount($amount); + ->setBaseAmount(abs($baseAmount)) + ->setFactorMultiplier(abs($percent)) + ->setAmount(abs($amount)); } elseif ($amountNode !== null) { $amount = (float) $amountNode->asText(); - $allowanceOrCharge->setAmount($amount); + $allowanceOrCharge->setAmount(abs($amount)); } else { throw new InvalidArgumentException('Missing both and ' . ' nodes from allowance/charge'); From a329676d180e887918249163a2911971aee290a9 Mon Sep 17 00:00:00 2001 From: Robert Stanciu Date: Tue, 10 Dec 2024 11:53:08 +0200 Subject: [PATCH 33/33] add real amount of invoice --- src/Models/InvoiceTotals.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Models/InvoiceTotals.php b/src/Models/InvoiceTotals.php index 26077ca..016f77b 100644 --- a/src/Models/InvoiceTotals.php +++ b/src/Models/InvoiceTotals.php @@ -100,6 +100,16 @@ class InvoiceTotals */ public $vatBreakdown = []; + /** + * Get total amount of the invoice/credit note + * + * @return float + */ + public function getTotalAmount(): float + { + return $this->taxInclusiveAmount + $this->roundingAmount; + } + /** * Create instance from invoice *