From 9018bd9a8a6e059d36437f97ea2b130e4ba7775d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20S=C3=A1nchez?= Date: Fri, 16 Oct 2026 18:13:34 +0200 Subject: [PATCH 1/2] =?UTF-8?q?A=C3=B1adir=20soporte=20para=20claves=20de?= =?UTF-8?q?=20operaci=C3=B3n=20en=20el=20Modelo=20349?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Se ha creado la clase Clave349 para determinar las claves de operación basadas en el tipo de factura (ventas o compras). - Se ha modificado el método queryOperators en Modelo349.php para incluir la lógica de claves de operación. - Se han actualizado los comentarios en Txt349Export.php para reflejar los cambios en la estructura de datos. - Se han añadido pruebas unitarias en Clave349Test.php para validar el correcto funcionamiento de las claves de operación. Archivos modificados: - Modelo349.php - Txt349Export.php - Clave349.php (nuevo) - Clave349Test.php (nuevo) --- Controller/Modelo349.php | 35 ++++++++++--- Lib/Clave349.php | 90 ++++++++++++++++++++++++++++++++++ Lib/Txt349Export.php | 4 +- Test/main/Clave349Test.php | 69 ++++++++++++++++++++++++++ Test/main/Txt349ExportTest.php | 20 ++++++++ 5 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 Lib/Clave349.php create mode 100644 Test/main/Clave349Test.php 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..d5d5e75 --- /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..3327e4b --- /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()], []); From 651c6c11d357f426aa87625699825778fcfc1c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20S=C3=A1nchez?= Date: Fri, 16 Oct 2026 18:19:23 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Actualiza=20la=20informaci=C3=B3n=20de=20co?= =?UTF-8?q?pyright=20en=20Clave349.php=20y=20Clave349Test.php?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se modifica el nombre del titular de los derechos de autor en los archivos Clave349.php y Clave349Test.php para reflejar el cambio de autoría. --- Lib/Clave349.php | 2 +- Test/main/Clave349Test.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/Clave349.php b/Lib/Clave349.php index d5d5e75..7436374 100644 --- a/Lib/Clave349.php +++ b/Lib/Clave349.php @@ -1,7 +1,7 @@ + * Copyright (C) 2026 Carlos Garcia Gomez * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as diff --git a/Test/main/Clave349Test.php b/Test/main/Clave349Test.php index 3327e4b..d21ab3c 100644 --- a/Test/main/Clave349Test.php +++ b/Test/main/Clave349Test.php @@ -1,7 +1,7 @@ + * Copyright (C) 2026 Carlos Garcia Gomez * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as