Skip to content

Commit

Permalink
Merge pull request #104 from eclipxe13/fix-exentos
Browse files Browse the repository at this point in the history
Fix SumasConceptos with Exentos (version 2.23.3)
  • Loading branch information
eclipxe13 authored Aug 11, 2022
2 parents ff6b5c7 + 44ad84d commit 500ec47
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 43 deletions.
8 changes: 8 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
- Merge methods from `\CfdiUtils\Nodes\NodeHasValueInterface` into `\CfdiUtils\Nodes\NodeInterface`.
- Remove deprecated constant `CfdiUtils\Retenciones\Retenciones::RET_NAMESPACE`.

## Version 2.23.3 2022-08-11

Fix CFDI 4.0, must include `Comprobante/Impuestos/Traslados/Traslado@TipoFactor=Exento` when exists at least one
node `Comprobante/Conceptos/Concepto/Impuestos/Traslados/Traslado@TipoFactor=Exento`.
The node must contain attribute `TipoFactor=Exento` and the rounded sum of the attributes `Base`
grouped by attribute `Impuesto`.
Thanks `BrodyAG` for noticing this issue, and `@yairtestas` for your guidance to find the solution.

## Version 2.23.2 2022-06-29

Use `Symfony/Process` instead of `ShellExec`.
Expand Down
35 changes: 34 additions & 1 deletion src/CfdiUtils/SumasConceptos/SumasConceptos.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ class SumasConceptos
*/
private $traslados = [];

/**
* @var array<string, array{Impuesto:string, TipoFactor:string, Base:float}>
*/
private $exentos = [];

/**
* @var array<string, array{Impuesto:string, Importe:float}>
*/
Expand Down Expand Up @@ -109,7 +114,9 @@ private function addConcepto(NodeInterface $concepto)

$traslados = $concepto->searchNodes('cfdi:Impuestos', 'cfdi:Traslados', 'cfdi:Traslado');
foreach ($traslados as $traslado) {
if ('Exento' !== $traslado['TipoFactor']) {
if ('Exento' === $traslado['TipoFactor']) {
$this->addExento($traslado);
} else {
$this->addTraslado($traslado);
}
}
Expand Down Expand Up @@ -168,6 +175,19 @@ private function addTraslado(NodeInterface $traslado)
$this->traslados[$key]['Base'] += (float) $traslado['Base'];
}

private function addExento(NodeInterface $exento)
{
$key = $this->impuestoKey($exento['Impuesto'], $exento['TipoFactor'], '');
if (! array_key_exists($key, $this->exentos)) {
$this->exentos[$key] = [
'TipoFactor' => $exento['TipoFactor'],
'Impuesto' => $exento['Impuesto'],
'Base' => 0.0,
];
}
$this->exentos[$key]['Base'] += (float) $exento['Base'];
}

private function addRetencion(NodeInterface $retencion)
{
$key = $this->impuestoKey($retencion['Impuesto']);
Expand Down Expand Up @@ -212,6 +232,14 @@ public function getTraslados(): array
return $this->traslados;
}

/**
* @return array<string, array{Impuesto:string, TipoFactor:string, Base:float}>
*/
public function getExentos(): array
{
return $this->exentos;
}

/**
* @return array<string, array{Impuesto:string, Importe:float}>
*/
Expand All @@ -225,6 +253,11 @@ public function hasTraslados(): bool
return ([] !== $this->traslados);
}

public function hasExentos(): bool
{
return ([] !== $this->exentos);
}

