diff --git a/Controller/Modelo130.php b/Controller/Modelo130.php
index 9fd40d0..6583645 100644
--- a/Controller/Modelo130.php
+++ b/Controller/Modelo130.php
@@ -21,12 +21,15 @@
use FacturaScripts\Core\Base\Controller;
use FacturaScripts\Core\DataSrc\Ejercicios;
+use FacturaScripts\Core\Response;
use FacturaScripts\Core\Tools;
use FacturaScripts\Core\Where;
use FacturaScripts\Dinamic\Model\Ejercicio;
+use FacturaScripts\Dinamic\Model\Empresa;
use FacturaScripts\Dinamic\Model\FormaPago;
use FacturaScripts\Dinamic\Model\Subcuenta130;
use FacturaScripts\Dinamic\Lib\Modelo130 as DinModelo130;
+use FacturaScripts\Dinamic\Lib\Modelo130Export as DinModelo130Export;
/**
* Description of Modelo130
@@ -171,6 +174,56 @@ public function privateCore(&$response, $user, $permissions)
$this->gastosJustificacionPct = (float)$this->request->request->get('gastosJustificacionPct', 7.0);
$this->result = DinModelo130::generate($this->codejercicio, $this->period, $this->applyGastosJustificacion, $this->todeduct, $this->gastosJustificacionPct);
+
+ if ($action === 'download') {
+ $this->downloadFile($response);
+ }
+ }
+
+ protected function downloadFile(Response $response): void
+ {
+ if (empty($this->result)) {
+ Tools::log()->warning('no-data');
+ return;
+ }
+
+ $empresa = new Empresa();
+ if (false === $empresa->load($this->result['exercise']->idempresa)) {
+ Tools::log()->error('company-not-found');
+ return;
+ }
+
+ // validamos los datos mínimos para no generar un fichero corrupto
+ $nif = DinModelo130Export::formatNif($empresa->cifnif ?? '');
+ if (strlen($nif) !== 9) {
+ Tools::log()->error('aeat-file-invalid-nif');
+ return;
+ }
+
+ if (empty(trim($empresa->nombre ?? ''))) {
+ Tools::log()->error('aeat-file-missing-company-name');
+ return;
+ }
+
+ $year = date('Y', strtotime($this->result['exercise']->fechainicio));
+ if (strlen($year) !== 4) {
+ Tools::log()->error('aeat-file-invalid-exercise');
+ return;
+ }
+
+ $content = DinModelo130Export::generate($this->result, $empresa, $this->period, $year);
+ if (strlen($content) !== DinModelo130Export::FILE_LENGTH) {
+ Tools::log()->error('aeat-file-invalid-length');
+ return;
+ }
+
+ $filename = 'modelo130_' . $year . '_' . DinModelo130Export::getPeriodNumber($this->period) . '.txt';
+
+ $response->headers->set('Content-Type', 'text/plain; charset=ISO-8859-1');
+ $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"');
+ $response->setContent(mb_convert_encoding($content, 'ISO-8859-1', 'UTF-8'));
+ $response->send();
+ exit;
}
protected function addDeductibleSubaccount(): void
diff --git a/Lib/Modelo130Export.php b/Lib/Modelo130Export.php
new file mode 100644
index 0000000..25ff419
--- /dev/null
+++ b/Lib/Modelo130Export.php
@@ -0,0 +1,211 @@
+
+ * 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\Modelo130\Lib;
+
+use FacturaScripts\Dinamic\Model\Empresa;
+
+/**
+ * Genera el fichero de presentación del Modelo 130 según el diseño de
+ * registro oficial de la AEAT (DR130e15v12, Orden HAP/258/2015, ejercicios
+ * 2019 y siguientes): sobre + zona (328 posiciones),
+ * página 1 de 600 posiciones y cierre .
+ *
+ * @author Abderrahim Darghal Belkacemi
+ */
+class Modelo130Export
+{
+ /** Longitud total del fichero: 328 de sobre + 600 de página + 18 de cierre. */
+ const FILE_LENGTH = 946;
+
+ /**
+ * Genera el contenido completo del fichero a partir del resultado
+ * calculado por Modelo130::generate().
+ */
+ public static function generate(array $result, Empresa $empresa, string $period, string $year): string
+ {
+ $periodo = static::getPeriodNumber($period);
+ $nif = static::formatNif($empresa->cifnif ?? '');
+
+ // registro inicial:
+ $content = '';
+
+ // zona : 70 blancos reservados + versión del programa (4) + 4 blancos
+ // + NIF de la empresa desarrolladora (9) + 213 blancos reservados
+ $content .= '' . str_repeat(' ', 300) . '';
+
+ // contenido de la página 1 (600 posiciones)
+ $content .= static::generatePage1($result, $empresa, $nif, $year, $periodo);
+
+ // registro de cierre
+ $content .= '';
+
+ return $content;
+ }
+
+ /**
+ * Tipo de declaración: I (ingreso), N (negativa) o B (resultado a deducir).
+ */
+ public static function getDeclarationType(float $result): string
+ {
+ if ($result > 0) {
+ return 'I';
+ }
+
+ return $result < 0 ? 'B' : 'N';
+ }
+
+ public static function getPeriodNumber(string $period): string
+ {
+ return match ($period) {
+ 'T2' => '2T',
+ 'T3' => '3T',
+ 'T4' => '4T',
+ default => '1T',
+ };
+ }
+
+ public static function formatAlphanumeric(string $value, int $length): string
+ {
+ // solo se admiten letras, números y blancos, alineados a la izquierda
+ $value = strtoupper(static::removeAccents($value));
+ $value = preg_replace('/[^A-Z0-9 ]/', '', $value);
+
+ return str_pad(substr($value, 0, $length), $length, ' ', STR_PAD_RIGHT);
+ }
+
+ public static function formatNif(string $value): string
+ {
+ $value = strtoupper(static::removeAccents($value));
+
+ return preg_replace('/[^A-Z0-9]/', '', $value);
+ }
+
+ public static function formatNumeric(float $value, bool $signed = false): string
+ {
+ // 17 posiciones: 15 enteros + 2 decimales, sin separador, con ceros a la izquierda;
+ // los importes negativos llevan una N en la primera posición
+ $cents = (int)round(abs($value) * 100);
+ if ($signed && $value < 0) {
+ return 'N' . str_pad((string)$cents, 16, '0', STR_PAD_LEFT);
+ }
+
+ return str_pad((string)$cents, 17, '0', STR_PAD_LEFT);
+ }
+
+ public static function splitDeclarantName(string $fullName): array
+ {
+ // el diseño de registro separa apellidos (60) y nombre (20); si el nombre de la
+ // empresa tiene varias palabras, la primera se toma como nombre y el resto como apellidos
+ $parts = preg_split('/\s+/', trim($fullName), -1, PREG_SPLIT_NO_EMPTY);
+ if (count($parts) < 2) {
+ return [trim($fullName), ''];
+ }
+
+ $nombre = array_shift($parts);
+ return [implode(' ', $parts), $nombre];
+ }
+
+ protected static function generatePage1(array $result, Empresa $empresa, string $nif, string $year, string $periodo): string
+ {
+ // I. Actividades económicas en estimación directa
+ $box01 = (float)$result['taxbaseIngresos'];
+ $box02 = round((float)$result['taxbaseGastos'] + (float)$result['gastosJustificacion'], 2);
+ $box03 = round($box01 - $box02, 2);
+ $box04 = (float)$result['afterdeduct'];
+ $box05 = max(0.0, (float)$result['positivosTrimestres']);
+ $box06 = (float)$result['taxbaseRetenciones'];
+ $box07 = round($box04 - $box05 - $box06, 2);
+
+ // II. Actividades agrícolas, ganaderas, forestales y pesqueras (no soportadas)
+ $box08 = $box09 = $box10 = $box11 = 0.0;
+
+ // III. Total liquidación: si la suma de [07] y [11] es negativa, se consigna cero
+ $box12 = max(0.0, round($box07 + $box11, 2));
+ $box13 = 0.0;
+ $box14 = round($box12 - $box13, 2);
+ $box15 = $box16 = 0.0;
+ $box17 = round($box14 - $box15 - $box16, 2);
+ $box18 = 0.0;
+ $box19 = round($box17 - $box18, 2);
+
+ [$apellidos, $nombre] = static::splitDeclarantName($empresa->nombre ?? '');
+
+ $page = '';
+
+ // indicador de página complementaria (en blanco)
+ $page .= ' ';
+
+ // tipo de declaración: I (ingreso), N (negativa) o B (resultado a deducir)
+ $page .= static::getDeclarationType($box19);
+
+ // declarante
+ $page .= static::formatAlphanumeric($nif, 9);
+ $page .= static::formatAlphanumeric($apellidos, 60);
+ $page .= static::formatAlphanumeric($nombre, 20);
+
+ // devengo
+ $page .= $year;
+ $page .= $periodo;
+
+ // liquidación: casillas [01] a [19], 17 posiciones cada una
+ $page .= static::formatNumeric($box01);
+ $page .= static::formatNumeric($box02);
+ $page .= static::formatNumeric($box03, true);
+ $page .= static::formatNumeric($box04);
+ $page .= static::formatNumeric($box05);
+ $page .= static::formatNumeric($box06);
+ $page .= static::formatNumeric($box07, true);
+ $page .= static::formatNumeric($box08);
+ $page .= static::formatNumeric($box09);
+ $page .= static::formatNumeric($box10);
+ $page .= static::formatNumeric($box11, true);
+ $page .= static::formatNumeric($box12);
+ $page .= static::formatNumeric($box13);
+ $page .= static::formatNumeric($box14, true);
+ $page .= static::formatNumeric($box15);
+ $page .= static::formatNumeric($box16);
+ $page .= static::formatNumeric($box17, true);
+ $page .= static::formatNumeric($box18);
+ $page .= static::formatNumeric($box19, true);
+
+ // declaración complementaria (blanco), justificante anterior (13) e IBAN (34)
+ $page .= ' ';
+ $page .= str_repeat(' ', 13);
+ $page .= str_repeat(' ', 34);
+
+ // reservado AEAT (96) y sello electrónico (13)
+ $page .= str_repeat(' ', 109);
+
+ // indicador de fin de registro
+ $page .= '';
+
+ return $page;
+ }
+
+ protected static function removeAccents(string $text): string
+ {
+ $unwanted = [
+ 'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u',
+ 'Á' => 'A', 'É' => 'E', 'Í' => 'I', 'Ó' => 'O', 'Ú' => 'U',
+ 'à' => 'a', 'è' => 'e', 'ì' => 'i', 'ò' => 'o', 'ù' => 'u',
+ 'ñ' => 'N', 'Ñ' => 'N', 'ü' => 'u', 'Ü' => 'U', 'ç' => 'C', 'Ç' => 'C'
+ ];
+ return strtr($text, $unwanted);
+ }
+}
diff --git a/Test/main/Modelo130FileTest.php b/Test/main/Modelo130FileTest.php
new file mode 100644
index 0000000..35eceaa
--- /dev/null
+++ b/Test/main/Modelo130FileTest.php
@@ -0,0 +1,313 @@
+
+ *
+ * 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\Model\Empresa;
+use FacturaScripts\Plugins\Modelo130\Lib\Modelo130Export;
+use PHPUnit\Framework\TestCase;
+use ReflectionClass;
+
+/**
+ * Tests del fichero de presentación AEAT del Modelo 130.
+ *
+ * Verifican que el fichero generado por Modelo130Export respeta el diseño
+ * de registro oficial publicado por la AEAT (DR130e15v12, Orden HAP/258/2015,
+ * ejercicios 2019 y siguientes): sobre + zona
+ * (328 posiciones), página 1 de 600 posiciones y cierre
+ * .
+ *
+ * @author Abderrahim Darghal Belkacemi
+ */
+final class Modelo130FileTest extends TestCase
+{
+ /** Posición (base 0) donde empieza la página 1 dentro del fichero. */
+ const PAGE_OFFSET = 328;
+
+ /** Posición (base 0) de la primera casilla dentro de la página. */
+ const BOXES_OFFSET = 108;
+
+ public function testCompleteFilePositiveResult(): void
+ {
+ $expected = ''
+ . '' . str_repeat(' ', 300) . ''
+ . ''
+ . ' ' // página complementaria: en blanco
+ . 'I' // tipo de declaración: ingreso
+ . '12345678Z' // NIF
+ . 'PEREZ GARCIA' . str_repeat(' ', 48) // apellidos (60)
+ . 'MARIA' . str_repeat(' ', 15) // nombre (20)
+ . '2026' // ejercicio
+ . '2T' // período
+ . '00000000002500050' // [01] ingresos 25.000,50
+ . '00000000001105027' // [02] gastos 10.000,25 + 1.050,02
+ . '00000000001395023' // [03] rendimiento neto 13.950,23
+ . '00000000000279005' // [04] 20% de [03] = 2.790,05
+ . '00000000000050000' // [05] trimestres anteriores 500,00
+ . '00000000000120075' // [06] retenciones 1.200,75
+ . '00000000000108930' // [07] = [04]-[05]-[06] = 1.089,30
+ . '00000000000000000' // [08] agrícolas: no soportado
+ . '00000000000000000' // [09]
+ . '00000000000000000' // [10]
+ . '00000000000000000' // [11]
+ . '00000000000108930' // [12] = [07]+[11]
+ . '00000000000000000' // [13]
+ . '00000000000108930' // [14] = [12]-[13]
+ . '00000000000000000' // [15]
+ . '00000000000000000' // [16]
+ . '00000000000108930' // [17] = [14]-[15]-[16]
+ . '00000000000000000' // [18]
+ . '00000000000108930' // [19] resultado
+ . ' ' // declaración complementaria
+ . str_repeat(' ', 13) // justificante anterior
+ . str_repeat(' ', 34) // IBAN
+ . str_repeat(' ', 96) // reservado AEAT
+ . str_repeat(' ', 13) // sello electrónico
+ . ''
+ . '';
+
+ $content = Modelo130Export::generate($this->sampleResult(), $this->sampleCompany(), 'T2', '2026');
+ $this->assertSame($expected, $content);
+ }
+
+ public function testCompleteFileNegativeResult(): void
+ {
+ $result = [
+ 'taxbaseIngresos' => 1000.00,
+ 'taxbaseGastos' => 5000.00,
+ 'gastosJustificacion' => 0.0,
+ 'afterdeduct' => 0.0,
+ 'positivosTrimestres' => 0.0,
+ 'taxbaseRetenciones' => 100.00,
+ ];
+
+ $expected = ''
+ . '' . str_repeat(' ', 300) . ''
+ . ''
+ . ' ' // página complementaria: en blanco
+ . 'N' // tipo de declaración: negativa
+ . '12345678Z' // NIF
+ . 'PEREZ GARCIA' . str_repeat(' ', 48) // apellidos (60)
+ . 'MARIA' . str_repeat(' ', 15) // nombre (20)
+ . '2026' // ejercicio
+ . '3T' // período
+ . '00000000000100000' // [01] ingresos 1.000,00
+ . '00000000000500000' // [02] gastos 5.000,00
+ . 'N0000000000400000' // [03] rendimiento neto -4.000,00
+ . '00000000000000000' // [04] no puede ser negativa
+ . '00000000000000000' // [05]
+ . '00000000000010000' // [06] retenciones 100,00
+ . 'N0000000000010000' // [07] = -100,00
+ . '00000000000000000' // [08]
+ . '00000000000000000' // [09]
+ . '00000000000000000' // [10]
+ . '00000000000000000' // [11]
+ . '00000000000000000' // [12] si [07]+[11] es negativa: cero
+ . '00000000000000000' // [13]
+ . '00000000000000000' // [14]
+ . '00000000000000000' // [15]
+ . '00000000000000000' // [16]
+ . '00000000000000000' // [17]
+ . '00000000000000000' // [18]
+ . '00000000000000000' // [19] resultado cero
+ . ' ' // declaración complementaria
+ . str_repeat(' ', 13) // justificante anterior
+ . str_repeat(' ', 34) // IBAN
+ . str_repeat(' ', 96) // reservado AEAT
+ . str_repeat(' ', 13) // sello electrónico
+ . ''
+ . '';
+
+ $content = Modelo130Export::generate($result, $this->sampleCompany(), 'T3', '2026');
+ $this->assertSame($expected, $content);
+ }
+
+ public function testFileStructure(): void
+ {
+ $content = Modelo130Export::generate($this->sampleResult(), $this->sampleCompany(), 'T2', '2026');
+
+ // longitud total exacta y sin saltos de línea
+ $this->assertSame(Modelo130Export::FILE_LENGTH, strlen($content));
+ $this->assertStringNotContainsString("\n", $content);
+ $this->assertStringNotContainsString("\r", $content);
+
+ // sobre: apertura, zona reservada en blanco y cierre
+ $this->assertSame('', substr($content, 0, 17));
+ $this->assertSame('', substr($content, 17, 5));
+ $this->assertSame(str_repeat(' ', 300), substr($content, 22, 300));
+ $this->assertSame('', substr($content, 322, 6));
+ $this->assertSame('', substr($content, -18));
+
+ // página 1: 600 posiciones con sus constantes de inicio y fin
+ $page = substr($content, self::PAGE_OFFSET, 600);
+ $this->assertSame(600, strlen($page));
+ $this->assertSame('', substr($page, 0, 11));
+ $this->assertSame('', substr($page, 588, 12));
+
+ // posiciones 432-588 de la página (complementaria, justificante,
+ // IBAN, reservado AEAT y sello electrónico) en blanco
+ $this->assertSame(str_repeat(' ', 157), substr($page, 431, 157));
+ }
+
+ public function testFileDeclarantAndAccrual(): void
+ {
+ $content = Modelo130Export::generate($this->sampleResult(), $this->sampleCompany(), 'T2', '2026');
+ $page = substr($content, self::PAGE_OFFSET, 600);
+
+ // indicador de página complementaria en blanco
+ $this->assertSame(' ', $page[11]);
+
+ // tipo de declaración I: el resultado de la muestra es positivo
+ $this->assertSame('I', $page[12]);
+
+ // declarante: NIF (9), apellidos (60) y nombre (20), en mayúsculas
+ // sin acentos; la primera palabra del nombre de la empresa se toma
+ // como nombre y el resto como apellidos
+ $this->assertSame('12345678Z', substr($page, 13, 9));
+ $this->assertSame(str_pad('PEREZ GARCIA', 60), substr($page, 22, 60));
+ $this->assertSame(str_pad('MARIA', 20), substr($page, 82, 20));
+
+ // devengo: ejercicio (4) y período (2)
+ $this->assertSame('2026', substr($page, 102, 4));
+ $this->assertSame('2T', substr($page, 106, 2));
+ }
+
+ public function testFileBoxesNegativeResult(): void
+ {
+ $result = [
+ 'taxbaseIngresos' => 1000.00,
+ 'taxbaseGastos' => 5000.00,
+ 'gastosJustificacion' => 0.0,
+ 'afterdeduct' => 0.0,
+ 'positivosTrimestres' => 0.0,
+ 'taxbaseRetenciones' => 100.00,
+ ];
+
+ $content = Modelo130Export::generate($result, $this->sampleCompany(), 'T3', '2026');
+ $page = substr($content, self::PAGE_OFFSET, 600);
+
+ // los importes negativos llevan una N en la primera posición del campo
+ $this->assertSame('N0000000000400000', $this->box($page, 3));
+ $this->assertSame('N0000000000010000', $this->box($page, 7));
+
+ // [04] no puede ser negativa
+ $this->assertSame(str_repeat('0', 17), $this->box($page, 4));
+
+ // si la suma de [07] y [11] es negativa, en [12] se consigna cero
+ $this->assertSame(str_repeat('0', 17), $this->box($page, 12));
+ $this->assertSame(str_repeat('0', 17), $this->box($page, 19));
+
+ // resultado cero: tipo de declaración N (negativa)
+ $this->assertSame('N', $page[12]);
+ }
+
+ public function testDeclarationType(): void
+ {
+ // I (ingreso), N (negativa) y B (a deducir)
+ $this->assertSame('I', Modelo130Export::getDeclarationType(100.0));
+ $this->assertSame('N', Modelo130Export::getDeclarationType(0.0));
+ $this->assertSame('B', Modelo130Export::getDeclarationType(-100.0));
+ }
+
+ public function testFormatNumeric(): void
+ {
+ // 17 posiciones: 15 enteros + 2 decimales, ceros a la izquierda
+ $this->assertSame('00000000000000000', Modelo130Export::formatNumeric(0.0));
+ $this->assertSame('00000000000012345', Modelo130Export::formatNumeric(123.45));
+ $this->assertSame('00000000000000001', Modelo130Export::formatNumeric(0.01));
+
+ // redondeo a céntimos
+ $this->assertSame('00000000000010000', Modelo130Export::formatNumeric(99.999));
+
+ // los campos sin signo nunca llevan N aunque el valor sea negativo
+ $this->assertSame('00000000000012345', Modelo130Export::formatNumeric(-123.45));
+
+ // los campos con signo llevan N en la primera posición si son negativos
+ $this->assertSame('N0000000000012345', Modelo130Export::formatNumeric(-123.45, true));
+ $this->assertSame('00000000000012345', Modelo130Export::formatNumeric(123.45, true));
+ $this->assertSame(17, strlen(Modelo130Export::formatNumeric(-123.45, true)));
+ }
+
+ public function testFormatAlphanumeric(): void
+ {
+ // mayúsculas, sin acentos, alineado a la izquierda con blancos
+ $this->assertSame('MARIA PEREZ ', Modelo130Export::formatAlphanumeric('María Pérez', 20));
+
+ // solo se admiten letras, números y blancos
+ $this->assertSame('ACME SL ', Modelo130Export::formatAlphanumeric('Acme, S.L.', 10));
+
+ // truncado a la longitud del campo
+ $this->assertSame('ABCDE', Modelo130Export::formatAlphanumeric('abcdefghij', 5));
+ }
+
+ public function testFormatNif(): void
+ {
+ $this->assertSame('12345678Z', Modelo130Export::formatNif('12345678-z'));
+ $this->assertSame('B12345678', Modelo130Export::formatNif(' b 12.345.678 '));
+ $this->assertSame('', Modelo130Export::formatNif(''));
+ }
+
+ public function testSplitDeclarantName(): void
+ {
+ // la primera palabra es el nombre y el resto los apellidos
+ $this->assertSame(['Pérez García', 'María'], Modelo130Export::splitDeclarantName('María Pérez García'));
+ $this->assertSame(['Pérez', 'María'], Modelo130Export::splitDeclarantName('María Pérez'));
+
+ // con una sola palabra, todo va a apellidos
+ $this->assertSame(['ACME', ''], Modelo130Export::splitDeclarantName('ACME'));
+ $this->assertSame(['', ''], Modelo130Export::splitDeclarantName(' '));
+ }
+
+ public function testGetPeriodNumber(): void
+ {
+ foreach (['T1' => '1T', 'T2' => '2T', 'T3' => '3T', 'T4' => '4T'] as $period => $expected) {
+ $this->assertSame($expected, Modelo130Export::getPeriodNumber($period));
+ }
+ }
+
+ /**
+ * Devuelve la casilla [n] de la página: 17 posiciones desde la 109.
+ */
+ private function box(string $page, int $num): string
+ {
+ return substr($page, self::BOXES_OFFSET + ($num - 1) * 17, 17);
+ }
+
+ private function sampleCompany(): Empresa
+ {
+ // se instancia sin constructor para no requerir conexión a base de datos
+ $empresa = (new ReflectionClass(Empresa::class))->newInstanceWithoutConstructor();
+ $empresa->cifnif = '12345678Z';
+ $empresa->nombre = 'María Pérez García';
+
+ return $empresa;
+ }
+
+ private function sampleResult(): array
+ {
+ return [
+ 'taxbaseIngresos' => 25000.50,
+ 'taxbaseGastos' => 10000.25,
+ 'gastosJustificacion' => 1050.02,
+ 'afterdeduct' => 2790.05,
+ 'positivosTrimestres' => 500.00,
+ 'taxbaseRetenciones' => 1200.75,
+ ];
+ }
+}
diff --git a/Translation/en_EN.json b/Translation/en_EN.json
index 3fa78a2..965e778 100644
--- a/Translation/en_EN.json
+++ b/Translation/en_EN.json
@@ -1,5 +1,11 @@
{
"acc-concept-irpf-130": "IRPF regularization %period%",
+ "aeat-file-invalid-exercise": "Cannot generate the AEAT file: the selected fiscal year is not valid.",
+ "aeat-file-invalid-length": "Cannot generate the AEAT file: the generated file does not have the expected length.",
+ "aeat-file-invalid-nif": "Cannot generate the AEAT file: the company tax ID is empty or not valid.",
+ "aeat-file-missing-company-name": "Cannot generate the AEAT file: the company name is empty.",
+ "company-not-found": "Company not found",
+ "download-aeat-file": "Download AEAT file",
"accounting-created-130": "An accounting entry has been created in fiscal year %codejercicio% with the concept: %concepto%",
"after-deduct": "about box 04",
"deductible-subaccounts": "Deductible Accounts",
diff --git a/Translation/es_ES.json b/Translation/es_ES.json
index 31ba53e..d076616 100644
--- a/Translation/es_ES.json
+++ b/Translation/es_ES.json
@@ -1,5 +1,11 @@
{
"acc-concept-irpf-130": "Regularización de IRPF %period%",
+ "aeat-file-invalid-exercise": "No se puede generar el fichero AEAT: el ejercicio seleccionado no es válido.",
+ "aeat-file-invalid-length": "No se puede generar el fichero AEAT: el fichero generado no tiene la longitud esperada.",
+ "aeat-file-invalid-nif": "No se puede generar el fichero AEAT: el NIF de la empresa está vacío o no es válido.",
+ "aeat-file-missing-company-name": "No se puede generar el fichero AEAT: el nombre de la empresa está vacío.",
+ "company-not-found": "Empresa no encontrada",
+ "download-aeat-file": "Descargar fichero AEAT",
"accounting-created-130": "Se ha creado el asiento en el ejercicio %codejercicio% con el concepto: %concepto%",
"after-deduct": "sobre casilla 03",
"deductible-subaccounts": "Cuentas deducibles",
diff --git a/View/Modelo130.html.twig b/View/Modelo130.html.twig
index 7291687..a239e16 100644
--- a/View/Modelo130.html.twig
+++ b/View/Modelo130.html.twig
@@ -146,6 +146,12 @@ return false;
{{ trans('preview') }}
+ {% if fsc.result is not empty %}
+
+ {% endif %}