Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions Controller/Modelo349.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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'))];
Expand All @@ -232,32 +233,52 @@ 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,
string $codField,
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 <> \'\'';

if (!empty($this->codserie)) {
$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) {
Expand All @@ -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;
Expand Down
90 changes: 90 additions & 0 deletions Lib/Clave349.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php
/**
* This file is part of Modelo349 plugin for FacturaScripts
* Copyright (C) 2026 Carlos Garcia Gomez <carlos@facturascripts.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/

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 <esteban@factura.city>
*/
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);
}
}
4 changes: 2 additions & 2 deletions Lib/Txt349Export.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions Test/main/Clave349Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php
/**
* This file is part of Modelo349 plugin for FacturaScripts
* Copyright (C) 2026 Carlos Garcia Gomez <carlos@facturascripts.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/

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 <esteban@factura.city>
*/
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));
}
}
20 changes: 20 additions & 0 deletions Test/main/Txt349ExportTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()], []);
Expand Down
Loading