public function hasRetenciones(): bool
{
return ([] !== $this->retenciones);
Expand Down
37 changes: 27 additions & 10 deletions src/CfdiUtils/SumasConceptos/SumasConceptosWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class SumasConceptosWriter
/** @var bool */
private $writeImpuestoBase;

/** @var bool */
private $writeExentos;

/**
* Writer constructor.
* @param Comprobante33|Comprobante40 $comprobante
Expand All @@ -34,8 +37,10 @@ public function __construct(
) {
if ($comprobante instanceof Comprobante33) {
$this->writeImpuestoBase = false;
$this->writeExentos = false;
} elseif ($comprobante instanceof Comprobante40) {
$this->writeImpuestoBase = true;
$this->writeExentos = true;
} else {
throw new InvalidArgumentException(
'The argument $comprobante must be a Comprobante (CFDI 3.3 or CFDI 4.0) element'
Expand Down Expand Up @@ -69,7 +74,10 @@ private function putImpuestosNode(): void
// obtain node reference
$impuestos = $this->comprobante->getImpuestos();
// if there is nothing to write then remove the children and exit
if (! $this->sumas->hasTraslados() && ! $this->sumas->hasRetenciones()) {
if (! $this->sumas->hasTraslados()
&& ! $this->sumas->hasRetenciones()
&& ! ($this->writeExentos && $this->sumas->hasExentos())
) {
$this->comprobante->children()->remove($impuestos);
return;
}
Expand All @@ -79,14 +87,19 @@ private function putImpuestosNode(): void
if ($this->sumas->hasTraslados()) {
$impuestos['TotalImpuestosTrasladados'] = $this->format($this->sumas->getImpuestosTrasladados());
$impuestos->getTraslados()->multiTraslado(
...$this->getImpuestosContents($this->sumas->getTraslados(), $this->writeImpuestoBase)
...$this->getImpuestosContents($this->sumas->getTraslados(), $this->writeImpuestoBase, true)
);
}
if ($this->writeExentos && $this->sumas->hasExentos()) {
$impuestos->getTraslados()->multiTraslado(
...$this->getImpuestosContents($this->sumas->getExentos(), $this->writeImpuestoBase, false)
);
}
// add retenciones when needed
if ($this->sumas->hasRetenciones()) {
$impuestos['TotalImpuestosRetenidos'] = $this->format($this->sumas->getImpuestosRetenidos());
$impuestos->getRetenciones()->multiRetencion(
...$this->getImpuestosContents($this->sumas->getRetenciones(), false)
...$this->getImpuestosContents($this->sumas->getRetenciones(), false, true)
);
}
}
Expand All @@ -110,16 +123,15 @@ private function putComplementoImpuestoLocalSumas(): void
$impLocal->attributes()->set('TotaldeTraslados', $this->format($this->sumas->getLocalesImpuestosTrasladados()));
}

private function getImpuestosContents(array $impuestos, bool $hasBase): array
private function getImpuestosContents(array $impuestos, bool $hasBase, bool $hasImporte): array
{
$return = [];
foreach ($impuestos as $impuesto) {
$impuesto['Base'] = $this->format($impuesto['Base'] ?? 0);
$impuesto['Importe'] = $this->format($impuesto['Importe']);
if (! $hasBase) {
unset($impuesto['Base']);
}
$return[] = $impuesto;
$impuesto['Base'] = ($hasBase) ? $this->format($impuesto['Base'] ?? 0) : null;
$impuesto['Importe'] = ($hasImporte) ? $this->format($impuesto['Importe']) : null;
$return[] = array_filter($impuesto, function ($value): bool {
return null !== $value;
});
}
return $return;
}
Expand Down Expand Up @@ -154,4 +166,9 @@ public function hasWriteImpuestoBase(): bool
{
return $this->writeImpuestoBase;
}

public function hasWriteExentos(): bool
{
return $this->writeExentos;
}
}
111 changes: 111 additions & 0 deletions tests/CfdiUtilsTests/CreateComprobanteWithOnlyExentosCaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace CfdiUtilsTests;

use CfdiUtils\CfdiCreator33;
use CfdiUtils\CfdiCreator40;
use CfdiUtils\Nodes\Node;
use CfdiUtils\Nodes\XmlNodeUtils;

final class CreateComprobanteWithOnlyExentosCaseTest extends TestCase
{
public function testCreateCcomprobante33WithOnlyExentosWriteImpuestos(): void
{
$creator = new CfdiCreator33([]);

$comprobante = $creator->comprobante();
$comprobante->addConcepto([
'ClaveProdServ' => '01010101',
'NoIdentificacion' => 'FOO',
'Cantidad' => '1',
'ClaveUnidad' => 'E48',
'Descripcion' => 'HONORARIOS MEDICOS',
'ValorUnitario' => '617.000000',
'Importe' => '617.000000',
'Descuento' => '144.271240',
'ObjetoImp' => '02',
])->addTraslado([
'Impuesto' => '002',
'TipoFactor' => 'Exento',
]);

// test sumasConceptos

$precision = 2;
$sumasConceptos = $creator->buildSumasConceptos($precision);
$this->assertTrue($sumasConceptos->hasExentos());
$this->assertFalse($sumasConceptos->hasTraslados());
$this->assertFalse($sumasConceptos->hasRetenciones());

$expectedExentos = [
'002:Exento:' => [
'TipoFactor' => 'Exento',
'Impuesto' => '002',
'Base' => 0,
],
];

$this->assertEquals($expectedExentos, $sumasConceptos->getExentos());

// test cfdi:Impuestos XML does not exists

$creator->addSumasConceptos($sumasConceptos, $precision);

$this->assertNull($comprobante->searchNode('cfdi:Impuestos'));
}

public function testCreateCcomprobante40WithOnlyExentosWriteImpuestos(): void
{
$creator = new CfdiCreator40([]);

$comprobante = $creator->comprobante();
$comprobante->addConcepto([
'ClaveProdServ' => '01010101',
'NoIdentificacion' => 'FOO',
'Cantidad' => '1',
'ClaveUnidad' => 'E48',
'Descripcion' => 'HONORARIOS MEDICOS',
'ValorUnitario' => '617.000000',
'Importe' => '617.000000',
'Descuento' => '144.271240',
'ObjetoImp' => '02',
])->addTraslado([
'Base' => '472.728760',
'Impuesto' => '002',
'TipoFactor' => 'Exento',
]);

// test sumasConceptos

$precision = 2;
$sumasConceptos = $creator->buildSumasConceptos($precision);
$this->assertTrue($sumasConceptos->hasExentos());
$this->assertFalse($sumasConceptos->hasTraslados());
$this->assertFalse($sumasConceptos->hasRetenciones());

$expectedExentos = [
'002:Exento:' => [
'TipoFactor' => 'Exento',
'Impuesto' => '002',
'Base' => 472.72876,
],
];

$this->assertEquals($expectedExentos, $sumasConceptos->getExentos());

// test cfdi:Impuestos XML

$creator->addSumasConceptos($sumasConceptos, $precision);

$expectedImpuestosNode = new Node('cfdi:Impuestos', [], [
new Node('cfdi:Traslados', [], [
new Node('cfdi:Traslado', ['TipoFactor' => 'Exento', 'Impuesto' => '002', 'Base' => '472.73']),
]),
]);

$this->assertXmlStringEqualsXmlString(
XmlNodeUtils::nodeToXmlString($expectedImpuestosNode),
XmlNodeUtils::nodeToXmlString($comprobante->searchNode('cfdi:Impuestos'))
);
}
}
Loading

0 comments on commit 500ec47

Please sign in to comment.