diff --git a/l10n_es_aeat_mod303_special_prorate_regularization/pyproject.toml b/l10n_es_aeat_mod303_special_prorate_regularization/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/l10n_es_aeat_mod303_special_prorate_regularization/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/l10n_es_aeat_vat_special_prorrate/README.rst b/l10n_es_aeat_vat_special_prorrate/README.rst new file mode 100644 index 000000000..f86f3448f --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/README.rst @@ -0,0 +1,64 @@ +=============================== +AEAT - Prorrata especial de IVA +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ca80aae12d33d2f1db6d6660cfa349770386cbcf591c27f04c6610f447414223 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-NuoBiT%2Fodoo--addons-lightgray.png?logo=github + :target: https://github.com/NuoBiT/odoo-addons/tree/18.0/l10n_es_aeat_vat_special_prorrate + :alt: NuoBiT/odoo-addons + +|badge1| |badge2| |badge3| + + Módulo para gestionar la prorrata especial del IVA en las facturas de + la AEAT + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* NuoBiT Solutions SL + +Contributors +------------ + +- `NuoBiT `__: + + - Eric Antones eantones@nuobit.com + - Deniz Gallo dgallo@nuobit.com + +Maintainers +----------- + +This module is part of the `NuoBiT/odoo-addons `_ project on GitHub. + +You are welcome to contribute. diff --git a/l10n_es_aeat_vat_special_prorrate/__init__.py b/l10n_es_aeat_vat_special_prorrate/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/l10n_es_aeat_vat_special_prorrate/__manifest__.py b/l10n_es_aeat_vat_special_prorrate/__manifest__.py new file mode 100644 index 000000000..1ca37460a --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright NuoBiT Solutions SL - Eric Antones +# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "AEAT - Prorrata especial de IVA", + "summary": "Módulo para gestionar la prorrata especial del IVA " + "en las facturas de la AEAT", + "version": "18.0.1.0.0", + "category": "Accounting", + "author": "NuoBiT Solutions SL", + "website": "https://github.com/NuoBiT/odoo-addons", + "license": "AGPL-3", + "depends": [ + "l10n_es_aeat_mod303", + "l10n_es_special_prorate", + ], + "data": [ + "security/ir.model.access.csv", + "security/aeat_map_special_prorrate_year.xml", + "views/aeat_map_special_prorrate_year_views.xml", + "views/res_company_view.xml", + ], +} diff --git a/l10n_es_aeat_vat_special_prorrate/i18n/es.po b/l10n_es_aeat_vat_special_prorrate/i18n/es.po new file mode 100644 index 000000000..693ed3261 --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/i18n/es.po @@ -0,0 +1,265 @@ +# This file contains the translation of the following modules: +# * l10n_es_aeat_vat_special_prorrate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-02-04 09:38+0000\n" +"PO-Revision-Date: 2022-02-04 09:38+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: l10n_es_aeat_vat_special_prorrate +#: sql_constraint:aeat.map.special.prorrate.year:0 +msgid "AEAT year must be unique" +msgstr "AEAT year must be unique" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.actions.act_window,name:l10n_es_aeat_vat_special_prorrate.action_aeat_map_special_prorrate_year +#: model:ir.ui.menu,name:l10n_es_aeat_vat_special_prorrate.menu_aeat_map_special_prorrate_year +#: model_terms:ir.ui.view,arch_db:l10n_es_aeat_vat_special_prorrate.aeat_map_special_prorrate_year_view_form +#: model_terms:ir.ui.view,arch_db:l10n_es_aeat_vat_special_prorrate.aeat_map_special_prorrate_year_view_tree +msgid "Aeat VAT special prorate map" +msgstr "AEAT prorrata especial IVA - Mapeo anual" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model_terms:ir.ui.view,arch_db:l10n_es_aeat_vat_special_prorrate.aeat_map_special_prorrate_year_view_form +msgid "Close" +msgstr "Cerrar" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py:35 +#: selection:aeat.map.special.prorrate.year,state:0 +#, python-format +msgid "Closed" +msgstr "Cerrado" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__company_id +msgid "Company" +msgstr "Compañía" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model_terms:ir.ui.view,arch_db:l10n_es_aeat_vat_special_prorrate.aeat_map_special_prorrate_year_view_form +msgid "Compute" +msgstr "Calcular" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: l10n_es_aeat_vat_special_prorrate +#: selection:account.tax,prorrate_type:0 +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/account_tax.py:14 +#, python-format +msgid "Deductible" +msgstr "Deducible" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__tax_final_percentage +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__tax_final_percentage_aux +msgid "Final tax %" +msgstr "Impuesto definitivo %" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py:34 +#: selection:aeat.map.special.prorrate.year,state:0 +#, python-format +msgid "Finale" +msgstr "Final" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__id +msgid "ID" +msgstr "ID" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/account_tax.py:26 +#, python-format +msgid "If a tax has prorate the year should exist on vat mapping" +msgstr "If a tax has prorate the year should exist on vat mapping" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/account_tax.py:59 +#, python-format +msgid "If a tax has prorate, it's only suported 'percent' type with price not included" +msgstr "If a tax has prorate, it's only suported 'percent' type with price not included" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model,name:l10n_es_aeat_vat_special_prorrate.model_account_invoice +msgid "Invoice" +msgstr "Factura" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py:85 +#, python-format +msgid "It's not possible to delete a closed prorate map" +msgstr "It's not possible to delete a closed prorate map" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py:196 +#, python-format +msgid "It's not possible to recompute a closed prorate" +msgstr "It's not possible to recompute a closed prorate" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_res_company__l10n_es_prorate_enabled +msgid "L10n ES Prorate Enabled" +msgstr "L10n ES prorrata activada" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model_terms:ir.ui.view,arch_db:l10n_es_aeat_vat_special_prorrate.view_company_aeat_form +msgid "Prorate" +msgstr "Prorrata" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year____last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__map_prorrate_next_year_id +msgid "Map Prorate Next Year" +msgstr "Map Prorate Next Year" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/account_tax.py:68 +#, python-format +msgid "Multiple taxes with the same prorate type under same parent is not suported" +msgstr "Multiple taxes with the same prorate type under same parent is not suported" + +#. module: l10n_es_aeat_vat_special_prorrate +#: selection:account.tax,prorrate_type:0 +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/account_tax.py:14 +#, python-format +msgid "Non-deductible" +msgstr "No deducible" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/account_invoice.py:33 +#, python-format +msgid "Only suported 'percent' type with price not included" +msgstr "Only suported 'percent' type with price not included" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_account_tax__prorrate_type +msgid "Prorate type" +msgstr "Tipo de prorrata" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model_terms:ir.ui.view,arch_db:l10n_es_aeat_vat_special_prorrate.aeat_map_special_prorrate_year_view_form +msgid "Recompute" +msgstr "Recalcular" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/account_tax.py:0 +#, python-format +msgid "" +"Runtime error: Prorate tax '%s' has %i %s repartition lines instead of " +"expected 2. This may indicate data corruption or constraint bypass." +msgstr "Error de ejecución: El impuesto de prorrata '%s' tiene %i líneas de reparto de %s en " +"lugar de las 2 esperadas. Esto puede indicar corrupción de datos o elusión de " +"restricciones." + +#. module: l10n_es_aeat_vat_special_prorrate +#: model_terms:ir.ui.view,arch_db:l10n_es_aeat_vat_special_prorrate.aeat_map_special_prorrate_year_view_form +msgid "Set to Temporary" +msgstr "Establecer a provisional" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__state +msgid "State" +msgstr "Estado" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model,name:l10n_es_aeat_vat_special_prorrate.model_account_tax +msgid "Tax" +msgstr "Impuesto" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__tax_percentage +msgid "Tax %" +msgstr "Impuesto %" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py:33 +#: selection:aeat.map.special.prorrate.year,state:0 +#, python-format +msgid "Temporary" +msgstr "Provisional" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py:223 +#, python-format +msgid "The final prorrate computed should be greater than zero" +msgstr "The final prorrate computed should be greater than zero" + +#. module: l10n_es_aeat_vat_special_prorrate +#: sql_constraint:aeat.map.special.prorrate.year:0 +msgid "The map prorrate must have one next prorrate only" +msgstr "The map prorrate must have one next prorrate only" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py:209 +#, python-format +msgid "The previous state to be able to close a prorrate should be 'Finale', not '%s'" +msgstr "The previous state to be able to close a prorrate should be 'Finale', not '%s'" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py:200 +#, python-format +msgid "The prorrate of previous year must be closed before compute the new one" +msgstr "The prorrate of previous year must be closed before compute the new one" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py:73 +#, python-format +msgid "The year of the next linked map prorrata must be the next chronological year" +msgstr "The year of the next linked map prorrata must be the next chronological year" + +#. module: l10n_es_aeat_vat_special_prorrate +#: code:addons/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py:79 +#, python-format +msgid "The year of the previous linked map prorrata must be the previous chronological year" +msgstr "The year of the previous linked map prorrata must be the previous chronological year" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model.fields,field_description:l10n_es_aeat_vat_special_prorrate.field_aeat_map_special_prorrate_year__year +msgid "Year" +msgstr "Año" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model,name:l10n_es_aeat_vat_special_prorrate.model_aeat_map_special_prorrate_year +msgid "aeat.map.special.prorrate.year" +msgstr "aeat.map.special.prorrate.year" + +#. module: l10n_es_aeat_vat_special_prorrate +#: model:ir.model,name:l10n_es_aeat_vat_special_prorrate.model_l10n_es_aeat_report_tax_mapping_transient +msgid "l10n.es.aeat.report.tax.mapping.transient" +msgstr "l10n.es.aeat.report.tax.mapping.transient" diff --git a/l10n_es_aeat_vat_special_prorrate/models/__init__.py b/l10n_es_aeat_vat_special_prorrate/models/__init__.py new file mode 100644 index 000000000..bd68f440a --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/models/__init__.py @@ -0,0 +1,5 @@ +from . import aeat_map_special_prorrate_year +from . import account_tax +from . import account_tax_repartition_line +from . import account_move +from . import res_company diff --git a/l10n_es_aeat_vat_special_prorrate/models/account_move.py b/l10n_es_aeat_vat_special_prorrate/models/account_move.py new file mode 100644 index 000000000..7cfa036de --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/models/account_move.py @@ -0,0 +1,27 @@ +# Copyright NuoBiT Solutions SL - Eric Antones +# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import models + + +class AccountMove(models.Model): + _inherit = "account.move" + + def _get_rounded_base_and_tax_lines(self, round_from_tax_lines=True): + if self.company_id.l10n_es_prorate_enabled and not self.env.context.get( + "prorate" + ): + prorate_ctx = self.env["account.tax"].prorate_context( + self, + self.date, + self.company_id, + ) + return super( + AccountMove, self.with_context(**prorate_ctx) + )._get_rounded_base_and_tax_lines( + round_from_tax_lines=round_from_tax_lines, + ) + return super()._get_rounded_base_and_tax_lines( + round_from_tax_lines=round_from_tax_lines, + ) diff --git a/l10n_es_aeat_vat_special_prorrate/models/account_tax.py b/l10n_es_aeat_vat_special_prorrate/models/account_tax.py new file mode 100644 index 000000000..2a0e4d3a7 --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/models/account_tax.py @@ -0,0 +1,69 @@ +# Copyright NuoBiT Solutions SL - Eric Antones +# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import models +from odoo.exceptions import ValidationError +from odoo.tools.translate import _ + + +class AccountTax(models.Model): + _inherit = "account.tax" + + def compute_all( + self, + price_unit, + currency=None, + quantity=1.0, + product=None, + partner=None, + is_refund=False, + handle_price_include=True, + ): + res = super().compute_all( + price_unit, + currency=currency, + quantity=quantity, + product=product, + partner=partner, + is_refund=is_refund, + handle_price_include=handle_price_include, + ) + # group repartition lines by tax + # Sort the taxes so that those without account_id come last. This is + # to have an Odoo like behavior when no rounding errors occur. + sorted_taxes = sorted( + res["taxes"], key=lambda x: x["account_id"] in (None, False) + ) + rlines_by_tax = {} + for tax in sorted_taxes: + rline = self.env["account.tax.repartition.line"].browse( + tax["tax_repartition_line_id"] + ) + if rline and rline.tax_id.prorate: + rline_type = "invoice" if rline.invoice_tax_id else "refund" + if rline.repartition_type == "tax" and rline.factor_percent == 100.0: + rlines_by_tax.setdefault((rline_type, rline.tax_id), []).append(tax) + # round the prorate pairs for each tax + for (rltype, tax), prorate_taxes in rlines_by_tax.items(): + # Defensive check: constraint on account.tax should guarantee exactly + # 2 with 100% but protect against data corruption since we're gonna + # index this list. + if len(prorate_taxes) != 2: + raise ValidationError( + _( + "Runtime error: Prorate tax '%(tax_name)s' " + "has %(lines_count)i %(rltype)s repartition " + "lines instead of expected 2. This may indicate " + "data corruption or constraint bypass." + ) + % { + "tax_name": tax.name, + "lines_count": len(prorate_taxes), + "rltype": rltype, + } + ) + base_tax_amount = sum(x["amount"] for x in prorate_taxes) + prorate_taxes[0]["amount"] = currency.round(prorate_taxes[0]["amount"]) + prorate_taxes[1]["amount"] = base_tax_amount - prorate_taxes[0]["amount"] + return res diff --git a/l10n_es_aeat_vat_special_prorrate/models/account_tax_repartition_line.py b/l10n_es_aeat_vat_special_prorrate/models/account_tax_repartition_line.py new file mode 100644 index 000000000..b0ce781c9 --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/models/account_tax_repartition_line.py @@ -0,0 +1,57 @@ +# Copyright NuoBiT Solutions SL - Eric Antones +# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.translate import _ + + +class AccountTaxRepartitionLine(models.Model): + _inherit = "account.tax.repartition.line" + + def get_prorrate_ratio(self, date, company_id): + if self.repartition_type != "tax": + return 1 + prorrate_map = self.env["aeat.map.special.prorrate.year"].search( + [ + ( + "company_id", + "=", + company_id or self.tax_id.company_id.id or self.env.company.id, + ), + ("year", "=", date.year), + ] + ) + if not prorrate_map: + raise ValidationError( + _("If a tax has prorate the year should exist on vat mapping") + ) + + prorate_ratio = prorrate_map.tax_percentage / 100 + if not self.account_id: + prorate_ratio = 1 - prorate_ratio + + return prorate_ratio + + @api.depends_context("prorate") + def _compute_factor(self): + prorate = self.env.context.get("prorate") + if prorate: + date_str, company_id = prorate + prorate = ( + fields.Date.to_date(date_str), + company_id, + ) + for record in self.filtered( + lambda x: x.tax_id.prorate and prorate and x.factor_percent == 100.0 + ): + record.factor = record.get_prorrate_ratio(*prorate) + return super( + AccountTaxRepartitionLine, + self.filtered( + lambda x: not x.tax_id.prorate + or not prorate + or x.factor_percent != 100.0 + ), + )._compute_factor() diff --git a/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py b/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py new file mode 100644 index 000000000..b3e4f842e --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/models/aeat_map_special_prorrate_year.py @@ -0,0 +1,241 @@ +# Copyright NuoBiT Solutions SL - Eric Antones +# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import math + +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.translate import _ + + +class MapSpecialProrateYear(models.Model): + _name = "aeat.map.special.prorrate.year" + _description = "Aeat VAT special prorate map" + + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + required=True, + readonly=True, + default=lambda self: self.env.company, + ) + + def _default_year(self): + last = self.search( + [("company_id", "=", self.env.company.id)], + order="company_id,year desc", + limit=1, + ) + return last and last.year + 1 or fields.Date.context_today(self).year + + year = fields.Integer(default=_default_year, required=True) + tax_percentage = fields.Float(string="Temporary %", required=True) + + map_prorrate_next_year_id = fields.Many2one( + comodel_name="aeat.map.special.prorrate.year", + ondelete="restrict", + required=False, + readonly=True, + store=True, + ) + + tax_final_percentage = fields.Float( + string="Final tax %", + readonly=True, + related="map_prorrate_next_year_id.tax_percentage", + ) + + tax_final_percentage_aux = fields.Float(string="Final tax % (aux)", readonly=True) + + state = fields.Selection( + selection=[ + ("temporary", "Temporary"), + ("finale", "Finale"), + ("closed", "Closed"), + ], + readonly=True, + default="temporary", + required=True, + ) + + _sql_constraints = [ + ("unique_year", "unique(year, company_id)", "AEAT year must be unique"), + ( + "unique_next", + "unique(map_prorrate_next_year_id)", + "The map prorate must have only one next year prorate", + ), + ] + + @api.model + def get_by_ukey(self, company_id, year): + return self.search( + [ + ("company_id", "=", company_id), + ("year", "=", year), + ] + ) + + def get_previous(self): + return self.search( + [ + ("map_prorrate_next_year_id", "=", self.id), + ] + ) + + @api.depends("year", "tax_percentage", "tax_final_percentage") + def _compute_display_name(self): + for rec in self: + percents = [rec.tax_percentage] + if rec.tax_final_percentage: + percents.append(rec.tax_final_percentage) + name_l = [f"{round(x, 2):g}" for x in percents] + rec.display_name = "%i: %s" % (rec.year, " -> ".join(name_l)) + + @api.constrains("map_prorrate_next_year_id", "year") + def _check_map_prorate_next_year(self): + for rec in self: + if ( + rec.map_prorrate_next_year_id + and rec.map_prorrate_next_year_id.year != rec.year + 1 + ): + raise ValidationError( + _( + "The year of the next linked map prorrata must be " + "the next chronological year" + ) + ) + + map_prorate_previous_year_id = rec.get_previous() + if ( + map_prorate_previous_year_id + and map_prorate_previous_year_id.year != rec.year - 1 + ): + raise ValidationError( + _( + "The year of the previous linked map prorrata must be " + "the previous chronological year" + ) + ) + + def unlink(self): + for rec in self: + if rec.state == "closed": + raise ValidationError( + _("It's not possible to delete a closed prorate map") + ) + map_prorate_previous_year_id = rec.get_previous() + if map_prorate_previous_year_id.state == "temporary": + map_prorate_previous_year_id.map_prorrate_next_year_id = False + return super(MapSpecialProrateYear, rec).unlink() + + def _compute_prorate_percent(self): + self.ensure_one() + date_from = f"{self.year}-01-01" + date_to = f"{self.year}-12-31" + + mod303 = self.env["l10n.es.aeat.mod303.report"].new( + {"company_id": self.company_id.id} + ) + + MapLine = self.env["l10n.es.aeat.map.tax.line"] + MapLineTax = self.env["l10n.es.aeat.map.tax.line.tax"] + + def _get_tax_xmlid_ids(xmlid_names): + return MapLineTax.search([("name", "in", xmlid_names)]) + + taxed_names = [ + "account_tax_template_s_iva4b", + "account_tax_template_s_iva4s", + "account_tax_template_s_iva10b", + "account_tax_template_s_iva10s", + "account_tax_template_s_iva21b", + "account_tax_template_s_iva21s", + "account_tax_template_s_iva21isp", + ] + mapline_vals = { + "move_type": "all", + "field_type": "base", + "sum_type": "both", + "exigible_type": "yes", + "tax_xmlid_ids": _get_tax_xmlid_ids(taxed_names), + } + map_line = MapLine.new(mapline_vals) + move_lines = mod303._get_tax_lines(date_from, date_to, map_line) + taxed = -sum(move_lines.mapped("balance")) + + # Get base amount of exempt operations + exempt_names = [ + "account_tax_template_s_iva0", + "account_tax_template_s_iva0_ns", + ] + mapline_vals["tax_xmlid_ids"] = _get_tax_xmlid_ids(exempt_names) + map_line = MapLine.new(mapline_vals) + move_lines = mod303._get_tax_lines(date_from, date_to, map_line) + exempt = -sum(move_lines.mapped("balance")) + + if not taxed and not exempt: + raise UserError(_("No taxable or exempt operations found")) + # Compute prorate percentage performing ceiling operation + return math.ceil(taxed / (taxed + exempt) * 100) + + def compute_prorate(self): + self.ensure_one() + + if self.state == "close": + raise ValidationError( + _("It's not possible to recompute a closed prorate") % self.state + ) + + prorate_map_previous_year = self.get_by_ukey(self.company_id.id, self.year - 1) + if prorate_map_previous_year and prorate_map_previous_year.state != "closed": + raise ValidationError( + _( + "The prorate of previous year must be " + "closed before compute the new one" + ) + ) + + self.tax_final_percentage_aux = self._compute_prorate_percent() + self.state = "finale" + + def close_prorate(self): + self.ensure_one() + if self.state not in ("finale",): + raise ValidationError( + _( + "The previous state to be able to close a prorate " + "should be 'Finale', not '%s'" + ) + % self.state + ) + + if self.map_prorrate_next_year_id: + if ( + self.map_prorrate_next_year_id.tax_percentage + != self.tax_final_percentage_aux + ): + self.map_prorrate_next_year_id.write( + { + "tax_percentage": self.tax_final_percentage_aux, + } + ) + else: + self.map_prorrate_next_year_id = self.create( + [ + { + "year": self.year + 1, + "tax_percentage": self.tax_final_percentage_aux, + } + ] + ) + self.state = "closed" + + if self.tax_final_percentage <= 0: + raise ValidationError( + _("The final prorate computed should be greater than zero") + ) + + def set_temporary(self): + self.tax_final_percentage_aux = False + self.state = "temporary" diff --git a/l10n_es_aeat_vat_special_prorrate/models/res_company.py b/l10n_es_aeat_vat_special_prorrate/models/res_company.py new file mode 100644 index 000000000..54449d9a7 --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/models/res_company.py @@ -0,0 +1,10 @@ +# Copyright NuoBiT Solutions SL - Kilian Niubo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + l10n_es_prorate_enabled = fields.Boolean(string="L10n ES Prorate Enabled") diff --git a/l10n_es_aeat_vat_special_prorrate/pyproject.toml b/l10n_es_aeat_vat_special_prorrate/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/l10n_es_aeat_vat_special_prorrate/readme/CONTRIBUTORS.md b/l10n_es_aeat_vat_special_prorrate/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..909507cba --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [NuoBiT](https://www.nuobit.com): + - Eric Antones + - Deniz Gallo diff --git a/l10n_es_aeat_vat_special_prorrate/readme/DESCRIPTION.md b/l10n_es_aeat_vat_special_prorrate/readme/DESCRIPTION.md new file mode 100644 index 000000000..9895c15bd --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +> Módulo para gestionar la prorrata especial del IVA en las facturas de +> la AEAT diff --git a/l10n_es_aeat_vat_special_prorrate/security/aeat_map_special_prorrate_year.xml b/l10n_es_aeat_vat_special_prorrate/security/aeat_map_special_prorrate_year.xml new file mode 100644 index 000000000..d148b37dd --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/security/aeat_map_special_prorrate_year.xml @@ -0,0 +1,12 @@ + + + + + Aeat VAT special prorate map multi-company rule + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + diff --git a/l10n_es_aeat_vat_special_prorrate/security/ir.model.access.csv b/l10n_es_aeat_vat_special_prorrate/security/ir.model.access.csv new file mode 100644 index 000000000..f79a0df0c --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_aeat_map_special_prorrate_year_user","Aeat VAT special prorrate map - Account User","model_aeat_map_special_prorrate_year","account.group_account_invoice",1,0,0,0 +"access_aeat_map_special_prorrate_year_manager","Aeat VAT special prorrate map - Account Manager","model_aeat_map_special_prorrate_year","account.group_account_manager",1,1,1,1 diff --git a/l10n_es_aeat_vat_special_prorrate/static/description/icon.png b/l10n_es_aeat_vat_special_prorrate/static/description/icon.png new file mode 100644 index 000000000..1cd641e79 Binary files /dev/null and b/l10n_es_aeat_vat_special_prorrate/static/description/icon.png differ diff --git a/l10n_es_aeat_vat_special_prorrate/static/description/index.html b/l10n_es_aeat_vat_special_prorrate/static/description/index.html new file mode 100644 index 000000000..2f10af06d --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/static/description/index.html @@ -0,0 +1,422 @@ + + + + + +AEAT - Prorrata especial de IVA + + + +
+

