diff --git a/src/Invoice.php b/src/Invoice.php index 1684c16..3fba3e5 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -16,6 +16,7 @@ use function array_splice; use function count; use function is_subclass_of; +use function round; class Invoice { const DEFAULT_DECIMALS = 8; @@ -85,6 +86,21 @@ public function getDecimals(string $field): int { } + /** + * Round value + * @param float $value Value to round + * @param string $field Field name + * @return float Rounded value + */ + public function round(float $value, string $field): float { + $rounded = round($value, $this->getDecimals($field)); + if ($rounded == 0) { + $rounded += 0; // To fix negative zero + } + return $rounded; + } + + /** * Set rounding matrix * @param array $matrix Rounding matrix diff --git a/src/Models/InvoiceTotals.php b/src/Models/InvoiceTotals.php index f58ea8e..68d4111 100644 --- a/src/Models/InvoiceTotals.php +++ b/src/Models/InvoiceTotals.php @@ -4,7 +4,6 @@ use Einvoicing\Invoice; use Einvoicing\Traits\VatTrait; use function array_values; -use function round; class InvoiceTotals { /** @@ -137,18 +136,18 @@ static public function fromInvoice(Invoice $inv, bool $round=true): InvoiceTotal // Round values if ($round) { - $totals->netAmount = round($totals->netAmount, $inv->getDecimals('invoice/netAmount')); - $totals->allowancesAmount = round($totals->allowancesAmount, $inv->getDecimals('invoice/allowancesChargesAmount')); - $totals->chargesAmount = round($totals->chargesAmount, $inv->getDecimals('invoice/allowancesChargesAmount')); - $totals->vatAmount = round($totals->vatAmount, $inv->getDecimals('invoice/vatAmount')); - $totals->taxExclusiveAmount = round($totals->taxExclusiveAmount, $inv->getDecimals('invoice/taxExclusiveAmount')); - $totals->taxInclusiveAmount = round($totals->taxInclusiveAmount, $inv->getDecimals('invoice/taxInclusiveAmount')); - $totals->paidAmount = round($totals->paidAmount, $inv->getDecimals('invoice/paidAmount')); - $totals->roundingAmount = round($totals->roundingAmount, $inv->getDecimals('invoice/roundingAmount')); - $totals->payableAmount = round($totals->payableAmount, $inv->getDecimals('invoice/payableAmount')); + $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 = round($item->taxableAmount, $inv->getDecimals('invoice/allowancesChargesAmount')); - $item->taxAmount = round($item->taxAmount, $inv->getDecimals('invoice/taxAmount')); + $item->taxableAmount = $inv->round($item->taxableAmount, 'invoice/allowancesChargesAmount'); + $item->taxAmount = $inv->round($item->taxAmount, 'invoice/taxAmount'); } } diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 19d8cc5..0cbca06 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -15,7 +15,6 @@ use Einvoicing\Payments\Transfer; use UXML\UXML; use function in_array; -use function round; class UblWriter extends AbstractWriter { const NS_INVOICE = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"; @@ -698,7 +697,7 @@ private function addAllowanceOrCharge( $this->addAmountNode( $xml, 'cbc:Amount', - round($item->getEffectiveAmount($baseAmount), $invoice->getDecimals('line/allowanceChargeAmount')), + $invoice->round($item->getEffectiveAmount($baseAmount), 'line/allowanceChargeAmount'), $invoice->getCurrency() ); @@ -707,7 +706,7 @@ private function addAllowanceOrCharge( $this->addAmountNode( $xml, 'cbc:BaseAmount', - round($baseAmount, $invoice->getDecimals('line/netAmount')), + $invoice->round($baseAmount, 'line/netAmount'), $invoice->getCurrency() ); } @@ -732,7 +731,7 @@ private function addTaxTotalNodes(UXML $parent, Invoice $invoice, InvoiceTotals $this->addAmountNode( $xml, 'cbc:TaxAmount', - round($totals->vatAmount, $invoice->getDecimals('invoice/taxAmount')), + $invoice->round($totals->vatAmount, 'invoice/taxAmount'), $totals->currency ); @@ -742,13 +741,13 @@ private function addTaxTotalNodes(UXML $parent, Invoice $invoice, InvoiceTotals $this->addAmountNode( $vatBreakdownNode, 'cbc:TaxableAmount', - round($item->taxableAmount, $invoice->getDecimals('invoice/allowancesChargesAmount')), + $invoice->round($item->taxableAmount, 'invoice/allowancesChargesAmount'), $totals->currency ); $this->addAmountNode( $vatBreakdownNode, 'cbc:TaxAmount', - round($item->taxAmount, $invoice->getDecimals('invoice/taxAmount')), + $invoice->round($item->taxAmount, 'invoice/taxAmount'), $totals->currency ); $this->addVatNode( @@ -767,7 +766,7 @@ private function addTaxTotalNodes(UXML $parent, Invoice $invoice, InvoiceTotals $this->addAmountNode( $parent->add('cac:TaxTotal'), 'cbc:TaxAmount', - round($customVatAmount, $invoice->getDecimals('invoice/taxAmount')), + $invoice->round($customVatAmount, 'invoice/taxAmount'), $totals->vatCurrency ?? $totals->currency ); } @@ -785,45 +784,45 @@ private function addDocumentTotalsNode(UXML $parent, Invoice $invoice, InvoiceTo // Build totals matrix $totalsMatrix = []; - $totalsMatrix['cbc:LineExtensionAmount'] = round( + $totalsMatrix['cbc:LineExtensionAmount'] = $invoice->round( $totals->netAmount, - $invoice->getDecimals('invoice/netAmount') + 'invoice/netAmount' ); - $totalsMatrix['cbc:TaxExclusiveAmount'] = round( + $totalsMatrix['cbc:TaxExclusiveAmount'] = $invoice->round( $totals->taxExclusiveAmount, - $invoice->getDecimals('invoice/taxExclusiveAmount') + 'invoice/taxExclusiveAmount' ); - $totalsMatrix['cbc:TaxInclusiveAmount'] = round( + $totalsMatrix['cbc:TaxInclusiveAmount'] = $invoice->round( $totals->taxInclusiveAmount, - $invoice->getDecimals('invoice/taxInclusiveAmount') + 'invoice/taxInclusiveAmount' ); if ($totals->allowancesAmount > 0) { - $totalsMatrix['cbc:AllowanceTotalAmount'] = round( + $totalsMatrix['cbc:AllowanceTotalAmount'] = $invoice->round( $totals->allowancesAmount, - $invoice->getDecimals('invoice/allowancesChargesAmount') + 'invoice/allowancesChargesAmount' ); } if ($totals->chargesAmount > 0) { - $totalsMatrix['cbc:ChargeTotalAmount'] = round( + $totalsMatrix['cbc:ChargeTotalAmount'] = $invoice->round( $totals->chargesAmount, - $invoice->getDecimals('invoice/allowancesChargesAmount') + 'invoice/allowancesChargesAmount' ); } if ($totals->paidAmount > 0) { - $totalsMatrix['cbc:PrepaidAmount'] = round( + $totalsMatrix['cbc:PrepaidAmount'] = $invoice->round( $totals->paidAmount, - $invoice->getDecimals('invoice/paidAmount') + 'invoice/paidAmount' ); } if ($totals->roundingAmount > 0) { - $totalsMatrix['cbc:PayableRoundingAmount'] = round( + $totalsMatrix['cbc:PayableRoundingAmount'] = $invoice->round( $totals->roundingAmount, - $invoice->getDecimals('invoice/roundingAmount') + 'invoice/roundingAmount' ); } - $totalsMatrix['cbc:PayableAmount'] = round( + $totalsMatrix['cbc:PayableAmount'] = $invoice->round( $totals->payableAmount, - $invoice->getDecimals('invoice/payableAmount') + 'invoice/payableAmount' ); // Create and append XML nodes @@ -868,7 +867,7 @@ private function addLineNode(UXML $parent, InvoiceLine $line, Invoice $invoice, $this->addAmountNode( $xml, 'cbc:LineExtensionAmount', - round($netAmount, $invoice->getDecimals('line/netAmount')), + $invoice->round($netAmount, 'line/netAmount'), $invoice->getCurrency() ); } @@ -960,7 +959,7 @@ private function addLineNode(UXML $parent, InvoiceLine $line, Invoice $invoice, $this->addAmountNode( $priceNode, 'cbc:PriceAmount', - round($price, $invoice->getDecimals('line/price')), + $invoice->round($price, 'line/price'), $invoice->getCurrency() ); } diff --git a/tests/InvoiceTest.php b/tests/InvoiceTest.php index f2264b5..491d369 100644 --- a/tests/InvoiceTest.php +++ b/tests/InvoiceTest.php @@ -52,6 +52,15 @@ public function testCannotRemoveOutOfBoundsLines(): void { $this->invoice->addLine(new InvoiceLine)->removeLine(1); } + public function testCanRoundNegativeZeroes(): void { + $this->assertEquals('-1', (string) $this->invoice->round(-0.9999, 'invoice/netAmount')); + $this->assertEquals('0', (string) $this->invoice->round(-0.0001, 'invoice/netAmount')); + $this->assertEquals('0', (string) $this->invoice->round(-0, 'invoice/netAmount')); + $this->assertEquals('0', (string) $this->invoice->round(0, 'invoice/netAmount')); + $this->assertEquals('0', (string) $this->invoice->round(0.0001, 'invoice/netAmount')); + $this->assertEquals('1', (string) $this->invoice->round(0.9999, 'invoice/netAmount')); + } + public function testDecimalMatrixIsUsed(): void { $this->invoice->setRoundingMatrix([ 'invoice/paidAmount' => 4,