From f5e6f0e9bd8c6bc004457d0ccfd879b4cd15639e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sun, 4 Dec 2022 10:32:16 +0100 Subject: [PATCH] Fixed rounding of amounts - Updated Invoice::getTotals() method - Updated InvoiceTotals::fromInvoice() method - Updated UblWriter class - Renamed rounding field "invoice/taxAmount" to "invoice/vatAmount" - Updated unit tests for InvoiceTest class > Fixes #31 --- src/Invoice.php | 7 ++- src/Models/InvoiceTotals.php | 65 ++++++++++++++------------- src/Writers/UblWriter.php | 85 ++++++++++-------------------------- tests/InvoiceTest.php | 13 +++--- 4 files changed, 66 insertions(+), 104 deletions(-) diff --git a/src/Invoice.php b/src/Invoice.php index 8526378..22af0d4 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -669,10 +669,9 @@ public function clearLines(): self { /** * Get invoice total - * @param boolean $round Whether to round values or not - * @return InvoiceTotals Invoice totals + * @return InvoiceTotals Invoice totals */ - public function getTotals(bool $round=true): InvoiceTotals { - return InvoiceTotals::fromInvoice($this, $round); + public function getTotals(): InvoiceTotals { + return InvoiceTotals::fromInvoice($this); } } diff --git a/src/Models/InvoiceTotals.php b/src/Models/InvoiceTotals.php index 68d4111..1917db4 100644 --- a/src/Models/InvoiceTotals.php +++ b/src/Models/InvoiceTotals.php @@ -86,11 +86,10 @@ class InvoiceTotals { /** * Create instance from invoice - * @param Invoice $inv Invoice instance - * @param boolean $round Whether to round values or not - * @return self Totals instance + * @param Invoice $inv Invoice instance + * @return self Totals instance */ - static public function fromInvoice(Invoice $inv, bool $round=true): InvoiceTotals { + static public function fromInvoice(Invoice $inv): InvoiceTotals { $totals = new self(); $vatMap = []; @@ -100,56 +99,60 @@ static public function fromInvoice(Invoice $inv, bool $round=true): InvoiceTotal // Process all invoice lines foreach ($inv->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); } + $totals->netAmount = $inv->round($totals->netAmount, 'invoice/netAmount'); - // Apply allowance and charge totals + // Process allowances foreach ($inv->getAllowances() as $item) { - $allowanceAmount = $item->getEffectiveAmount($totals->netAmount); + $allowanceAmount = $inv->round($item->getEffectiveAmount($totals->netAmount), 'line/allowanceChargeAmount'); $totals->allowancesAmount += $allowanceAmount; self::updateVatMap($vatMap, $item, -$allowanceAmount); } + $totals->allowancesAmount = $inv->round($totals->allowancesAmount, 'invoice/allowancesChargesAmount'); + + // Process charges foreach ($inv->getCharges() as $item) { - $chargeAmount = $item->getEffectiveAmount($totals->netAmount); + $chargeAmount = $inv->round($item->getEffectiveAmount($totals->netAmount), 'line/allowanceChargeAmount'); $totals->chargesAmount += $chargeAmount; self::updateVatMap($vatMap, $item, $chargeAmount); } + $totals->chargesAmount = $inv->round($totals->chargesAmount, 'invoice/allowancesChargesAmount'); // Calculate VAT amounts foreach ($vatMap as $item) { - $item->taxAmount = $item->taxableAmount * ($item->rate / 100); + $item->taxableAmount = $inv->round($item->taxableAmount, 'invoice/allowancesChargesAmount'); + $item->taxAmount = $inv->round($item->taxableAmount * ($item->rate / 100), 'invoice/vatAmount'); $totals->vatAmount += $item->taxAmount; } + $totals->vatAmount = $inv->round($totals->vatAmount, 'invoice/vatAmount'); - // Calculate rest of properties - $totals->taxExclusiveAmount = $totals->netAmount - $totals->allowancesAmount + $totals->chargesAmount; - $totals->taxInclusiveAmount = $totals->taxExclusiveAmount + $totals->vatAmount; - $totals->paidAmount = $inv->getPaidAmount(); - $totals->roundingAmount = $inv->getRoundingAmount(); + // Add custom VAT amount $totals->customVatAmount = $inv->getCustomVatAmount(); - $totals->payableAmount = $totals->taxInclusiveAmount - $totals->paidAmount + $totals->roundingAmount; + if ($totals->customVatAmount !== null) { + $totals->customVatAmount = $inv->round($inv->getCustomVatAmount(), 'invoice/vatAmount'); + } // Attach VAT breakdown $totals->vatBreakdown = array_values($vatMap); - // Round values - if ($round) { - $totals->netAmount = $inv->round($totals->netAmount, 'invoice/netAmount'); - $totals->allowancesAmount = $inv->round($totals->allowancesAmount, 'invoice/allowancesChargesAmount'); - $totals->chargesAmount = $inv->round($totals->chargesAmount, 'invoice/allowancesChargesAmount'); - $totals->vatAmount = $inv->round($totals->vatAmount, 'invoice/vatAmount'); - $totals->taxExclusiveAmount = $inv->round($totals->taxExclusiveAmount, 'invoice/taxExclusiveAmount'); - $totals->taxInclusiveAmount = $inv->round($totals->taxInclusiveAmount, 'invoice/taxInclusiveAmount'); - $totals->paidAmount = $inv->round($totals->paidAmount, 'invoice/paidAmount'); - $totals->roundingAmount = $inv->round($totals->roundingAmount, 'invoice/roundingAmount'); - $totals->payableAmount = $inv->round($totals->payableAmount, 'invoice/payableAmount'); - foreach ($totals->vatBreakdown as $item) { - $item->taxableAmount = $inv->round($item->taxableAmount, 'invoice/allowancesChargesAmount'); - $item->taxAmount = $inv->round($item->taxAmount, 'invoice/taxAmount'); - } - } + // Calculate rest of properties + $totals->taxExclusiveAmount = $inv->round( + $totals->netAmount - $totals->allowancesAmount + $totals->chargesAmount, + 'invoice/taxExclusiveAmount' + ); + $totals->taxInclusiveAmount = $inv->round( + $totals->taxExclusiveAmount + $totals->vatAmount, + 'invoice/taxInclusiveAmount' + ); + $totals->paidAmount = $inv->round($inv->getPaidAmount(), 'invoice/paidAmount'); + $totals->roundingAmount = $inv->round($inv->getRoundingAmount(), 'invoice/roundingAmount'); + $totals->payableAmount = $inv->round( + $totals->taxInclusiveAmount - $totals->paidAmount + $totals->roundingAmount, + 'invoice/payableAmount' + ); return $totals; } diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index b807d4f..18af343 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -25,7 +25,7 @@ class UblWriter extends AbstractWriter { * @inheritdoc */ public function export(Invoice $invoice): string { - $totals = $invoice->getTotals(false); + $totals = $invoice->getTotals(); $xml = UXML::newInstance('Invoice', null, [ 'xmlns' => self::NS_INVOICE, 'xmlns:cac' => self::NS_CAC, @@ -169,8 +169,8 @@ public function export(Invoice $invoice): string { } // Invoice totals - $this->addTaxTotalNodes($xml, $invoice, $totals); - $this->addDocumentTotalsNode($xml, $invoice, $totals); + $this->addTaxTotalNodes($xml, $totals); + $this->addDocumentTotalsNode($xml, $totals); // Invoice lines $lines = $invoice->getLines(); @@ -657,7 +657,7 @@ 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 Unrounded invoice totals or NULL in case at line level + * @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( @@ -721,36 +721,20 @@ private function addAllowanceOrCharge( /** * Add tax total nodes - * @param UXML $parent Parent element - * @param Invoice $invoice Invoice instance - * @param InvoiceTotals $totals Unrounded invoice totals + * @param UXML $parent Parent element + * @param InvoiceTotals $totals Invoice totals */ - private function addTaxTotalNodes(UXML $parent, Invoice $invoice, InvoiceTotals $totals) { + private function addTaxTotalNodes(UXML $parent, InvoiceTotals $totals) { $xml = $parent->add('cac:TaxTotal'); // Add tax amount - $this->addAmountNode( - $xml, - 'cbc:TaxAmount', - $invoice->round($totals->vatAmount, 'invoice/taxAmount'), - $totals->currency - ); + $this->addAmountNode($xml, 'cbc:TaxAmount', $totals->vatAmount, $totals->currency); // Add each tax details foreach ($totals->vatBreakdown as $item) { $vatBreakdownNode = $xml->add('cac:TaxSubtotal'); - $this->addAmountNode( - $vatBreakdownNode, - 'cbc:TaxableAmount', - $invoice->round($item->taxableAmount, 'invoice/allowancesChargesAmount'), - $totals->currency - ); - $this->addAmountNode( - $vatBreakdownNode, - 'cbc:TaxAmount', - $invoice->round($item->taxAmount, 'invoice/taxAmount'), - $totals->currency - ); + $this->addAmountNode($vatBreakdownNode, 'cbc:TaxableAmount', $item->taxableAmount, $totals->currency); + $this->addAmountNode($vatBreakdownNode, 'cbc:TaxAmount', $item->taxAmount, $totals->currency); $this->addVatNode( $vatBreakdownNode, 'cac:TaxCategory', @@ -767,7 +751,7 @@ private function addTaxTotalNodes(UXML $parent, Invoice $invoice, InvoiceTotals $this->addAmountNode( $parent->add('cac:TaxTotal'), 'cbc:TaxAmount', - $invoice->round($customVatAmount, 'invoice/taxAmount'), + $customVatAmount, $totals->vatCurrency ?? $totals->currency ); } @@ -776,55 +760,30 @@ private function addTaxTotalNodes(UXML $parent, Invoice $invoice, InvoiceTotals /** * Add document totals node - * @param UXML $parent Parent element - * @param Invoice $invoice Invoice instance - * @param InvoiceTotals $totals Unrounded invoice totals + * @param UXML $parent Parent element + * @param InvoiceTotals $totals Invoice totals */ - private function addDocumentTotalsNode(UXML $parent, Invoice $invoice, InvoiceTotals $totals) { + private function addDocumentTotalsNode(UXML $parent, InvoiceTotals $totals) { $xml = $parent->add('cac:LegalMonetaryTotal'); // Build totals matrix $totalsMatrix = []; - $totalsMatrix['cbc:LineExtensionAmount'] = $invoice->round( - $totals->netAmount, - 'invoice/netAmount' - ); - $totalsMatrix['cbc:TaxExclusiveAmount'] = $invoice->round( - $totals->taxExclusiveAmount, - 'invoice/taxExclusiveAmount' - ); - $totalsMatrix['cbc:TaxInclusiveAmount'] = $invoice->round( - $totals->taxInclusiveAmount, - 'invoice/taxInclusiveAmount' - ); + $totalsMatrix['cbc:LineExtensionAmount'] = $totals->netAmount; + $totalsMatrix['cbc:TaxExclusiveAmount'] = $totals->taxExclusiveAmount; + $totalsMatrix['cbc:TaxInclusiveAmount'] = $totals->taxInclusiveAmount; if ($totals->allowancesAmount > 0) { - $totalsMatrix['cbc:AllowanceTotalAmount'] = $invoice->round( - $totals->allowancesAmount, - 'invoice/allowancesChargesAmount' - ); + $totalsMatrix['cbc:AllowanceTotalAmount'] = $totals->allowancesAmount; } if ($totals->chargesAmount > 0) { - $totalsMatrix['cbc:ChargeTotalAmount'] = $invoice->round( - $totals->chargesAmount, - 'invoice/allowancesChargesAmount' - ); + $totalsMatrix['cbc:ChargeTotalAmount'] = $totals->chargesAmount; } if ($totals->paidAmount > 0) { - $totalsMatrix['cbc:PrepaidAmount'] = $invoice->round( - $totals->paidAmount, - 'invoice/paidAmount' - ); + $totalsMatrix['cbc:PrepaidAmount'] = $totals->paidAmount; } if ($totals->roundingAmount > 0) { - $totalsMatrix['cbc:PayableRoundingAmount'] = $invoice->round( - $totals->roundingAmount, - 'invoice/roundingAmount' - ); + $totalsMatrix['cbc:PayableRoundingAmount'] = $totals->roundingAmount; } - $totalsMatrix['cbc:PayableAmount'] = $invoice->round( - $totals->payableAmount, - 'invoice/payableAmount' - ); + $totalsMatrix['cbc:PayableAmount'] = $totals->payableAmount; // Create and append XML nodes foreach ($totalsMatrix as $field=>$amount) { diff --git a/tests/InvoiceTest.php b/tests/InvoiceTest.php index 3c597b5..165a3db 100644 --- a/tests/InvoiceTest.php +++ b/tests/InvoiceTest.php @@ -86,17 +86,17 @@ public function testCanRoundNegativeZeroes(): void { public function testDecimalMatrixIsUsed(): void { $this->invoice->setRoundingMatrix([ 'invoice/paidAmount' => 4, - 'invoice/netAmount' => 8, - '' => 3 + 'invoice/netAmount' => 5, + '' => 8, ])->setPaidAmount(123.456789) ->setRoundingAmount(987.654321) ->addLine((new InvoiceLine)->setPrice(12.121212121)) ->addLine((new InvoiceLine)->setPrice(34.343434343)); $totals = $this->invoice->getTotals(); - $this->assertEquals(123.4568, $totals->paidAmount); - $this->assertEquals(987.654, $totals->roundingAmount); - $this->assertEquals(46.46464646, $totals->netAmount); + $this->assertEquals(123.4568, $totals->paidAmount); + $this->assertEquals(987.654321, $totals->roundingAmount); + $this->assertEquals(46.46465, $totals->netAmount); } public function testTotalAmountsAreCalculatedCorrectly(): void { @@ -105,13 +105,14 @@ public function testTotalAmountsAreCalculatedCorrectly(): void { $firstLine = (new InvoiceLine)->setPrice(100)->setVatRate(10); $secondLine = (new InvoiceLine)->setPrice(200.5)->setVatRate(21); $this->invoice->clearLines() + ->setRoundingMatrix(['' => 8]) // Increase decimal precision ->setPaidAmount(10.2) ->addLine($firstLine) ->addLine($secondLine) ->addAllowance($allowance) ->addCharge($charge); - $totals = $this->invoice->getTotals(false); + $totals = $this->invoice->getTotals(); $this->assertEquals(300.5, $totals->netAmount); $this->assertEquals(12.34, $totals->allowancesAmount); $this->assertEquals(22.5375, $totals->chargesAmount);