diff --git a/Controller/Modelo130.php b/Controller/Modelo130.php index 9ab123a..57010d1 100644 --- a/Controller/Modelo130.php +++ b/Controller/Modelo130.php @@ -20,9 +20,9 @@ namespace FacturaScripts\Plugins\Modelo130\Controller; use FacturaScripts\Core\Base\Controller; -use FacturaScripts\Core\Base\DataBase\DataBaseWhere; use FacturaScripts\Core\DataSrc\Ejercicios; use FacturaScripts\Core\Tools; +use FacturaScripts\Core\Where; use FacturaScripts\Dinamic\Model\Asiento; use FacturaScripts\Dinamic\Model\Ejercicio; use FacturaScripts\Dinamic\Model\FacturaCliente; @@ -59,6 +59,12 @@ class Modelo130 extends Controller /** @var Subcuenta130 */ public $deductibleSubaccount; + /** @var Partida[] */ + public $incomeEntries = []; + + /** @var Subcuenta130 */ + public $incomeSubaccount; + /** @var string */ public $period = 'T1'; @@ -123,6 +129,12 @@ public function getAllExercises(?int $idempresa): array return $list; } + public function getDeductibleSubaccounts(): array + { + $where = [new Where('tipo', Subcuenta130::TIPO_DEDUCIBLE)]; + return (new Subcuenta130())->all($where, ['codsubcuenta' => 'ASC'], 0, 0); + } + /** * @param string|null $codejercicio * @return Ejercicio @@ -135,6 +147,12 @@ public function getExercise(?string $codejercicio): Ejercicio return $exercise; } + public function getIncomeSubaccounts(): array + { + $where = [new Where('tipo', Subcuenta130::TIPO_INGRESO)]; + return (new Subcuenta130())->all($where, ['codsubcuenta' => 'ASC'], 0, 0); + } + public function getPageData(): array { $data = parent::getPageData(); @@ -158,6 +176,7 @@ public function privateCore(&$response, $user, $permissions) { parent::privateCore($response, $user, $permissions); $this->deductibleSubaccount = new Subcuenta130(); + $this->incomeSubaccount = new Subcuenta130(); $this->paymentMethods = FormaPago::all(); @@ -173,6 +192,12 @@ public function privateCore(&$response, $user, $permissions) case 'delete-deductible-subaccount': return $this->deleteDeductibleSubaccount(); + case 'add-income-subaccount': + return $this->addIncomeSubaccount(); + + case 'delete-income-subaccount': + return $this->deleteIncomeSubaccount(); + case 'gen-accounting': return $this->createAccountingEntry(); } @@ -180,6 +205,7 @@ public function privateCore(&$response, $user, $permissions) $this->loadDates(); $this->loadInvoices(); $this->loadAsientos(); + $this->loadIncomeAsientos(); $this->loadResults(); } @@ -202,6 +228,26 @@ protected function addDeductibleSubaccount(): bool return true; } + protected function addIncomeSubaccount(): bool + { + $this->activeTab = 'income-subaccount'; + + if (false === $this->validateFormToken()) { + return false; + } + + $subaccount = new Subcuenta130(); + $subaccount->codsubcuenta = $this->request->request->get('codsubcuenta'); + $subaccount->tipo = Subcuenta130::TIPO_INGRESO; + if (false === $subaccount->save()) { + Tools::log()->error('record-save-error'); + return false; + } + + Tools::log()->notice('record-updated-correctly'); + return true; + } + protected function autocompleteSubaccount(): void { $this->setTemplate(false); @@ -248,6 +294,29 @@ protected function deleteDeductibleSubaccount(): bool return false; } + protected function deleteIncomeSubaccount(): bool + { + $this->activeTab = 'income-subaccount'; + + if (false === $this->validateFormToken()) { + return false; + } + + $subaccount = new Subcuenta130(); + if (false === $subaccount->load($this->request->request->get('id'))) { + Tools::log()->error('record-not-found'); + return false; + } + + if (false === $subaccount->delete()) { + Tools::log()->error('record-deleted-error'); + return false; + } + + Tools::log()->notice('record-deleted-correctly'); + return false; + } + // Traemos del codejercicio y period elegido idempresa, dateStart y dateEnd protected function loadDates(): void { @@ -289,20 +358,20 @@ protected function loadInvoices(): void $whereFtrasProveedores = [ // Para buscar en el margen de fechas del periodo - new DataBaseWhere('fecha', date('Y-m-d', strtotime($this->dateStart)), '>='), - new DataBaseWhere('fecha', date('Y-m-d', strtotime($this->dateEnd)), '<='), + new Where('fecha', date('Y-m-d', strtotime($this->dateStart)), '>='), + new Where('fecha', date('Y-m-d', strtotime($this->dateEnd)), '<='), // Para buscar ftras solo de la empresa/Ejercicio elegido - new DataBaseWhere('idempresa', $this->idempresa), + new Where('idempresa', $this->idempresa), ]; $whereFtrasClientes = [ // Para buscar en el margen de fechas del periodo - new DataBaseWhere('fecha', date('Y-m-d', strtotime($this->dateStart)), '>='), - new DataBaseWhere('fecha', date('Y-m-d', strtotime($this->dateEnd)), '<='), + new Where('fecha', date('Y-m-d', strtotime($this->dateStart)), '>='), + new Where('fecha', date('Y-m-d', strtotime($this->dateEnd)), '<='), // Para buscar ftras solo de la empresa/Ejercicio elegido - new DataBaseWhere('idempresa', $this->idempresa), + new Where('idempresa', $this->idempresa), ]; // Preparamos el orderBy de como vamos a traer las facturas (fecha + numero ftra) @@ -343,13 +412,42 @@ protected function getAccountingEntrySubaccounts(): array { $codsubs = []; $subaccount130 = new Subcuenta130(); - foreach ($subaccount130->all([], [], 0, 0) as $subaccount) { + $where = [new Where('tipo', Subcuenta130::TIPO_DEDUCIBLE)]; + foreach ($subaccount130->all($where, [], 0, 0) as $subaccount) { $codsubs[] = $subaccount->codsubcuenta; } return self::sanitizeSubaccountCodes($codsubs); } + protected function loadIncomeAsientos(): void + { + $codsubs = []; + $sub = new Subcuenta130(); + $where = [new Where('tipo', Subcuenta130::TIPO_INGRESO)]; + foreach ($sub->all($where, [], 0, 0) as $subaccount) { + $codsubs[] = $subaccount->codsubcuenta; + } + $codsubs = self::sanitizeSubaccountCodes($codsubs); + + if (empty($codsubs)) { + return; + } + + $sql = 'SELECT * FROM ' . Partida::tableName() . ' as p' + . ' LEFT JOIN ' . Asiento::tableName() . ' as a ON p.idasiento = a.idasiento' + . ' WHERE ' . $this->getSqlValueCondition('a.idempresa', $this->idempresa) + . ' AND a.fecha BETWEEN ' . $this->dataBase->var2str(date('Y-m-d', strtotime($this->dateStart))) + . ' AND ' . $this->dataBase->var2str(date('Y-m-d', strtotime($this->dateEnd))) + . ' AND p.codsubcuenta IN (' . $this->getSqlValueList($codsubs) . ')' + . ' AND a.operacion IS ' . $this->dataBase->var2str(Asiento::OPERATION_GENERAL) + . ' ORDER BY numero ASC'; + + foreach ($this->dataBase->select($sql) as $row) { + $this->incomeEntries[] = new Partida($row); + } + } + protected function getSqlValueCondition(string $field, $value): string { if (null === $value) { @@ -391,6 +489,10 @@ protected function loadResults(): void $this->taxbaseRetenciones += $invoice->totalirpf; } + foreach ($this->incomeEntries as $partida) { + $this->taxbaseIngresos += $partida->haber; + } + foreach ($this->supplierInvoices as $invoice) { $this->taxbaseGastos += $invoice->neto; } diff --git a/Model/Subcuenta130.php b/Model/Subcuenta130.php index 3521100..adc305a 100644 --- a/Model/Subcuenta130.php +++ b/Model/Subcuenta130.php @@ -19,11 +19,11 @@ namespace FacturaScripts\Plugins\Modelo130\Model; -use FacturaScripts\Core\Base\DataBase\DataBaseWhere; use FacturaScripts\Core\Session; use FacturaScripts\Core\Template\ModelClass; use FacturaScripts\Core\Template\ModelTrait; use FacturaScripts\Core\Tools; +use FacturaScripts\Core\Where; use FacturaScripts\Dinamic\Model\Subcuenta; use FacturaScripts\Dinamic\Model\User; @@ -31,6 +31,9 @@ class Subcuenta130 extends ModelClass { use ModelTrait; + const TIPO_DEDUCIBLE = 'deducible'; + const TIPO_INGRESO = 'ingreso'; + /** @var string */ public $codsubcuenta; @@ -52,10 +55,19 @@ class Subcuenta130 extends ModelClass /** @var string */ public $nick; + /** @var string */ + public $tipo = self::TIPO_DEDUCIBLE; + + public function clear(): void + { + parent::clear(); + $this->tipo = self::TIPO_DEDUCIBLE; + } + public function getSubcuenta(): Subcuenta { $subcuenta = new Subcuenta(); - $where = [new DataBaseWhere('codsubcuenta', $this->codsubcuenta)]; + $where = [new Where('codsubcuenta', $this->codsubcuenta)]; $subcuenta->loadWhere($where); return $subcuenta; } @@ -89,6 +101,9 @@ public function test(): bool $this->codsubcuenta = trim(Tools::noHtml((string)$this->codsubcuenta)); $this->name = Tools::noHtml($this->name); + $this->tipo = in_array($this->tipo, [self::TIPO_DEDUCIBLE, self::TIPO_INGRESO], true) + ? $this->tipo + : self::TIPO_DEDUCIBLE; if (strlen($this->codsubcuenta) < 1 || strlen($this->codsubcuenta) > 15) { Tools::log()->warning('invalid-column-lenght', [ diff --git a/Table/subcuentas_130.xml b/Table/subcuentas_130.xml index 191e884..0bd5775 100644 --- a/Table/subcuentas_130.xml +++ b/Table/subcuentas_130.xml @@ -30,6 +30,12 @@ nick character varying(50) + + tipo + character varying(20) + 'deducible' + NO + subcuentas_130_pkey PRIMARY KEY (id) diff --git a/Test/main/Model/Subcuenta130Test.php b/Test/main/Model/Subcuenta130Test.php index eb4a91b..c16b755 100644 --- a/Test/main/Model/Subcuenta130Test.php +++ b/Test/main/Model/Subcuenta130Test.php @@ -82,6 +82,68 @@ public function testCreateEmptySubcuenta130Fails(): void $this->assertFalse($subcuenta130->save(), 'empty-subcuenta130-should-fail'); } + public function testSubcuenta130TieneTipoDeduciblePorDefecto(): void + { + $sub130 = new Subcuenta130(); + $this->assertSame(Subcuenta130::TIPO_DEDUCIBLE, $sub130->tipo); + } + + public function testTipoInvalidoFuerzaTipoDeducible(): void + { + $exercise = $this->getRandomExercise(); + + $account = new Cuenta(); + $account->codcuenta = '9998'; + $account->codejercicio = $exercise->codejercicio; + $account->descripcion = 'Test tipo invalido'; + $this->assertTrue($account->save(), 'cant-save-account'); + + $subaccount = new Subcuenta(); + $subaccount->codcuenta = $account->codcuenta; + $subaccount->codejercicio = $exercise->codejercicio; + $subaccount->codsubcuenta = '9998000000'; + $subaccount->descripcion = 'Test tipo invalido'; + $this->assertTrue($subaccount->save(), 'cant-save-subaccount'); + + $sub130 = new Subcuenta130(); + $sub130->codsubcuenta = $subaccount->codsubcuenta; + $sub130->tipo = 'tipo_invalido_xyz'; + $this->assertTrue($sub130->save(), 'cant-save-subcuenta130'); + $this->assertSame(Subcuenta130::TIPO_DEDUCIBLE, $sub130->tipo, 'invalid-tipo-should-fallback-to-deducible'); + + $this->assertTrue($sub130->delete()); + $this->assertTrue($subaccount->delete()); + $this->assertTrue($account->delete()); + } + + public function testCreateSubcuenta130ConTipoIngreso(): void + { + $exercise = $this->getRandomExercise(); + + $account = new Cuenta(); + $account->codcuenta = '9997'; + $account->codejercicio = $exercise->codejercicio; + $account->descripcion = 'Test ingreso'; + $this->assertTrue($account->save(), 'cant-save-account'); + + $subaccount = new Subcuenta(); + $subaccount->codcuenta = $account->codcuenta; + $subaccount->codejercicio = $exercise->codejercicio; + $subaccount->codsubcuenta = '9997000000'; + $subaccount->descripcion = 'Test ingreso'; + $this->assertTrue($subaccount->save(), 'cant-save-subaccount'); + + $sub130 = new Subcuenta130(); + $sub130->codsubcuenta = $subaccount->codsubcuenta; + $sub130->tipo = Subcuenta130::TIPO_INGRESO; + $this->assertTrue($sub130->save(), 'cant-save-subcuenta130-ingreso'); + $this->assertSame(Subcuenta130::TIPO_INGRESO, $sub130->tipo, 'tipo-should-be-ingreso'); + + $this->assertTrue($sub130->delete()); + $this->assertTrue($subaccount->delete()); + $this->assertTrue($account->delete()); + } + protected function getRandomExercise(): Ejercicio { $model = new Ejercicio(); diff --git a/Test/main/Modelo130ControllerTest.php b/Test/main/Modelo130ControllerTest.php index cbe5e1f..472fbe7 100644 --- a/Test/main/Modelo130ControllerTest.php +++ b/Test/main/Modelo130ControllerTest.php @@ -19,6 +19,12 @@ namespace FacturaScripts\Test\Plugins; +use FacturaScripts\Dinamic\Model\Asiento; +use FacturaScripts\Dinamic\Model\Cuenta; +use FacturaScripts\Dinamic\Model\Ejercicio; +use FacturaScripts\Dinamic\Model\Partida; +use FacturaScripts\Dinamic\Model\Subcuenta; +use FacturaScripts\Dinamic\Model\Subcuenta130; use FacturaScripts\Plugins\Modelo130\Controller\Modelo130; use FacturaScripts\Test\Traits\DefaultSettingsTrait; use FacturaScripts\Test\Traits\LogErrorsTrait; @@ -32,6 +38,7 @@ final class Modelo130ControllerTest extends TestCase public static function setUpBeforeClass(): void { self::setDefaultSettings(); + self::installAccountingPlan(); } public function testSanitizeSubaccountCodesRemovesEmptyAndDuplicateValues(): void @@ -62,6 +69,118 @@ public function testGetSqlValueListQuotesValues(): void $this->assertSame("'6400000000','9999.1'", $controller->sqlValueList(['6400000000', '9999.1'])); } + public function testGetDeductibleSubaccountsFiltraPorTipo(): void + { + $sub130Deducible = new Subcuenta130(); + $sub130Deducible->codsubcuenta = 'test_deducible'; + $sub130Deducible->tipo = Subcuenta130::TIPO_DEDUCIBLE; + $this->assertTrue($sub130Deducible->save(), 'cant-save-deducible'); + + $sub130Ingreso = new Subcuenta130(); + $sub130Ingreso->codsubcuenta = 'test_ingreso'; + $sub130Ingreso->tipo = Subcuenta130::TIPO_INGRESO; + $this->assertTrue($sub130Ingreso->save(), 'cant-save-ingreso'); + + $controller = new Modelo130TestAccess(Modelo130TestAccess::class); + $codes = array_map(fn($s) => $s->codsubcuenta, $controller->getDeductibleSubaccounts()); + + $this->assertContains('test_deducible', $codes, 'deducible-should-be-in-list'); + $this->assertNotContains('test_ingreso', $codes, 'ingreso-should-not-be-in-deductible-list'); + + $this->assertTrue($sub130Deducible->delete()); + $this->assertTrue($sub130Ingreso->delete()); + } + + public function testGetIncomeSubaccountsFiltraPorTipo(): void + { + $sub130Deducible = new Subcuenta130(); + $sub130Deducible->codsubcuenta = 'test2_deducible'; + $sub130Deducible->tipo = Subcuenta130::TIPO_DEDUCIBLE; + $this->assertTrue($sub130Deducible->save(), 'cant-save-deducible'); + + $sub130Ingreso = new Subcuenta130(); + $sub130Ingreso->codsubcuenta = 'test2_ingreso'; + $sub130Ingreso->tipo = Subcuenta130::TIPO_INGRESO; + $this->assertTrue($sub130Ingreso->save(), 'cant-save-ingreso'); + + $controller = new Modelo130TestAccess(Modelo130TestAccess::class); + $codes = array_map(fn($s) => $s->codsubcuenta, $controller->getIncomeSubaccounts()); + + $this->assertContains('test2_ingreso', $codes, 'ingreso-should-be-in-list'); + $this->assertNotContains('test2_deducible', $codes, 'deducible-should-not-be-in-income-list'); + + $this->assertTrue($sub130Deducible->delete()); + $this->assertTrue($sub130Ingreso->delete()); + } + + public function testIncomeEntriesSeAcumulanEnTaxbaseIngresos(): void + { + // conseguir un ejercicio existente (all() garantiza que el codejercicio está en BD) + $ejercicios = (new Ejercicio())->all([], ['codejercicio' => 'DESC'], 0, 1); + $this->assertNotEmpty($ejercicios, 'no-exercise-found'); + $ejercicio = $ejercicios[0]; + + // crear cuenta y subcuenta de ingreso (9996 para no colisionar con el plan contable) + $account = new Cuenta(); + $account->codcuenta = '9996'; + $account->codejercicio = $ejercicio->codejercicio; + $account->descripcion = 'Test subvención ingreso modelo130'; + $this->assertTrue($account->save(), 'cant-save-account'); + + $subcuenta = new Subcuenta(); + $subcuenta->codcuenta = '9996'; + $subcuenta->codejercicio = $ejercicio->codejercicio; + $subcuenta->codsubcuenta = '9996000000'; + $subcuenta->descripcion = 'Test subvención ingreso modelo130'; + $this->assertTrue($subcuenta->save(), 'cant-save-subcuenta'); + + // crear asiento con partida haber=1500 (simula una subvención de explotación) + $asiento = new Asiento(); + $asiento->concepto = 'Test subvención Kit Digital modelo130'; + $asiento->fecha = date('Y') . '-02-15'; + $asiento->importe = 1500; + $asiento->idempresa = $ejercicio->idempresa; + $asiento->codejercicio = $ejercicio->codejercicio; + $this->assertTrue($asiento->save(), 'cant-save-asiento'); + + $partida = new Partida(); + $partida->idasiento = $asiento->idasiento; + $partida->codsubcuenta = '9996000000'; + $partida->concepto = 'Test subvención Kit Digital modelo130'; + $partida->haber = 1500.0; + $this->assertTrue($partida->save(), 'cant-save-partida'); + + // registrar la subcuenta como ingreso en el modelo 130 + $sub130 = new Subcuenta130(); + $sub130->codsubcuenta = '9996000000'; + $sub130->tipo = Subcuenta130::TIPO_INGRESO; + $this->assertTrue($sub130->save(), 'cant-save-subcuenta130'); + + // configurar el controlador con el periodo T1 del año actual + $controller = new Modelo130TestAccess(Modelo130TestAccess::class); + $controller->setTestContext( + $ejercicio->idempresa, + date('01-01-Y'), + date('31-03-Y') + ); + + // ejecutar la carga de asientos de ingresos + $controller->callLoadIncomeAsientos(); + + // verificar que la partida aparece en incomeEntries + $this->assertNotEmpty($controller->incomeEntries, 'income-entries-should-not-be-empty'); + + $haberTotal = array_sum(array_map(fn($p) => (float)$p->haber, $controller->incomeEntries)); + $this->assertEquals(1500.0, $haberTotal, 'income-haber-total-should-be-1500'); + + // cleanup + $this->assertTrue($sub130->delete()); + $this->assertTrue($partida->delete()); + $this->assertTrue($asiento->delete()); + $this->assertTrue($subcuenta->delete()); + $this->assertTrue($account->delete()); + } + protected function tearDown(): void { $this->logErrors(); @@ -84,4 +203,16 @@ public function sqlValueList(array $values): string { return $this->getSqlValueList($values); } + + public function setTestContext(int $idempresa, string $dateStart, string $dateEnd): void + { + $this->idempresa = $idempresa; + $this->dateStart = $dateStart; + $this->dateEnd = $dateEnd; + } + + public function callLoadIncomeAsientos(): void + { + $this->loadIncomeAsientos(); + } } diff --git a/Translation/es_ES.json b/Translation/es_ES.json index 7910578..afd4aed 100644 --- a/Translation/es_ES.json +++ b/Translation/es_ES.json @@ -1,6 +1,7 @@ { "after-deduct": "sobre casilla 04", "deductible-subaccounts": "Cuentas deducibles", + "income-subaccounts": "Cuentas de ingresos", "model-130": "Modelo 130", "model-130-desc": "Se calcula sumando y acumulando cada trimestre, por ejemplo, si visualizamos el 3º trimestre veremos la suma de los trimestres 1, 2 y 3.", "model-130-p": "El Modelo 130 es una declaración trimestral del impuesto de la renta de las personas físicas (IRPF) en el que se liquida el pago fraccionado de este impuesto, a cuenta de la declaración anual que se realiza el año siguiente.", diff --git a/View/Modelo130.html.twig b/View/Modelo130.html.twig index a1fb4db..62be020 100644 --- a/View/Modelo130.html.twig +++ b/View/Modelo130.html.twig @@ -43,6 +43,40 @@ return false; } }); + + $("#findIncomeSubaccount").autocomplete({ + source: function (request, response) { + $.ajax({ + method: "POST", + url: '{{ fsc.url() }}', + data: {action: 'autocomplete-subaccount', term: request.term}, + dataType: "json", + success: function (results) { + let values = []; + results.forEach(function (element) { + if (element.key === null || element.key === element.value) { + values.push(element); + } else { + values.push({key: element.key, value: element.key + " | " + element.value}); + } + }); + response(values); + }, + error: function (msg) { + alert(msg.status + " " + msg.responseText); + } + }); + }, + select: function (event, ui) { + if (ui.item.key !== null) { + $('form[name="add-income-subaccount"] input[name="codsubcuenta"]').val(ui.item.key); + } + }, + open: function (event, ui) { + $(this).autocomplete('widget').css('z-index', 1500); + return false; + } + }); }); {% endblock %} @@ -241,12 +275,28 @@ id="deductible-subaccounts-tab" data-bs-toggle="tab" href="#deductible-subaccounts" role="tab" aria-controls="deductible-subaccounts" - aria-selected="{{ fsc.tabDeductibleSubaccount ? 'true' : 'false' }}" + aria-selected="{{ fsc.activeTab == 'deductible-subaccount' ? 'true' : 'false' }}" title="{{ trans('deductible-subaccounts') }}"> {{ trans('deductible-subaccounts') }} - {% if fsc.deductibleSubaccount.count() %} - {{ fsc.deductibleSubaccount.count() }} + {% set deductibleList = fsc.getDeductibleSubaccounts() %} + {% if deductibleList|length %} + {{ deductibleList|length }} + {% endif %} + + + @@ -469,7 +519,7 @@ - {% for subaccount in fsc.deductibleSubaccount.all({}, {'codsubcuenta':'ASC'}, 0, 0) %} + {% for subaccount in fsc.getDeductibleSubaccounts() %} {{ subaccount.codsubcuenta }} {{ subaccount.getSubcuenta().descripcion }} @@ -505,6 +555,54 @@ +
+
+ + + + + + + + + + {% for subaccount in fsc.getIncomeSubaccounts() %} + + + + + + {% endfor %} + +
{{ trans('code') }}{{ trans('description') }}
{{ subaccount.codsubcuenta }}{{ subaccount.getSubcuenta().descripcion }} +
+ {{ formToken() }} + + + +
+
+
+
+ {{ formToken() }} + + +
+
+
+ + +
+
+
+
+