Skip to content

Commit

Permalink
Merge pull request #136 from josemmo/develop
Browse files Browse the repository at this point in the history
v1.7.8
  • Loading branch information
josemmo authored Aug 6, 2023
2 parents 00c093b + e406808 commit 3c490fa
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 27 deletions.
4 changes: 2 additions & 2 deletions doc/ejemplos/envio-faceb2b.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require_once 'ruta/hacia/vendor/autoload.php';

use josemmo\Facturae\Facturae;
use josemmo\Facturae\FacturaeFile;
use josemmo\Facturae\Face\FaceB2bClient;
use josemmo\Facturae\Face\Faceb2bClient;

// Creamos una factura válida (ver ejemplo simple)
$fac = new Facturae();
Expand All @@ -24,7 +24,7 @@ $invoice = new FacturaeFile();
$invoice->loadData($fac->export(), "test-invoice.xsig");

// Creamos una conexión con FACe
$faceb2b = new FaceB2bClient("path_to_certificate.pfx", null, "passphrase");
$faceb2b = new Faceb2bClient("path_to_certificate.pfx", null, "passphrase");
//$faceb2b->setProduction(false); // Descomenta esta línea para entorno de desarrollo

// Subimos la factura a FACeB2B
Expand Down
2 changes: 1 addition & 1 deletion src/Facturae.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* Class for creating electronic invoices that comply with the Spanish FacturaE format.
*/
class Facturae {
const VERSION = "1.7.7";
const VERSION = "1.7.8";
const USER_AGENT = "FacturaePHP/" . self::VERSION;

const SCHEMA_3_2 = "3.2";
Expand Down
22 changes: 16 additions & 6 deletions src/FacturaeItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function __construct($properties=array()) {
// Catalog taxes property (backward compatibility)
if (isset($properties['taxes'])) {
foreach ($properties['taxes'] as $r=>$tax) {
if (empty($r)) continue;
if (!is_array($tax)) $tax = array("rate"=>$tax, "amount"=>0);
if (!isset($tax['isWithheld'])) { // Get value by default
$tax['isWithheld'] = Facturae::isWithheldTax($r);
Expand Down Expand Up @@ -115,18 +116,26 @@ public function getData($fac) {
$unitPriceWithoutTax = $this->unitPriceWithoutTax;
$totalAmountWithoutTax = $quantity * $unitPriceWithoutTax;

// NOTE: Special case for Schema v3.2
// In this schema, an item's total cost (<TotalCost />) has 6 decimals but taxable bases only have 2.
// We round the first property when using line precision mode to prevent the pair from having different values.
if ($fac->getSchemaVersion() === Facturae::SCHEMA_3_2) {
$totalAmountWithoutTax = $fac->pad($totalAmountWithoutTax, 'Tax/TaxableBase', Facturae::PRECISION_LINE);
}

// Process charges and discounts
$grossAmount = $totalAmountWithoutTax;
foreach (['discounts', 'charges'] as $i=>$groupTag) {
$factor = ($i == 0) ? -1 : 1;
foreach ($this->{$groupTag} as $group) {
if (isset($group['rate'])) {
$rate = $group['rate'];
$rate = $fac->pad($group['rate'], 'DiscountCharge/Rate', Facturae::PRECISION_LINE);
$amount = $totalAmountWithoutTax * ($rate / 100);
} else {
$rate = null;
$amount = $group['amount'];
}
$amount = $fac->pad($amount, 'DiscountCharge/Amount', Facturae::PRECISION_LINE);
$addProps[$groupTag][] = array(
"reason" => $group['reason'],
"rate" => $rate,
Expand All @@ -141,12 +150,13 @@ public function getData($fac) {
$totalTaxesWithheld = 0;
foreach (['taxesOutputs', 'taxesWithheld'] as $i=>$taxesGroup) {
foreach ($this->{$taxesGroup} as $type=>$tax) {
$taxRate = $tax['rate'];
$surcharge = $tax['surcharge'];
$taxAmount = $grossAmount * ($taxRate / 100);
$surchargeAmount = $grossAmount * ($surcharge / 100);
$taxRate = $fac->pad($tax['rate'], 'Tax/TaxRate', Facturae::PRECISION_LINE);
$surcharge = $fac->pad($tax['surcharge'], 'Tax/EquivalenceSurcharge', Facturae::PRECISION_LINE);
$taxableBase = $fac->pad($grossAmount, 'Tax/TaxableBase', Facturae::PRECISION_LINE);
$taxAmount = $fac->pad($taxableBase*($taxRate/100), 'Tax/TaxAmount', Facturae::PRECISION_LINE);
$surchargeAmount = $fac->pad($taxableBase*($surcharge/100), 'Tax/EquivalenceSurchargeAmount', Facturae::PRECISION_LINE);
$addProps[$taxesGroup][$type] = array(
"base" => $grossAmount,
"base" => $taxableBase,
"rate" => $taxRate,
"surcharge" => $surcharge,
"amount" => $taxAmount,
Expand Down
2 changes: 1 addition & 1 deletion src/FacturaeTraits/PropertiesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,7 @@ public function getTotals() {
if (!isset($totals[$taxGroup][$type])) {
$totals[$taxGroup][$type] = array();
}
$taxKey = $tax['rate'] . ":" . $tax['surcharge'];
$taxKey = floatval($tax['rate']) . ":" . floatval($tax['surcharge']);
if (!isset($totals[$taxGroup][$type][$taxKey])) {
$totals[$taxGroup][$type][$taxKey] = array(
"base" => 0,
Expand Down
83 changes: 66 additions & 17 deletions tests/PrecisionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,37 @@
use josemmo\Facturae\FacturaeItem;

final class PrecisionTest extends AbstractTest {
private function _runTest($schema, $precision) {
/**
* @param string $schema Invoice schema
* @param string $precision Rounding precision mode
*/
private function runTestWithParams($schema, $precision) {
$fac = $this->getBaseInvoice($schema);
$fac->setPrecision($precision);

// Add items
$amounts = [37.76, 26.8, 5.5];
foreach ($amounts as $i=>$amount) {
$items = [
['unitPriceWithoutTax'=>16.90, 'quantity'=>3.40, 'tax'=>10],
['unitPriceWithoutTax'=>5.90, 'quantity'=>1.20, 'tax'=>10],
['unitPriceWithoutTax'=>8.90, 'quantity'=>1.00, 'tax'=>10],
['unitPriceWithoutTax'=>8.90, 'quantity'=>1.75, 'tax'=>10],
['unitPriceWithoutTax'=>6.90, 'quantity'=>2.65, 'tax'=>10],
['unitPriceWithoutTax'=>5.90, 'quantity'=>1.80, 'tax'=>10],
['unitPriceWithoutTax'=>8.90, 'quantity'=>1.95, 'tax'=>10],
['unitPriceWithoutTax'=>3.00, 'quantity'=>11.30, 'tax'=>10],
['unitPriceWithoutTax'=>5.90, 'quantity'=>46.13, 'tax'=>10],
['unitPriceWithoutTax'=>37.76, 'quantity'=>1, 'tax'=>21],
['unitPriceWithoutTax'=>13.40, 'quantity'=>2, 'tax'=>21],
['unitPriceWithoutTax'=>5.50, 'quantity'=>1, 'tax'=>21]
];
foreach ($items as $i=>$item) {
$fac->addItem(new FacturaeItem([
"name" => "Línea de producto #$i",
"quantity" => 1,
"unitPriceWithoutTax" => $amount,
"taxes" => [Facturae::TAX_IVA => 21]
"unitPriceWithoutTax" => $item['unitPriceWithoutTax'],
"quantity" => $item['quantity'],
"taxes" => [
Facturae::TAX_IVA => $item['tax']
]
]));
}

Expand All @@ -32,15 +51,45 @@ private function _runTest($schema, $precision) {
$actualTotal = floatval($beforeTaxes + $taxOutputs - $taxesWithheld);
$this->assertEqualsWithDelta($actualTotal, $invoiceTotal, 0.000000001, 'Incorrect invoice totals element');

// Validate total invoice amount
if ($precision === Facturae::PRECISION_INVOICE) {
$expectedTotal = round(array_sum($amounts)*1.21, 2);
} else {
$expectedTotal = array_sum(array_map(function($amount) {
return round($amount*1.21, 2);
}, $amounts));
// Calculate expected invoice totals
$expectedTotal = 0;
$expectedTaxes = [];
$decimals = ($precision === Facturae::PRECISION_INVOICE) ? 15 : 2;
foreach ($items as $item) {
if (!isset($expectedTaxes[$item['tax']])) {
$expectedTaxes[$item['tax']] = [
"base" => 0,
"amount" => 0
];
}
$taxableBase = round($item['unitPriceWithoutTax'] * $item['quantity'], $decimals);
$taxAmount = round($taxableBase * ($item['tax']/100), $decimals);
$expectedTotal += $taxableBase + $taxAmount;
$expectedTaxes[$item['tax']]['base'] += $taxableBase;
$expectedTaxes[$item['tax']]['amount'] += $taxAmount;
}
foreach ($expectedTaxes as $key=>$value) {
$expectedTaxes[$key]['base'] = round($value['base'], 2);
$expectedTaxes[$key]['amount'] = round($value['amount'], 2);
}
$expectedTotal = round($expectedTotal, 2);

// Validate invoice total
// NOTE: When in invoice precision mode, we use a 1 cent tolerance as this mode prioritizes accurate invoice total
// over invoice lines totals. This is the maximum tolerance allowed by the FacturaE specification.
$tolerance = ($precision === Facturae::PRECISION_INVOICE) ? 0.01 : 0.000000001;
$this->assertEqualsWithDelta($expectedTotal, $invoiceTotal, $tolerance, 'Incorrect total invoice amount');

// Validate tax totals
foreach ($invoiceXml->TaxesOutputs->Tax as $taxNode) {
$rate = (float) $taxNode->TaxRate;
$actualBase = (float) $taxNode->TaxableBase->TotalAmount;
$actualAmount = (float) $taxNode->TaxAmount->TotalAmount;
$expectedBase = $expectedTaxes[$rate]['base'];
$expectedAmount = $expectedTaxes[$rate]['amount'];
$this->assertEqualsWithDelta($expectedBase, $actualBase, 0.000000001, "Incorrect taxable base for $rate% rate");
$this->assertEqualsWithDelta($expectedAmount, $actualAmount, 0.000000001, "Incorrect tax amount for $rate% rate");
}
$this->assertEqualsWithDelta($expectedTotal, $invoiceTotal, 0.000000001, 'Incorrect total invoice amount');
}


Expand All @@ -49,7 +98,7 @@ private function _runTest($schema, $precision) {
*/
public function testLinePrecision() {
foreach ([Facturae::SCHEMA_3_2, Facturae::SCHEMA_3_2_1] as $schema) {
$this->_runTest($schema, Facturae::PRECISION_LINE);
$this->runTestWithParams($schema, Facturae::PRECISION_LINE);
}
}

Expand All @@ -58,8 +107,8 @@ public function testLinePrecision() {
* Test invoice precision
*/
public function testInvoicePrecision() {
foreach ([Facturae::SCHEMA_3_2, Facturae::SCHEMA_3_2_1] as $schema) {
$this->_runTest($schema, Facturae::PRECISION_INVOICE);
foreach ([Facturae::SCHEMA_3_2_1] as $schema) {
$this->runTestWithParams($schema, Facturae::PRECISION_INVOICE);
}
}
}

0 comments on commit 3c490fa

Please sign in to comment.