diff --git a/Controller/EditRegularizacionImpuesto.php b/Controller/EditRegularizacionImpuesto.php index b4999b1..186b30a 100644 --- a/Controller/EditRegularizacionImpuesto.php +++ b/Controller/EditRegularizacionImpuesto.php @@ -16,87 +16,62 @@ * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ - namespace FacturaScripts\Plugins\Modelo303\Controller; +use Exception; use FacturaScripts\Core\Base\DataBase\DataBaseWhere; use FacturaScripts\Core\DataSrc\Impuestos; use FacturaScripts\Core\DataSrc\Series; use FacturaScripts\Core\Lib\ExtendedController\BaseView; use FacturaScripts\Core\Lib\ExtendedController\EditController; -use FacturaScripts\Core\Model\Asiento; +use FacturaScripts\Core\Lib\SubAccountTools; use FacturaScripts\Core\Tools; -use FacturaScripts\Dinamic\Lib\Accounting\VatRegularizationToAccounting; -use FacturaScripts\Dinamic\Lib\SubAccountTools; -use FacturaScripts\Dinamic\Model\Join\PartidaImpuestoResumen; -use FacturaScripts\Dinamic\Model\Partida; use FacturaScripts\Dinamic\Model\RegularizacionImpuesto; +use FacturaScripts\Dinamic\Lib\Accounting\VatRegularizationToAccounting; +use FacturaScripts\Plugins\Modelo303\Lib\Modelo303; /** * Controller to list the items in the RegularizacionImpuesto model * - * @author Carlos García Gómez - * @author Jose Antonio Cuello Principal - * @author Cristo M. Estévez Hernández + * @author Carlos García Gómez + * @author Jose Antonio Cuello Principal */ class EditRegularizacionImpuesto extends EditController { - /** @var float */ - public float $purchases; - - /** @var float */ - public float $sales; - - /** @var float */ - public float $total; - - /** @var array */ - public array $modelo303 = []; + /** @var ?Modelo303 */ + public ?Modelo303 $modelo303; + /** + * Returns the class name of the model to use in the editView. + */ public function getModelClassName(): string { return 'RegularizacionImpuesto'; } + /** + * Return the basic data for this page. + * + * @return array + */ public function getPageData(): array { $data = parent::getPageData(); $data['menu'] = 'reports'; - $data['title'] = 'model-303-390'; + $data['title'] = 'model-303'; $data['icon'] = 'fa-solid fa-balance-scale-right'; return $data; } /** - * Calculates the amounts for the different sections of the regularization + * Create accounting entry action procedure. * - * @param PartidaImpuestoResumen[] $data + * @return void */ - protected function calculateAmounts(array $data): void - { - // Init totals values - $this->sales = 0.0; - $this->purchases = 0.0; - - $subAccountTools = new SubAccountTools(); - foreach ($data as $row) { - if ($subAccountTools->isOutputTax($row->codcuentaesp)) { - $this->sales += $row->cuotaiva + $row->cuotarecargo; - continue; - } - - if ($subAccountTools->isInputTax($row->codcuentaesp)) { - $this->purchases += $row->cuotaiva + $row->cuotarecargo; - } - } - - $this->total = $this->sales - $this->purchases; - } - protected function createAccountingEntryAction(): void { $reg = new RegularizacionImpuesto(); - $code = $this->request->get('code'); + $code = $this->request->input('code'); if (false === $reg->load($code)) { Tools::log()->warning('record-not-found'); return; @@ -132,15 +107,44 @@ protected function createViews(): void $this->setTabsPosition('bottom'); $this->createViewsTaxSummary(); + $this->createViewsTaxDetail(); + $this->createViewsTaxLine('ListPartidaImpuesto-1', 'purchases', 'fas fa-sign-in-alt'); + $this->createViewsTaxLine('ListPartidaImpuesto-2', 'sales', 'fas fa-sign-out-alt'); $this->createViewsEntryLine(); } + /** + * Add view for account entry detail. + * + * @param string $viewName + * @return void + */ protected function createViewsEntryLine(string $viewName = 'ListPartida'): void { $this->addListView($viewName, 'Partida', 'accounting-entry', 'fa-solid fa-balance-scale'); $this->disableButtons($viewName, true); } + /** + * Add view for tax detail list. + * + * @param string $viewName + * @return void + */ + protected function createViewsTaxDetail(string $viewName = 'ListPartidaImpuestoResumen'): void + { + $this->addListView($viewName, 'Join\PartidaImpuestoResumen', 'tax-detail'); + $this->disableButtons($viewName, false); + } + + /** + * Add invoices view for tax lines. + * + * @param string $viewName + * @param string $caption + * @param string $icon + * @return void + */ protected function createViewsTaxLine(string $viewName, string $caption, string $icon): void { $this->addListView($viewName, 'Join\PartidaImpuesto', $caption, $icon) @@ -152,242 +156,154 @@ protected function createViewsTaxLine(string $viewName, string $caption, string $this->disableButtons($viewName); } - protected function createViewsTaxSummary(string $viewName = 'ListPartidaImpuestoResumen'): void + /** + * Add view for tax summary form. + * + * @param string $viewName + * @return void + */ + protected function createViewsTaxSummary(string $viewName = 'Modelo303'): void { - $this->addHtmlView($viewName, 'Modelo303', 'Impuesto', 'summary', 'fa-solid fa-list-alt'); + $this->addHtmlView($viewName, $viewName, 'RegularizacionImpuesto', 'summary', 'fa-solid fa-list-alt'); $this->disableButtons($viewName); - } - - protected function disableButtons(string $viewName, bool $clickable = false): void - { - $this->setSettings($viewName, 'btnDelete', false); - $this->setSettings($viewName, 'btnNew', false); - $this->setSettings($viewName, 'checkBoxes', false); - $this->setSettings($viewName, 'clickable', $clickable); + $this->modelo303 = new Modelo303(); } /** * Run the actions that alter data before reading it. * * @param string $action - * * @return bool */ protected function execPreviousAction($action): bool { - switch ($action) { - case 'create-accounting-entry': - $this->createAccountingEntryAction(); - return true; + if ($action == 'create-accounting-entry') { + $this->createAccountingEntryAction(); + return true; } return parent::execPreviousAction($action); } + /** + * Export action procedure. + * + * @return void + */ protected function exportAction(): void { $this->exportManager->setOrientation('landscape'); parent::exportAction(); } - protected function getListPartida(BaseView $view): void - { - $idasiento = $this->getViewModelValue('EditRegularizacionImpuesto', 'idasiento'); - if (!empty($idasiento)) { - $where = [new DataBaseWhere('idasiento', $idasiento)]; - $view->loadData(false, $where, ['orden' => 'ASC']); - } - } - - protected function getListPartidaImpuesto(BaseView $view, int $group): void - { - $id = $this->getViewModelValue($this->getMainViewName(), 'idregiva'); - if (!empty($id)) { - $where = $this->getPartidaImpuestoWhere($group); - $orderBy = ['asientos.fecha' => 'ASC', 'partidas.codserie' => 'ASC', 'partidas.factura' => 'ASC']; - $view->loadData(false, $where, $orderBy); - } - } - - protected function getListPartidaImpuestoResumen(BaseView $view): void + /** + * Load data view procedure + * + * @param string $viewName + * @param BaseView $view + * @throws Exception + */ + protected function loadData($viewName, $view): void { - $impuestos = Impuestos::all(); - - // obtenemos los codigos de subcuentas de los impuestos - $subcuentas = array_values(array_unique(array_filter(array_merge( - array_column($impuestos, 'codsubcuentarep'), - array_column($impuestos, 'codsubcuentasop'), - )))); - - // Obtenemos los asientos para poder filtrar - // por fecha. Asi nos aseguramos que se filtra - // primero por fecha de devengo y si no existe - // por fecha de factura - $asientos = Asiento::all([ - new DataBaseWhere('codejercicio', $this->getModel()->codejercicio), - new DataBaseWhere('fecha', $this->getModel()->fechainicio, '>='), - new DataBaseWhere('fecha', $this->getModel()->fechafin, '<'), - ]); - $idsAsientos = array_unique(array_column($asientos, Asiento::primaryColumn())); - - if(empty($idsAsientos)) { - Tools::log()->warning('accounting-entry-not-found'); - return; - } - - $partidas = Partida::all([ - new DataBaseWhere('idasiento', $idsAsientos, 'IN'), - new DataBaseWhere('codsubcuenta', $subcuentas, 'IN') - ]); - - // agrupamos por subcuenta - $partidasAgrupadas = []; - foreach ($partidas as $partida) { - $partidasAgrupadas[$partida->codsubcuenta][] = $partida; - } - - // inicializamos el modelo303 - $this->modelo303 = []; - for ($i = 0; $i <= 200; $i++) { - $this->modelo303[sprintf('%02d', $i)] = 0.00; - } - - // set default values - $this->modelo303['02'] = 4.00; - $this->modelo303['05'] = 10.00; - $this->modelo303['08'] = 21.00; - $this->modelo303['157'] = 1.75; - $this->modelo303['169'] = 0.5; - $this->modelo303['20'] = 1.4; - $this->modelo303['23'] = 5.2; - - // obtenemos los códigos de subcuentas agrupados según tipo iva - // esto lo hacemos por si existen varios impuestos - // del mismo iva y distintas subcuentas - $subcuentasSegunIVA = []; - foreach ($impuestos as $impuesto) { - $subcuentasSegunIVA[$impuesto->iva]['repercutido'][] = $impuesto->codsubcuentarep; - $subcuentasSegunIVA[$impuesto->iva]['soportado'][] = $impuesto->codsubcuentasop; - } - - // obtenemos los codigos de subcuentas agrupados según tipo recargo - // esto lo hacemos por si existen varios impuestos - // del mismo recargo y distintas subcuentas - $subcuentasSegunRecargo = []; - foreach ($impuestos as $impuesto) { - $subcuentasSegunRecargo[$impuesto->recargo]['repercutido'][] = $impuesto->codsubcuentarepre; - $subcuentasSegunRecargo[$impuesto->recargo]['soportado'][] = $impuesto->codsubcuentasopre; - } - - foreach ($partidasAgrupadas as $subcuenta => $movimientos) { - foreach ($movimientos as $mov) { - // IVA 4% - if (in_array($subcuenta, $subcuentasSegunIVA[4]['repercutido'])) { - $this->modelo303['01'] += $mov->baseimponible; - $this->modelo303['03'] += $mov->haber; - } - - // IVA 10% - if (in_array($subcuenta, $subcuentasSegunIVA[10]['repercutido'])) { - $this->modelo303['04'] += $mov->baseimponible; - $this->modelo303['06'] += $mov->haber; - } - - // IVA 21% - if (in_array($subcuenta, $subcuentasSegunIVA[21]['repercutido'])) { - $this->modelo303['07'] += $mov->baseimponible; - $this->modelo303['09'] += $mov->haber; - } + switch ($viewName) { + case 'EditRegularizacionImpuesto': + parent::loadData($viewName, $view); + $this->settingsMainView(); + break; - // IVA 0% - if (in_array($subcuenta, $subcuentasSegunIVA[0]['repercutido'])) { - $this->modelo303['150'] += $mov->baseimponible; - $this->modelo303['152'] += $mov->haber; + case 'ListPartidaImpuestoResumen': + $mainModel = $this->getModel(); + $where = [ + new DataBaseWhere('partidas.codsubcuenta', '477%', 'LIKE'), + new DataBaseWhere('partidas.codsubcuenta', '472%', 'LIKE', 'OR'), + new DataBaseWhere('asientos.idempresa', $mainModel->idempresa), + new DataBaseWhere('asientos.fecha', $mainModel->fechainicio, '>='), + new DataBaseWhere('asientos.fecha', $mainModel->fechafin, '<='), + new DataBaseWhere('COALESCE(series.siniva, false)', false), + ]; + + if (false === empty($mainModel->idasiento)) { + $where[] = new DataBaseWhere('partidas.idasiento', $mainModel->idasiento, '<>'); } + $view->loadData(false, $where, [ + 'COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)' => 'ASC', + 'partidas.codsubcuenta' => 'ASC', + ]); - // RECARGO 1.75% - if (in_array($subcuenta, $subcuentasSegunRecargo[1.75]['repercutido'])) { - $this->modelo303['156'] += $mov->baseimponible; - $this->modelo303['158'] += $mov->haber; - } + $this->modelo303->loadFromResumen($view->cursor); // Load data into Modelo303 View + break; - // RECARGO 0.5% - if (in_array($subcuenta, $subcuentasSegunRecargo[0.5]['repercutido'])) { - $this->modelo303['168'] += $mov->baseimponible; - $this->modelo303['170'] += $mov->haber; - } + case 'ListPartida': + $this->getListPartida($view); + break; - // RECARGO 1.4% - if (in_array($subcuenta, $subcuentasSegunRecargo[1.4]['repercutido'])) { - $this->modelo303['19'] += $mov->baseimponible; - $this->modelo303['21'] += $mov->haber; - } + case 'ListPartidaImpuesto-1': + $this->getListPartidaImpuesto($view, SubAccountTools::SPECIAL_GROUP_TAX_INPUT); + break; - // RECARGO 5.2% - if (in_array($subcuenta, $subcuentasSegunRecargo[5.2]['repercutido'])) { - $this->modelo303['22'] += $mov->baseimponible; - $this->modelo303['24'] += $mov->haber; - } - } + case 'ListPartidaImpuesto-2': + $this->getListPartidaImpuesto($view, SubAccountTools::SPECIAL_GROUP_TAX_OUTPUT); + break; } + } - // Total cuota devengada - $this->modelo303['27'] = $this->modelo303['152'] + $this->modelo303['167'] + $this->modelo303['03'] + $this->modelo303['155'] + $this->modelo303['06'] + $this->modelo303['09'] + $this->modelo303['11'] + $this->modelo303['13'] + $this->modelo303['15'] + $this->modelo303['158'] + $this->modelo303['170'] + $this->modelo303['18'] + $this->modelo303['21'] + $this->modelo303['24'] + $this->modelo303['26']; - - /** - * IVA DEDUCIBLE - */ - - // Por cuotas soportadas en operaciones interiores corrientes - foreach ($partidasAgrupadas as $subcuenta => $movimientos) { - foreach ($movimientos as $mov) { - // IVA 4% - if (in_array($subcuenta, $subcuentasSegunIVA[4]['soportado'])) { - $this->modelo303['28'] += $mov->baseimponible; - $this->modelo303['29'] += $mov->debe; - } - - // IVA 10% - if (in_array($subcuenta, $subcuentasSegunIVA[10]['soportado'])) { - $this->modelo303['28'] += $mov->baseimponible; - $this->modelo303['29'] += $mov->debe; - } + /** + * 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); + } - // IVA 21% - if (in_array($subcuenta, $subcuentasSegunIVA[21]['soportado'])) { - $this->modelo303['28'] += $mov->baseimponible; - $this->modelo303['29'] += $mov->debe; - } - } + /** + * Load data for accounting entry. + * + * @param BaseView $view + * @return void + */ + private function getListPartida(BaseView $view): void + { + if (false === empty($this->getModel()->idasiento)) { + $where = [new DataBaseWhere('idasiento', $this->getModel()->idasiento)]; + $view->loadData(false, $where, ['orden' => 'ASC']); } + } - // Total a deducir - $this->modelo303['45'] = $this->modelo303['29'] + $this->modelo303['31'] + $this->modelo303['33'] + $this->modelo303['35'] + $this->modelo303['37'] + $this->modelo303['39'] + $this->modelo303['41'] + $this->modelo303['42'] + $this->modelo303['43'] + $this->modelo303['44']; + /** + * Load data into invoices view for tax lines. + * + * @param BaseView $view + * @param int $group + * @return void + */ + private function getListPartidaImpuesto(BaseView $view, int $group): void + { + $id = $this->getModel()->idregiva; + if (empty($id)) { + return; + } - // Resultado régimen general - $this->modelo303['46'] = $this->modelo303['27'] - $this->modelo303['45']; + $where = $this->getPartidaImpuestoWhere($group); + $orderBy = ['asientos.fecha' => 'ASC', 'partidas.codserie' => 'ASC', 'partidas.factura' => 'ASC']; + $view->loadData(false, $where, $orderBy); } /** * Get DataBaseWhere filter for tax group * * @param int $group - * * @return DataBaseWhere[] */ - protected function getPartidaImpuestoWhere(int $group): array + private function getPartidaImpuestoWhere(int $group): array { - $saTools = new SubAccountTools(); - $where = [ - new DataBaseWhere('asientos.codejercicio', $this->getModel()->codejercicio), - new DataBaseWhere('asientos.fecha', $this->getModel()->fechainicio, '>='), - new DataBaseWhere('asientos.fecha', $this->getModel()->fechafin, '<='), - new DataBaseWhere('series.siniva', false), - new DataBaseWhere('partidas.baseimponible', 0, '!='), - new DataBaseWhere('COALESCE(partidas.iva, 0)', 0, '>', 'OR'), - $saTools->whereForSpecialAccounts('COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)', $group) - ]; - // obtenemos todos los ids de los asientos de las regularizaciones $ids = []; foreach (RegularizacionImpuesto::all() as $reg) { @@ -395,60 +311,40 @@ protected function getPartidaImpuestoWhere(int $group): array $ids[] = $reg->idasiento; } } - if (!empty($ids)) { - array_unshift($where, new DataBaseWhere('asientos.idasiento', implode(',', $ids), 'NOT IN')); - } - return $where; + $subAccountTools = new SubAccountTools(); + return [ + new DataBaseWhere('asientos.idasiento', implode(',', $ids), 'NOT IN'), + new DataBaseWhere('asientos.codejercicio', $this->getModel()->codejercicio), + new DataBaseWhere('asientos.fecha', $this->getModel()->fechainicio, '>='), + new DataBaseWhere('asientos.fecha', $this->getModel()->fechafin, '<='), + new DataBaseWhere('COALESCE(series.siniva, 0)', 0), + new DataBaseWhere('partidas.baseimponible', 0, '!='), + new DataBaseWhere('COALESCE(partidas.iva, 0)', 0, '>', 'OR'), + $subAccountTools->whereForSpecialAccounts('COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)', $group) + ]; } /** - * Load data view procedure + * Settings for main view. * - * @param string $viewName - * @param BaseView $view + * @return void + * @throws Exception */ - protected function loadData($viewName, $view): void + private function settingsMainView(): void { - switch ($viewName) { - case 'EditRegularizacionImpuesto': - parent::loadData($viewName, $view); - if (false === $view->model->exists()) { - $view->disableColumn('tax-credit-account', false, 'true'); - $view->disableColumn('tax-debit-account', false, 'true'); - } - break; + $viewName = $this->getMainViewName(); + $exists = $this->getModel()->exists(); + $this->views[$viewName]->disableColumn('tax-credit-account', $exists, 'true') + ->disableColumn('tax-debit-account', $exists, 'true'); - case 'ListPartida': - $this->getListPartida($view); - break; - - case 'ListPartidaImpuestoResumen': - $this->getListPartidaImpuestoResumen($view); - $this->setCreateAcEntryButton($viewName); - 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; - } - } - - protected function setCreateAcEntryButton(string $viewName): void - { - $idasiento = $this->getViewModelValue($this->getMainViewName(), 'idasiento'); - if (empty($idasiento)) { + if (empty($this->getModel()->idasiento)) { $this->addButton($viewName, [ 'action' => 'create-accounting-entry', + 'label' => 'create-accounting-entry', + 'icon' => 'fa-solid fa-balance-scale', 'color' => 'success', 'confirm' => true, - 'icon' => 'fa-solid fa-balance-scale', - 'label' => 'create-accounting-entry', - 'row' => 'actions' ]); } } diff --git a/Controller/ListRegularizacionImpuesto.php b/Controller/ListRegularizacionImpuesto.php index d9d2117..90edf52 100644 --- a/Controller/ListRegularizacionImpuesto.php +++ b/Controller/ListRegularizacionImpuesto.php @@ -49,39 +49,47 @@ protected function createViews(): void $this->createViewsModel390(); } + /** + * Create the list view for Model 303. + * + * @param string $viewName + * @return void + */ protected function createViewsModel303(string $viewName = 'ListRegularizacionImpuesto'): void { + $exercises = $this->codeModel->all('ejercicios', 'codejercicio', 'nombre'); $this->addView($viewName, 'RegularizacionImpuesto', 'model-303', 'fa-solid fa-book') + // Search and Orderby + ->addSearchFields(['codsubcuentaacr', 'codsubcuentadeu']) ->addOrderBy(['fechainicio'], 'start-date', 2) ->addOrderBy(['codejercicio||periodo'], 'period') - ->addSearchFields(['codsubcuentaacr', 'codsubcuentadeu']); - - // añadimos filtros - $this->addFilterSelectWhere($viewName, 'status', [ - ['label' => Tools::lang()->trans('model-303'), 'where' => [new DataBaseWhere('periodo', 'Y', '!=')]] - ]); - - $this->addFilterSelect($viewName, 'idempresa', 'company', 'idempresa', Empresas::codeModel()); - - $exercises = $this->codeModel->all('ejercicios', 'codejercicio', 'nombre'); - $this->addFilterSelect($viewName, 'codejercicio', 'exercise', 'codejercicio', $exercises); + // Filters + ->addFilterSelect('idempresa', 'company', 'idempresa', Empresas::codeModel()) + ->addFilterSelect('codejercicio', 'exercise', 'codejercicio', $exercises) + ->addFilterSelectWhere('status', [ + ['label' => Tools::lang()->trans('model-303'), 'where' => [new DataBaseWhere('periodo', 'Y', '!=')]] + ]); } + /** + * Create the list view for Model 390. + * + * @param string $viewName + * @return void + */ protected function createViewsModel390(string $viewName = 'ListRegularizacionImpuesto-390'): void { + $exercises = $this->codeModel->all('ejercicios', 'codejercicio', 'nombre'); $this->addView($viewName, 'RegularizacionImpuesto', 'model-390', 'fa-solid fa-book') + // Search and Orderby ->addOrderBy(['fechainicio'], 'start-date', 2) ->addOrderBy(['codejercicio||periodo'], 'period') - ->addSearchFields(['codsubcuentaacr', 'codsubcuentadeu']); - - // añadimos filtros - $this->addFilterSelectWhere($viewName, 'status', [ - ['label' => Tools::lang()->trans('model-390'), 'where' => [new DataBaseWhere('periodo', 'Y')]] - ]); - - $this->addFilterSelect($viewName, 'idempresa', 'company', 'idempresa', Empresas::codeModel()); - - $exercises = $this->codeModel->all('ejercicios', 'codejercicio', 'nombre'); - $this->addFilterSelect($viewName, 'codejercicio', 'exercise', 'codejercicio', $exercises); + ->addSearchFields(['codsubcuentaacr', 'codsubcuentadeu']) + // Filters + ->addFilterSelect('idempresa', 'company', 'idempresa', Empresas::codeModel()) + ->addFilterSelect('codejercicio', 'exercise', 'codejercicio', $exercises) + ->addFilterSelectWhere('status', [ + ['label' => Tools::lang()->trans('model-390'), 'where' => [new DataBaseWhere('periodo', 'Y')]] + ]); } } diff --git a/Lib/Accounting/VatRegularizationToAccounting.php b/Lib/Accounting/VatRegularizationToAccounting.php index 09139ce..97ee365 100644 --- a/Lib/Accounting/VatRegularizationToAccounting.php +++ b/Lib/Accounting/VatRegularizationToAccounting.php @@ -175,14 +175,12 @@ private function checkInvoicesWithoutAccEntry($reg): bool new DataBaseWhere('idasiento', 'IS NULL') ]; - $facturasClienteSinAsiento = FacturaCliente::all($where); - - $facturasProveedorSinAsiento = FacturaProveedor::all($where); - - if (count($facturasClienteSinAsiento) > 0 || count($facturasProveedorSinAsiento) > 0) { + $facturasSinAsiento = FacturaCliente::all($where, [], 0, 1); + if (false === empty($facturasSinAsiento)) { return false; } - return true; + $facturasSinAsiento = FacturaProveedor::all($where, [], 0, 1); + return empty($facturasSinAsiento); } } diff --git a/Lib/Modelo303.php b/Lib/Modelo303.php new file mode 100644 index 0000000..55d1b5c --- /dev/null +++ b/Lib/Modelo303.php @@ -0,0 +1,242 @@ + + * + * 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\Modelo303\Lib; + +use FacturaScripts\Core\DataSrc\Impuestos; +use FacturaScripts\Core\Tools; +use FacturaScripts\Plugins\Modelo303\Model\Join\PartidaImpuestoResumen; + +/** + * Class to handle Modelo 303 tax form data. + * + * @author Jose Antonio Cuello Principal + */ +class Modelo303 +{ + private const MAX_SQUARE = 200; + + /** + * Stores all model squares. + * Each key is the AEAT square number. + * '01' => 0.00, '02' => 0.00, ... + */ + private array $square; + + /** + * Structure for know to assign values to squares. + * + * @var array>> + */ + private array $casillaMap = [ + /* + * IVA devengado (repercutido). + */ + // Ventas nacionales (régimen general) + 'IVAREP' => [ + '2' => ['base' => '165', 'cuota' => '167'], + '4' => ['base' => '01', 'cuota' => '03'], + '7.5' => ['base' => '153', 'cuota' => '155'], + '10' => ['base' => '04', 'cuota' => '06'], + '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']], + + // Recargo de equivalencia + 'IVARRE' => [ + '1.75' => ['base' => '156', 'cuota' => '158'], + '0.26' => ['base' => '168', 'cuota' => '170'], + '1' => ['base' => '16', 'cuota' => '18'], + '1.4' => ['base' => '19', 'cuota' => '21'], + '5.2' => ['base' => '22', 'cuota' => '24'], + ], + + // Operaciones exentas + 'IVAREX' => ['0' => ['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'], + ], + + // Compras en importaciones + 'IVASIM' => ['21' => ['base' => '32', 'cuota' => '33']], + + // Compras en adquisiciones intracomunitarias + 'IVASUE' => ['21' => ['base' => '36', 'cuota' => '37']], + + // Operaciones exentas + 'IVASEX' => ['0' => ['base' => '60', 'cuota' => null]], + ]; + + /** + * Initializes the tax rates for each square. + */ + public function __construct() + { + $this->square = array_fill_keys( + array_map(fn($i) => sprintf('%02d', $i), range(0, self::MAX_SQUARE)), + 0.00 + ); + + $this->square['02'] = 4.00; + $this->square['05'] = 10.00; + $this->square['08'] = 21.00; + $this->square['17'] = 1.00; + $this->square['20'] = 1.40; + $this->square['23'] = 5.20; + $this->square['154'] = 7.50; + $this->square['157'] = 1.75; + $this->square['169'] = 0.26; + $this->square['166'] = 2.00; + } + + /** + * Get the value of a specific square. + * + * @param string $square + * @return float + */ + public function casilla(string $square): float + { + return $this->square[$square] ?? 0.00; + } + + /** + * Get the value of a specific square formatted as a string. + * + * @param string $square + * @return string + */ + public function casillaStr(string $square, bool $showEmpty = false): string + { + $value = $this->casilla($square); + if (empty($value) && false === $showEmpty ) { + return ''; + } + return Tools::number($value, 2); + } + + /** + * Loads summary data from an array of PartidaImpuestoResumen. + * + * @param PartidaImpuestoResumen[] $resumen + */ + public function loadFromResumen(array $resumen): void + { + foreach ($resumen as $item) { + $this->addMovimiento( + $item->codcuentaesp ?? '', + (float) $item->iva, + (float) $item->recargo, + (float) $item->baseimponible, + (float) $item->cuota + ); + } + $this->calculateTotals(); + } + + /** + * 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. + * + * @param string $tipo + * @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 + { + if (false === isset($this->casillaMap[$tipo])) { + return; + } + + // Determine the correct group based on the tax rate. + $tax = ($tipo === 'IVARRE') ? $recargo : $iva; + $key = rtrim(rtrim(number_format($tax, 1, '.', ''), '0'), '.'); + $grupo = $this->casillaMap[$tipo][$key] + ?? $this->casillaMap[$tipo][(string)(int)$tax] + ?? $this->casillaMap[$tipo]['*'] + ?? null; + + if ($grupo === null) { + return; + } + + // Update base and quota squares. + if (false === empty($grupo['base'])) { + $this->square[$grupo['base']] += $base; + } + + 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; + } + } + + /** + * Calculate total squares based on individual entries. + * + * @return void + */ + private function calculateTotals(): void + { + // Total cuota devengada + $this->square['27'] = $this->square['03'] + + $this->square['06'] + + $this->square['09'] + + $this->square['11'] + + $this->square['13'] + + $this->square['15'] + + $this->square['18'] + + $this->square['21'] + + $this->square['24'] + + $this->square['26']; + + // Total a deducir + $this->square['45'] = $this->square['29'] + + $this->square['31'] + + $this->square['33'] + + $this->square['35'] + + $this->square['37'] + + $this->square['39'] + + $this->square['41'] + + $this->square['42'] + + $this->square['43'] + + $this->square['44']; + + // Resultado régimen general + $this->square['46'] = $this->square['27'] - $this->square['45']; + } +} diff --git a/Model/Join/PartidaImpuestoResumen.php b/Model/Join/PartidaImpuestoResumen.php index 6e4a2e3..1623ea1 100644 --- a/Model/Join/PartidaImpuestoResumen.php +++ b/Model/Join/PartidaImpuestoResumen.php @@ -16,7 +16,6 @@ * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ - namespace FacturaScripts\Plugins\Modelo303\Model\Join; use FacturaScripts\Core\Model\Base\JoinModel; @@ -25,37 +24,9 @@ * Auxiliary model to load a resume of accounting entries with VAT * * @author Jose Antonio Cuello Principal - * @author Carlos García Gómez - * - * @property float $baseimponible - * @property string $codcuentaesp - * @property string $codejercicio - * @property string $codsubcuenta - * @property float $cuotaiva - * @property float $cuotarecargo - * @property string $descripcion - * @property int $idsubcuenta - * @property float $iva - * @property float $recargo - * @property float $total */ class PartidaImpuestoResumen extends JoinModel { - - /** - * Reset the values of all model view properties. - */ - public function clear(): void - { - parent::clear(); - $this->baseimponible = 0.0; - $this->iva = 0.0; - $this->recargo = 0.0; - $this->cuotaiva = 0.0; - $this->cuotarecargo = 0.0; - $this->total = 0.0; - } - /** * Returns an array of fields for the select clausule. * @@ -64,16 +35,19 @@ public function clear(): void protected function getFields(): array { return [ - 'baseimponible' => 'SUM(partidas.baseimponible)', - 'codcuentaesp' => 'COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)', - 'codejercicio' => 'asientos.codejercicio', 'codsubcuenta' => 'partidas.codsubcuenta', + 'iva' => 'COALESCE(partidas.iva, 0)', + 'recargo' => 'COALESCE(partidas.recargo, 0)', + 'descripcion' => 'subcuentas.descripcion', - 'idsubcuenta' => 'partidas.idsubcuenta', - 'iva' => 'partidas.iva', - 'recargo' => 'partidas.recargo', - 'debe' => 'SUM(partidas.debe)', - 'haber' => 'SUM(partidas.haber)', + + 'codcuentaesp' => 'COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)', + 'tipo_desc' => 'cuentasesp.descripcion', + + 'baseimponible' => 'ROUND(SUM(partidas.baseimponible), 2)', + 'debe' => 'ROUND(SUM(partidas.debe), 2)', + 'haber' => 'ROUND(SUM(partidas.haber), 2)', + 'cuota' => 'ROUND(SUM(' . $this->sqlForCuota() . '), 2)', ]; } @@ -84,14 +58,12 @@ protected function getFields(): array */ protected function getGroupFields(): string { - return 'asientos.codejercicio,' - . 'COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp),' - . 'cuentasesp.descripcion,' - . 'partidas.codsubcuenta,' - . 'subcuentas.descripcion,' - . 'partidas.idsubcuenta,' - . 'partidas.iva,' - . 'partidas.recargo'; + return 'partidas.codsubcuenta' + . ', partidas.iva' + . ', partidas.recargo' + . ', subcuentas.descripcion' + . ', COALESCE(subcuentas.codcuentaesp, cuentas.codcuentaesp)' + . ', cuentasesp.descripcion'; } /** @@ -101,12 +73,12 @@ protected function getGroupFields(): string */ protected function getSQLFrom(): string { - return 'asientos' - . ' LEFT JOIN partidas ON partidas.idasiento = asientos.idasiento' - . ' LEFT JOIN subcuentas ON subcuentas.idsubcuenta = partidas.idsubcuenta' - . ' LEFT 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'; + return 'partidas' + . ' INNER JOIN asientos on asientos.idasiento = partidas.idasiento' + . ' 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'; } /** @@ -117,38 +89,25 @@ protected function getSQLFrom(): string protected function getTables(): array { return [ - 'asientos', 'partidas', + 'asientos', 'subcuentas', 'cuentas', - 'cuentasesp' + 'cuentasesp', + 'series', ]; } /** - * Assign the values of the $data array to the model properties. + * SQL snippet to calculate the cuota field. * - * @param array $data + * @return string */ - protected function loadFromData(array $data): void + private function sqlForCuota(): string { - 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']; - $this->cuotaiva = 0.0; - } - - $this->total = $this->baseimponible + $this->cuotaiva + $this->cuotarecargo; + return 'CASE WHEN partidas.baseimponible < 0 AND (partidas.debe + partidas.haber) > 0 + THEN (partidas.debe + partidas.haber) * -1 + ELSE partidas.debe + partidas.haber + END'; } } diff --git a/Translation/es_ES.json b/Translation/es_ES.json index 942dbe3..ae20f5f 100644 --- a/Translation/es_ES.json +++ b/Translation/es_ES.json @@ -6,5 +6,16 @@ "model-303": "Modelo 303", "model-303-390": "Modelos 303 y 390", "model-390": "Modelo 390", - "year-model-390": "Año (modelo 390)" + "year-model-390": "Año (modelo 390)", + "---NEW---": "--------------------------", + "model-303-new": "Modelos 303 (2025)", + "tax-detail": "Detalle de impuestos", + "vat-accrued": "IVA repercutido", + "quota": "Cuota", + "total-accrued-fee": "Cuota total repercutida", + "deductible-vat": "IVA deducible", + "base": "Base", + "total-to-deduct": "Total a deducir", + "general-regime-result": "Resultado", + "total-result-of-the-general-regime": "Resultado total" } \ No newline at end of file diff --git a/View/Modelo303.html.twig b/View/Modelo303.html.twig index 7ea7a34..9b072f0 100644 --- a/View/Modelo303.html.twig +++ b/View/Modelo303.html.twig @@ -19,178 +19,178 @@ 150 - {{ fsc.modelo303['150'] is empty ? '' : number(fsc.modelo303['150'], 2) }} + {{ fsc.modelo303.casillaStr('150') }} 151 - {{ fsc.modelo303['151'] is empty ? '' : number(fsc.modelo303['151'], 2) }} + {{ fsc.modelo303.casillaStr('151', true) }} 152 - {{ fsc.modelo303['152'] is empty ? '' : number(fsc.modelo303['152'], 2) }} + {{ fsc.modelo303.casillaStr('152') }} 165 - {{ fsc.modelo303['165'] is empty ? '' : number(fsc.modelo303['165'], 2) }} + {{ fsc.modelo303.casillaStr('165') }} 166 - {{ fsc.modelo303['166'] is empty ? '' : number(fsc.modelo303['166'], 2) }} + {{ fsc.modelo303.casillaStr('166') }} 167 - {{ fsc.modelo303['167'] is empty ? '' : number(fsc.modelo303['167'], 2) }} + {{ fsc.modelo303.casillaStr('167') }} 01 - {{ fsc.modelo303['01'] is empty ? '' : number(fsc.modelo303['01'], 2) }} + {{ fsc.modelo303.casillaStr('01') }} 02 - {{ fsc.modelo303['02'] is empty ? '' : number(fsc.modelo303['02'], 2) }} + {{ fsc.modelo303.casillaStr('02') }} 03 - {{ fsc.modelo303['03'] is empty ? '' : number(fsc.modelo303['03'], 2) }} + {{ fsc.modelo303.casillaStr('03') }} 153 - {{ fsc.modelo303['153'] is empty ? '' : number(fsc.modelo303['153'], 2) }} + {{ fsc.modelo303.casillaStr('153') }} 154 - {{ fsc.modelo303['154'] is empty ? '' : number(fsc.modelo303['154'], 2) }} + {{ fsc.modelo303.casillaStr('154') }} 155 - {{ fsc.modelo303['155'] is empty ? '' : number(fsc.modelo303['155'], 2) }} + {{ fsc.modelo303.casillaStr('155') }} 04 - {{ fsc.modelo303['04'] is empty ? '' : number(fsc.modelo303['04'], 2) }} + {{ fsc.modelo303.casillaStr('04') }} 05 - {{ fsc.modelo303['05'] is empty ? '' : number(fsc.modelo303['05'], 2) }} + {{ fsc.modelo303.casillaStr('05') }} 06 - {{ fsc.modelo303['06'] is empty ? '' : number(fsc.modelo303['06'], 2) }} + {{ fsc.modelo303.casillaStr('06') }} 07 - {{ fsc.modelo303['07'] is empty ? '' : number(fsc.modelo303['07'], 2) }} + {{ fsc.modelo303.casillaStr('07') }} 08 - {{ fsc.modelo303['08'] is empty ? '' : number(fsc.modelo303['08'], 2) }} + {{ fsc.modelo303.casillaStr('08') }} 09 - {{ fsc.modelo303['09'] is empty ? '' : number(fsc.modelo303['09'], 2) }} + {{ fsc.modelo303.casillaStr('09') }} 10 - {{ fsc.modelo303['10'] is empty ? '' : number(fsc.modelo303['10'], 2) }} + {{ fsc.modelo303.casillaStr('10') }} - + 11 - {{ fsc.modelo303['11'] is empty ? '' : number(fsc.modelo303['11'], 2) }} + {{ fsc.modelo303.casillaStr('11') }} 12 - {{ fsc.modelo303['12'] is empty ? '' : number(fsc.modelo303['12'], 2) }} + {{ fsc.modelo303.casillaStr('12') }} - + 13 - {{ fsc.modelo303['13'] is empty ? '' : number(fsc.modelo303['13'], 2) }} + {{ fsc.modelo303.casillaStr('13') }} 14 - {{ fsc.modelo303['14'] is empty ? '' : number(fsc.modelo303['14'], 2) }} + {{ fsc.modelo303.casillaStr('14') }} - + 15 - {{ fsc.modelo303['15'] is empty ? '' : number(fsc.modelo303['15'], 2) }} + {{ fsc.modelo303.casillaStr('15') }} 156 - {{ fsc.modelo303['156'] is empty ? '' : number(fsc.modelo303['156'], 2) }} + {{ fsc.modelo303.casillaStr('156') }} 157 - {{ fsc.modelo303['157'] is empty ? '' : number(fsc.modelo303['157'], 2) }} + {{ fsc.modelo303.casillaStr('157') }} 158 - {{ fsc.modelo303['158'] is empty ? '' : number(fsc.modelo303['158'], 2) }} + {{ fsc.modelo303.casillaStr('158') }} 168 - {{ fsc.modelo303['168'] is empty ? '' : number(fsc.modelo303['168'], 2) }} + {{ fsc.modelo303.casillaStr('168') }} 169 - {{ fsc.modelo303['169'] is empty ? '' : number(fsc.modelo303['169'], 2) }} + {{ fsc.modelo303.casillaStr('169') }} 170 - {{ fsc.modelo303['170'] is empty ? '' : number(fsc.modelo303['170'], 2) }} + {{ fsc.modelo303.casillaStr('170') }} 16 - {{ fsc.modelo303['16'] is empty ? '' : number(fsc.modelo303['16'], 2) }} + {{ fsc.modelo303.casillaStr('16') }} 17 - {{ fsc.modelo303['17'] is empty ? '' : number(fsc.modelo303['17'], 2) }} + {{ fsc.modelo303.casillaStr('17') }} 18 - {{ fsc.modelo303['18'] is empty ? '' : number(fsc.modelo303['18'], 2) }} + {{ fsc.modelo303.casillaStr('18') }} 19 - {{ fsc.modelo303['19'] is empty ? '' : number(fsc.modelo303['19'], 2) }} + {{ fsc.modelo303.casillaStr('19') }} 20 - {{ fsc.modelo303['20'] is empty ? '' : number(fsc.modelo303['20'], 2) }} + {{ fsc.modelo303.casillaStr('20') }} 21 - {{ fsc.modelo303['21'] is empty ? '' : number(fsc.modelo303['21'], 2) }} + {{ fsc.modelo303.casillaStr('21') }} 22 - {{ fsc.modelo303['22'] is empty ? '' : number(fsc.modelo303['22'], 2) }} + {{ fsc.modelo303.casillaStr('22') }} 23 - {{ fsc.modelo303['23'] is empty ? '' : number(fsc.modelo303['23'], 2) }} + {{ fsc.modelo303.casillaStr('23') }} 24 - {{ fsc.modelo303['24'] is empty ? '' : number(fsc.modelo303['24'], 2) }} + {{ fsc.modelo303.casillaStr('24') }} 25 - {{ fsc.modelo303['25'] is empty ? '' : number(fsc.modelo303['25'], 2) }} + {{ fsc.modelo303.casillaStr('25') }} - + 26 - {{ fsc.modelo303['26'] is empty ? '' : number(fsc.modelo303['26'], 2) }} + {{ fsc.modelo303.casillaStr('26') }} @@ -201,7 +201,7 @@ {{ trans('total-accrued-fee') }} 27 - {{ fsc.modelo303['27'] is empty ? '' : number(fsc.modelo303['27'], 2) }} + {{ fsc.modelo303.casillaStr('27') }} @@ -225,14 +225,14 @@ - {{ trans('deductible-vat-28') }} + {{ trans('deductible-vat') }} 28 - {{ fsc.modelo303['28'] is empty ? '' : number(fsc.modelo303['28'], 2) }} + {{ fsc.modelo303.casillaStr('28') }} 29 - {{ fsc.modelo303['29'] is empty ? '' : number(fsc.modelo303['29'], 2) }} + {{ fsc.modelo303.casillaStr('29') }} @@ -243,7 +243,7 @@ {{ trans('total-to-deduct') }} 45 - {{ fsc.modelo303['45'] is empty ? '' : number(fsc.modelo303['45'], 2) }} + {{ fsc.modelo303.casillaStr('45') }} @@ -259,7 +259,7 @@ {{ trans('total-result-of-the-general-regime') }} 46 - {{ fsc.modelo303['46'] is empty ? '' : number(fsc.modelo303['46'], 2) }} + {{ fsc.modelo303.casillaStr('46') }} diff --git a/XMLView/ListPartidaImpuestoResumen.xml b/XMLView/ListPartidaImpuestoResumen.xml index 30e0178..161ae82 100644 --- a/XMLView/ListPartidaImpuestoResumen.xml +++ b/XMLView/ListPartidaImpuestoResumen.xml @@ -16,63 +16,34 @@ * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * - * @author Carlos García Gómez * @author Jose Antonio Cuello Principal --> - - + + - - - - - - + - + - - - - - - - - - + - - - - - - + - - + + - - + + - - - - - - - - - - - \ No newline at end of file + diff --git a/facturascripts.ini b/facturascripts.ini index a3d3901..198432a 100644 --- a/facturascripts.ini +++ b/facturascripts.ini @@ -1,4 +1,4 @@ name = 'Modelo303' description = 'Modelo 303 y 390 de la Hacienda española para la declaración trimestral y anual de IVA.' -version = 2.7 -min_version = 2025 \ No newline at end of file +version = 2.8 +min_version = 2025