AEAT - Prorrata especial de IVA

+ + +

Beta License: AGPL-3 NuoBiT/odoo-addons

+
+Módulo para gestionar la prorrata especial del IVA en las facturas de +la AEAT
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • NuoBiT Solutions SL
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the NuoBiT/odoo-addons project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/l10n_es_aeat_vat_special_prorrate/views/aeat_map_special_prorrate_year_views.xml b/l10n_es_aeat_vat_special_prorrate/views/aeat_map_special_prorrate_year_views.xml new file mode 100644 index 000000000..62a8399e0 --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/views/aeat_map_special_prorrate_year_views.xml @@ -0,0 +1,89 @@ + + + + + aeat.map.special.prorate.year.view.form + aeat.map.special.prorrate.year + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + + aeat.map.special.prorate.year.view.list + aeat.map.special.prorrate.year + + + + + + + + + + + + + Aeat VAT special prorate map + aeat.map.special.prorrate.year + list,form + + + +
diff --git a/l10n_es_aeat_vat_special_prorrate/views/res_company_view.xml b/l10n_es_aeat_vat_special_prorrate/views/res_company_view.xml new file mode 100644 index 000000000..1b4b0aada --- /dev/null +++ b/l10n_es_aeat_vat_special_prorrate/views/res_company_view.xml @@ -0,0 +1,22 @@ + + + + + res.company.aeat.form + res.company + + + + + + + + + + + + + + diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..8703a2054 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo-addon-l10n_es_special_prorate@git+https://github.com/nuobit/odoo-addons.git@refs/pull/800/head#subdirectory=l10n_es_special_prorate