diff --git a/Controller/Modelo349.php b/Controller/Modelo349.php index 3a50c49..39e8454 100644 --- a/Controller/Modelo349.php +++ b/Controller/Modelo349.php @@ -24,6 +24,7 @@ use FacturaScripts\Core\DataSrc\Empresas; use FacturaScripts\Core\DataSrc\Paises; use FacturaScripts\Core\Tools; +use FacturaScripts\Dinamic\Lib\Clave349; use FacturaScripts\Dinamic\Lib\Txt349Export; use FacturaScripts\Dinamic\Model\Serie; @@ -218,10 +219,10 @@ protected function loadData(): void $euIn = $this->getEuCountriesIn(); $this->salesData = $this->queryOperators( - 'facturascli', 'clientes', 'codcliente', 'idcontactofact', $dates, $euIn, 'E' + 'facturascli', 'clientes', 'codcliente', 'idcontactofact', $dates, $euIn, true ); $this->purchasesData = $this->queryOperators( - 'facturasprov', 'proveedores', 'codproveedor', 'idcontacto', $dates, $euIn, 'A' + 'facturasprov', 'proveedores', 'codproveedor', 'idcontacto', $dates, $euIn, false ); $this->salesTotals = ['total' => array_sum(array_column($this->salesData, 'base'))]; @@ -232,6 +233,20 @@ protected function loadData(): void } } + /** + * Obtiene los operadores intracomunitarios del periodo, acumulados por operador y clave de operación. + * Solo se incluyen las facturas cuya operación es intracomunitaria (bienes o servicios), + * ya que la clave se deduce del campo operacion de la factura. + * + * @param string $facturasTable Tabla de facturas (facturascli o facturasprov). + * @param string $terceroTable Tabla del tercero (clientes o proveedores). + * @param string $codField Campo que relaciona la factura con el tercero. + * @param string $contactField Campo del tercero con el contacto del que se toma el país. + * @param array $dates Fechas de inicio ('from') y fin ('to') del periodo. + * @param string $euIn Lista SQL de países de la UE. + * @param bool $esVenta true para ventas, false para compras. + * @return array + */ protected function queryOperators( string $facturasTable, string $terceroTable, @@ -239,15 +254,21 @@ protected function queryOperators( string $contactField, array $dates, string $euIn, - string $clave + bool $esVenta ): array { - $sql = 'SELECT t.cifnif, t.razonsocial, ct.codpais, SUM(f.neto) AS base' + $operaciones = array_map( + fn($operacion) => $this->dataBase->var2str($operacion), + Clave349::operaciones($esVenta) + ); + + $sql = 'SELECT t.cifnif, t.razonsocial, ct.codpais, f.operacion, SUM(f.neto) AS base' . ' FROM ' . $facturasTable . ' f' . ' INNER JOIN ' . $terceroTable . ' t ON t.' . $codField . ' = f.' . $codField . ' LEFT JOIN contactos ct ON ct.idcontacto = t.' . $contactField . ' WHERE f.fecha >= ' . $this->dataBase->var2str($dates['from']) . ' AND f.fecha <= ' . $this->dataBase->var2str($dates['to']) . ' AND f.editable = ' . $this->dataBase->var2str(false) + . ' AND f.operacion IN (' . implode(', ', $operaciones) . ')' . ' AND ct.codpais IN (' . $euIn . ')' . ' AND t.cifnif IS NOT NULL AND t.cifnif <> \'\''; @@ -255,9 +276,9 @@ protected function queryOperators( $sql .= ' AND f.codserie = ' . $this->dataBase->var2str($this->codserie); } - $sql .= ' GROUP BY t.cifnif, t.razonsocial, ct.codpais' + $sql .= ' GROUP BY t.cifnif, t.razonsocial, ct.codpais, f.operacion' . ' HAVING SUM(f.neto) > 0' - . ' ORDER BY t.cifnif ASC;'; + . ' ORDER BY t.cifnif ASC, f.operacion ASC;'; $result = []; foreach ($this->dataBase->select($sql) as $row) { @@ -266,7 +287,7 @@ protected function queryOperators( 'nombre' => $row['razonsocial'], 'codpais' => $row['codpais'], 'base' => round((float)$row['base'], 2), - 'clave' => $clave, + 'clave' => Clave349::porOperacion($esVenta, $row['operacion']), ]; } return $result; diff --git a/Lib/Clave349.php b/Lib/Clave349.php new file mode 100644 index 0000000..7436374 --- /dev/null +++ b/Lib/Clave349.php @@ -0,0 +1,90 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +namespace FacturaScripts\Plugins\Modelo349\Lib; + +use FacturaScripts\Core\Lib\InvoiceOperation; + +/** + * Determina la clave de operación del Modelo 349 a partir del campo "Operación" de la factura. + * + * Solo las facturas marcadas como intracomunitarias (bienes o servicios) forman parte del modelo. + * El resto, aunque el tercero sea de la UE, no tiene clave y queda fuera de la declaración. + * + * @author Esteban Sánchez Martínez + */ +class Clave349 +{ + /** @var string Adquisición intracomunitaria de bienes (compras). */ + const ADQUISICION_BIENES = 'A'; + + /** @var string Adquisición intracomunitaria de servicios (compras). */ + const ADQUISICION_SERVICIOS = 'I'; + + /** @var string Entrega intracomunitaria de bienes (ventas). */ + const ENTREGA_BIENES = 'E'; + + /** @var string Prestación intracomunitaria de servicios (ventas). */ + const PRESTACION_SERVICIOS = 'S'; + + /** + * Claves por operación de la factura para las ventas. + * + * @var array + */ + const VENTAS = [ + InvoiceOperation::INTRA_COMMUNITY => self::ENTREGA_BIENES, + InvoiceOperation::INTRA_COMMUNITY_SERVICES => self::PRESTACION_SERVICIOS, + ]; + + /** + * Claves por operación de la factura para las compras. + * + * @var array + */ + const COMPRAS = [ + InvoiceOperation::INTRA_COMMUNITY => self::ADQUISICION_BIENES, + InvoiceOperation::INTRA_COMMUNITY_SERVICES => self::ADQUISICION_SERVICIOS, + ]; + + /** + * Devuelve la clave de operación que corresponde a la operación de una factura, + * o null si esa operación no se declara en el Modelo 349. + * + * @param bool $esVenta true para facturas de cliente, false para facturas de proveedor. + * @param string|null $operacion Valor del campo operacion de la factura. + * @return string|null + */ + public static function porOperacion(bool $esVenta, ?string $operacion): ?string + { + $mapa = $esVenta ? self::VENTAS : self::COMPRAS; + return $mapa[$operacion ?? ''] ?? null; + } + + /** + * Devuelve las operaciones de factura que se declaran en el Modelo 349. + * + * @param bool $esVenta true para facturas de cliente, false para facturas de proveedor. + * @return array + */ + public static function operaciones(bool $esVenta): array + { + return array_keys($esVenta ? self::VENTAS : self::COMPRAS); + } +} diff --git a/Lib/Txt349Export.php b/Lib/Txt349Export.php index 6d69b90..1a4040f 100644 --- a/Lib/Txt349Export.php +++ b/Lib/Txt349Export.php @@ -37,8 +37,8 @@ class Txt349Export * * @param int $year * @param string $periodo '1T'|'2T'|'3T'|'4T'|'01'-'12' - * @param array $salesData Entregas intracomunitarias (clave E) - * @param array $purchasesData Adquisiciones intracomunitarias (clave A) + * @param array $salesData Ventas intracomunitarias (clave E entregas de bienes, S prestaciones de servicios) + * @param array $purchasesData Compras intracomunitarias (clave A adquisiciones de bienes, I de servicios) * @param string $justificante Número de 13 dígitos; si vacío se genera automáticamente * @param string $declarationType ''|'C'|'S' * @param string $justificanteAnterior diff --git a/Test/main/Clave349Test.php b/Test/main/Clave349Test.php new file mode 100644 index 0000000..d21ab3c --- /dev/null +++ b/Test/main/Clave349Test.php @@ -0,0 +1,69 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +namespace FacturaScripts\Test\Plugins; + +use FacturaScripts\Core\Lib\InvoiceOperation; +use FacturaScripts\Dinamic\Lib\Clave349; +use PHPUnit\Framework\TestCase; + +/** + * Comprueba que la clave del Modelo 349 se deduce de la operación de la factura. + * + * @author Esteban Sánchez Martínez + */ +final class Clave349Test extends TestCase +{ + public function testVentasDeBienesSonClaveE(): void + { + $this->assertSame('E', Clave349::porOperacion(true, InvoiceOperation::INTRA_COMMUNITY)); + } + + public function testVentasDeServiciosSonClaveS(): void + { + $this->assertSame('S', Clave349::porOperacion(true, InvoiceOperation::INTRA_COMMUNITY_SERVICES)); + } + + public function testComprasDeBienesSonClaveA(): void + { + $this->assertSame('A', Clave349::porOperacion(false, InvoiceOperation::INTRA_COMMUNITY)); + } + + public function testComprasDeServiciosSonClaveI(): void + { + $this->assertSame('I', Clave349::porOperacion(false, InvoiceOperation::INTRA_COMMUNITY_SERVICES)); + } + + public function testOperacionesNoIntracomunitariasNoTienenClave(): void + { + foreach ([true, false] as $esVenta) { + $this->assertNull(Clave349::porOperacion($esVenta, null)); + $this->assertNull(Clave349::porOperacion($esVenta, '')); + $this->assertNull(Clave349::porOperacion($esVenta, InvoiceOperation::EXPORT)); + $this->assertNull(Clave349::porOperacion($esVenta, InvoiceOperation::REVERSE_CHARGE)); + } + } + + public function testOperacionesDeclarablesSoloSonLasIntracomunitarias(): void + { + $esperado = [InvoiceOperation::INTRA_COMMUNITY, InvoiceOperation::INTRA_COMMUNITY_SERVICES]; + $this->assertSame($esperado, Clave349::operaciones(true)); + $this->assertSame($esperado, Clave349::operaciones(false)); + } +} diff --git a/Test/main/Txt349ExportTest.php b/Test/main/Txt349ExportTest.php index acfae22..5b46529 100644 --- a/Test/main/Txt349ExportTest.php +++ b/Test/main/Txt349ExportTest.php @@ -351,6 +351,26 @@ public function testExportClavePurchasesIsA(): void $this->assertEquals('A', $lines[1][132], 'La clave de adquisición intracomunitaria debe ser A'); } + public function testExportClaveSalesServicesIsS(): void + { + $operator = array_merge($this->sampleSalesOperator(), ['clave' => 'S']); + $lines = $this->exportLines([$operator], []); + if (empty($lines)) { + $this->markTestSkipped('No hay ejercicios disponibles'); + } + $this->assertEquals('S', $lines[1][132], 'La clave de prestación intracomunitaria de servicios debe ser S'); + } + + public function testExportClavePurchasesServicesIsI(): void + { + $operator = array_merge($this->samplePurchasesOperator(), ['clave' => 'I']); + $lines = $this->exportLines([], [$operator]); + if (empty($lines)) { + $this->markTestSkipped('No hay ejercicios disponibles'); + } + $this->assertEquals('I', $lines[1][132], 'La clave de adquisición intracomunitaria de servicios debe ser I'); + } + public function testExportNormalDeclaracionType(): void { $lines = $this->exportLines([$this->sampleSalesOperator()], []);