Skip to content

Commit

Permalink
Fixed rounding of amounts
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
josemmo committed Dec 4, 2022
1 parent b669eb9 commit f5e6f0e
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 104 deletions.
7 changes: 3 additions & 4 deletions src/Invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
65 changes: 34 additions & 31 deletions src/Models/InvoiceTotals.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand All @@ -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;
}
Expand Down
85 changes: 22 additions & 63 deletions src/Writers/UblWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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',
Expand All @@ -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
);
}
Expand All @@ -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) {
Expand Down
13 changes: 7 additions & 6 deletions tests/InvoiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down

0 comments on commit f5e6f0e

Please sign in to comment.