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()], []);