From 365b1e131fc8d5f37b49072128146d69835d2e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Fern=C3=A1ndez=20Gim=C3=A9nez?= Date: Thu, 11 Jun 2026 16:32:39 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat:=204542=20mapear=20casillas=20del=20mo?= =?UTF-8?q?delo=20303=20seg=C3=BAn=20el=20tipo=20de=20operaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El reparto a casillas se decidía solo por la cuenta especial y el tipo impositivo, ignorando la operación de la factura. Así, una compra con inversión del sujeto pasivo caía en las casillas de ventas nacionales (07/09) en lugar de 12/13 + 28/29, y las intracomunitarias no se separaban correctamente. - PartidaImpuestoResumen aporta la operación (JOIN por idasiento mediante subconsultas que solo exponen idasiento y operacion, para no crear columnas ambiguas) y el tipo de documento (compra/venta). - Modelo303 enruta por operación: ISP -> 12/13 + 28/29; intracomunitarias y servicios -> 10/11 + 36/37; importación -> 32/33; ventas exentas (intracom 59, exportación 60, ISP 122) desde las facturas. Las partidas de autorrepercusión de ventas intracomunitarias (neto cero) se excluyen del régimen general. - casillaMap con comodín para que ninguna tasa se pierda y avisos cuando un importe no encaja en ninguna casilla. - La vista muestra las casillas informativas 59, 60 y 122. - Test unitario del reparto de casillas para todos los casos. Co-Authored-By: Claude Opus 4.8 (1M context) --- Lib/Modelo303.php | 287 +++++++++++++++++++++----- Model/Join/PartidaImpuestoResumen.php | 34 ++- Test/main/Modelo303SquaresTest.php | 202 ++++++++++++++++++ Translation/es_ES.json | 7 +- View/Modelo303.html.twig | 41 ++++ 5 files changed, 516 insertions(+), 55 deletions(-) create mode 100644 Test/main/Modelo303SquaresTest.php diff --git a/Lib/Modelo303.php b/Lib/Modelo303.php index 9983f21..2d169f5 100644 --- a/Lib/Modelo303.php +++ b/Lib/Modelo303.php @@ -21,7 +21,10 @@ use FacturaScripts\Core\Base\DataBase; use FacturaScripts\Core\Tools; +use FacturaScripts\Core\Where; +use FacturaScripts\Dinamic\Lib\InvoiceOperation; use FacturaScripts\Dinamic\Model\Ejercicio; +use FacturaScripts\Dinamic\Model\FacturaCliente; use FacturaScripts\Dinamic\Model\Join\PartidaImpuestoResumen; /** @@ -34,11 +37,11 @@ class Modelo303 private const MAX_SQUARE = 200; /** - * Stores all model squares. - * Each key is the AEAT square number. - * '01' => 0.00, '02' => 0.00, ... + * Warnings collected while loading data (amounts not assigned to any square). + * + * @var string[] */ - private array $square; + private array $avisos = []; /** * Structure for know to assign values to squares. @@ -58,11 +61,10 @@ class Modelo303 '21' => ['base' => '07', 'cuota' => '09'], ], - // Adquisiciones intracomunitarias - 'IVARUE' => ['21' => ['base' => '10', 'cuota' => '11']], - - // Operaciones con inversión del sujeto pasivo - // TODO: 'xxxxx' => ['21' => ['base' => '12', 'cuota' => '13']], + // Adquisiciones intracomunitarias (cualquier tipo va a 10/11) + 'IVARUE' => [ + '*' => ['base' => '10', 'cuota' => '11'], + ], // Recargo de equivalencia 'IVARRE' => [ @@ -74,38 +76,38 @@ class Modelo303 ], // Operaciones exentas - 'IVAREX' => ['0' => ['base' => '150', 'cuota' => null]], + 'IVAREX' => ['*' => ['base' => '150', 'cuota' => null]], /* * IVA soportado (deducible) * Compras nacionales (régimen general) */ 'IVASOP' => [ - '21' => ['base' => '28', 'cuota' => '29'], - '10' => ['base' => '28', 'cuota' => '29'], - '4' => ['base' => '28', 'cuota' => '29'], + '*' => ['base' => '28', 'cuota' => '29'], ], - // Compras en importaciones + // Compras en importaciones (cualquier tipo va a 32/33) 'IVASIM' => [ - '21' => ['base' => '32', 'cuota' => '33'], - '10' => ['base' => '32', 'cuota' => '33'], - '4' => ['base' => '32', 'cuota' => '33'], - '0' => ['base' => '32', 'cuota' => '33'], + '*' => ['base' => '32', 'cuota' => '33'], ], - // Compras en adquisiciones intracomunitarias + // Compras en adquisiciones intracomunitarias (cualquier tipo va a 36/37) 'IVASUE' => [ - '21' => ['base' => '36', 'cuota' => '37'], - '10' => ['base' => '36', 'cuota' => '37'], - '4' => ['base' => '36', 'cuota' => '37'], - '0' => ['base' => '36', 'cuota' => '37'], + '*' => ['base' => '36', 'cuota' => '37'], ], - // Operaciones exentas - 'IVASEX' => ['0' => ['base' => '60', 'cuota' => null]], + // Operaciones exentas: las compras exentas no se deducen ni figuran en el + // resultado del régimen general; se reconocen para no generar avisos. + 'IVASEX' => ['*' => ['base' => null, 'cuota' => null]], ]; + /** + * Stores all model squares. + * Each key is the AEAT square number. + * '01' => 0.00, '02' => 0.00, ... + */ + private array $square; + /** * Initializes the tax rates for each square. */ @@ -154,6 +156,17 @@ public function casillaStr(string $square, bool $showEmpty = false): string return Tools::number($value, 2); } + /** + * Returns the list of warnings collected while loading data + * (amounts that could not be assigned to any square). + * + * @return string[] + */ + public function getAvisos(): array + { + return $this->avisos; + } + /** * Loads summary data from an array of PartidaImpuestoResumen. * @@ -164,6 +177,8 @@ public function loadFromResumen(array $resumen): void foreach ($resumen as $item) { $this->addMovimiento( $item->codcuentaesp ?? '', + $item->operacion ?? '', + $item->tipodoc ?? '', (float) $item->iva, (float) $item->recargo, (float) $item->baseimponible, @@ -173,6 +188,35 @@ public function loadFromResumen(array $resumen): void $this->calculateTotals(); } + /** + * Loads the exempt/informative tax bases of sales invoices (without VAT entries) + * into their squares: intracommunity (59), exports (60) and reverse charge (122). + * + * @param int $idempresa + * @param string $codejercicio + * @param string $dateStart + * @param string $dateEnd + */ + public function loadFromSalesInvoices(int $idempresa, string $codejercicio, string $dateStart, string $dateEnd): void + { + $where = [ + Where::eq('idempresa', $idempresa), + Where::eq('codejercicio', $codejercicio), + Where::gte('fecha', $dateStart), + Where::lte('fecha', $dateEnd), + Where::in('operacion', [ + InvoiceOperation::INTRA_COMMUNITY, + InvoiceOperation::INTRA_COMMUNITY_SERVICES, + InvoiceOperation::EXPORT, + InvoiceOperation::REVERSE_CHARGE, + ]), + ]; + + foreach (FacturaCliente::all($where, [], 0, 0) as $invoice) { + $this->addBaseExentaVenta((string)$invoice->operacion, (float)$invoice->neto); + } + } + public static function treasury(string $codejercicio, string $period): array { // comprobamos que el ejercicio existe @@ -192,21 +236,76 @@ public static function treasury(string $codejercicio, string $period): array ]; } + /** + * Adds the exempt tax base of a sales invoice to its informative square. + * + * @param string $operacion + * @param float $base + * @return void + */ + private function addBaseExentaVenta(string $operacion, float $base): void + { + switch ($operacion) { + case InvoiceOperation::EXPORT: + $this->square['60'] += $base; + break; + + case InvoiceOperation::INTRA_COMMUNITY: + case InvoiceOperation::INTRA_COMMUNITY_SERVICES: + $this->square['59'] += $base; + break; + + case InvoiceOperation::REVERSE_CHARGE: + $this->square['122'] += $base; + break; + } + } + + /** + * Adds a base and/or quota amount to the given squares. + * + * @param string|null $baseSquare + * @param string|null $cuotaSquare + * @param float $base + * @param float $cuota + * @return void + */ + private function addCasilla(?string $baseSquare, ?string $cuotaSquare, float $base, float $cuota): void + { + if (false === empty($baseSquare)) { + $this->square[$baseSquare] += $base; + } + + if (false === empty($cuotaSquare)) { + $this->square[$cuotaSquare] += $cuota; + } + } + /** * Add a tax movement to the model (base + quota by type and rate) - * - Determine the correct square based on the type and tax rate. - * - Update the base and quota squares accordingly. + * - Special operations (reverse charge, intracommunity, import) are routed by + * the invoice operation type to their specific squares. + * - The rest follows the casillaMap by special account and tax rate. * - * @param string $tipo + * @param string $tipo cuenta especial de IVA (IVAREP, IVASOP, IVARUE, ...) + * @param string $operacion tipo de operación de la factura (intracomunitaria, inv-sujeto-pasivo, ...) + * @param string $tipodoc 'compra' o 'venta' * @param float $iva * @param float $recargo * @param float $base * @param float $cuota * @return void */ - private function addMovimiento(string $tipo, float $iva, float $recargo, float $base, float $cuota): void + private function addMovimiento(string $tipo, string $operacion, string $tipodoc, float $iva, float $recargo, float $base, float $cuota): void { + // Las operaciones especiales (ISP, intracomunitarias, importación) deciden la casilla + // por el tipo de operación de la factura, no solo por la cuenta especial. + if ($this->addMovimientoEspecial($tipo, $operacion, $tipodoc, $base, $cuota)) { + return; + } + if (false === isset($this->casillaMap[$tipo])) { + $this->registrarNoMapeado($tipo, $operacion, $base, $cuota); return; } @@ -219,20 +318,77 @@ private function addMovimiento(string $tipo, float $iva, float $recargo, float $ ?? null; if ($grupo === null) { + $this->registrarNoMapeado($tipo, $operacion, $base, $cuota, $tax); return; } - // Update base and quota squares. - if (false === empty($grupo['base'])) { - $this->square[$grupo['base']] += $base; + // For recargo, if cuota is zero, calculate it from base and recargo rate + if ($tipo === 'IVARRE' && $cuota == 0.0 && $recargo > 0.0) { + $cuota = $base * ($recargo / 100.0); } - if (false === empty($grupo['cuota'])) { - // For recargo, if cuota is zero, calculate it from base and recargo rate - if ($tipo === 'IVARRE' && $cuota == 0.0 && $recargo > 0.0) { - $cuota = $base * ($recargo / 100.0); - } - $this->square[$grupo['cuota']] += $cuota; + $this->addCasilla($grupo['base'] ?? null, $grupo['cuota'] ?? null, $base, $cuota); + } + + /** + * Routes movements of special invoice operations (reverse charge, intracommunity, import) + * to their specific squares. Returns true if the movement has been handled (or intentionally + * skipped) and must not follow the standard mapping. + * + * @param string $tipo + * @param string $operacion + * @param string $tipodoc + * @param float $base + * @param float $cuota + * @return bool + */ + private function addMovimientoEspecial(string $tipo, string $operacion, string $tipodoc, float $base, float $cuota): bool + { + $esDevengado = in_array($tipo, ['IVAREP', 'IVARUE', 'IVAREX'], true); + $esDeducible = in_array($tipo, ['IVASOP', 'IVASUE', 'IVASIM', 'IVASEX'], true); + + // El recargo de equivalencia y cuentas no fiscales siguen el tratamiento estándar. + if (false === $esDevengado && false === $esDeducible) { + return false; + } + + switch ($operacion) { + case InvoiceOperation::REVERSE_CHARGE: + // Inversión del sujeto pasivo en COMPRAS: autorrepercusión. + // Devengado → 12/13, deducible → 28/29 (el IVA se compensa). + if ($tipodoc === 'compra') { + $esDevengado + ? $this->addCasilla('12', '13', $base, $cuota) + : $this->addCasilla('28', '29', $base, $cuota); + } + // En ventas con ISP el vendedor no repercute IVA; la base informativa (122) + // se carga desde las facturas en loadFromSalesInvoices(). + return true; + + case InvoiceOperation::INTRA_COMMUNITY: + case InvoiceOperation::INTRA_COMMUNITY_SERVICES: + // Adquisición intracomunitaria (COMPRA): devengado → 10/11, deducible → 36/37. + if ($tipodoc === 'compra') { + $esDevengado + ? $this->addCasilla('10', '11', $base, $cuota) + : $this->addCasilla('36', '37', $base, $cuota); + } + // Entrega intracomunitaria (VENTA): exenta. Las partidas de autorrepercusión + // (IVARUE/IVASUE) se compensan y no forman parte del régimen general; la base + // informativa (casilla 59) se carga desde las facturas. + return true; + + case InvoiceOperation::IMPORT: + // Importación (COMPRA): el IVA lo liquida la aduana (DUA). Solo el soportado deducible. + if ($esDeducible) { + $this->addCasilla('32', '33', $base, $cuota); + } + // El IVA devengado de importación (casilla 77 / IVA diferido) no tiene origen + // contable automático en FacturaScripts. + return true; + + default: + return false; } } @@ -271,23 +427,31 @@ private function calculateTotals(): void $this->square['46'] = $this->square['27'] - $this->square['45']; } - protected static function treasurySaldoCuenta(string $cuenta, string $desde, string $hasta, DataBase $dataBase): float + /** + * Records a warning for an amount that could not be assigned to any square, + * so the user can understand why the summary may not match the purchases/sales tabs. + * + * @param string $tipo + * @param string $operacion + * @param float $base + * @param float $cuota + * @param float|null $tax + * @return void + */ + private function registrarNoMapeado(string $tipo, string $operacion, float $base, float $cuota, ?float $tax = null): void { - $saldo = 0.0; - - if ($dataBase->tableExists('partidas')) { - // calculamos el saldo de todos aquellos asientos que afecten a caja - $sql = "select sum(debe-haber) as total from partidas where codsubcuenta LIKE " . $dataBase->var2str($cuenta) - . " and idasiento in (select idasiento from asientos where fecha >= " . $dataBase->var2str($desde) - . " and fecha <= " . $dataBase->var2str($hasta) . ");"; - - $data = $dataBase->select($sql); - if ($data && $data[0]['total'] !== null) { - $saldo = floatval($data[0]['total']); - } + // No avisamos si no hay importe relevante. + if (empty($base) && empty($cuota)) { + return; } - return $saldo; + $this->avisos[] = Tools::lang()->trans('model303-amount-without-square', [ + '%type%' => $tipo, + '%rate%' => $tax === null ? '-' : Tools::number($tax, 2), + '%operation%' => $operacion === '' ? '-' : $operacion, + '%base%' => Tools::number($base, 2), + '%quota%' => Tools::number($cuota, 2), + ]); } protected static function treasuryDates(Ejercicio $exercise, string $period): array @@ -320,4 +484,23 @@ protected static function treasuryDates(Ejercicio $exercise, string $period): ar ], }; } + + protected static function treasurySaldoCuenta(string $cuenta, string $desde, string $hasta, DataBase $dataBase): float + { + $saldo = 0.0; + + if ($dataBase->tableExists('partidas')) { + // calculamos el saldo de todos aquellos asientos que afecten a caja + $sql = "select sum(debe-haber) as total from partidas where codsubcuenta LIKE " . $dataBase->var2str($cuenta) + . " and idasiento in (select idasiento from asientos where fecha >= " . $dataBase->var2str($desde) + . " and fecha <= " . $dataBase->var2str($hasta) . ");"; + + $data = $dataBase->select($sql); + if ($data && $data[0]['total'] !== null) { + $saldo = floatval($data[0]['total']); + } + } + + return $saldo; + } } diff --git a/Model/Join/PartidaImpuestoResumen.php b/Model/Join/PartidaImpuestoResumen.php index d63c850..b17fdf8 100644 --- a/Model/Join/PartidaImpuestoResumen.php +++ b/Model/Join/PartidaImpuestoResumen.php @@ -45,6 +45,15 @@ protected function getFields(): array 'codcuentaesp' => 'COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)', 'tipo_desc' => 'cuentasesp.descripcion', + // operación de la factura (compra o venta) que originó el asiento, + // necesaria para decidir las casillas del 303 en operaciones especiales + // (intracomunitaria, inversión del sujeto pasivo, importación, etc.) + 'operacion' => "COALESCE(facturasprov.operacion, facturascli.operacion, '')", + + // tipo de documento: una compra y una venta intracomunitarias generan las mismas + // partidas de IVA (IVARUE/IVASUE), pero van a casillas distintas del 303. + 'tipodoc' => $this->sqlForTipoDoc(), + 'baseimponible' => $this->sqlForRoundedSum('partidas.baseimponible'), 'debe' => $this->sqlForRoundedSum('partidas.debe'), 'haber' => $this->sqlForRoundedSum('partidas.haber'), @@ -65,7 +74,9 @@ protected function getGroupFields(): string . ', partidas.recargo' . ', subcuentas.descripcion' . ', COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)' - . ', cuentasesp.descripcion'; + . ', cuentasesp.descripcion' + . ", COALESCE(facturasprov.operacion, facturascli.operacion, '')" + . ', ' . $this->sqlForTipoDoc(); } /** @@ -80,7 +91,12 @@ protected function getSQLFrom(): string . ' INNER JOIN subcuentas on subcuentas.idsubcuenta = partidas.idsubcuenta' . ' INNER JOIN cuentas on cuentas.idcuenta = subcuentas.idcuenta' . ' LEFT JOIN cuentasesp on cuentasesp.codcuentaesp = coalesce(subcuentas.codcuentaesp, cuentas.codcuentaesp)' - . ' LEFT JOIN series on series.codserie = partidas.codserie'; + . ' LEFT JOIN series on series.codserie = partidas.codserie' + // enlazamos la factura por el asiento contable para conocer el tipo de operación. + // Usamos subconsultas que solo exponen idasiento y operacion para no introducir + // columnas duplicadas (p.ej. "fecha") que harían ambiguos otros filtros. + . ' LEFT JOIN (SELECT idasiento, operacion FROM facturasprov) facturasprov on facturasprov.idasiento = asientos.idasiento' + . ' LEFT JOIN (SELECT idasiento, operacion FROM facturascli) facturascli on facturascli.idasiento = asientos.idasiento'; } /** @@ -97,6 +113,8 @@ protected function getTables(): array 'cuentas', 'cuentasesp', 'series', + 'facturasprov', + 'facturascli', ]; } @@ -137,4 +155,16 @@ private function sqlForRoundedSum(string $field): string { return 'ROUND(CAST(SUM(' . $field . ') AS DECIMAL(20, 6)), 2)'; } + + /** + * SQL snippet to know if the entry comes from a purchase or a sale invoice. + * + * @return string + */ + private function sqlForTipoDoc(): string + { + return "CASE WHEN facturasprov.idasiento IS NOT NULL THEN 'compra' + WHEN facturascli.idasiento IS NOT NULL THEN 'venta' + ELSE '' END"; + } } diff --git a/Test/main/Modelo303SquaresTest.php b/Test/main/Modelo303SquaresTest.php new file mode 100644 index 0000000..f55705c --- /dev/null +++ b/Test/main/Modelo303SquaresTest.php @@ -0,0 +1,202 @@ + + * + * 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\Dinamic\Lib\InvoiceOperation; +use FacturaScripts\Plugins\Modelo303\Lib\Modelo303; +use PHPUnit\Framework\TestCase; + +/** + * Pruebas unitarias del reparto de importes a casillas del modelo 303. + * No necesita base de datos: alimenta Modelo303 con filas simuladas del resumen. + */ +final class Modelo303SquaresTest extends TestCase +{ + public function testCompraImportacion(): void + { + $modelo = new Modelo303(); + $modelo->loadFromResumen([ + $this->row('IVASIM', 21, 5000.0, 1050.0, InvoiceOperation::IMPORT, 'compra'), + ]); + + $this->assertEqualsWithDelta(5000.0, $modelo->casilla('32'), 0.001); + $this->assertEqualsWithDelta(1050.0, $modelo->casilla('33'), 0.001); + } + + public function testCompraIntracomunitaria(): void + { + $modelo = new Modelo303(); + // intracomunitaria con cuentas específicas IVARUE/IVASUE + $modelo->loadFromResumen([ + $this->row('IVARUE', 21, 4000.0, 840.0, InvoiceOperation::INTRA_COMMUNITY, 'compra'), + $this->row('IVASUE', 21, 4000.0, 840.0, InvoiceOperation::INTRA_COMMUNITY, 'compra'), + ]); + + // devengado -> 10/11 + $this->assertEqualsWithDelta(4000.0, $modelo->casilla('10'), 0.001); + $this->assertEqualsWithDelta(840.0, $modelo->casilla('11'), 0.001); + // deducible -> 36/37 + $this->assertEqualsWithDelta(4000.0, $modelo->casilla('36'), 0.001); + $this->assertEqualsWithDelta(840.0, $modelo->casilla('37'), 0.001); + $this->assertEqualsWithDelta(0.0, $modelo->casilla('46'), 0.001); + } + + public function testCompraIntracomunitariaConCuentasGenericas(): void + { + $modelo = new Modelo303(); + // intracomunitaria pero contabilizada en IVAREP/IVASOP genéricos: + // el tipo de operación debe forzar igualmente 10/11 y 36/37 + $modelo->loadFromResumen([ + $this->row('IVAREP', 21, 1000.0, 210.0, InvoiceOperation::INTRA_COMMUNITY_SERVICES, 'compra'), + $this->row('IVASOP', 21, 1000.0, 210.0, InvoiceOperation::INTRA_COMMUNITY_SERVICES, 'compra'), + ]); + + $this->assertEqualsWithDelta(1000.0, $modelo->casilla('10'), 0.001); + $this->assertEqualsWithDelta(210.0, $modelo->casilla('11'), 0.001); + $this->assertEqualsWithDelta(1000.0, $modelo->casilla('36'), 0.001); + $this->assertEqualsWithDelta(210.0, $modelo->casilla('37'), 0.001); + // no debe ir a las casillas nacionales (07/09) ni a las de ISP (12/13) + $this->assertEqualsWithDelta(0.0, $modelo->casilla('09'), 0.001); + $this->assertEqualsWithDelta(0.0, $modelo->casilla('13'), 0.001); + } + + public function testCompraInversionSujetoPasivo(): void + { + $modelo = new Modelo303(); + // autorrepercusión: IVAREP (devengado) + IVASOP (deducible), mismo importe + $modelo->loadFromResumen([ + $this->row('IVAREP', 21, 3000.0, 630.0, InvoiceOperation::REVERSE_CHARGE, 'compra'), + $this->row('IVASOP', 21, 3000.0, 630.0, InvoiceOperation::REVERSE_CHARGE, 'compra'), + ]); + + // devengado -> 12/13 + $this->assertEqualsWithDelta(3000.0, $modelo->casilla('12'), 0.001); + $this->assertEqualsWithDelta(630.0, $modelo->casilla('13'), 0.001); + // deducible -> 28/29 + $this->assertEqualsWithDelta(3000.0, $modelo->casilla('28'), 0.001); + $this->assertEqualsWithDelta(630.0, $modelo->casilla('29'), 0.001); + + // el IVA se compensa: resultado del régimen general (46) = 0 + $this->assertEqualsWithDelta(0.0, $modelo->casilla('46'), 0.001); + // y NO debe contaminar las casillas de ventas nacionales (07/09) + $this->assertEqualsWithDelta(0.0, $modelo->casilla('09'), 0.001); + } + + public function testCompraNacionalDeducible(): void + { + $modelo = new Modelo303(); + $modelo->loadFromResumen([ + $this->row('IVASOP', 21, 2000.0, 420.0, '', 'compra'), + // tipo no contemplado en el mapa antiguo (5%): debe sumarse igual via comodín + $this->row('IVASOP', 5, 100.0, 5.0, '', 'compra'), + ]); + + $this->assertEqualsWithDelta(2100.0, $modelo->casilla('28'), 0.001); + $this->assertEqualsWithDelta(425.0, $modelo->casilla('29'), 0.001); + $this->assertEqualsWithDelta(425.0, $modelo->casilla('45'), 0.001); + $this->assertEmpty($modelo->getAvisos()); + } + + public function testImporteSinCasillaGeneraAviso(): void + { + $modelo = new Modelo303(); + // cuenta especial desconocida con importe: debe generar aviso y no perderse en silencio + $modelo->loadFromResumen([ + $this->row('IVAXXX', 21, 1000.0, 210.0, '', 'venta'), + ]); + + $this->assertNotEmpty($modelo->getAvisos()); + } + + public function testRecargoEquivalencia(): void + { + $modelo = new Modelo303(); + $modelo->loadFromResumen([ + $this->row('IVARRE', 21, 1000.0, 52.0, '', 'venta', 5.2), + ]); + + // recargo 5.2 -> base 22 / cuota 24 + $this->assertEqualsWithDelta(1000.0, $modelo->casilla('22'), 0.001); + $this->assertEqualsWithDelta(52.0, $modelo->casilla('24'), 0.001); + } + + public function testVentaIntracomunitariaNoContaminaRegimenGeneral(): void + { + $modelo = new Modelo303(); + // una venta intracomunitaria genera IVARUE/IVASUE (neto cero); NO debe sumar en 10/11/36/37 + $modelo->loadFromResumen([ + $this->row('IVARUE', 21, 7000.0, 1470.0, InvoiceOperation::INTRA_COMMUNITY, 'venta'), + $this->row('IVASUE', 21, 7000.0, 1470.0, InvoiceOperation::INTRA_COMMUNITY, 'venta'), + ]); + + $this->assertEqualsWithDelta(0.0, $modelo->casilla('10'), 0.001); + $this->assertEqualsWithDelta(0.0, $modelo->casilla('11'), 0.001); + $this->assertEqualsWithDelta(0.0, $modelo->casilla('36'), 0.001); + $this->assertEqualsWithDelta(0.0, $modelo->casilla('37'), 0.001); + $this->assertEqualsWithDelta(0.0, $modelo->casilla('46'), 0.001); + } + + public function testVentaNacionalRegimenGeneral(): void + { + $modelo = new Modelo303(); + $modelo->loadFromResumen([ + $this->row('IVAREP', 21, 1000.0, 210.0), + $this->row('IVAREP', 10, 500.0, 50.0), + $this->row('IVAREP', 4, 100.0, 4.0), + ]); + + // 21% -> base 07 / cuota 09 + $this->assertEqualsWithDelta(1000.0, $modelo->casilla('07'), 0.001); + $this->assertEqualsWithDelta(210.0, $modelo->casilla('09'), 0.001); + // 10% -> base 04 / cuota 06 + $this->assertEqualsWithDelta(500.0, $modelo->casilla('04'), 0.001); + $this->assertEqualsWithDelta(50.0, $modelo->casilla('06'), 0.001); + // 4% -> base 01 / cuota 03 + $this->assertEqualsWithDelta(100.0, $modelo->casilla('01'), 0.001); + $this->assertEqualsWithDelta(4.0, $modelo->casilla('03'), 0.001); + + // total devengado (casilla 27) = 210 + 50 + 4 + $this->assertEqualsWithDelta(264.0, $modelo->casilla('27'), 0.001); + $this->assertEmpty($modelo->getAvisos()); + } + + /** + * Crea una fila equivalente a una de PartidaImpuestoResumen. + */ + private function row( + string $codcuentaesp, + float $iva, + float $base, + float $cuota, + string $operacion = '', + string $tipodoc = '', + float $recargo = 0.0 + ): object { + return (object)[ + 'codcuentaesp' => $codcuentaesp, + 'operacion' => $operacion, + 'tipodoc' => $tipodoc, + 'iva' => $iva, + 'recargo' => $recargo, + 'baseimponible' => $base, + 'cuota' => $cuota, + ]; + } +} diff --git a/Translation/es_ES.json b/Translation/es_ES.json index 61aec41..d6e83a6 100644 --- a/Translation/es_ES.json +++ b/Translation/es_ES.json @@ -21,5 +21,10 @@ "year-model-390": "Año (modelo 390)", "deductible-vat-28": "por operaciones interiores corrientes", "deductible-vat-32": "por importaciones", - "deductible-vat-36": "por adquisiciones intracomunitarias" + "deductible-vat-36": "por adquisiciones intracomunitarias", + "info-operations-no-vat": "Operaciones sin repercusión de IVA (informativas)", + "intra-community-deliveries": "Entregas intracomunitarias de bienes y servicios", + "exports": "Exportaciones y operaciones asimiladas", + "sales-reverse-charge": "Ventas con inversión del sujeto pasivo", + "model303-amount-without-square": "Hay un importe que no se refleja en ninguna casilla del modelo 303: cuenta %type%, tipo %rate%%, operación %operation%, base %base%, cuota %quota%. Revise la configuración de cuentas especiales o el tipo de operación de la factura." } diff --git a/View/Modelo303.html.twig b/View/Modelo303.html.twig index dccd27c..f8d39b1 100644 --- a/View/Modelo303.html.twig +++ b/View/Modelo303.html.twig @@ -289,3 +289,44 @@ +
+
+
+
+ {{ trans('info-operations-no-vat') }} +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{{ trans('tax-base') }}
{{ trans('intra-community-deliveries') }}59 + {{ fsc.modelo303.casillaStr('59') }} +
{{ trans('exports') }}60 + {{ fsc.modelo303.casillaStr('60') }} +
{{ trans('sales-reverse-charge') }}122 + {{ fsc.modelo303.casillaStr('122') }} +
+
+
+
From fa342409abad3f00a6638ea8b6c229562df8652b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Fern=C3=A1ndez=20Gim=C3=A9nez?= Date: Thu, 11 Jun 2026 16:32:52 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix:=204542=20corregir=20desfase=20de=20c?= =?UTF-8?q?=C3=A9ntimos=20unificando=20consultas=20y=20cuota?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Las pestañas Resumen, Compras/Ventas y Cuentas no cuadraban porque usaban tres consultas distintas, con filtros y cálculo de la cuota diferentes. - EditRegularizacionImpuesto: nuevo criterio común (commonTaxWhere) usado por las tres consultas (misma empresa+ejercicio+fechas, exclusión de todos los asientos de regularización, series.siniva=false, exclusión de apertura/cierre y selección de cuentas por grupo de cuenta especial en lugar del LIKE 477%/472%). Además carga las bases exentas de ventas y muestra los avisos del resumen. - PartidaImpuesto: la cuota es siempre el importe contable real (debe/haber con corrección de signo en bases negativas), nunca un recálculo base*tipo; el destino IVA/recargo lo decide la cuenta especial. - VatRegularizationToAccounting: getSubtotals se alinea con el mismo criterio (idempresa, siniva y exclusión de regularizaciones). Co-Authored-By: Claude Opus 4.8 (1M context) --- Controller/EditRegularizacionImpuesto.php | 204 ++++++++++-------- .../VatRegularizationToAccounting.php | 67 +++--- Model/Join/PartidaImpuesto.php | 28 +-- 3 files changed, 169 insertions(+), 130 deletions(-) diff --git a/Controller/EditRegularizacionImpuesto.php b/Controller/EditRegularizacionImpuesto.php index 3cc6de3..4ccc63b 100644 --- a/Controller/EditRegularizacionImpuesto.php +++ b/Controller/EditRegularizacionImpuesto.php @@ -93,6 +93,44 @@ protected function checkInvoicesWithoutAccounting($model): void } } + /** + * Builds the common filter used by the summary, purchases/sales and accounting-entry + * queries, so the three of them always work over the SAME set of accounting entries. + * + * @param int $group grupo de cuentas especiales (TAX_ALL, TAX_INPUT, TAX_OUTPUT) + * @return array + */ + private function commonTaxWhere(int $group): array + { + $model = $this->getModel(); + $excludedOperations = implode(',', [Asiento::OPERATION_OPENING, Asiento::OPERATION_CLOSING]); + + // ids de los asientos de TODAS las regularizaciones (para excluirlos) + $regIds = []; + foreach (RegularizacionImpuesto::all() as $reg) { + if ($reg->idasiento) { + $regIds[] = $reg->idasiento; + } + } + + $subAccountTools = new SubAccountTools(); + $where = [ + Where::eq('asientos.idempresa', $model->idempresa), + Where::eq('asientos.codejercicio', $model->codejercicio), + Where::gte('asientos.fecha', $model->fechainicio), + Where::lte('asientos.fecha', $model->fechafin), + Where::eq('COALESCE(series.siniva, false)', false), + Where::notIn("COALESCE(asientos.operacion, '')", $excludedOperations), + $subAccountTools->whereForSpecialAccounts('COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)', $group), + ]; + + if (false === empty($regIds)) { + $where[] = Where::notIn('asientos.idasiento', implode(',', $regIds)); + } + + return $where; + } + /** * Create accounting entry action procedure. * @@ -199,6 +237,22 @@ protected function createViewsTaxSummary(string $viewName = 'Modelo303'): void $this->modelo303 = new Modelo303(); } + /** + * Setup actions for view. + * + * @param string $viewName + * @param bool $clickable + * @return void + */ + private function disableButtons(string $viewName, bool $clickable = false): void + { + $this->setSettings($viewName, 'btnDelete', false) + ->setSettings('btnNew', false) + ->setSettings('checkBoxes', false) + ->setSettings('clickable', $clickable) + ->setSettings('btnPrint', true); + } + /** * Run the actions that alter data before reading it. * @@ -270,78 +324,6 @@ protected function exportAction(): void $this->exportManager->show($this->response); } - /** - * Load data view procedure - * - * @param string $viewName - * @param BaseView $view - * @throws Exception - */ - protected function loadData($viewName, $view): void - { - switch ($viewName) { - case 'EditRegularizacionImpuesto': - parent::loadData($viewName, $view); - $this->settingsMainView(); - break; - - case 'ListPartidaImpuestoResumen': - $mainModel = $this->getModel(); - $excludedOperations = implode(',', [Asiento::OPERATION_OPENING, Asiento::OPERATION_CLOSING]); - $where = [ - Where::sub([ - Where::like('partidas.codsubcuenta', '477%'), - Where::orLike('partidas.codsubcuenta', '472%'), - ]), - Where::eq('asientos.idempresa', $mainModel->idempresa), - Where::gte('asientos.fecha', $mainModel->fechainicio), - Where::lte('asientos.fecha', $mainModel->fechafin), - Where::eq('COALESCE(series.siniva, false)', false), - Where::notIn("COALESCE(asientos.operacion, '')", $excludedOperations), - ]; - - if (false === empty($mainModel->idasiento)) { - $where[] = Where::notEq('partidas.idasiento', $mainModel->idasiento); - } - $view->loadData(false, $where, [ - 'COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)' => 'ASC', - 'partidas.codsubcuenta' => 'ASC', - ]); - - $this->modelo303->loadFromResumen($view->cursor); // Load data into Modelo303 View - $this->checkInvoicesWithoutAccounting($mainModel); - break; - - case 'ListPartida': - $this->getListPartida($view); - break; - - case 'ListPartidaImpuesto-1': - $this->getListPartidaImpuesto($view, SubAccountTools::SPECIAL_GROUP_TAX_INPUT); - break; - - case 'ListPartidaImpuesto-2': - $this->getListPartidaImpuesto($view, SubAccountTools::SPECIAL_GROUP_TAX_OUTPUT); - break; - } - } - - /** - * Setup actions for view. - * - * @param string $viewName - * @param bool $clickable - * @return void - */ - private function disableButtons(string $viewName, bool $clickable = false): void - { - $this->setSettings($viewName, 'btnDelete', false) - ->setSettings('btnNew', false) - ->setSettings('checkBoxes', false) - ->setSettings('clickable', $clickable) - ->setSettings('btnPrint', true); - } - /** * Load data for accounting entry. * @@ -383,27 +365,65 @@ private function getListPartidaImpuesto(BaseView $view, int $group): void */ private function getPartidaImpuestoWhere(int $group): array { - // obtenemos todos los ids de los asientos de las regularizaciones - $ids = []; - foreach (RegularizacionImpuesto::all() as $reg) { - if ($reg->idasiento) { - $ids[] = $reg->idasiento; - } - } + return $this->commonTaxWhere($group); + } - $subAccountTools = new SubAccountTools(); - return [ - Where::notIn('asientos.idasiento', implode(',', $ids)), - Where::eq('asientos.codejercicio', $this->getModel()->codejercicio), - Where::gte('asientos.fecha', $this->getModel()->fechainicio), - Where::lte('asientos.fecha', $this->getModel()->fechafin), - Where::eq('COALESCE(series.siniva, 0)', 0), - Where::sub([ - Where::notEq('partidas.baseimponible', 0), - Where::orGt('COALESCE(partidas.iva, 0)', 0), - ]), - $subAccountTools->whereForSpecialAccounts('COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)', $group) - ]; + /** + * Load data view procedure + * + * @param string $viewName + * @param BaseView $view + * @throws Exception + */ + protected function loadData($viewName, $view): void + { + switch ($viewName) { + case 'EditRegularizacionImpuesto': + parent::loadData($viewName, $view); + $this->settingsMainView(); + break; + + case 'ListPartidaImpuestoResumen': + $mainModel = $this->getModel(); + + // mismo criterio que las pestañas Compras/Ventas y que el asiento de regularización + $where = $this->commonTaxWhere(SubAccountTools::SPECIAL_GROUP_TAX_ALL); + $view->loadData(false, $where, [ + 'COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)' => 'ASC', + 'partidas.codsubcuenta' => 'ASC', + ]); + + // cargamos el resumen de IVA (casillas del régimen general) + $this->modelo303->loadFromResumen($view->cursor); + + // cargamos las bases exentas/informativas de ventas (casillas 59, 60 y 122) + $this->modelo303->loadFromSalesInvoices( + (int)$mainModel->idempresa, + (string)$mainModel->codejercicio, + (string)$mainModel->fechainicio, + (string)$mainModel->fechafin + ); + + // mostramos los avisos de importes que no encajan en ninguna casilla + foreach ($this->modelo303->getAvisos() as $aviso) { + Tools::log()->warning($aviso); + } + + $this->checkInvoicesWithoutAccounting($mainModel); + break; + + case 'ListPartida': + $this->getListPartida($view); + break; + + case 'ListPartidaImpuesto-1': + $this->getListPartidaImpuesto($view, SubAccountTools::SPECIAL_GROUP_TAX_INPUT); + break; + + case 'ListPartidaImpuesto-2': + $this->getListPartidaImpuesto($view, SubAccountTools::SPECIAL_GROUP_TAX_OUTPUT); + break; + } } /** diff --git a/Lib/Accounting/VatRegularizationToAccounting.php b/Lib/Accounting/VatRegularizationToAccounting.php index 7e19ab7..3fecc43 100644 --- a/Lib/Accounting/VatRegularizationToAccounting.php +++ b/Lib/Accounting/VatRegularizationToAccounting.php @@ -123,18 +123,59 @@ protected function addAccountingTaxLines(Asiento $accEntry, RegularizacionImpues return true; } + /** + * Comprueba si hay facturas sin asiento contable + * + * @param $reg + * + * @return bool + */ + private function checkInvoicesWithoutAccEntry($reg): bool + { + $where = [ + Where::eq('codejercicio', $reg->codejercicio), + Where::eq('idempresa', $reg->idempresa), + Where::gte('fecha', $reg->fechainicio), + Where::lte('fecha', $reg->fechafin), + Where::isNull('idasiento'), + ]; + + $facturasSinAsiento = FacturaCliente::all($where, [], 0, 1); + if (false === empty($facturasSinAsiento)) { + return false; + } + + $facturasSinAsiento = FacturaProveedor::all($where, [], 0, 1); + return empty($facturasSinAsiento); + } + protected function getSubtotals(RegularizacionImpuesto $reg): array { $accTools = new SubAccountTools(); $field = 'COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)'; $excludedOperations = implode(',', [Asiento::OPERATION_OPENING, Asiento::OPERATION_CLOSING]); + + // mismo criterio que las pestañas Resumen/Compras/Ventas para que el asiento cuadre con ellas + $regIds = []; + foreach (RegularizacionImpuesto::all() as $other) { + if ($other->idasiento) { + $regIds[] = $other->idasiento; + } + } + $where = [ + Where::eq('asientos.idempresa', $reg->idempresa), Where::eq('asientos.codejercicio', $reg->codejercicio), Where::gte('asientos.fecha', $reg->fechainicio), Where::lte('asientos.fecha', $reg->fechafin), + Where::eq('COALESCE(series.siniva, false)', false), Where::notIn("COALESCE(asientos.operacion, '')", $excludedOperations), $accTools->whereForSpecialAccounts($field, SubAccountTools::SPECIAL_GROUP_TAX_ALL) ]; + + if (false === empty($regIds)) { + $where[] = Where::notIn('asientos.idasiento', implode(',', $regIds)); + } $orderBy = [ $field => 'ASC', 'partidas.iva' => 'ASC', @@ -159,30 +200,4 @@ protected function getSubtotals(RegularizacionImpuesto $reg): array return $subtotals; } - - /** - * Comprueba si hay facturas sin asiento contable - * - * @param $reg - * - * @return bool - */ - private function checkInvoicesWithoutAccEntry($reg): bool - { - $where = [ - Where::eq('codejercicio', $reg->codejercicio), - Where::eq('idempresa', $reg->idempresa), - Where::gte('fecha', $reg->fechainicio), - Where::lte('fecha', $reg->fechafin), - Where::isNull('idasiento'), - ]; - - $facturasSinAsiento = FacturaCliente::all($where, [], 0, 1); - if (false === empty($facturasSinAsiento)) { - return false; - } - - $facturasSinAsiento = FacturaProveedor::all($where, [], 0, 1); - return empty($facturasSinAsiento); - } } diff --git a/Model/Join/PartidaImpuesto.php b/Model/Join/PartidaImpuesto.php index dd0cb64..b37e700 100644 --- a/Model/Join/PartidaImpuesto.php +++ b/Model/Join/PartidaImpuesto.php @@ -127,19 +127,23 @@ protected function loadFromData(array $data): void { parent::loadFromData($data); - if ($this->iva > 0 && $this->recargo > 0) { - $this->cuotaiva = $this->baseimponible * ($this->iva / 100.0); - $this->cuotarecargo = $this->baseimponible * ($this->recargo / 100.0); - } elseif ($this->iva > 0) { - $this->cuotaiva = $this->codcuentaesp === 'IVAREP' - ? $data['haber'] - $data['debe'] - : $data['debe'] - $data['haber']; - $this->cuotarecargo = 0.0; - } else { - $this->cuotarecargo = $this->codcuentaesp === 'IVAREP' - ? $data['haber'] - $data['debe'] - : $data['debe'] - $data['haber']; + // La cuota es SIEMPRE el importe realmente contabilizado (debe/haber), nunca un + // recálculo base*tipo (que introduce desfases de céntimos). Usamos la misma fórmula + // que PartidaImpuestoResumen: debe+haber da el importe en positivo (una de las dos + // columnas siempre es 0) y solo invertimos el signo en bases negativas (rectificativas). + $debe = (float)($data['debe'] ?? 0.0); + $haber = (float)($data['haber'] ?? 0.0); + $cuota = ($this->baseimponible < 0 && ($debe + $haber) > 0) + ? ($debe + $haber) * -1 + : $debe + $haber; + + // El destino (IVA o recargo) lo determina la cuenta especial, igual que en el resumen. + if ($this->codcuentaesp === 'IVARRE') { + $this->cuotarecargo = $cuota; $this->cuotaiva = 0.0; + } else { + $this->cuotaiva = $cuota; + $this->cuotarecargo = 0.0; } // si el campo factura está vacío, buscamos la factura con este asiento From 9dfd2c8af922e4f7d3caa23fd7c032801b448297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Fern=C3=A1ndez=20Gim=C3=A9nez?= Date: Fri, 12 Jun 2026 09:36:39 +0200 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20actualizar=20estructura=20de=20tabl?= =?UTF-8?q?as=20en=20el=20modelo=20303=20para=20mejorar=20la=20presentaci?= =?UTF-8?q?=C3=B3n=20de=20datos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- View/Modelo303.html.twig | 684 ++++++++++++++++++++------------------- 1 file changed, 353 insertions(+), 331 deletions(-) diff --git a/View/Modelo303.html.twig b/View/Modelo303.html.twig index f8d39b1..631fb83 100644 --- a/View/Modelo303.html.twig +++ b/View/Modelo303.html.twig @@ -1,332 +1,354 @@
-
-
-
- {{ trans('vat-accrued') }} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ trans('tax-base') }}{{ trans('type') }} %{{ trans('quota') }}
150 - {{ fsc.modelo303.casillaStr('150') }}151 - {{ fsc.modelo303.casillaStr('151', true) }}152 - {{ fsc.modelo303.casillaStr('152') }}
165 - {{ fsc.modelo303.casillaStr('165') }}166 - {{ fsc.modelo303.casillaStr('166') }}167 - {{ fsc.modelo303.casillaStr('167') }}
01 - {{ fsc.modelo303.casillaStr('01') }} - 02 - {{ fsc.modelo303.casillaStr('02') }}03 - {{ fsc.modelo303.casillaStr('03') }} -
153 - {{ fsc.modelo303.casillaStr('153') }}154 - {{ fsc.modelo303.casillaStr('154') }}155 - {{ fsc.modelo303.casillaStr('155') }}
04 - {{ fsc.modelo303.casillaStr('04') }} - 05 - {{ fsc.modelo303.casillaStr('05') }}06 - {{ fsc.modelo303.casillaStr('06') }} -
07 - {{ fsc.modelo303.casillaStr('07') }} - 08 - {{ fsc.modelo303.casillaStr('08') }}09 - {{ fsc.modelo303.casillaStr('09') }} -
10 - {{ fsc.modelo303.casillaStr('10') }} - 11 - {{ fsc.modelo303.casillaStr('11') }} -
12 - {{ fsc.modelo303.casillaStr('12') }} - 13 - {{ fsc.modelo303.casillaStr('13') }} -
14 - {{ fsc.modelo303.casillaStr('14') }} - 15 - {{ fsc.modelo303.casillaStr('15') }} -
156 - {{ fsc.modelo303.casillaStr('156') }}157 - {{ fsc.modelo303.casillaStr('157') }}158 - {{ fsc.modelo303.casillaStr('158') }}
168 - {{ fsc.modelo303.casillaStr('168') }}169 - {{ fsc.modelo303.casillaStr('169') }}170 - {{ fsc.modelo303.casillaStr('170') }}
16 - {{ fsc.modelo303.casillaStr('16') }} - 17 - {{ fsc.modelo303.casillaStr('17') }}18 - {{ fsc.modelo303.casillaStr('18') }} -
19 - {{ fsc.modelo303.casillaStr('19') }} - 20 - {{ fsc.modelo303.casillaStr('20') }}21 - {{ fsc.modelo303.casillaStr('21') }} -
22 - {{ fsc.modelo303.casillaStr('22') }} - 23 - {{ fsc.modelo303.casillaStr('23') }}24 - {{ fsc.modelo303.casillaStr('24') }} -
25 - {{ fsc.modelo303.casillaStr('25') }} - 26 - {{ fsc.modelo303.casillaStr('26') }} -
- - - - - - - - -
{{ trans('total-accrued-fee') }}27 - {{ fsc.modelo303.casillaStr('27') }} -
-
-
-
-
-
- {{ trans('deductible-vat') }} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ trans('base') }}{{ trans('quota') }}
{{ trans('deductible-vat-28') }}28 - {{ fsc.modelo303.casillaStr('28') }} - 29 - {{ fsc.modelo303.casillaStr('29') }} -
{{ trans('deductible-vat-32') }}32 - {{ fsc.modelo303.casillaStr('32') }} - 33 - {{ fsc.modelo303.casillaStr('33') }} -
{{ trans('deductible-vat-36') }}36 - {{ fsc.modelo303.casillaStr('36') }} - 37 - {{ fsc.modelo303.casillaStr('37') }} -
- - - - - - - - -
{{ trans('total-to-deduct') }}45 - {{ fsc.modelo303.casillaStr('45') }} -
-
-
-
- {{ trans('general-regime-result') }} -
- - - - - - - - -
{{ trans('total-result-of-the-general-regime') }}46 - {{ fsc.modelo303.casillaStr('46') }} -
-
-
-
-
-
-
-
- {{ trans('info-operations-no-vat') }} -
- - - - - - - - - - - - - - - - - - - - - - - - - -
{{ trans('tax-base') }}
{{ trans('intra-community-deliveries') }}59 - {{ fsc.modelo303.casillaStr('59') }} -
{{ trans('exports') }}60 - {{ fsc.modelo303.casillaStr('60') }} -
{{ trans('sales-reverse-charge') }}122 - {{ fsc.modelo303.casillaStr('122') }} -
-
-
-
+
+
+
+ + {{ trans('vat-accrued') }} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ trans('tax-base') }}{{ trans('type') }} + %{{ trans('quota') }}
150 + {{ fsc.modelo303.casillaStr('150') }} + 151 + {{ fsc.modelo303.casillaStr('151', true) }} + 152 + {{ fsc.modelo303.casillaStr('152') }} +
165 + {{ fsc.modelo303.casillaStr('165') }} + 166 + {{ fsc.modelo303.casillaStr('166') }} + 167 + {{ fsc.modelo303.casillaStr('167') }} +
01 + {{ fsc.modelo303.casillaStr('01') }} + 02 + {{ fsc.modelo303.casillaStr('02') }} + 03 + {{ fsc.modelo303.casillaStr('03') }} +
153 + {{ fsc.modelo303.casillaStr('153') }} + 154 + {{ fsc.modelo303.casillaStr('154') }} + 155 + {{ fsc.modelo303.casillaStr('155') }} +
04 + {{ fsc.modelo303.casillaStr('04') }} + 05 + {{ fsc.modelo303.casillaStr('05') }} + 06 + {{ fsc.modelo303.casillaStr('06') }} +
07 + {{ fsc.modelo303.casillaStr('07') }} + 08 + {{ fsc.modelo303.casillaStr('08') }} + 09 + {{ fsc.modelo303.casillaStr('09') }} +
10 + {{ fsc.modelo303.casillaStr('10') }} + 11 + {{ fsc.modelo303.casillaStr('11') }} +
12 + {{ fsc.modelo303.casillaStr('12') }} + 13 + {{ fsc.modelo303.casillaStr('13') }} +
14 + {{ fsc.modelo303.casillaStr('14') }} + 15 + {{ fsc.modelo303.casillaStr('15') }} +
156 + {{ fsc.modelo303.casillaStr('156') }} + 157 + {{ fsc.modelo303.casillaStr('157') }} + 158 + {{ fsc.modelo303.casillaStr('158') }} +
168 + {{ fsc.modelo303.casillaStr('168') }} + 169 + {{ fsc.modelo303.casillaStr('169') }} + 170 + {{ fsc.modelo303.casillaStr('170') }} +
16 + {{ fsc.modelo303.casillaStr('16') }} + 17 + {{ fsc.modelo303.casillaStr('17') }} + 18 + {{ fsc.modelo303.casillaStr('18') }} +
19 + {{ fsc.modelo303.casillaStr('19') }} + 20 + {{ fsc.modelo303.casillaStr('20') }} + 21 + {{ fsc.modelo303.casillaStr('21') }} +
22 + {{ fsc.modelo303.casillaStr('22') }} + 23 + {{ fsc.modelo303.casillaStr('23') }} + 24 + {{ fsc.modelo303.casillaStr('24') }} +
25 + {{ fsc.modelo303.casillaStr('25') }} + 26 + {{ fsc.modelo303.casillaStr('26') }} +
+ + + + + + + + +
{{ trans('total-accrued-fee') }}27 + {{ fsc.modelo303.casillaStr('27') }} +
+
+
+
+
+
+ + {{ trans('deductible-vat') }} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ trans('base') }}{{ trans('quota') }}
{{ trans('deductible-vat-28') }}28 + {{ fsc.modelo303.casillaStr('28') }} + 29 + {{ fsc.modelo303.casillaStr('29') }} +
{{ trans('deductible-vat-32') }}32 + {{ fsc.modelo303.casillaStr('32') }} + 33 + {{ fsc.modelo303.casillaStr('33') }} +
{{ trans('deductible-vat-36') }}36 + {{ fsc.modelo303.casillaStr('36') }} + 37 + {{ fsc.modelo303.casillaStr('37') }} +
+ + + + + + + + +
{{ trans('total-to-deduct') }}45 + {{ fsc.modelo303.casillaStr('45') }} +
+
+
+
+ + {{ trans('general-regime-result') }} +
+ + + + + + + + +
{{ trans('total-result-of-the-general-regime') }}46 + {{ fsc.modelo303.casillaStr('46') }} +
+
+
+
+ + {{ trans('info-operations-no-vat') }} +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{{ trans('tax-base') }}
{{ trans('intra-community-deliveries') }}59 + {{ fsc.modelo303.casillaStr('59') }} +
{{ trans('exports') }}60 + {{ fsc.modelo303.casillaStr('60') }} +
{{ trans('sales-reverse-charge') }}122 + {{ fsc.modelo303.casillaStr('122') }} +
+
+
+ \ No newline at end of file