From 2da33e67dc7c52c58df30b1d492621d96cd80472 Mon Sep 17 00:00:00 2001 From: Jose Zambudio Date: Fri, 10 May 2024 07:43:02 +0200 Subject: [PATCH 01/15] =?UTF-8?q?[ADD]=20env=C3=ADo=20de=20los=20registros?= =?UTF-8?q?=20de=20facturaci=C3=B3n=20a=20la=20sede=20electr=C3=B3nica=20d?= =?UTF-8?q?e=20la=20Agencia=20Tributaria=20en=20el=20momento=20de=20su=20p?= =?UTF-8?q?roducci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- l10n_es_aeat_verifactu/README.rst | 85 ++++ l10n_es_aeat_verifactu/__init__.py | 1 + l10n_es_aeat_verifactu/__manifest__.py | 27 ++ .../data/aeat_sii_tax_agency_data.xml | 13 + l10n_es_aeat_verifactu/data/neutralize.sql | 2 + l10n_es_aeat_verifactu/models/__init__.py | 6 + .../models/account_fiscal_position.py | 13 + l10n_es_aeat_verifactu/models/account_move.py | 67 +++ .../models/aeat_tax_agency.py | 37 ++ l10n_es_aeat_verifactu/models/res_company.py | 11 + l10n_es_aeat_verifactu/models/res_partner.py | 22 + .../models/verifactu_mixin.py | 120 +++++ l10n_es_aeat_verifactu/readme/CONFIGURE.rst | 0 .../readme/CONTRIBUTORS.rst | 1 + l10n_es_aeat_verifactu/readme/DESCRIPTION.rst | 1 + l10n_es_aeat_verifactu/readme/INSTALL.rst | 0 l10n_es_aeat_verifactu/readme/ROADMAP.rst | 5 + l10n_es_aeat_verifactu/readme/USAGE.rst | 0 .../static/description/index.html | 436 ++++++++++++++++++ .../views/account_fiscal_position_view.xml | 22 + .../views/account_move_view.xml | 61 +++ .../views/aeat_tax_agency_view.xml | 24 + .../views/res_company_view.xml | 28 ++ .../views/res_partner_view.xml | 18 + .../odoo/addons/l10n_es_aeat_verifactu | 1 + setup/l10n_es_aeat_verifactu/setup.py | 6 + 26 files changed, 1007 insertions(+) create mode 100644 l10n_es_aeat_verifactu/README.rst create mode 100644 l10n_es_aeat_verifactu/__init__.py create mode 100644 l10n_es_aeat_verifactu/__manifest__.py create mode 100644 l10n_es_aeat_verifactu/data/aeat_sii_tax_agency_data.xml create mode 100644 l10n_es_aeat_verifactu/data/neutralize.sql create mode 100644 l10n_es_aeat_verifactu/models/__init__.py create mode 100644 l10n_es_aeat_verifactu/models/account_fiscal_position.py create mode 100644 l10n_es_aeat_verifactu/models/account_move.py create mode 100644 l10n_es_aeat_verifactu/models/aeat_tax_agency.py create mode 100644 l10n_es_aeat_verifactu/models/res_company.py create mode 100644 l10n_es_aeat_verifactu/models/res_partner.py create mode 100644 l10n_es_aeat_verifactu/models/verifactu_mixin.py create mode 100644 l10n_es_aeat_verifactu/readme/CONFIGURE.rst create mode 100644 l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst create mode 100644 l10n_es_aeat_verifactu/readme/DESCRIPTION.rst create mode 100644 l10n_es_aeat_verifactu/readme/INSTALL.rst create mode 100644 l10n_es_aeat_verifactu/readme/ROADMAP.rst create mode 100644 l10n_es_aeat_verifactu/readme/USAGE.rst create mode 100644 l10n_es_aeat_verifactu/static/description/index.html create mode 100644 l10n_es_aeat_verifactu/views/account_fiscal_position_view.xml create mode 100644 l10n_es_aeat_verifactu/views/account_move_view.xml create mode 100644 l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml create mode 100644 l10n_es_aeat_verifactu/views/res_company_view.xml create mode 100644 l10n_es_aeat_verifactu/views/res_partner_view.xml create mode 120000 setup/l10n_es_aeat_verifactu/odoo/addons/l10n_es_aeat_verifactu create mode 100644 setup/l10n_es_aeat_verifactu/setup.py diff --git a/l10n_es_aeat_verifactu/README.rst b/l10n_es_aeat_verifactu/README.rst new file mode 100644 index 00000000000..4e960f97afe --- /dev/null +++ b/l10n_es_aeat_verifactu/README.rst @@ -0,0 +1,85 @@ +======================= +Comunicación Veri*FACTU +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f266e60912fa651e7b5f2be5fde444a4cc9a53f259d83ff07ad36040c96713d7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-OCA%2Fl10n--spain-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-spain/tree/16.0/l10n_es_aeat_verifactu + :alt: OCA/l10n-spain +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-spain-16-0/l10n-spain-16-0-l10n_es_aeat_verifactu + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/l10n-spain&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Módulo para la presentación inmediata de la facturación. + +**Table of contents** + +.. contents:: + :local: + +Known issues / Roadmap +====================== + + * Refactorización SII en l10n_es_aeat + * Creación documento a enviar a Veri*FACTU + * Creación cabecera Veri*FACTU + * Conexión WSDL + * Queue + Encadenamiento + +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 +~~~~~~~ + +* Aures Tic + +Contributors +~~~~~~~~~~~~ + +* Jose Zambudio + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/l10n-spain `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/l10n_es_aeat_verifactu/__init__.py b/l10n_es_aeat_verifactu/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/l10n_es_aeat_verifactu/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/l10n_es_aeat_verifactu/__manifest__.py b/l10n_es_aeat_verifactu/__manifest__.py new file mode 100644 index 00000000000..8c0e613a623 --- /dev/null +++ b/l10n_es_aeat_verifactu/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright 2024 Aures Tic - Jose Zambudio +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Comunicación Veri*FACTU", + "version": "16.0.1.0.0", + "category": "Accounting & Finance", + "website": "https://github.com/OCA/l10n-spain", + "author": "Aures Tic," "Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": False, + "installable": True, + # "external_dependencies": {"python": ["zeep", "requests"]}, + "depends": [ + "l10n_es", + "l10n_es_aeat", + # "queue_job", + ], + "data": [ + "data/aeat_sii_tax_agency_data.xml", + "views/aeat_tax_agency_view.xml", + "views/account_move_view.xml", + "views/account_fiscal_position_view.xml", + "views/res_company_view.xml", + "views/res_partner_view.xml", + ], +} diff --git a/l10n_es_aeat_verifactu/data/aeat_sii_tax_agency_data.xml b/l10n_es_aeat_verifactu/data/aeat_sii_tax_agency_data.xml new file mode 100644 index 00000000000..caeafa5c295 --- /dev/null +++ b/l10n_es_aeat_verifactu/data/aeat_sii_tax_agency_data.xml @@ -0,0 +1,13 @@ + + + + + https://prewww2.aeat.es/static_files/common/internet/dep/aplicaciones/es/aeat/tikeV1.0/cont/ws/SistemaFacturacion.wsdl + https://prewww2.aeat.es/static_files/common/internet/dep/aplicaciones/es/aeat/tikeV1.0/cont/ws/SistemaFacturacion.wsdl + + diff --git a/l10n_es_aeat_verifactu/data/neutralize.sql b/l10n_es_aeat_verifactu/data/neutralize.sql new file mode 100644 index 00000000000..e66e1b2ec36 --- /dev/null +++ b/l10n_es_aeat_verifactu/data/neutralize.sql @@ -0,0 +1,2 @@ +-- DISABLE SII ON COMPANIES +UPDATE res_company SET verifactu_test = true; diff --git a/l10n_es_aeat_verifactu/models/__init__.py b/l10n_es_aeat_verifactu/models/__init__.py new file mode 100644 index 00000000000..548b3f0636c --- /dev/null +++ b/l10n_es_aeat_verifactu/models/__init__.py @@ -0,0 +1,6 @@ +from . import res_company +from . import verifactu_mixin +from . import account_move +from . import aeat_tax_agency +from . import account_fiscal_position +from . import res_partner diff --git a/l10n_es_aeat_verifactu/models/account_fiscal_position.py b/l10n_es_aeat_verifactu/models/account_fiscal_position.py new file mode 100644 index 00000000000..284b0739fe4 --- /dev/null +++ b/l10n_es_aeat_verifactu/models/account_fiscal_position.py @@ -0,0 +1,13 @@ +# Copyright 2024 Aures TIC - Jose Zambudio +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class AccountFiscalPosition(models.Model): + _inherit = "account.fiscal.position" + + verifactu_enabled = fields.Boolean( + related="company_id.verifactu_enabled", + readonly=True, + ) diff --git a/l10n_es_aeat_verifactu/models/account_move.py b/l10n_es_aeat_verifactu/models/account_move.py new file mode 100644 index 00000000000..6209adaf959 --- /dev/null +++ b/l10n_es_aeat_verifactu/models/account_move.py @@ -0,0 +1,67 @@ +from odoo import api, models + +VERIFACTU_VALID_INVOICE_STATES = ["posted"] + + +class AccountMove(models.Model): + _name = "account.move" + _inherit = ["account.move", "verifactu.mixin"] + + @api.depends( + "company_id", + "company_id.verifactu_enabled", + "move_type", + "fiscal_position_id", + "fiscal_position_id.aeat_active", + ) + def _compute_verifactu_enabled(self): + """Compute if the invoice is enabled for the veri*FACTU""" + for invoice in self: + if invoice.company_id.verifactu_enabled and invoice.is_invoice(): + invoice.verifactu_enabled = ( + invoice.fiscal_position_id + and invoice.fiscal_position_id.aeat_active + ) or not invoice.fiscal_position_id + else: + invoice.verifactu_enabled = False + + def _get_document_date(self): + """ + TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that + it should be directly in l10n_es_aeat + """ + return self.invoice_date + + def _aeat_get_partner(self): + """ + TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that + it should be directly in l10n_es_aeat + """ + return self.commercial_partner_id + + def _get_document_fiscal_date(self): + """ + TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that + it should be directly in l10n_es_aeat + """ + return self.date + + def _get_mapping_key(self): + """ + TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that + it should be directly in l10n_es_aeat + """ + return self.move_type + + def _get_valid_document_states(self): + return VERIFACTU_VALID_INVOICE_STATES + + def _get_document_serial_number(self): + """ + TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that + it should be directly in l10n_es_aeat + """ + serial_number = (self.name or "")[0:60] + if self.thirdparty_invoice: + serial_number = self.thirdparty_number[0:60] + return serial_number diff --git a/l10n_es_aeat_verifactu/models/aeat_tax_agency.py b/l10n_es_aeat_verifactu/models/aeat_tax_agency.py new file mode 100644 index 00000000000..c3fdf3f5e2c --- /dev/null +++ b/l10n_es_aeat_verifactu/models/aeat_tax_agency.py @@ -0,0 +1,37 @@ +# Copyright 2024 Aures Tic - Jose Zambudio +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + +VERIFACTU_WDSL_MAPPING = { + "out_invoice": "verifactu_wsdl_out", + "out_refund": "verifactu_wsdl_out", +} +VERIFACTU_PORT_NAME_MAPPING = { + "out_invoice": "SuministroInformacion", + "out_refund": "SuministroInformacion", +} + + +class AeatTaxAgency(models.Model): + _inherit = "aeat.tax.agency" + + verifactu_wsdl_out = fields.Char(string="SuministroInformacion WSDL") + verifactu_wsdl_out_test_address = fields.Char( + string="SuministroInformacion Test Address" + ) + + def _connect_params_verifactu(self, mapping_key, company): + self.ensure_one() + wsdl_field = VERIFACTU_WDSL_MAPPING[mapping_key] + wsdl_test_field = wsdl_field + "_test_address" + port_name = VERIFACTU_PORT_NAME_MAPPING[mapping_key] + address = getattr(self, wsdl_test_field) if company.verifactu_test else False + if not address and company.verifactu_test: + # If not test address is provides we try to get it using the port name. + port_name += "Pruebas" + return { + "wsdl": getattr(self, wsdl_field), + "address": address, + "port_name": port_name, + } diff --git a/l10n_es_aeat_verifactu/models/res_company.py b/l10n_es_aeat_verifactu/models/res_company.py new file mode 100644 index 00000000000..e725d621a35 --- /dev/null +++ b/l10n_es_aeat_verifactu/models/res_company.py @@ -0,0 +1,11 @@ +# Copyright 2024 Jose Zambudio +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + verifactu_enabled = fields.Boolean(string="Enable veri*FACTU") + verifactu_test = fields.Boolean(string="Is it the veri*FACTU test environment?") diff --git a/l10n_es_aeat_verifactu/models/res_partner.py b/l10n_es_aeat_verifactu/models/res_partner.py new file mode 100644 index 00000000000..74b42d4d28a --- /dev/null +++ b/l10n_es_aeat_verifactu/models/res_partner.py @@ -0,0 +1,22 @@ +# Copyright 2024 Aures TIC - Jose Zambudio +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + verifactu_enabled = fields.Boolean( + compute="_compute_verifactu_enabled", + ) + + @api.depends("company_id") + def _compute_verifactu_enabled(self): + verifactu_enabled = any(self.env.companies.mapped("verifactu_enabled")) + for partner in self: + partner.verifactu_enabled = ( + partner.company_id.verifactu_enabled + if partner.company_id + else verifactu_enabled + ) diff --git a/l10n_es_aeat_verifactu/models/verifactu_mixin.py b/l10n_es_aeat_verifactu/models/verifactu_mixin.py new file mode 100644 index 00000000000..b52da921b2b --- /dev/null +++ b/l10n_es_aeat_verifactu/models/verifactu_mixin.py @@ -0,0 +1,120 @@ +from odoo import _, fields, models +from odoo.exceptions import UserError + +from odoo.addons.l10n_es_aeat.models.aeat_mixin import round_by_keys + +VERIFACTU_VERSION = "0.12.2" + + +class VerifactuMixin(models.AbstractModel): + _name = "verifactu.mixin" + _inherit = "aeat.mixin" + _description = "Verifactu Mixin" + + verifactu_enabled = fields.Boolean( + string="Enable AEAT", + compute="_compute_verifactu_enabled", + ) + + def _compute_verifactu_enabled(self): + raise NotImplementedError + + def _connect_params_aeat(self, mapping_key): + self.ensure_one() + agency = self.company_id.tax_agency_id + if not agency: + # We use spanish agency by default to keep old behavior with + # ir.config parameters. In the future it might be good to reinforce + # to explicitly set a tax agency in the company by raising an error + # here. + agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain") + return agency._connect_params_verifactu(mapping_key, self.company_id) + + def _get_aeat_header(self, tipo_comunicacion=False, cancellation=False): + """Builds VERIFACTU send header + + :param tipo_comunicacion String 'A0': new reg, 'A1': modification + :param cancellation Bool True when the communitacion es for document + cancellation + :return Dict with header data depending on cancellation + """ + self.ensure_one() + if not self.company_id.vat: + raise UserError( + _("No VAT configured for the company '{}'").format(self.company_id.name) + ) + header = { + "IDVersion": VERIFACTU_VERSION, + "ObligadoEmision": { + "NombreRazon": self.company_id.name[0:120], + "NIF": self.company_id.partner_id._parse_aeat_vat_info()[2], + }, + # "TipoRegistroAEAT": , + # "FechaFinVeriactu": , + } + # if not cancellation: + # header.update({"TipoComunicacion": tipo_comunicacion}) + return header + + def _get_aeat_invoice_dict(self): + self.ensure_one() + inv_dict = {} + mapping_key = self._get_mapping_key() + if mapping_key in ["out_invoice", "out_refund"]: + inv_dict = self._get_aeat_invoice_dict_out() + else: + raise NotImplementedError + round_by_keys( + inv_dict, + [ + "BaseImponible", + "CuotaRepercutida", + "CuotaSoportada", + "TipoRecargoEquivalencia", + "CuotaRecargoEquivalencia", + "ImportePorArticulos7_14_Otros", + "ImporteTAIReglasLocalizacion", + "ImporteTotal", + "BaseRectificada", + "CuotaRectificada", + "CuotaDeducible", + "ImporteCompensacionREAGYP", + ], + ) + return inv_dict + + def _get_aeat_invoice_dict_out(self, cancel=False): + """Build dict with data to send to AEAT WS for document types: + out_invoice and out_refund. + + :param cancel: It indicates if the dictionary is for sending a + cancellation of the document. + :return: documents (dict) : Dict XML with data for this document. + """ + self.ensure_one() + document_date = self._change_date_format(self._get_document_date()) + company = self.company_id + fiscal_year = self._get_document_fiscal_year() + period = self._get_document_period() + serial_number = self._get_document_serial_number() + inv_dict = { + "IDFactura": { + "IDEmisorFactura": { + "NIF": company.partner_id._parse_aeat_vat_info()[2] + }, + "NumSerieFactura": serial_number, + "FechaExpedicionFactura": document_date, + }, + "PeriodoLiquidacion": { + "Ejercicio": fiscal_year, + "Periodo": period, + }, + } + return inv_dict + + def _aeat_check_exceptions(self): + """Inheritable method for exceptions control when sending veri*FACTU invoices.""" + res = super()._aeat_check_exceptions() + if self.company_id.verifactu_enabled and not self.verifactu_enabled: + raise UserError(_("This invoice is not veri*FACTU enabled.")) + return res diff --git a/l10n_es_aeat_verifactu/readme/CONFIGURE.rst b/l10n_es_aeat_verifactu/readme/CONFIGURE.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst b/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..d93d14febba --- /dev/null +++ b/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jose Zambudio diff --git a/l10n_es_aeat_verifactu/readme/DESCRIPTION.rst b/l10n_es_aeat_verifactu/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..68aea6818e9 --- /dev/null +++ b/l10n_es_aeat_verifactu/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Módulo para la presentación inmediata de la facturación. diff --git a/l10n_es_aeat_verifactu/readme/INSTALL.rst b/l10n_es_aeat_verifactu/readme/INSTALL.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/l10n_es_aeat_verifactu/readme/ROADMAP.rst b/l10n_es_aeat_verifactu/readme/ROADMAP.rst new file mode 100644 index 00000000000..18f879648d8 --- /dev/null +++ b/l10n_es_aeat_verifactu/readme/ROADMAP.rst @@ -0,0 +1,5 @@ + * Refactorización SII en l10n_es_aeat + * Creación documento a enviar a Veri*FACTU + * Creación cabecera Veri*FACTU + * Conexión WSDL + * Queue + Encadenamiento diff --git a/l10n_es_aeat_verifactu/readme/USAGE.rst b/l10n_es_aeat_verifactu/readme/USAGE.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/l10n_es_aeat_verifactu/static/description/index.html b/l10n_es_aeat_verifactu/static/description/index.html new file mode 100644 index 00000000000..9bb768f78c6 --- /dev/null +++ b/l10n_es_aeat_verifactu/static/description/index.html @@ -0,0 +1,436 @@ + + + + + +Comunicación Veri*FACTU + + + +
+

Comunicación Veri*FACTU

+ + +

Beta License: AGPL-3 OCA/l10n-spain Translate me on Weblate Try me on Runboat

+

Módulo para la presentación inmediata de la facturación.

+

Table of contents

+ +
+

Known issues / Roadmap

+
+
    +
  • Refactorización SII en l10n_es_aeat
  • +
  • Creación documento a enviar a Veri*FACTU
  • +
  • Creación cabecera Veri*FACTU
  • +
  • Conexión WSDL
  • +
  • Queue + Encadenamiento
  • +
+
+
+
+

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

+
    +
  • Aures Tic
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/l10n-spain project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/l10n_es_aeat_verifactu/views/account_fiscal_position_view.xml b/l10n_es_aeat_verifactu/views/account_fiscal_position_view.xml new file mode 100644 index 00000000000..c04865cee7d --- /dev/null +++ b/l10n_es_aeat_verifactu/views/account_fiscal_position_view.xml @@ -0,0 +1,22 @@ + + + + + account.fiscal.position.form + account.fiscal.position + + + + + + + + + + + diff --git a/l10n_es_aeat_verifactu/views/account_move_view.xml b/l10n_es_aeat_verifactu/views/account_move_view.xml new file mode 100644 index 00000000000..71a7918c97c --- /dev/null +++ b/l10n_es_aeat_verifactu/views/account_move_view.xml @@ -0,0 +1,61 @@ + + + + + account.invoice.verifactu.form + account.move + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml b/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml new file mode 100644 index 00000000000..1b2cd6e7efc --- /dev/null +++ b/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml @@ -0,0 +1,24 @@ + + + + + aeat.tax.agency.form - l10n_es_aeat_verifactu_oca + aeat.tax.agency + + + + + + + + + + + + + + + diff --git a/l10n_es_aeat_verifactu/views/res_company_view.xml b/l10n_es_aeat_verifactu/views/res_company_view.xml new file mode 100644 index 00000000000..b4cb7ee1e6f --- /dev/null +++ b/l10n_es_aeat_verifactu/views/res_company_view.xml @@ -0,0 +1,28 @@ + + + + + res.company.verifactu.form + res.company + + + + + + + + + + + + + + + + + + diff --git a/l10n_es_aeat_verifactu/views/res_partner_view.xml b/l10n_es_aeat_verifactu/views/res_partner_view.xml new file mode 100644 index 00000000000..53ad4c37316 --- /dev/null +++ b/l10n_es_aeat_verifactu/views/res_partner_view.xml @@ -0,0 +1,18 @@ + + + + + res.partner + + + + + + + + + diff --git a/setup/l10n_es_aeat_verifactu/odoo/addons/l10n_es_aeat_verifactu b/setup/l10n_es_aeat_verifactu/odoo/addons/l10n_es_aeat_verifactu new file mode 120000 index 00000000000..fdc0cc9a69d --- /dev/null +++ b/setup/l10n_es_aeat_verifactu/odoo/addons/l10n_es_aeat_verifactu @@ -0,0 +1 @@ +../../../../l10n_es_aeat_verifactu \ No newline at end of file diff --git a/setup/l10n_es_aeat_verifactu/setup.py b/setup/l10n_es_aeat_verifactu/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/l10n_es_aeat_verifactu/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From fef9103a7a61476e6fc4bf6caf6ebe0ca259c3a6 Mon Sep 17 00:00:00 2001 From: Andreu Orensanz Date: Fri, 10 May 2024 07:51:03 +0200 Subject: [PATCH 02/15] =?UTF-8?q?[IMP]=20l10n=5Fes=5Faeat=5Fverifactu:=20A?= =?UTF-8?q?ctivar=20Journal=20Hash=20para=20compa=C3=B1=C3=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- l10n_es_aeat_verifactu/README.rst | 3 + l10n_es_aeat_verifactu/__manifest__.py | 3 +- l10n_es_aeat_verifactu/models/__init__.py | 1 + .../models/account_journal.py | 64 +++++++++++++++++++ .../readme/CONTRIBUTORS.rst | 2 + .../static/description/index.html | 3 + .../views/account_journal_views.xml | 21 ++++++ 7 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 l10n_es_aeat_verifactu/models/account_journal.py create mode 100644 l10n_es_aeat_verifactu/views/account_journal_views.xml diff --git a/l10n_es_aeat_verifactu/README.rst b/l10n_es_aeat_verifactu/README.rst index 4e960f97afe..79685bcd841 100644 --- a/l10n_es_aeat_verifactu/README.rst +++ b/l10n_es_aeat_verifactu/README.rst @@ -61,11 +61,14 @@ Authors ~~~~~~~ * Aures Tic +* ForgeFlow Contributors ~~~~~~~~~~~~ * Jose Zambudio +* Laura Cazorla +* Andreu Orensanz Maintainers ~~~~~~~~~~~ diff --git a/l10n_es_aeat_verifactu/__manifest__.py b/l10n_es_aeat_verifactu/__manifest__.py index 8c0e613a623..cf6c8c61551 100644 --- a/l10n_es_aeat_verifactu/__manifest__.py +++ b/l10n_es_aeat_verifactu/__manifest__.py @@ -6,7 +6,7 @@ "version": "16.0.1.0.0", "category": "Accounting & Finance", "website": "https://github.com/OCA/l10n-spain", - "author": "Aures Tic," "Odoo Community Association (OCA)", + "author": "Aures Tic, ForgeFlow," "Odoo Community Association (OCA)", "license": "AGPL-3", "application": False, "installable": True, @@ -23,5 +23,6 @@ "views/account_fiscal_position_view.xml", "views/res_company_view.xml", "views/res_partner_view.xml", + "views/account_journal_views.xml", ], } diff --git a/l10n_es_aeat_verifactu/models/__init__.py b/l10n_es_aeat_verifactu/models/__init__.py index 548b3f0636c..0604daf79d1 100644 --- a/l10n_es_aeat_verifactu/models/__init__.py +++ b/l10n_es_aeat_verifactu/models/__init__.py @@ -1,3 +1,4 @@ +from . import account_journal from . import res_company from . import verifactu_mixin from . import account_move diff --git a/l10n_es_aeat_verifactu/models/account_journal.py b/l10n_es_aeat_verifactu/models/account_journal.py new file mode 100644 index 00000000000..722a8e89403 --- /dev/null +++ b/l10n_es_aeat_verifactu/models/account_journal.py @@ -0,0 +1,64 @@ +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class AccountJournal(models.Model): + _inherit = "account.journal" + + restrict_mode_hash_table = fields.Boolean( + compute="_compute_restrict_mode_hash_table", + store=True, + readonly=False, + ) + + restrict_mode_hash_table_readonly = fields.Boolean( + store=True, + compute="_compute_restrict_mode_hash_table", + ) + + @api.depends( + "company_id", "company_id.verifactu_enabled", "company_id.country_code", "type" + ) + def _compute_restrict_mode_hash_table(self): + for record in self: + record.restrict_mode_hash_table_readonly = False + if ( + record.company_id.verifactu_enabled + and record.company_id.country_code == "ES" + and record.type == "sale" + ): + record.restrict_mode_hash_table = True + record.restrict_mode_hash_table_readonly = True + + @api.model + def check_hash_modification(self, country_code, journal_type, verifactu_enabled): + if country_code == "ES" and journal_type == "sale" and verifactu_enabled: + raise ValidationError( + _("You can't have a sale journal in Spain with veri*FACTU enabled.") + ) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if ( + "restrict_mode_hash_table" in vals + and not vals["restrict_mode_hash_table"] + ): + company = self.env["res.company"].browse(vals.get("company_id")) + self.check_hash_modification( + company.country_code, vals.get("type"), company.verifactu_enabled + ) + return super().create(vals_list) + + def write(self, vals): + if "restrict_mode_hash_table" in vals and not vals["restrict_mode_hash_table"]: + for record in self: + new_company_id = vals.get("company_id", record.company_id.id) + new_company = self.env["res.company"].browse(new_company_id) + new_type = vals.get("type", record.type) + new_country_code = new_company.country_code + new_verifactu_enabled = new_company.verifactu_enabled + record.check_hash_modification( + new_country_code, new_type, new_verifactu_enabled + ) + return super().write(vals) diff --git a/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst b/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst index d93d14febba..59e855e1d69 100644 --- a/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst +++ b/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst @@ -1 +1,3 @@ * Jose Zambudio +* Laura Cazorla +* Andreu Orensanz diff --git a/l10n_es_aeat_verifactu/static/description/index.html b/l10n_es_aeat_verifactu/static/description/index.html index 9bb768f78c6..80c088acd59 100644 --- a/l10n_es_aeat_verifactu/static/description/index.html +++ b/l10n_es_aeat_verifactu/static/description/index.html @@ -410,12 +410,15 @@

Credits

Authors

  • Aures Tic
  • +
  • ForgeFlow

Contributors

diff --git a/l10n_es_aeat_verifactu/views/account_journal_views.xml b/l10n_es_aeat_verifactu/views/account_journal_views.xml new file mode 100644 index 00000000000..6eed8c12365 --- /dev/null +++ b/l10n_es_aeat_verifactu/views/account_journal_views.xml @@ -0,0 +1,21 @@ + + + + account.journal.form - l10n_es_aeat_verifactu + account.journal + + + + + + + {'readonly': [('restrict_mode_hash_table_readonly', '=', True)]} + + + + From 6d2f1777e16d80828c2fd62cdc5bf425e6836777 Mon Sep 17 00:00:00 2001 From: Jose Zambudio Date: Tue, 3 Sep 2024 09:18:04 +0200 Subject: [PATCH 03/15] [IMP] Adaptando a refactor de l10n_es_aeat+sii --- l10n_es_aeat_verifactu/models/res_partner.py | 11 ++++++++--- l10n_es_aeat_verifactu/views/res_partner_view.xml | 8 ++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/l10n_es_aeat_verifactu/models/res_partner.py b/l10n_es_aeat_verifactu/models/res_partner.py index 74b42d4d28a..59c82c333d1 100644 --- a/l10n_es_aeat_verifactu/models/res_partner.py +++ b/l10n_es_aeat_verifactu/models/res_partner.py @@ -8,15 +8,20 @@ class ResPartner(models.Model): _inherit = "res.partner" verifactu_enabled = fields.Boolean( - compute="_compute_verifactu_enabled", + compute="_compute_aeat_sending_enabled", ) @api.depends("company_id") - def _compute_verifactu_enabled(self): + def _compute_aeat_sending_enabled(self): + res = super()._compute_aeat_sending_enabled() verifactu_enabled = any(self.env.companies.mapped("verifactu_enabled")) for partner in self: - partner.verifactu_enabled = ( + verifactu_enabled = ( partner.company_id.verifactu_enabled if partner.company_id else verifactu_enabled ) + partner.verifactu_enabled = verifactu_enabled + if verifactu_enabled: + partner.aeat_sending_enabled = True + return res diff --git a/l10n_es_aeat_verifactu/views/res_partner_view.xml b/l10n_es_aeat_verifactu/views/res_partner_view.xml index 53ad4c37316..f8d5d9987a9 100644 --- a/l10n_es_aeat_verifactu/views/res_partner_view.xml +++ b/l10n_es_aeat_verifactu/views/res_partner_view.xml @@ -4,14 +4,10 @@ res.partner - + - + - From 729c5b24b58589bfcdcd2f65df776f4376ba9b5a Mon Sep 17 00:00:00 2001 From: almumu Date: Mon, 9 Sep 2024 13:03:24 +0200 Subject: [PATCH 04/15] Add verifactu hash code (#55) * Add verifactu hash code --- l10n_es_aeat_verifactu/__manifest__.py | 1 + .../models/account_journal.py | 6 ++ l10n_es_aeat_verifactu/models/account_move.py | 91 ++++++++++++++++++- .../models/verifactu_mixin.py | 31 ++++++- l10n_es_aeat_verifactu/tests/__init__.py | 1 + .../tests/test_10n_es_aeat_verifactu.py | 45 +++++++++ .../views/account_move_view.xml | 3 + 7 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 l10n_es_aeat_verifactu/tests/__init__.py create mode 100644 l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py diff --git a/l10n_es_aeat_verifactu/__manifest__.py b/l10n_es_aeat_verifactu/__manifest__.py index cf6c8c61551..53a000f3fed 100644 --- a/l10n_es_aeat_verifactu/__manifest__.py +++ b/l10n_es_aeat_verifactu/__manifest__.py @@ -1,4 +1,5 @@ # Copyright 2024 Aures Tic - Jose Zambudio +# Copyright 2024 Aures TIC - Almudena de La Puente # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { diff --git a/l10n_es_aeat_verifactu/models/account_journal.py b/l10n_es_aeat_verifactu/models/account_journal.py index 722a8e89403..5e32b6492ae 100644 --- a/l10n_es_aeat_verifactu/models/account_journal.py +++ b/l10n_es_aeat_verifactu/models/account_journal.py @@ -5,6 +5,12 @@ class AccountJournal(models.Model): _inherit = "account.journal" + # TODEL? + # no vamos autilizar el hash de odoo porque la estructura no nos sirve + # para verifactu, por lo que este código no nos terminaría de valer. + # De momento lo dejamos hasta saber cómo vamos a controlar + # el tema de la factura anterior enviada a verifactu para el cálculo + # del hash, y el control de modificaciones en las facturas ya enviadas. restrict_mode_hash_table = fields.Boolean( compute="_compute_restrict_mode_hash_table", store=True, diff --git a/l10n_es_aeat_verifactu/models/account_move.py b/l10n_es_aeat_verifactu/models/account_move.py index 6209adaf959..712283252e3 100644 --- a/l10n_es_aeat_verifactu/models/account_move.py +++ b/l10n_es_aeat_verifactu/models/account_move.py @@ -1,4 +1,10 @@ -from odoo import api, models +# Copyright 2024 Aures TIC - Almudena de La Puente +# Copyright 2024 Aures Tic - Jose Zambudio +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import pytz + +from odoo import _, api, fields, models VERIFACTU_VALID_INVOICE_STATES = ["posted"] @@ -7,6 +13,38 @@ class AccountMove(models.Model): _name = "account.move" _inherit = ["account.move", "verifactu.mixin"] + verifactu_document_type = fields.Selection( + selection=lambda self: self._get_verifactu_docuyment_types(), + default="F1", + ) + + def _get_verifactu_docuyment_types(self): + return [ + ("F1", _("FACTURA (ART. 6, 7.2 Y 7.3 DEL RD 1619/2012)")), + ( + "F2", + _( + """FACTURA SIMPLIFICADA Y FACTURAS SIN IDENTIFICACIÓN DEL DESTINATARIO + (ART. 6.1.D RD 1619/2012)""" + ), + ), + ( + "R1", + _("FACTURA RECTIFICATIVA (Art 80.1 y 80.2 y error fundado en derecho)"), + ), + ("R2", _("FACTURA RECTIFICATIVA (Art. 80.3)")), + ("R3", _("FACTURA RECTIFICATIVA (Art. 80.4)")), + ("R4", _("FACTURA RECTIFICATIVA (Resto)")), + ("R5", _("FACTURA RECTIFICATIVA EN FACTURAS SIMPLIFICADAS")), + ( + "F3", + _( + """FACTURA EMITIDA EN SUSTITUCIÓN DE FACTURAS SIMPLIFICADAS FACTURADAS + Y DECLARADAS""" + ), + ), + ] + @api.depends( "company_id", "company_id.verifactu_enabled", @@ -30,7 +68,7 @@ def _get_document_date(self): TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that it should be directly in l10n_es_aeat """ - return self.invoice_date + return self._change_date_format(self.invoice_date) def _aeat_get_partner(self): """ @@ -65,3 +103,52 @@ def _get_document_serial_number(self): if self.thirdparty_invoice: serial_number = self.thirdparty_number[0:60] return serial_number + + def _get_verifactu_issuer(self): + return self.company_id.partner_id._parse_aeat_vat_info()[2] + + def _get_verifactu_document_type(self): + return self.verifactu_document_type or "F1" + + def _get_verifactu_amount_tax(self): + return self.amount_tax + + def _get_verifactu_amount_total(self): + return self.amount_total + + def _get_verifactu_previous_hash(self): + # TODO store it? search it by some kind of sequence? + return "" + + def _get_verifactu_registration_date(self): + # Date format must be ISO 8601 + return pytz.utc.localize(self.create_date).isoformat() + + @api.model + def _get_verifactu_hash_string(self): + """Gets the verifactu hash string""" + if ( + not self.verifactu_enabled + or self.state == "draft" + or self.move_type not in ("out_invoice", "out_refund") + ): + return "" + issuerID = self._get_verifactu_issuer() + serialNumber = self._get_document_serial_number() + expeditionDate = self._get_document_date() + documentType = self._get_verifactu_document_type() + amountTax = self._get_verifactu_amount_tax() + amountTotal = self._get_verifactu_amount_total() + previousHash = self._get_verifactu_previous_hash() + registrationDate = self._get_verifactu_registration_date() + verifactu_hash_string = ( + f"IDEmisorFactura={issuerID}&" + f"NumSerieFactura={serialNumber}&" + f"FechaExpedicionFactura={expeditionDate}&" + f"TipoFactura={documentType}&" + f"CuotaTotal={amountTax}&" + f"ImporteTotal={amountTotal}&" + f"Huella={previousHash}&" + f"FechaHoraHusoGenRegistro={registrationDate}" + ) + return verifactu_hash_string diff --git a/l10n_es_aeat_verifactu/models/verifactu_mixin.py b/l10n_es_aeat_verifactu/models/verifactu_mixin.py index b52da921b2b..7b40f0343de 100644 --- a/l10n_es_aeat_verifactu/models/verifactu_mixin.py +++ b/l10n_es_aeat_verifactu/models/verifactu_mixin.py @@ -1,9 +1,16 @@ -from odoo import _, fields, models +# Copyright 2024 Aures TIC - Jose Zambudio +# Copyright 2024 Aures TIC - Almudena de La Puente +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from hashlib import sha256 + +from odoo import _, api, fields, models from odoo.exceptions import UserError from odoo.addons.l10n_es_aeat.models.aeat_mixin import round_by_keys VERIFACTU_VERSION = "0.12.2" +VERIFACTU_DATE_FORMAT = "%d-%m-%Y" class VerifactuMixin(models.AbstractModel): @@ -15,6 +22,8 @@ class VerifactuMixin(models.AbstractModel): string="Enable AEAT", compute="_compute_verifactu_enabled", ) + verifactu_hash_string = fields.Char(compute="_compute_verifactu_hash") + verifactu_hash = fields.Char(compute="_compute_verifactu_hash") def _compute_verifactu_enabled(self): raise NotImplementedError @@ -118,3 +127,23 @@ def _aeat_check_exceptions(self): if self.company_id.verifactu_enabled and not self.verifactu_enabled: raise UserError(_("This invoice is not veri*FACTU enabled.")) return res + + def _change_date_format(self, date): + datetimeobject = fields.Date.to_date(date) + new_date = datetimeobject.strftime(VERIFACTU_DATE_FORMAT) + return new_date + + @api.model + def _get_verifactu_hash_string(self): + raise NotImplementedError + + def _compute_verifactu_hash(self): + # TODO by the moment those fields are not stored, + # but they must be stored because are unalterable + # when invoice is sent to verifactu, because the hash depends + # on previous sent hash.. + for record in self: + verifactu_hash_values = record._get_verifactu_hash_string() + record.verifactu_hash_string = verifactu_hash_values + hash_string = sha256(verifactu_hash_values.encode("utf-8")) + record.verifactu_hash = hash_string.hexdigest().upper() diff --git a/l10n_es_aeat_verifactu/tests/__init__.py b/l10n_es_aeat_verifactu/tests/__init__.py new file mode 100644 index 00000000000..5733e163bf3 --- /dev/null +++ b/l10n_es_aeat_verifactu/tests/__init__.py @@ -0,0 +1 @@ +from . import test_10n_es_aeat_verifactu diff --git a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py new file mode 100644 index 00000000000..15167871d9c --- /dev/null +++ b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py @@ -0,0 +1,45 @@ +# Copyright 2024 Aures TIC - Almudena de La Puente +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from hashlib import sha256 + +from odoo.addons.l10n_es_aeat.tests.test_l10n_es_aeat_certificate import ( + TestL10nEsAeatCertificateBase, +) +from odoo.addons.l10n_es_aeat.tests.test_l10n_es_aeat_mod_base import ( + TestL10nEsAeatModBase, +) + + +class TestL10nEsAeatSiiBase(TestL10nEsAeatModBase, TestL10nEsAeatCertificateBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_verifactu_hash_code(self): + # based on AEAT Verifactu documentation + # https://www.agenciatributaria.es/static_files/AEAT_Desarrolladores/EEDD/IVA/VERI-FACTU/Veri-Factu_especificaciones_huella_hash_registros.pdf # noqa: B950 + expected_hash = ( + "3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60" + ) + issuerID = "89890001K" + serialNumber = "12345678/G33" + expeditionDate = "01-01-2024" + documentType = "F1" + amountTax = "12.35" + amountTotal = "123.45" + previousHash = "" + registrationDate = "2024-01-01T19:20:30+01:00" + verifactu_hash_string = ( + f"IDEmisorFactura={issuerID}&" + f"NumSerieFactura={serialNumber}&" + f"FechaExpedicionFactura={expeditionDate}&" + f"TipoFactura={documentType}&" + f"CuotaTotal={amountTax}&" + f"ImporteTotal={amountTotal}&" + f"Huella={previousHash}&" + f"FechaHoraHusoGenRegistro={registrationDate}" + ) + sha_hash_code = sha256(verifactu_hash_string.encode("utf-8")) + hash_code = sha_hash_code.hexdigest().upper() + self.assertEqual(hash_code, expected_hash) diff --git a/l10n_es_aeat_verifactu/views/account_move_view.xml b/l10n_es_aeat_verifactu/views/account_move_view.xml index 71a7918c97c..30de813e7bf 100644 --- a/l10n_es_aeat_verifactu/views/account_move_view.xml +++ b/l10n_es_aeat_verifactu/views/account_move_view.xml @@ -18,6 +18,9 @@ name="group_aeat_information" > + + + Date: Thu, 7 Nov 2024 11:32:31 +0100 Subject: [PATCH 05/15] =?UTF-8?q?Env=C3=ADo=20de=20facturas=20a=20verifact?= =?UTF-8?q?u=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Primera versión funcional de envío de facturas a verifactu --- l10n_es_aeat_verifactu/README.rst | 71 +++- l10n_es_aeat_verifactu/__init__.py | 1 + l10n_es_aeat_verifactu/__manifest__.py | 15 +- .../data/aeat_verifactu_map_data.xml | 79 ++++ .../data/aeat_verifactu_registration_keys.xml | 306 +++++++++++++++ ...xml => aeat_verifactu_tax_agency_data.xml} | 2 +- l10n_es_aeat_verifactu/data/neutralize.sql | 2 +- .../i18n/l10n_es_aeat_verifactu.pot | 14 + l10n_es_aeat_verifactu/models/__init__.py | 3 + .../models/account_fiscal_position.py | 27 +- .../models/account_journal.py | 4 + l10n_es_aeat_verifactu/models/account_move.py | 291 ++++++++++++-- .../models/aeat_tax_agency.py | 6 +- .../models/aeat_verifactu_map.py | 54 +++ .../models/aeat_verifactu_map_lines.py | 18 + .../aeat_verifactu_registration_keys.py | 27 ++ l10n_es_aeat_verifactu/models/res_company.py | 5 + .../models/verifactu_mixin.py | 361 ++++++++++++++++-- l10n_es_aeat_verifactu/readme/CONFIGURE.rst | 41 ++ .../readme/CONTRIBUTORS.rst | 1 + l10n_es_aeat_verifactu/readme/INSTALL.rst | 8 + l10n_es_aeat_verifactu/readme/ROADMAP.rst | 13 +- .../security/ir.model.access.csv | 8 + .../static/description/index.html | 95 ++++- ...tu_out_invoice_s_iva10b_s_iva21s_dict.json | 59 +++ ...ctu_out_invoice_s_iva21s_s_req52_dict.json | 53 +++ ...efund_s_iva10b_s_iva10b_s_iva21s_dict.json | 61 +++ .../tests/test_10n_es_aeat_verifactu.py | 155 +++++++- .../views/account_fiscal_position_view.xml | 5 + ...nal_views.xml => account_journal_view.xml} | 0 .../views/account_move_view.xml | 26 +- .../views/aeat_tax_agency_view.xml | 2 +- .../views/aeat_verifactu_map_lines_view.xml | 17 + .../views/aeat_verifactu_map_view.xml | 55 +++ .../aeat_verifactu_registration_keys_view.xml | 45 +++ .../views/res_company_view.xml | 5 + l10n_es_aeat_verifactu/wizards/__init__.py | 1 + .../wizards/account_move_reversal.py | 16 + 38 files changed, 1840 insertions(+), 112 deletions(-) create mode 100644 l10n_es_aeat_verifactu/data/aeat_verifactu_map_data.xml create mode 100644 l10n_es_aeat_verifactu/data/aeat_verifactu_registration_keys.xml rename l10n_es_aeat_verifactu/data/{aeat_sii_tax_agency_data.xml => aeat_verifactu_tax_agency_data.xml} (79%) create mode 100644 l10n_es_aeat_verifactu/i18n/l10n_es_aeat_verifactu.pot create mode 100644 l10n_es_aeat_verifactu/models/aeat_verifactu_map.py create mode 100644 l10n_es_aeat_verifactu/models/aeat_verifactu_map_lines.py create mode 100644 l10n_es_aeat_verifactu/models/aeat_verifactu_registration_keys.py create mode 100644 l10n_es_aeat_verifactu/security/ir.model.access.csv create mode 100644 l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json create mode 100644 l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json create mode 100644 l10n_es_aeat_verifactu/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json rename l10n_es_aeat_verifactu/views/{account_journal_views.xml => account_journal_view.xml} (100%) create mode 100644 l10n_es_aeat_verifactu/views/aeat_verifactu_map_lines_view.xml create mode 100644 l10n_es_aeat_verifactu/views/aeat_verifactu_map_view.xml create mode 100644 l10n_es_aeat_verifactu/views/aeat_verifactu_registration_keys_view.xml create mode 100644 l10n_es_aeat_verifactu/wizards/__init__.py create mode 100644 l10n_es_aeat_verifactu/wizards/account_move_reversal.py diff --git a/l10n_es_aeat_verifactu/README.rst b/l10n_es_aeat_verifactu/README.rst index 79685bcd841..f561f953ce5 100644 --- a/l10n_es_aeat_verifactu/README.rst +++ b/l10n_es_aeat_verifactu/README.rst @@ -35,14 +35,74 @@ Módulo para la presentación inmediata de la facturación. .. contents:: :local: +Installation +============ + +Para instalar esté módulo necesita: + +#. Libreria Python Zeep, se puede instalar con el comando 'pip install zeep' +#. Libreria Python Requests, se puede instalar con el comando 'pip install requests' + +y el módulo `queue_job` que se encuentra en: + +https://github.com/OCA/queue + +Configuration +============= + +Para configurar este módulo es necesario: + +#. En la compañia se almacenan las URLs del servicio SOAP de hacienda. + Estas URLs pueden cambiar según comunidades +#. Los certificados deben alojarse en una carpeta accesible por la instalación + de Odoo. +#. Preparar el certificado. El certificado enviado por la FMNT es en formato + p12, este certificado no se puede usar directamente con Zeep. Se tiene que + extraer la clave pública y la clave privada. + +En Linux se pueden usar los siguientes comandos: + +- Clave pública: "openssl pkcs12 -in Certificado.p12 -nokeys -out publicCert.crt -nodes" +- Clave privada: "openssl pkcs12 -in Certifcado.p12 -nocerts -out privateKey.pem -nodes" + +Además, el módulo `queue_job` necesita estar configurado de una de estas formas: + +#. Ajustando variables de entorno: + + ODOO_QUEUE_JOB_CHANNELS=root:4 + + u otro canal de configuración. Por defecto es root:1 + + Si xmlrpc_port no está definido: ODOO_QUEUE_JOB_PORT=8069 + +#. Otra alternativa es usuando un fichero de configuración: + + [options] + (...) + workers = 4 + server_wide_modules = web,base_sparse_field,queue_job + + (...) + [queue_job] + channels = root:4 + +#. Por último, arrancando Odoo con --load=web,base_sparse_field,queue_job y --workers más grande que 1. + +Más información http://odoo-connector.com + +#. Establecer en las posiciones fiscales la clave de impuestos y la clave de registro verifactu. + Known issues / Roadmap ====================== - * Refactorización SII en l10n_es_aeat - * Creación documento a enviar a Veri*FACTU - * Creación cabecera Veri*FACTU - * Conexión WSDL - * Queue + Encadenamiento + * Refactorización SII-Verifactu en l10n_es_aeat cuando estén todos los procesos claros + * Envío de Facturas simplificadas, exentas, a terceros.. + * Encadenamiento, obtener factura anterior y almacenamiento del hash inalterable. + * Datas de mapeos de impuestos, ya hay algunos. + * Datos reales del desarrollador del sistema informático. + * Envío con Queue. + * Modificación de facturas enviadas. + * Anulación de facturas enviadas. Bug Tracker =========== @@ -67,6 +127,7 @@ Contributors ~~~~~~~~~~~~ * Jose Zambudio +* Almudena de La Puente * Laura Cazorla * Andreu Orensanz diff --git a/l10n_es_aeat_verifactu/__init__.py b/l10n_es_aeat_verifactu/__init__.py index 0650744f6bc..aee8895e7a3 100644 --- a/l10n_es_aeat_verifactu/__init__.py +++ b/l10n_es_aeat_verifactu/__init__.py @@ -1 +1,2 @@ from . import models +from . import wizards diff --git a/l10n_es_aeat_verifactu/__manifest__.py b/l10n_es_aeat_verifactu/__manifest__.py index 53a000f3fed..fbabed4eb9f 100644 --- a/l10n_es_aeat_verifactu/__manifest__.py +++ b/l10n_es_aeat_verifactu/__manifest__.py @@ -11,19 +11,26 @@ "license": "AGPL-3", "application": False, "installable": True, - # "external_dependencies": {"python": ["zeep", "requests"]}, + "external_dependencies": {"python": ["zeep", "requests"]}, "depends": [ "l10n_es", "l10n_es_aeat", - # "queue_job", + "account_invoice_refund_link", + "queue_job", ], "data": [ - "data/aeat_sii_tax_agency_data.xml", + "data/aeat_verifactu_tax_agency_data.xml", + "data/aeat_verifactu_registration_keys.xml", + "data/aeat_verifactu_map_data.xml", + "security/ir.model.access.csv", "views/aeat_tax_agency_view.xml", "views/account_move_view.xml", "views/account_fiscal_position_view.xml", "views/res_company_view.xml", "views/res_partner_view.xml", - "views/account_journal_views.xml", + "views/account_journal_view.xml", + "views/aeat_verifactu_map_view.xml", + "views/aeat_verifactu_map_lines_view.xml", + "views/aeat_verifactu_registration_keys_view.xml", ], } diff --git a/l10n_es_aeat_verifactu/data/aeat_verifactu_map_data.xml b/l10n_es_aeat_verifactu/data/aeat_verifactu_map_data.xml new file mode 100644 index 00000000000..812dffb1eca --- /dev/null +++ b/l10n_es_aeat_verifactu/data/aeat_verifactu_map_data.xml @@ -0,0 +1,79 @@ + + + + + Verifactu + + + S1 + + + Operación Sujeta y No exenta - Sin inversión del sujeto pasivo. + + + S2 + + + Operación Sujeta y No exenta - Con Inversión del sujeto pasivo + + + N1 + + + Operación No Sujeta artículo 7, 14, otros. + + + + N2 + + + Operación No Sujeta por Reglas de localización. + + + + RE + + + Recargo Equivalencia + + + diff --git a/l10n_es_aeat_verifactu/data/aeat_verifactu_registration_keys.xml b/l10n_es_aeat_verifactu/data/aeat_verifactu_registration_keys.xml new file mode 100644 index 00000000000..72ffff457bd --- /dev/null +++ b/l10n_es_aeat_verifactu/data/aeat_verifactu_registration_keys.xml @@ -0,0 +1,306 @@ + + + + + 01 + Operación de régimen general + 01 + + + 02 + Exportación + 01 + + + 03 + Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección + 01 + + + 04 + Régimen especial oro de inversión + 01 + + + 05 + Régimen especial agencias de viajes + 01 + + + 06 + Régimen especial grupo de entidades en IVA (Nivel Avanzado) + 01 + + + 07 + Régimen especial criterio de caja + 01 + + + 08 + Operaciones sujetas al IPSI / IGIC (Impuesto sobre la Producción, los Servicios y la Importación / Impuesto General Indirecto Canario) + 01 + + + 09 + Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena (D.A.4ª RD1619/2012) + 01 + + + 10 + Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial, de autor u otros por cuenta de sus socios, asociados o colegiados efectuados por sociedades, asociaciones, colegios profesionales u otras entidades que realicen estas funciones de cobro + 01 + + + 11 + Operaciones de arrendamiento de local de negocio. + 01 + + + 14 + Factura con IVA pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública. + 01 + + + 15 + Factura con IVA pendiente de devengo en operaciones de tracto sucesivo + 01 + + + 17 + Operación acogida a alguno de los regímenes previstos en el Capítulo XI del Título IX (OSS e IOSS) + 01 + + + 18 + Recargo de equivalencia. + 01 + + + 19 + Operaciones de actividades incluidas en el Régimen Especial de Agricultura, Ganadería y Pesca (REAGYP) + 01 + + + 20 + Régimen simplificado + 01 + + + + + 01 + Operación de régimen general + 03 + + + 02 + Exportación + 03 + + + 03 + Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección + 03 + + + 04 + Régimen especial oro de inversión + 03 + + + 05 + Régimen especial agencias de viajes + 03 + + + 06 + Régimen especial grupo de entidades en IGIC (Nivel Avanzado) + 01 + + + 07 + Régimen especial criterio de caja + 03 + + + 08 + Operaciones sujetas al IPSI / IVA (Impuesto sobre la Producción, los Servicios y la Importación / Impuesto sobre el Valor Añadido). + 03 + + + 09 + Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena (D.A.4ª RD1619/2012) + 03 + + + 10 + Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial, de autor u otros por cuenta de sus socios, asociados o colegiados efectuados por sociedades, asociaciones, colegios profesionales u otras entidades que realicen estas funciones de cobro. + 03 + + + 11 + Operaciones de arrendamiento de local de negocio. + 03 + + + 14 + Factura con IGIC pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública. + 03 + + + 15 + Factura con IGIC pendiente de devengo en operaciones de tracto sucesivo. + 03 + + + 17 + Régimen especial de comerciante minorista + 03 + + + 18 + Régimen especial del pequeño empresario o profesional + 03 + + + 19 + Operaciones interiores exentas por aplicación artículo 25 Ley 19/1994 + 03 + + + diff --git a/l10n_es_aeat_verifactu/data/aeat_sii_tax_agency_data.xml b/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml similarity index 79% rename from l10n_es_aeat_verifactu/data/aeat_sii_tax_agency_data.xml rename to l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml index caeafa5c295..c63852a39f6 100644 --- a/l10n_es_aeat_verifactu/data/aeat_sii_tax_agency_data.xml +++ b/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml @@ -8,6 +8,6 @@ >https://prewww2.aeat.es/static_files/common/internet/dep/aplicaciones/es/aeat/tikeV1.0/cont/ws/SistemaFacturacion.wsdl https://prewww2.aeat.es/static_files/common/internet/dep/aplicaciones/es/aeat/tikeV1.0/cont/ws/SistemaFacturacion.wsdl + >https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP diff --git a/l10n_es_aeat_verifactu/data/neutralize.sql b/l10n_es_aeat_verifactu/data/neutralize.sql index e66e1b2ec36..18ac80f93b7 100644 --- a/l10n_es_aeat_verifactu/data/neutralize.sql +++ b/l10n_es_aeat_verifactu/data/neutralize.sql @@ -1,2 +1,2 @@ --- DISABLE SII ON COMPANIES +-- DISABLE VERIFACTU ON COMPANIES UPDATE res_company SET verifactu_test = true; diff --git a/l10n_es_aeat_verifactu/i18n/l10n_es_aeat_verifactu.pot b/l10n_es_aeat_verifactu/i18n/l10n_es_aeat_verifactu.pot new file mode 100644 index 00000000000..568b68adb8e --- /dev/null +++ b/l10n_es_aeat_verifactu/i18n/l10n_es_aeat_verifactu.pot @@ -0,0 +1,14 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_es_aeat_verifactu +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \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" diff --git a/l10n_es_aeat_verifactu/models/__init__.py b/l10n_es_aeat_verifactu/models/__init__.py index 0604daf79d1..c202ad7fd47 100644 --- a/l10n_es_aeat_verifactu/models/__init__.py +++ b/l10n_es_aeat_verifactu/models/__init__.py @@ -5,3 +5,6 @@ from . import aeat_tax_agency from . import account_fiscal_position from . import res_partner +from . import aeat_verifactu_map +from . import aeat_verifactu_map_lines +from . import aeat_verifactu_registration_keys diff --git a/l10n_es_aeat_verifactu/models/account_fiscal_position.py b/l10n_es_aeat_verifactu/models/account_fiscal_position.py index 284b0739fe4..da7df1d3cc5 100644 --- a/l10n_es_aeat_verifactu/models/account_fiscal_position.py +++ b/l10n_es_aeat_verifactu/models/account_fiscal_position.py @@ -1,7 +1,8 @@ # Copyright 2024 Aures TIC - Jose Zambudio +# Copyright 2024 Aures TIC - Almudena de La Puente # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import fields, models +from odoo import api, fields, models class AccountFiscalPosition(models.Model): @@ -11,3 +12,27 @@ class AccountFiscalPosition(models.Model): related="company_id.verifactu_enabled", readonly=True, ) + verifactu_tax_key = fields.Selection( + selection="_get_verifactu_tax_keys", + ) + verifactu_registration_key = fields.Many2one( + "aeat.verifactu.registration.keys", + ondelete="restrict", + ) + + @api.model + def default_verifactu_tax_key(self): + return "01" + + @api.model + def _get_verifactu_tax_keys(self): + return [ + ("01", "Impuesto sobre el Valor Añadido (IVA)"), + ( + "02", + """Impuesto sobre la Producción, los Servicios y + la Importación (IPSI) de Ceuta y Melilla""", + ), + ("03", "Impuesto General Indirecto Canario (IGIC)"), + ("05", "Otros"), + ] diff --git a/l10n_es_aeat_verifactu/models/account_journal.py b/l10n_es_aeat_verifactu/models/account_journal.py index 5e32b6492ae..781836c2b96 100644 --- a/l10n_es_aeat_verifactu/models/account_journal.py +++ b/l10n_es_aeat_verifactu/models/account_journal.py @@ -1,3 +1,7 @@ +# Copyright 2024 Aures TIC - Jose Zambudio +# Copyright 2024 Aures TIC - Almudena de La Puente +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + from odoo import _, api, fields, models from odoo.exceptions import ValidationError diff --git a/l10n_es_aeat_verifactu/models/account_move.py b/l10n_es_aeat_verifactu/models/account_move.py index 712283252e3..0e1cb0bca18 100644 --- a/l10n_es_aeat_verifactu/models/account_move.py +++ b/l10n_es_aeat_verifactu/models/account_move.py @@ -5,6 +5,7 @@ import pytz from odoo import _, api, fields, models +from odoo.exceptions import UserError VERIFACTU_VALID_INVOICE_STATES = ["posted"] @@ -13,21 +14,8 @@ class AccountMove(models.Model): _name = "account.move" _inherit = ["account.move", "verifactu.mixin"] - verifactu_document_type = fields.Selection( - selection=lambda self: self._get_verifactu_docuyment_types(), - default="F1", - ) - - def _get_verifactu_docuyment_types(self): - return [ - ("F1", _("FACTURA (ART. 6, 7.2 Y 7.3 DEL RD 1619/2012)")), - ( - "F2", - _( - """FACTURA SIMPLIFICADA Y FACTURAS SIN IDENTIFICACIÓN DEL DESTINATARIO - (ART. 6.1.D RD 1619/2012)""" - ), - ), + verifactu_refund_specific_invoice_type = fields.Selection( + selection=[ ( "R1", _("FACTURA RECTIFICATIVA (Art 80.1 y 80.2 y error fundado en derecho)"), @@ -36,14 +24,23 @@ def _get_verifactu_docuyment_types(self): ("R3", _("FACTURA RECTIFICATIVA (Art. 80.4)")), ("R4", _("FACTURA RECTIFICATIVA (Resto)")), ("R5", _("FACTURA RECTIFICATIVA EN FACTURAS SIMPLIFICADAS")), - ( - "F3", - _( - """FACTURA EMITIDA EN SUSTITUCIÓN DE FACTURAS SIMPLIFICADAS FACTURADAS - Y DECLARADAS""" - ), - ), - ] + ], + help="Fill this field when the refund are one of the specific cases" + " of article 80 of LIVA for notifying to Vertifactu with the proper" + " invoice type.", + ) + + @api.depends("move_type") + def _compute_verifactu_refund_type(self): + for record in self: + if record.move_type == "out_refund": + record.verifactu_refund_type = "I" + else: + record.verifactu_refund_type = False + + @api.depends("amount_total") + def _compute_verifactu_macrodata(self): + return super()._compute_verifactu_macrodata() @api.depends( "company_id", @@ -63,12 +60,27 @@ def _compute_verifactu_enabled(self): else: invoice.verifactu_enabled = False + def _get_verifactu_document_type(self): + invoice_type = "" + if self.move_type in ["out_invoice", "out_refund"]: + is_simplified = self._is_aeat_simplified_invoice() + invoice_type = "F2" if is_simplified else "F1" + if self.move_type == "out_refund": + if self.verifactu_refund_specific_invoice_type: + invoice_type = self.verifactu_refund_specific_invoice_type + else: + invoice_type = "R5" if is_simplified else "R1" + return invoice_type + + def _get_verifactu_description(self): + return self.verifactu_description or self.company_id.verifactu_description + def _get_document_date(self): """ TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that it should be directly in l10n_es_aeat """ - return self._change_date_format(self.invoice_date) + return self.invoice_date def _aeat_get_partner(self): """ @@ -107,14 +119,11 @@ def _get_document_serial_number(self): def _get_verifactu_issuer(self): return self.company_id.partner_id._parse_aeat_vat_info()[2] - def _get_verifactu_document_type(self): - return self.verifactu_document_type or "F1" - def _get_verifactu_amount_tax(self): - return self.amount_tax + return self.amount_tax_signed def _get_verifactu_amount_total(self): - return self.amount_total + return self.amount_total_signed def _get_verifactu_previous_hash(self): # TODO store it? search it by some kind of sequence? @@ -122,9 +131,16 @@ def _get_verifactu_previous_hash(self): def _get_verifactu_registration_date(self): # Date format must be ISO 8601 - return pytz.utc.localize(self.create_date).isoformat() + """ + TODO + enviamos fecha creación, fecha factura o fecha actual? + """ + return ( + pytz.utc.localize(self.create_date) + .astimezone() + .isoformat(timespec="seconds") + ) - @api.model def _get_verifactu_hash_string(self): """Gets the verifactu hash string""" if ( @@ -135,7 +151,7 @@ def _get_verifactu_hash_string(self): return "" issuerID = self._get_verifactu_issuer() serialNumber = self._get_document_serial_number() - expeditionDate = self._get_document_date() + expeditionDate = self._change_date_format(self._get_document_date()) documentType = self._get_verifactu_document_type() amountTax = self._get_verifactu_amount_tax() amountTotal = self._get_verifactu_amount_total() @@ -152,3 +168,214 @@ def _get_verifactu_hash_string(self): f"FechaHoraHusoGenRegistro={registrationDate}" ) return verifactu_hash_string + + def _get_verifactu_invoice_dict_out(self, cancel=False): + """Build dict with data to send to AEAT WS for document types: + out_invoice and out_refund. + + :param cancel: It indicates if the dictionary is for sending a + cancellation of the document. + :return: documents (dict) : Dict XML with data for this document. + """ + self.ensure_one() + document_date = self._change_date_format(self._get_document_date()) + company = self.company_id + serial_number = self._get_document_serial_number() + taxes_dict, amount_tax, amount_total = self._get_verifactu_taxes_and_total() + company_vat = company.partner_id._parse_aeat_vat_info()[2] + verifactu_doc_type = self._get_verifactu_document_type() + registroAlta = {} + inv_dict = { + "IDVersion": self._get_verifactu_version(), + "IDFactura": { + "IDEmisorFactura": company_vat, + "NumSerieFactura": serial_number, + "FechaExpedicionFactura": document_date, + }, + "NombreRazonEmisor": self.company_id.name[0:120], + "TipoFactura": verifactu_doc_type, + } + if self.move_type == "out_refund": + inv_dict["TipoRectificativa"] = self.verifactu_refund_type + if self.verifactu_refund_type == "I": + inv_dict["FacturasRectificadas"] = [] + origin = self.reversed_entry_id + if origin: + orig_document_date = self._change_date_format( + origin._get_document_date() + ) + orig_serial_number = origin._get_document_serial_number() + origin_data = { + "IDFacturaRectificada": { + "IDEmisorFactura": company_vat, + "NumSerieFactura": orig_serial_number, + "FechaExpedicionFactura": orig_document_date, + } + } + inv_dict["FacturasRectificadas"].append(origin_data) + # inv_dict["ImporteRectificacion"] = { + # "BaseRectificada": abs(origin.amount_untaxed_signed), + # "CuotaRectificada": abs( + # origin.amount_total_signed - origin.amount_untaxed_signed + # ), + # } + inv_dict.update( + { + "DescripcionOperacion": self._get_verifactu_description(), + } + ) + if verifactu_doc_type not in ("F2", "R5"): + inv_dict.update( + { + "Destinatarios": self._get_receiver_dict(), + } + ) + + inv_dict.update( + { + "Desglose": taxes_dict, + "CuotaTotal": amount_tax, + "ImporteTotal": amount_total, + "Encadenamiento": self._get_chaining_invoice_dict(), + "SistemaInformatico": self._get_verifactu_developer_dict(), + "FechaHoraHusoGenRegistro": self._get_verifactu_registration_date(), + "TipoHuella": "01", # SHA-256 + "Huella": self.verifactu_hash, + } + ) + registroAlta.setdefault("RegistroAlta", inv_dict) + return registroAlta + + def _get_chaining_invoice_dict(self): + """ + TODO + si no es el primer registro, hay que enviar el registro anterior. + Cuando sepamos cuál es el registro anterior + prev_invoice = self._get_previous_invoice() + return + { + "RegistroAnterior" = { + "IDEmisorFactura": prev_invoice._get_verifactu_issuer() + "NumSerieFactura": prev_invoice._get_document_serial_number() + "FechaExpedicionFactura": prev_invoice._change_date_format( + prev_invoice._get_document_date()) + "Huella": prev_invoice.verifactu_hash + } + } + mientras tanto para pruebas vamos a decir siempre que es el primer registro + """ + return {"PrimerRegistro": "S"} + + def _get_verifactu_tax_dict(self, tax_line, tax_lines): + """Get the Verifactu tax dictionary for the passed tax line. + + :param self: Single invoice record. + :param tax_line: Tax line that is being analyzed. + :param tax_lines: Dictionary of processed invoice taxes for further operations + (like REQ). + :return: A dictionary with the corresponding Verifactu tax values. + """ + tax = tax_line["tax"] + tax_base_amount = tax_line["base"] + if tax.amount_type == "group": + tax_type = abs(tax.children_tax_ids.filtered("amount")[:1].amount) + else: + tax_type = abs(tax.amount) + tax_dict = { + "TipoImpositivo": str(tax_type), + "BaseImponibleOimporteNoSujeto": tax_base_amount, + } + key = "CuotaRepercutida" + tax_dict[key] = tax_line["amount"] + # Recargo de equivalencia + req_tax = self._get_verifactu_tax_req(tax) + if req_tax: + tax_dict["TipoRecargoEquivalencia"] = req_tax.amount + tax_dict["CuotaRecargoEquivalencia"] = tax_lines[req_tax]["amount"] + return tax_dict + + def _get_verifactu_tax_req(self, tax): + """Get the associated req tax for the specified tax. + + :param self: Single invoice record. + :param tax: Initial tax for searching for the RE linked tax. + :return: REQ tax (or empty recordset) linked to the provided tax. + """ + self.ensure_one() + document_date = self._get_document_fiscal_date() + taxes_req = self._get_verifactu_taxes_map(["RE"], document_date) + re_lines = self.line_ids.filtered( + lambda x: tax in x.tax_ids and x.tax_ids & taxes_req + ) + req_tax = re_lines.mapped("tax_ids") & taxes_req + if len(req_tax) > 1: + raise UserError(_("There's a mismatch in taxes for RE. Check them.")) + return req_tax + + def _get_verifactu_taxes_and_total(self): + self.ensure_one() + taxes_dict = {} + taxes_dict.setdefault("DetalleDesglose", []) + tax_lines = self._get_aeat_tax_info() + document_date = self._get_document_fiscal_date() + taxes_S1 = self._get_verifactu_taxes_map(["S1"], document_date) + taxes_S2 = self._get_verifactu_taxes_map(["S2"], document_date) + taxes_N1 = self._get_verifactu_taxes_map(["N1"], document_date) + taxes_N2 = self._get_verifactu_taxes_map(["N2"], document_date) + breakdown_taxes = taxes_S1 + taxes_S2 + taxes_N1 + taxes_N2 + for tax_line in tax_lines.values(): + tax = tax_line["tax"] + if tax in breakdown_taxes: + operation_type = self._get_operation_type( + tax_line, taxes_S1, taxes_S2, taxes_N1, taxes_N2 + ) + tax_dict = { + "Impuesto": self.verifactu_tax_key, + "ClaveRegimen": self.verifactu_registration_key_code, + "CalificacionOperacion": operation_type, + } + # si es exenta: + # "OperacionExenta": "", # TODO + tax_dict.update(self._get_verifactu_tax_dict(tax_line, tax_lines)) + taxes_dict["DetalleDesglose"].append(tax_dict) + return ( + taxes_dict, + self._get_verifactu_amount_tax(), + self._get_verifactu_amount_total(), + ) + + def _get_operation_type(self, tax_line, taxes_S1, taxes_S2, taxes_N1, taxes_N2): + """ + S1 Operación Sujeta y No exenta - Sin inversión del sujeto pasivo. + S2 Operación Sujeta y No exenta - Con Inversión del sujeto pasivo + N1 Operación No Sujeta artículo 7, 14, otros. + N2 Operación No Sujeta por Reglas de localización. + """ + tax = tax_line["tax"] + if tax in taxes_S1: + return "S1" + elif tax in taxes_S2: + return "S2" + elif tax in taxes_N1: + return "N1" + elif tax in taxes_N2: + return "N2" + return "S1" + + def _get_receiver_dict(self): + self.ensure_one() + receiver = self._aeat_get_partner() + vat_info = receiver._parse_aeat_vat_info() + return { + "IDDestinatario": { + "NombreRazon": receiver.name, + "NIF": vat_info[2], + # "IDOtro": { + # "IDType": vat_info[1], + # "ID": vat_info[0], + # } + } + } + + def cancel_verifactu(self): + raise NotImplementedError diff --git a/l10n_es_aeat_verifactu/models/aeat_tax_agency.py b/l10n_es_aeat_verifactu/models/aeat_tax_agency.py index c3fdf3f5e2c..adbc1b91a0b 100644 --- a/l10n_es_aeat_verifactu/models/aeat_tax_agency.py +++ b/l10n_es_aeat_verifactu/models/aeat_tax_agency.py @@ -1,4 +1,5 @@ # Copyright 2024 Aures Tic - Jose Zambudio +# Copyright 2024 Aures TIC - Almudena de La Puente # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import fields, models @@ -8,8 +9,8 @@ "out_refund": "verifactu_wsdl_out", } VERIFACTU_PORT_NAME_MAPPING = { - "out_invoice": "SuministroInformacion", - "out_refund": "SuministroInformacion", + "out_invoice": "SistemaVerifactu", + "out_refund": "SistemaVerifactu", } @@ -28,7 +29,6 @@ def _connect_params_verifactu(self, mapping_key, company): port_name = VERIFACTU_PORT_NAME_MAPPING[mapping_key] address = getattr(self, wsdl_test_field) if company.verifactu_test else False if not address and company.verifactu_test: - # If not test address is provides we try to get it using the port name. port_name += "Pruebas" return { "wsdl": getattr(self, wsdl_field), diff --git a/l10n_es_aeat_verifactu/models/aeat_verifactu_map.py b/l10n_es_aeat_verifactu/models/aeat_verifactu_map.py new file mode 100644 index 00000000000..16c5779d5ed --- /dev/null +++ b/l10n_es_aeat_verifactu/models/aeat_verifactu_map.py @@ -0,0 +1,54 @@ +# Copyright 2024 Aures TIC - Almudena de La Puente +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import _, api, exceptions, fields, models + + +class AeatVerifactuMap(models.Model): + _name = "aeat.verifactu.map" + _description = "Aeat Verifactu Map" + + name = fields.Char(string="Model", required=True) + date_from = fields.Date() + date_to = fields.Date() + map_lines = fields.One2many( + comodel_name="aeat.verifactu.map.lines", + inverse_name="verifactu_map_id", + string="Lines", + ) + + @api.constrains("date_from", "date_to") + def _unique_date_range(self): + for record in self: + record._unique_date_range_one() + + def _unique_date_range_one(self): + # Based in l10n_es_aeat module + domain = [("id", "!=", self.id)] + if self.date_from and self.date_to: + domain += [ + "|", + "&", + ("date_from", "<=", self.date_to), + ("date_from", ">=", self.date_from), + "|", + "&", + ("date_to", "<=", self.date_to), + ("date_to", ">=", self.date_from), + "|", + "&", + ("date_from", "=", False), + ("date_to", ">=", self.date_from), + "|", + "&", + ("date_to", "=", False), + ("date_from", "<=", self.date_to), + ] + elif self.date_from: + domain += [("date_to", ">=", self.date_from)] + elif self.date_to: + domain += [("date_from", "<=", self.date_to)] + date_lst = self.search(domain) + if date_lst: + raise exceptions.UserError( + _("Error! The dates of the record overlap with an existing " "record.") + ) diff --git a/l10n_es_aeat_verifactu/models/aeat_verifactu_map_lines.py b/l10n_es_aeat_verifactu/models/aeat_verifactu_map_lines.py new file mode 100644 index 00000000000..feb808a70de --- /dev/null +++ b/l10n_es_aeat_verifactu/models/aeat_verifactu_map_lines.py @@ -0,0 +1,18 @@ +# Copyright 2024 Aures TIC - Almudena de La Puente +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class AeatVertiactuMapLines(models.Model): + _name = "aeat.verifactu.map.lines" + _description = "Aeat Verifactu Map Lines" + + code = fields.Char(required=True) + name = fields.Char() + taxes = fields.Many2many(comodel_name="account.tax.template") + verifactu_map_id = fields.Many2one( + comodel_name="aeat.verifactu.map", + string="Aeat Verifactu Map", + ondelete="cascade", + ) diff --git a/l10n_es_aeat_verifactu/models/aeat_verifactu_registration_keys.py b/l10n_es_aeat_verifactu/models/aeat_verifactu_registration_keys.py new file mode 100644 index 00000000000..512e446e168 --- /dev/null +++ b/l10n_es_aeat_verifactu/models/aeat_verifactu_registration_keys.py @@ -0,0 +1,27 @@ +# Copyright 2024 Aures TIC - Almudena de La Puente +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class AeatVerifactuMappingRegistrationKeys(models.Model): + _name = "aeat.verifactu.registration.keys" + _description = "Aeat Verifactu Registration Keys" + + code = fields.Char(required=True, size=2) + name = fields.Char(required=True) + verifactu_tax_key = fields.Selection( + selection="_get_verifactu_tax_keys", + required=True, + ) + + def name_get(self): + vals = [] + for record in self: + name = "[{}]-{}".format(record.code, record.name) + vals.append(tuple([record.id, name])) + return vals + + @api.model + def _get_verifactu_tax_keys(self): + return self.env["account.fiscal.position"]._get_verifactu_tax_keys() diff --git a/l10n_es_aeat_verifactu/models/res_company.py b/l10n_es_aeat_verifactu/models/res_company.py index e725d621a35..54321f241b2 100644 --- a/l10n_es_aeat_verifactu/models/res_company.py +++ b/l10n_es_aeat_verifactu/models/res_company.py @@ -9,3 +9,8 @@ class ResCompany(models.Model): verifactu_enabled = fields.Boolean(string="Enable veri*FACTU") verifactu_test = fields.Boolean(string="Is it the veri*FACTU test environment?") + verifactu_description = fields.Text( + default="/", + size=500, + help="The description for Verifactu invoices if not set", + ) diff --git a/l10n_es_aeat_verifactu/models/verifactu_mixin.py b/l10n_es_aeat_verifactu/models/verifactu_mixin.py index 7b40f0343de..dfa921a4b68 100644 --- a/l10n_es_aeat_verifactu/models/verifactu_mixin.py +++ b/l10n_es_aeat_verifactu/models/verifactu_mixin.py @@ -2,15 +2,38 @@ # Copyright 2024 Aures TIC - Almudena de La Puente # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import json +import logging from hashlib import sha256 +from requests import Session + from odoo import _, api, fields, models -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError +from odoo.modules.registry import Registry +from odoo.tools.float_utils import float_compare from odoo.addons.l10n_es_aeat.models.aeat_mixin import round_by_keys -VERIFACTU_VERSION = "0.12.2" +########################################### +# revisar los imports que no hagan falta +# cuando funcione bien el _connect_aeat sin tener que poner +# el forbid_entities, y se pueda borrar la función _connect_verifactu +# de este fichero para usar la del aeat_mixin + + +_logger = logging.getLogger(__name__) + +try: + from zeep import Client, Settings + from zeep.plugins import HistoryPlugin + from zeep.transports import Transport +except (ImportError, IOError) as err: + _logger.debug(err) + +VERIFACTU_VERSION = 1.0 VERIFACTU_DATE_FORMAT = "%d-%m-%Y" +VERIFACTU_MACRODATA_LIMIT = 100000000.0 class VerifactuMixin(models.AbstractModel): @@ -24,10 +47,63 @@ class VerifactuMixin(models.AbstractModel): ) verifactu_hash_string = fields.Char(compute="_compute_verifactu_hash") verifactu_hash = fields.Char(compute="_compute_verifactu_hash") + verifactu_refund_type = fields.Selection( + selection=[ + # ('S', 'By substitution'), - en sii no está soportado, aquí igual? + ("I", "By differences"), + ], + compute="_compute_verifactu_refund_type", + store=True, + readonly=False, + ) + verifactu_description = fields.Text( + copy=False, + ) + verifactu_macrodata = fields.Boolean( + string="MacroData", + help="Check to confirm that the document has an absolute amount " + "greater o equal to 100 000 000,00 euros.", + compute="_compute_verifactu_macrodata", + ) + verifactu_csv = fields.Char(copy=False, readonly=True) + verifactu_return = fields.Text(copy=False, readonly=True) + verifactu_registration_key = fields.Many2one( + comodel_name="aeat.verifactu.registration.keys", + compute="_compute_verifactu_registration_key", + store=True, + readonly=False, + ) + verifactu_tax_key = fields.Selection( + string="Verifactu tax key", + selection="_get_verifactu_tax_keys", + compute="_compute_verifactu_tax_key", + store=True, + readonly=False, + ) + verifactu_registration_key_code = fields.Char( + compute="_compute_verifactu_registration_key_code", + readonly=True, + string="Verifactu Code", + ) def _compute_verifactu_enabled(self): raise NotImplementedError + def _compute_verifactu_macrodata(self): + for document in self: + document.verifactu_macrodata = ( + float_compare( + abs(document._get_verifactu_amount_total()), + VERIFACTU_MACRODATA_LIMIT, + precision_digits=2, + ) + >= 0 + ) + + @api.model + def _get_verifactu_tax_keys(self): + return self.env["account.fiscal.position"]._get_verifactu_tax_keys() + def _connect_params_aeat(self, mapping_key): self.ensure_one() agency = self.company_id.tax_agency_id @@ -53,73 +129,65 @@ def _get_aeat_header(self, tipo_comunicacion=False, cancellation=False): _("No VAT configured for the company '{}'").format(self.company_id.name) ) header = { - "IDVersion": VERIFACTU_VERSION, "ObligadoEmision": { "NombreRazon": self.company_id.name[0:120], "NIF": self.company_id.partner_id._parse_aeat_vat_info()[2], }, - # "TipoRegistroAEAT": , - # "FechaFinVeriactu": , } - # if not cancellation: - # header.update({"TipoComunicacion": tipo_comunicacion}) return header - def _get_aeat_invoice_dict(self): + def _get_verifactu_invoice_dict(self): self.ensure_one() inv_dict = {} mapping_key = self._get_mapping_key() if mapping_key in ["out_invoice", "out_refund"]: - inv_dict = self._get_aeat_invoice_dict_out() + inv_dict = self._get_verifactu_invoice_dict_out() else: raise NotImplementedError round_by_keys( inv_dict, [ - "BaseImponible", + "BaseImponibleOimporteNoSujeto", "CuotaRepercutida", - "CuotaSoportada", "TipoRecargoEquivalencia", "CuotaRecargoEquivalencia", - "ImportePorArticulos7_14_Otros", - "ImporteTAIReglasLocalizacion", + "CuotaTotal", "ImporteTotal", "BaseRectificada", "CuotaRectificada", - "CuotaDeducible", - "ImporteCompensacionREAGYP", ], ) return inv_dict def _get_aeat_invoice_dict_out(self, cancel=False): - """Build dict with data to send to AEAT WS for document types: - out_invoice and out_refund. + raise NotImplementedError - :param cancel: It indicates if the dictionary is for sending a - cancellation of the document. - :return: documents (dict) : Dict XML with data for this document. + def _get_verifactu_developer_dict(self): """ - self.ensure_one() - document_date = self._change_date_format(self._get_document_date()) - company = self.company_id - fiscal_year = self._get_document_fiscal_year() - period = self._get_document_period() - serial_number = self._get_document_serial_number() - inv_dict = { - "IDFactura": { - "IDEmisorFactura": { - "NIF": company.partner_id._parse_aeat_vat_info()[2] - }, - "NumSerieFactura": serial_number, - "FechaExpedicionFactura": document_date, - }, - "PeriodoLiquidacion": { - "Ejercicio": fiscal_year, - "Periodo": period, + TODO + Datos del desarrollador del sistema informático + """ + return { + "NombreRazon": _("Asoc Española de Odoo"), + "NIF": "G87846952", + "NombreSistemaInformatico": "odoo", + "IdSistemaInformatico": "11", + "Version": "1.0", + "NumeroInstalacion": "1", + "TipoUsoPosibleSoloVerifactu": "N", + "TipoUsoPosibleMultiOT": "S", + "IndicadorMultiplesOT": "S", + "IDOtro": { + "IDType": "", + "ID": "", }, } - return inv_dict + + def _get_previous_invoice(self): + raise NotImplementedError + + def _get_chaining_invoice_dict(self): + raise NotImplementedError def _aeat_check_exceptions(self): """Inheritable method for exceptions control when sending veri*FACTU invoices.""" @@ -133,7 +201,6 @@ def _change_date_format(self, date): new_date = datetimeobject.strftime(VERIFACTU_DATE_FORMAT) return new_date - @api.model def _get_verifactu_hash_string(self): raise NotImplementedError @@ -147,3 +214,219 @@ def _compute_verifactu_hash(self): record.verifactu_hash_string = verifactu_hash_values hash_string = sha256(verifactu_hash_values.encode("utf-8")) record.verifactu_hash = hash_string.hexdigest().upper() + + def _get_verifactu_document_type(self): + raise NotImplementedError() + + def _get_verifactu_description(self): + raise NotImplementedError() + + def _get_verifactu_taxes_and_total(self): + raise NotImplementedError + + def _get_verifactu_version(self): + return VERIFACTU_VERSION + + def _get_receiver_dict(self): + raise NotImplementedError + + def _compute_verifactu_refund_type(self): + self.verifactu_refund_type = False + + def _is_aeat_simplified_invoice(self): + """Inheritable method to allow control when an + invoice are simplified or normal""" + partner = self._aeat_get_partner() + return partner.aeat_simplified_invoice + + def _get_verifactu_jobs_field_name(self): + raise NotImplementedError + + def send_verifactu(self): + """General public method for filtering out of the starting recordset the records + that shouldn't be sent to Verifactu: + + - Documents of companies with Verifactu not enabled (through verifactu_enabled). + - Documents not applicable to be sent to Verifactu (through verifactu_enabled). + - Documents in non applicable states (for example, cancelled invoices). + - Documents already sent to Verifactu. + - Documents with sending jobs pending to be executed. + """ + valid_states = self._get_valid_document_states() + for document in self: + if ( + not document.verifactu_enabled + or document.state not in valid_states + or document.aeat_state in ["sent", "cancelled"] + ): + continue + document._process_verifactu_send() + + def _process_verifactu_send(self): + """ + Process document sending to Verifactu + TODO : use connector + """ + for record in self: + record.confirm_verifactu_one_document() + + def confirm_verifactu_one_document(self): + self.sudo()._send_document_to_verifactu() + + def _send_document_to_verifactu(self): + for document in self.filtered( + lambda i: i.state in self._get_valid_document_states() + ): + if document.aeat_state == "not_sent": + tipo_comunicacion = "A0" + else: + tipo_comunicacion = "A1" + header = document._get_aeat_header(tipo_comunicacion) + doc_vals = { + "aeat_header_sent": json.dumps(header, indent=4), + } + try: + inv_dict = document._get_verifactu_invoice_dict() + except Exception as fault: + raise ValidationError(fault) from fault + try: + mapping_key = document._get_mapping_key() + serv = document._connect_verifactu(mapping_key) + doc_vals["aeat_content_sent"] = json.dumps(inv_dict, indent=4) + if mapping_key in ["out_invoice", "out_refund"]: + res = serv.RegFactuSistemaFacturacion(header, inv_dict) + res_line = res["RespuestaLinea"][0] + if res["EstadoEnvio"] == "Correcto": + doc_vals.update( + { + "aeat_state": "sent", + "verifactu_csv": res["CSV"], + "aeat_send_failed": False, + } + ) + elif ( + res["EstadoEnvio"] == "ParcialmenteCorrecto" + and res_line["EstadoRegistro"] == "AceptadoConErrores" + ): + doc_vals.update( + { + "aeat_state": "sent_w_errors", + "verifactu_csv": res["CSV"], + "aeat_send_failed": True, + } + ) + else: + doc_vals["aeat_send_failed"] = True + doc_vals["verifactu_return"] = res + send_error = False + if res_line["CodigoErrorRegistro"]: + send_error = "{} | {}".format( + str(res_line["CodigoErrorRegistro"]), + str(res_line["DescripcionErrorRegistro"]), + ) + doc_vals["aeat_send_error"] = send_error + document.write(doc_vals) + except Exception as fault: + new_cr = Registry(self.env.cr.dbname).cursor() + env = api.Environment(new_cr, self.env.uid, self.env.context) + document = env[document._name].browse(document.id) + doc_vals.update( + { + "aeat_send_failed": True, + "aeat_send_error": repr(fault)[:200], + "verifactu_return": repr(fault), + "aeat_content_sent": json.dumps(inv_dict, indent=4), + } + ) + document.write(doc_vals) + new_cr.commit() + new_cr.close() + raise ValidationError(fault) from fault + + def _connect_verifactu(self, mapping_key): + # de momento no puedo el _connect_aeat del aeat_mixin porque si no pongo + # forbid_entities en settings del Client da error de entities forbiden + self.ensure_one() + public_crt, private_key = self.env["l10n.es.aeat.certificate"].get_certificates( + company=self.company_id + ) + params = self._connect_params_aeat(mapping_key) + session = Session() + session.cert = (public_crt, private_key) + transport = Transport(session=session) + history = HistoryPlugin() + settings = Settings(forbid_entities=False) + client = Client( + wsdl=params["wsdl"], + transport=transport, + plugins=[history], + settings=settings, + ) + return self._bind_service(client, params["port_name"], params["address"]) + + def _bind_service(self, client, port_name, address=None): + self.ensure_one() + service = client._get_service("sfVerifactu") + port = client._get_port(service, port_name) + address = address or port.binding_options["address"] + return client.create_service(port.binding.name, address) + + @api.model + def _get_verifactu_taxes_map(self, codes, date): + """Return the codes that correspond to verifactu map line codes. + + :param codes: List of code strings to get the mapping. + :param date: Date to map + :return: Recordset with the corresponding codes + """ + map_obj = self.env["aeat.verifactu.map"].sudo().with_context(active_test=False) + verifactu_map = map_obj.search( + [ + "|", + ("date_from", "<=", date), + ("date_from", "=", False), + "|", + ("date_to", ">=", date), + ("date_to", "=", False), + ], + limit=1, + ) + tax_templates = verifactu_map.map_lines.filtered( + lambda x: x.code in codes + ).taxes + return self.company_id.get_taxes_from_templates(tax_templates) + + @api.depends("fiscal_position_id") + def _compute_verifactu_tax_key(self): + for document in self: + document.verifactu_tax_key = ( + document.fiscal_position_id.verifactu_tax_key or "01" + ) + + @api.depends("fiscal_position_id") + def _compute_verifactu_registration_key(self): + for document in self: + if document.fiscal_position_id: + key = document.fiscal_position_id.verifactu_registration_key + if key: + document.verifactu_registration_key = key + else: + domain = [ + ("code", "=", "01"), + ( + "verifactu_tax_key", + "=", + "iva", + ), + ] + verifactu_key_obj = self.env["aeat.verifactu.registration.keys"] + document.verifactu_registration_key = verifactu_key_obj.search( + domain, limit=1 + ) + + @api.depends("verifactu_registration_key") + def _compute_verifactu_registration_key_code(self): + for record in self: + record.verifactu_registration_key_code = ( + record.verifactu_registration_key.code + ) diff --git a/l10n_es_aeat_verifactu/readme/CONFIGURE.rst b/l10n_es_aeat_verifactu/readme/CONFIGURE.rst index e69de29bb2d..f07af189f6a 100644 --- a/l10n_es_aeat_verifactu/readme/CONFIGURE.rst +++ b/l10n_es_aeat_verifactu/readme/CONFIGURE.rst @@ -0,0 +1,41 @@ +Para configurar este módulo es necesario: + +#. En la compañia se almacenan las URLs del servicio SOAP de hacienda. + Estas URLs pueden cambiar según comunidades +#. Los certificados deben alojarse en una carpeta accesible por la instalación + de Odoo. +#. Preparar el certificado. El certificado enviado por la FMNT es en formato + p12, este certificado no se puede usar directamente con Zeep. Se tiene que + extraer la clave pública y la clave privada. + +En Linux se pueden usar los siguientes comandos: + +- Clave pública: "openssl pkcs12 -in Certificado.p12 -nokeys -out publicCert.crt -nodes" +- Clave privada: "openssl pkcs12 -in Certifcado.p12 -nocerts -out privateKey.pem -nodes" + +Además, el módulo `queue_job` necesita estar configurado de una de estas formas: + +#. Ajustando variables de entorno: + + ODOO_QUEUE_JOB_CHANNELS=root:4 + + u otro canal de configuración. Por defecto es root:1 + + Si xmlrpc_port no está definido: ODOO_QUEUE_JOB_PORT=8069 + +#. Otra alternativa es usuando un fichero de configuración: + + [options] + (...) + workers = 4 + server_wide_modules = web,base_sparse_field,queue_job + + (...) + [queue_job] + channels = root:4 + +#. Por último, arrancando Odoo con --load=web,base_sparse_field,queue_job y --workers más grande que 1. + +Más información http://odoo-connector.com + +#. Establecer en las posiciones fiscales la clave de impuestos y la clave de registro verifactu. diff --git a/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst b/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst index 59e855e1d69..29280226b91 100644 --- a/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst +++ b/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst @@ -1,3 +1,4 @@ * Jose Zambudio +* Almudena de La Puente * Laura Cazorla * Andreu Orensanz diff --git a/l10n_es_aeat_verifactu/readme/INSTALL.rst b/l10n_es_aeat_verifactu/readme/INSTALL.rst index e69de29bb2d..0b8967682d7 100644 --- a/l10n_es_aeat_verifactu/readme/INSTALL.rst +++ b/l10n_es_aeat_verifactu/readme/INSTALL.rst @@ -0,0 +1,8 @@ +Para instalar esté módulo necesita: + +#. Libreria Python Zeep, se puede instalar con el comando 'pip install zeep' +#. Libreria Python Requests, se puede instalar con el comando 'pip install requests' + +y el módulo `queue_job` que se encuentra en: + +https://github.com/OCA/queue diff --git a/l10n_es_aeat_verifactu/readme/ROADMAP.rst b/l10n_es_aeat_verifactu/readme/ROADMAP.rst index 18f879648d8..19a5faee1a4 100644 --- a/l10n_es_aeat_verifactu/readme/ROADMAP.rst +++ b/l10n_es_aeat_verifactu/readme/ROADMAP.rst @@ -1,5 +1,8 @@ - * Refactorización SII en l10n_es_aeat - * Creación documento a enviar a Veri*FACTU - * Creación cabecera Veri*FACTU - * Conexión WSDL - * Queue + Encadenamiento + * Refactorización SII-Verifactu en l10n_es_aeat cuando estén todos los procesos claros + * Envío de Facturas simplificadas, exentas, a terceros.. + * Encadenamiento, obtener factura anterior y almacenamiento del hash inalterable. + * Datas de mapeos de impuestos, ya hay algunos. + * Datos reales del desarrollador del sistema informático. + * Envío con Queue. + * Modificación de facturas enviadas. + * Anulación de facturas enviadas. diff --git a/l10n_es_aeat_verifactu/security/ir.model.access.csv b/l10n_es_aeat_verifactu/security/ir.model.access.csv new file mode 100644 index 00000000000..08b7ca05456 --- /dev/null +++ b/l10n_es_aeat_verifactu/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_model_aeat_verifactu_map_admin,aeat.verifactu.map admin,model_aeat_verifactu_map,base.group_system,1,1,1,1 +access_model_aeat_verifactu_map_aeat,aeat.verifactu.map aeat,model_aeat_verifactu_map,l10n_es_aeat.group_account_aeat,1,0,0,0 +access_model_aeat_verifactu_map_lines_admin,aeat.verifactu.map.lines admin,model_aeat_verifactu_map_lines,base.group_system,1,1,1,1 +access_model_aeat_verifactu_map_lines_aeat,aeat.verifactu.map.lines aeat,model_aeat_verifactu_map_lines,l10n_es_aeat.group_account_aeat,1,0,0,0 +access_model_aeat_verifactu_registration_keys_admin,aeat.verifactu.registration.keys admin,model_aeat_verifactu_registration_keys,base.group_system,1,1,1,1 +access_model_aeat_verifactu_registration_keys_aeat,aeat.verifactu.registration.keys aeat,model_aeat_verifactu_registration_keys,l10n_es_aeat.group_account_aeat,1,0,0,0 +access_model_aeat_verifactu_registration_keys_aeat_account,aeat.verifactu.registration.keys aeat,model_aeat_verifactu_registration_keys,account.group_account_invoice,1,0,0,0 diff --git a/l10n_es_aeat_verifactu/static/description/index.html b/l10n_es_aeat_verifactu/static/description/index.html index 80c088acd59..d1abdd6c295 100644 --- a/l10n_es_aeat_verifactu/static/description/index.html +++ b/l10n_es_aeat_verifactu/static/description/index.html @@ -374,30 +374,90 @@

Comunicación Veri*FACTU

Table of contents

+
+

Installation

+

Para instalar esté módulo necesita:

+
    +
  1. Libreria Python Zeep, se puede instalar con el comando ‘pip install zeep’
  2. +
  3. Libreria Python Requests, se puede instalar con el comando ‘pip install requests’
  4. +
+

y el módulo queue_job que se encuentra en:

+

https://github.com/OCA/queue

+
+
+

Configuration

+

Para configurar este módulo es necesario:

+
    +
  1. En la compañia se almacenan las URLs del servicio SOAP de hacienda. +Estas URLs pueden cambiar según comunidades
  2. +
  3. Los certificados deben alojarse en una carpeta accesible por la instalación +de Odoo.
  4. +
  5. Preparar el certificado. El certificado enviado por la FMNT es en formato +p12, este certificado no se puede usar directamente con Zeep. Se tiene que +extraer la clave pública y la clave privada.
  6. +
+

En Linux se pueden usar los siguientes comandos:

+
    +
  • Clave pública: “openssl pkcs12 -in Certificado.p12 -nokeys -out publicCert.crt -nodes”
  • +
  • Clave privada: “openssl pkcs12 -in Certifcado.p12 -nocerts -out privateKey.pem -nodes”
  • +
+

Además, el módulo queue_job necesita estar configurado de una de estas formas:

+
    +
  1. Ajustando variables de entorno:

    +
    +

    ODOO_QUEUE_JOB_CHANNELS=root:4

    +
    +

    u otro canal de configuración. Por defecto es root:1

    +

    Si xmlrpc_port no está definido: ODOO_QUEUE_JOB_PORT=8069

    +
  2. +
  3. Otra alternativa es usuando un fichero de configuración:

    +
    +

    [options] +(…) +workers = 4 +server_wide_modules = web,base_sparse_field,queue_job

    +

    (…) +[queue_job] +channels = root:4

    +
    +
  4. +
  5. Por último, arrancando Odoo con –load=web,base_sparse_field,queue_job y –workers más grande que 1.

    +
  6. +
+

Más información http://odoo-connector.com

+
    +
  1. Establecer en las posiciones fiscales la clave de impuestos y la clave de registro verifactu.
  2. +
+
-

Known issues / Roadmap

+

Known issues / Roadmap

    -
  • Refactorización SII en l10n_es_aeat
  • -
  • Creación documento a enviar a Veri*FACTU
  • -
  • Creación cabecera Veri*FACTU
  • -
  • Conexión WSDL
  • -
  • Queue + Encadenamiento
  • +
  • Refactorización SII-Verifactu en l10n_es_aeat cuando estén todos los procesos claros
  • +
  • Envío de Facturas simplificadas, exentas, a terceros..
  • +
  • Encadenamiento, obtener factura anterior y almacenamiento del hash inalterable.
  • +
  • Datas de mapeos de impuestos, ya hay algunos.
  • +
  • Datos reales del desarrollador del sistema informático.
  • +
  • Envío con Queue.
  • +
  • Modificación de facturas enviadas.
  • +
  • Anulación de facturas enviadas.
-

Bug Tracker

+

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 @@ -405,24 +465,25 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Aures Tic
  • ForgeFlow
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association diff --git a/l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json b/l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json new file mode 100644 index 00000000000..c0e4e0bf4cf --- /dev/null +++ b/l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json @@ -0,0 +1,59 @@ +{ + "RegistroAlta": { + "IDVersion": 1.0, + "IDFactura": { + "IDEmisorFactura": "G87846952", + "NumSerieFactura": "TEST001", + "FechaExpedicionFactura": "01-01-2024" + }, + "NombreRazonEmisor": "Spanish test company", + "TipoFactura": "F1", + "DescripcionOperacion": "/", + "Destinatarios": { + "IDDestinatario": { + "NombreRazon": "Test partner", + "NIF": "89890001K" + } + }, + "Desglose": { + "DetalleDesglose": [ + { + "Impuesto": "01", + "ClaveRegimen": "01", + "CalificacionOperacion": "S1", + "TipoImpositivo": "10.0", + "BaseImponibleOimporteNoSujeto": 100.0, + "CuotaRepercutida": 10.0 + }, + { + "Impuesto": "01", + "ClaveRegimen": "01", + "CalificacionOperacion": "S1", + "TipoImpositivo": "21.0", + "BaseImponibleOimporteNoSujeto": 200.0, + "CuotaRepercutida": 42.0 + } + ] + }, + "CuotaTotal": 52.0, + "ImporteTotal": 352.0, + "Encadenamiento": { + "PrimerRegistro": "S" + }, + "SistemaInformatico": { + "NombreRazon": "Asoc Española de Odoo", + "NIF": "G87846952", + "NombreSistemaInformatico": "odoo", + "IdSistemaInformatico": "11", + "Version": "1.0", + "NumeroInstalacion": "1", + "TipoUsoPosibleSoloVerifactu": "N", + "TipoUsoPosibleMultiOT": "S", + "IndicadorMultiplesOT": "S", + "IDOtro": { + "IDType": "", + "ID": "" + } + } + } +} diff --git a/l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json b/l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json new file mode 100644 index 00000000000..83b9f52390c --- /dev/null +++ b/l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json @@ -0,0 +1,53 @@ +{ + "RegistroAlta": { + "IDVersion": 1.0, + "IDFactura": { + "IDEmisorFactura": "G87846952", + "NumSerieFactura": "TEST001", + "FechaExpedicionFactura": "01-01-2024" + }, + "NombreRazonEmisor": "Spanish test company", + "TipoFactura": "F1", + "DescripcionOperacion": "/", + "Destinatarios": { + "IDDestinatario": { + "NombreRazon": "Test partner", + "NIF": "89890001K" + } + }, + "Desglose": { + "DetalleDesglose": [ + { + "Impuesto": "01", + "ClaveRegimen": "01", + "CalificacionOperacion": "S1", + "TipoImpositivo": "21.0", + "BaseImponibleOimporteNoSujeto": 200.0, + "CuotaRepercutida": 42.0, + "TipoRecargoEquivalencia": 5.2, + "CuotaRecargoEquivalencia": 10.4 + } + ] + }, + "CuotaTotal": 52.4, + "ImporteTotal": 252.4, + "Encadenamiento": { + "PrimerRegistro": "S" + }, + "SistemaInformatico": { + "NombreRazon": "Asoc Española de Odoo", + "NIF": "G87846952", + "NombreSistemaInformatico": "odoo", + "IdSistemaInformatico": "11", + "Version": "1.0", + "NumeroInstalacion": "1", + "TipoUsoPosibleSoloVerifactu": "N", + "TipoUsoPosibleMultiOT": "S", + "IndicadorMultiplesOT": "S", + "IDOtro": { + "IDType": "", + "ID": "" + } + } + } +} diff --git a/l10n_es_aeat_verifactu/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json b/l10n_es_aeat_verifactu/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json new file mode 100644 index 00000000000..92ea6ec68e0 --- /dev/null +++ b/l10n_es_aeat_verifactu/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json @@ -0,0 +1,61 @@ +{ + "RegistroAlta": { + "IDVersion": 1.0, + "IDFactura": { + "IDEmisorFactura": "G87846952", + "NumSerieFactura": "TEST001", + "FechaExpedicionFactura": "01-01-2024" + }, + "NombreRazonEmisor": "Spanish test company", + "TipoFactura": "R1", + "TipoRectificativa": "I", + "DescripcionOperacion": "/", + "Destinatarios": { + "IDDestinatario": { + "NombreRazon": "Test partner", + "NIF": "89890001K" + } + }, + "Desglose": { + "DetalleDesglose": [ + { + "Impuesto": "01", + "ClaveRegimen": "01", + "CalificacionOperacion": "S1", + "TipoImpositivo": "10.0", + "BaseImponibleOimporteNoSujeto": -200.0, + "CuotaRepercutida": -20.0 + }, + { + "Impuesto": "01", + "ClaveRegimen": "01", + "CalificacionOperacion": "S1", + "TipoImpositivo": "21.0", + "BaseImponibleOimporteNoSujeto": -200.0, + "CuotaRepercutida": -42.0 + } + ] + }, + "CuotaTotal": -62.0, + "ImporteTotal": -462.0, + "Encadenamiento": { + "PrimerRegistro": "S" + }, + "FacturasRectificadas": [], + "SistemaInformatico": { + "NombreRazon": "Asoc Española de Odoo", + "NIF": "G87846952", + "NombreSistemaInformatico": "odoo", + "IdSistemaInformatico": "11", + "Version": "1.0", + "NumeroInstalacion": "1", + "TipoUsoPosibleSoloVerifactu": "N", + "TipoUsoPosibleMultiOT": "S", + "IndicadorMultiplesOT": "S", + "IDOtro": { + "IDType": "", + "ID": "" + } + } + } +} diff --git a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py index 15167871d9c..e0797cf8316 100644 --- a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py +++ b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py @@ -1,8 +1,11 @@ # Copyright 2024 Aures TIC - Almudena de La Puente # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +import json from hashlib import sha256 +from odoo.modules.module import get_resource_path + from odoo.addons.l10n_es_aeat.tests.test_l10n_es_aeat_certificate import ( TestL10nEsAeatCertificateBase, ) @@ -11,10 +14,54 @@ ) -class TestL10nEsAeatSiiBase(TestL10nEsAeatModBase, TestL10nEsAeatCertificateBase): +class TestL10nEsAeatVerifactuBase(TestL10nEsAeatModBase, TestL10nEsAeatCertificateBase): @classmethod def setUpClass(cls): super().setUpClass() + cls.maxDiff = None + cls.fp_nacional = cls.env.ref(f"l10n_es.{cls.company.id}_fp_nacional") + cls.fp_registration_key_01 = cls.env.ref( + "l10n_es_aeat_verifactu.aeat_verifactu_registration_keys_01" + ) + cls.fp_nacional.verifactu_registration_key = cls.fp_registration_key_01 + cls.fp_recargo = cls.env.ref(f"l10n_es.{cls.company.id}_fp_recargo") + cls.fp_recargo.verifactu_registration_key = cls.fp_registration_key_01 + cls.partner = cls.env["res.partner"].create( + {"name": "Test partner", "vat": "89890001K"} + ) + cls.product = cls.env["product.product"].create({"name": "Test product"}) + cls.account_expense = cls.env.ref( + "l10n_es.%s_account_common_600" % cls.company.id + ) + cls.invoice = cls.env["account.move"].create( + { + "company_id": cls.company.id, + "partner_id": cls.partner.id, + "invoice_date": "2024-01-01", + "move_type": "out_invoice", + "invoice_line_ids": [ + ( + 0, + 0, + { + "product_id": cls.product.id, + "account_id": cls.account_expense.id, + "name": "Test line", + "price_unit": 100, + "quantity": 1, + }, + ) + ], + } + ) + cls.company.write( + { + "verifactu_enabled": True, + "verifactu_test": True, + "vat": "G87846952", + "tax_agency_id": cls.env.ref("l10n_es_aeat.aeat_tax_agency_spain"), + } + ) def test_verifactu_hash_code(self): # based on AEAT Verifactu documentation @@ -43,3 +90,109 @@ def test_verifactu_hash_code(self): sha_hash_code = sha256(verifactu_hash_string.encode("utf-8")) hash_code = sha_hash_code.hexdigest().upper() self.assertEqual(hash_code, expected_hash) + + def _create_and_test_invoice_verifactu_dict( + self, inv_type, lines, extra_vals, module=None + ): + vals = [] + tax_names = [] + for line in lines: + taxes = self.env["account.tax"] + for tax in line[1]: + if "." in tax: + xml_id = tax + else: + xml_id = "l10n_es.{}_account_tax_template_{}".format( + self.company.id, tax + ) + taxes += self.env.ref(xml_id) + tax_names.append(tax) + vals.append({"price_unit": line[0], "taxes": taxes}) + return self._compare_verifactu_dict( + "verifactu_{}_{}_dict.json".format(inv_type, "_".join(tax_names)), + inv_type, + vals, + extra_vals=extra_vals, + module=module, + ) + + def _compare_verifactu_dict( + self, json_file, inv_type, lines, extra_vals=None, module=None + ): + """Helper method for creating an invoice according arguments, and + comparing the expected verifactu dict with . + """ + module = module or "l10n_es_aeat_verifactu" + vals = { + "name": "TEST001", + "partner_id": self.partner.id, + "invoice_date": "2024-01-01", + "move_type": inv_type, + "invoice_line_ids": [], + } + for line in lines: + vals["invoice_line_ids"].append( + ( + 0, + 0, + { + "product_id": self.product.id, + "account_id": self.account_expense.id, + "name": "Test line", + "price_unit": line["price_unit"], + "quantity": 1, + "tax_ids": [(6, 0, line["taxes"].ids)], + }, + ) + ) + if extra_vals: + vals.update(extra_vals) + invoice = self.env["account.move"].create(vals) + result_dict = invoice._get_verifactu_invoice_dict() + result_dict["RegistroAlta"].pop("FechaHoraHusoGenRegistro") + result_dict["RegistroAlta"].pop("TipoHuella") + result_dict["RegistroAlta"].pop("Huella") + path = get_resource_path(module, "tests/json", json_file) + if not path: + raise Exception("Incorrect JSON file: %s" % json_file) + with open(path, "r") as f: + expected_dict = json.loads(f.read()) + self.assertEqual(expected_dict, result_dict) + return invoice + + +class TestL10nEsAeatVerifactu(TestL10nEsAeatVerifactuBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_get_verifactu_invoice_data(self): + mapping = [ + ( + "out_invoice", + [(100, ["s_iva10b"]), (200, ["s_iva21s"])], + { + "fiscal_position_id": self.fp_nacional.id, + "verifactu_registration_key": self.fp_registration_key_01.id, + }, + ), + ( + "out_refund", + [(100, ["s_iva10b"]), (100, ["s_iva10b"]), (200, ["s_iva21s"])], + { + "fiscal_position_id": self.fp_nacional.id, + "verifactu_registration_key": self.fp_registration_key_01.id, + }, + ), + ( + "out_invoice", + [(200, ["s_iva21s", "s_req52"])], + { + "fiscal_position_id": self.fp_recargo.id, + "verifactu_registration_key": self.fp_registration_key_01.id, + }, + ), + ] + for inv_type, lines, extra_vals in mapping: + self._create_and_test_invoice_verifactu_dict(inv_type, lines, extra_vals) + return diff --git a/l10n_es_aeat_verifactu/views/account_fiscal_position_view.xml b/l10n_es_aeat_verifactu/views/account_fiscal_position_view.xml index c04865cee7d..bdf506678ef 100644 --- a/l10n_es_aeat_verifactu/views/account_fiscal_position_view.xml +++ b/l10n_es_aeat_verifactu/views/account_fiscal_position_view.xml @@ -15,6 +15,11 @@ attrs="{'invisible': [('verifactu_enabled', '=', False)]}" > + + diff --git a/l10n_es_aeat_verifactu/views/account_journal_views.xml b/l10n_es_aeat_verifactu/views/account_journal_view.xml similarity index 100% rename from l10n_es_aeat_verifactu/views/account_journal_views.xml rename to l10n_es_aeat_verifactu/views/account_journal_view.xml diff --git a/l10n_es_aeat_verifactu/views/account_move_view.xml b/l10n_es_aeat_verifactu/views/account_move_view.xml index 30de813e7bf..410da260ac4 100644 --- a/l10n_es_aeat_verifactu/views/account_move_view.xml +++ b/l10n_es_aeat_verifactu/views/account_move_view.xml @@ -1,5 +1,6 @@ @@ -7,6 +8,22 @@ account.move + - - + + + + + + @@ -31,6 +52,7 @@ + - + diff --git a/l10n_es_aeat_verifactu/views/aeat_verifactu_map_lines_view.xml b/l10n_es_aeat_verifactu/views/aeat_verifactu_map_lines_view.xml new file mode 100644 index 00000000000..c8c9e219cec --- /dev/null +++ b/l10n_es_aeat_verifactu/views/aeat_verifactu_map_lines_view.xml @@ -0,0 +1,17 @@ + + + + + aeat.verifactu.map.lines.view.tree + aeat.verifactu.map.lines + + + + + + + + + + diff --git a/l10n_es_aeat_verifactu/views/aeat_verifactu_map_view.xml b/l10n_es_aeat_verifactu/views/aeat_verifactu_map_view.xml new file mode 100644 index 00000000000..f9e8733b012 --- /dev/null +++ b/l10n_es_aeat_verifactu/views/aeat_verifactu_map_view.xml @@ -0,0 +1,55 @@ + + + + + aeat.verifactu.map.view.tree + aeat.verifactu.map + + + + + + + + + + aeat.verifactu.map.view.form + aeat.verifactu.map + +
+ + + + + + + + + + + + + + +
+
+
+ + Aeat Verifactu Map + aeat.verifactu.map + tree,form + + + +
diff --git a/l10n_es_aeat_verifactu/views/aeat_verifactu_registration_keys_view.xml b/l10n_es_aeat_verifactu/views/aeat_verifactu_registration_keys_view.xml new file mode 100644 index 00000000000..708321561a2 --- /dev/null +++ b/l10n_es_aeat_verifactu/views/aeat_verifactu_registration_keys_view.xml @@ -0,0 +1,45 @@ + + + + + aeat.verifactu.registration.keys.view.tree + aeat.verifactu.registration.keys + + + + + + + + + + aeat.verifactu.registration.keys.view.search + aeat.verifactu.registration.keys + + + + + + + + + + + + + AEAT Verifactu Registration Keys + aeat.verifactu.registration.keys + tree,form + + + diff --git a/l10n_es_aeat_verifactu/views/res_company_view.xml b/l10n_es_aeat_verifactu/views/res_company_view.xml index b4cb7ee1e6f..24a8acd59a5 100644 --- a/l10n_es_aeat_verifactu/views/res_company_view.xml +++ b/l10n_es_aeat_verifactu/views/res_company_view.xml @@ -21,6 +21,11 @@
+ + + + +
diff --git a/l10n_es_aeat_verifactu/wizards/__init__.py b/l10n_es_aeat_verifactu/wizards/__init__.py new file mode 100644 index 00000000000..715d1bd6df0 --- /dev/null +++ b/l10n_es_aeat_verifactu/wizards/__init__.py @@ -0,0 +1 @@ +from . import account_move_reversal diff --git a/l10n_es_aeat_verifactu/wizards/account_move_reversal.py b/l10n_es_aeat_verifactu/wizards/account_move_reversal.py new file mode 100644 index 00000000000..6733d138318 --- /dev/null +++ b/l10n_es_aeat_verifactu/wizards/account_move_reversal.py @@ -0,0 +1,16 @@ +# Copyright 2024 Aures TIC - Almudena de La Puente +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from odoo import models + + +class AccountMoveReversal(models.TransientModel): + _inherit = "account.move.reversal" + + def reverse_moves(self): + res = super().reverse_moves() + self.move_ids.filtered(lambda mov: mov.move_type == "out_invoice").mapped( + "reversal_move_id" + ).write({"verifactu_refund_type": "I"}) + return res From 7772f4f98387247ddf142b9fc0208f55e2f71f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Iv=C3=A1n=20Ant=C3=B3n=20Prieto?= Date: Thu, 28 Nov 2024 11:28:41 +0100 Subject: [PATCH 06/15] [ADD] Primera version QR Verifactu (#62) --- l10n_es_aeat_verifactu/__manifest__.py | 2 + .../data/aeat_verifactu_tax_agency_data.xml | 2 + l10n_es_aeat_verifactu/models/account_move.py | 14 +++ .../models/aeat_tax_agency.py | 2 + .../models/verifactu_mixin.py | 63 +++++++++++ .../readme/CONTRIBUTORS.rst | 1 + .../tests/test_10n_es_aeat_verifactu.py | 101 ++++++++++++++++++ .../views/aeat_tax_agency_view.xml | 7 ++ .../views/report_invoice.xml | 17 +++ 9 files changed, 209 insertions(+) create mode 100644 l10n_es_aeat_verifactu/views/report_invoice.xml diff --git a/l10n_es_aeat_verifactu/__manifest__.py b/l10n_es_aeat_verifactu/__manifest__.py index fbabed4eb9f..509982f39e8 100644 --- a/l10n_es_aeat_verifactu/__manifest__.py +++ b/l10n_es_aeat_verifactu/__manifest__.py @@ -17,6 +17,7 @@ "l10n_es_aeat", "account_invoice_refund_link", "queue_job", + "account", ], "data": [ "data/aeat_verifactu_tax_agency_data.xml", @@ -32,5 +33,6 @@ "views/aeat_verifactu_map_view.xml", "views/aeat_verifactu_map_lines_view.xml", "views/aeat_verifactu_registration_keys_view.xml", + "views/report_invoice.xml", ], } diff --git a/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml b/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml index c63852a39f6..b3ecf8e7b81 100644 --- a/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml +++ b/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml @@ -9,5 +9,7 @@ https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP + https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR + https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR
diff --git a/l10n_es_aeat_verifactu/models/account_move.py b/l10n_es_aeat_verifactu/models/account_move.py index 0e1cb0bca18..a4da671077d 100644 --- a/l10n_es_aeat_verifactu/models/account_move.py +++ b/l10n_es_aeat_verifactu/models/account_move.py @@ -3,6 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import pytz +from collections import OrderedDict from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -377,5 +378,18 @@ def _get_receiver_dict(self): } } + def _get_verifactu_qr_values(self): + """Get the QR values for the verifactu""" + self.ensure_one() + company_vat = self.company_id.partner_id._parse_aeat_vat_info()[2] + return OrderedDict( + [ + ("nif", company_vat), + ("numserie", self.name), + ("fecha", self.invoice_date.strftime("%d-%m-%Y")), + ("importe", self.amount_total), + ] + ) + def cancel_verifactu(self): raise NotImplementedError diff --git a/l10n_es_aeat_verifactu/models/aeat_tax_agency.py b/l10n_es_aeat_verifactu/models/aeat_tax_agency.py index adbc1b91a0b..dbf95e6bcc1 100644 --- a/l10n_es_aeat_verifactu/models/aeat_tax_agency.py +++ b/l10n_es_aeat_verifactu/models/aeat_tax_agency.py @@ -21,6 +21,8 @@ class AeatTaxAgency(models.Model): verifactu_wsdl_out_test_address = fields.Char( string="SuministroInformacion Test Address" ) + verifactu_qr_base_url = fields.Char(string="QR Base URL") + verifactu_qr_base_url_test_address = fields.Char(string="QR Base URL Test") def _connect_params_verifactu(self, mapping_key, company): self.ensure_one() diff --git a/l10n_es_aeat_verifactu/models/verifactu_mixin.py b/l10n_es_aeat_verifactu/models/verifactu_mixin.py index dfa921a4b68..3520c8e5334 100644 --- a/l10n_es_aeat_verifactu/models/verifactu_mixin.py +++ b/l10n_es_aeat_verifactu/models/verifactu_mixin.py @@ -2,9 +2,12 @@ # Copyright 2024 Aures TIC - Almudena de La Puente # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import base64 +import io import json import logging from hashlib import sha256 +from urllib.parse import urlencode from requests import Session @@ -15,6 +18,14 @@ from odoo.addons.l10n_es_aeat.models.aeat_mixin import round_by_keys +_logger = logging.getLogger(__name__) + +try: + import qrcode +except (ImportError, IOError) as err: + qrcode = None + _logger.error(err) + ########################################### # revisar los imports que no hagan falta # cuando funcione bien el _connect_aeat sin tener que poner @@ -85,6 +96,8 @@ class VerifactuMixin(models.AbstractModel): readonly=True, string="Verifactu Code", ) + verifactu_qr_url = fields.Char("URL", compute="_compute_verifactu_qr_url") + verifactu_qr = fields.Binary(string="QR", compute="_compute_verifactu_qr") def _compute_verifactu_enabled(self): raise NotImplementedError @@ -100,6 +113,56 @@ def _compute_verifactu_macrodata(self): >= 0 ) + def _compute_verifactu_qr_url(self): + """Returns the URL to be used in the QR code. A sample URL would be (urlencoded): + https://prewww2.aeat.es/wlpl/TIKECONT/ValidarQR?nif=89890001K&numserie=12345678%26G33&fecha=01-01-2024&importe=241.4 + """ + for record in self: + agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain") + if record.company_id.verifactu_test: + qr_base_url = agency.verifactu_qr_base_url_test_address + else: + qr_base_url = agency.verifactu_qr_base_url + + qr_values = record._get_verifactu_qr_values() + + # Check all values are ASCII between 32 and 126 + for value in qr_values.values(): + try: + str(value).encode("ascii") + except UnicodeEncodeError: + raise UserError(_("QR URL value '{}' is not ASCII").format(value)) + + # Build QR URL + qr_url = "{}?{}".format( + qr_base_url, + urlencode(qr_values, encoding="utf-8"), + ) + + record.verifactu_qr_url = qr_url + + def _compute_verifactu_qr(self): + # If qrcode module is not available, we can't generate QR codes + if not qrcode: + _logger.error("qrcode module is not available") + return + for record in self: + if record.state != "posted" or not record.verifactu_enabled: + record.verifactu_qr = False + continue + qr = qrcode.QRCode( + border=0, error_correction=qrcode.constants.ERROR_CORRECT_M + ) + qr.add_data(record.verifactu_qr_url) + qr.make() + img = qr.make_image() + with io.BytesIO() as temp: + img.save(temp, format="PNG") + record.verifactu_qr = base64.b64encode(temp.getvalue()) + + def _get_verifactu_qr_values(self): + raise NotImplementedError + @api.model def _get_verifactu_tax_keys(self): return self.env["account.fiscal.position"]._get_verifactu_tax_keys() diff --git a/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst b/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst index 29280226b91..13ba0bcd75c 100644 --- a/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst +++ b/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst @@ -2,3 +2,4 @@ * Almudena de La Puente * Laura Cazorla * Andreu Orensanz +* Iván Antón diff --git a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py index e0797cf8316..a0b50a61ee6 100644 --- a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py +++ b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py @@ -3,6 +3,7 @@ import json from hashlib import sha256 +from urllib.parse import urlparse, parse_qs from odoo.modules.module import get_resource_path @@ -196,3 +197,103 @@ def test_get_verifactu_invoice_data(self): for inv_type, lines, extra_vals in mapping: self._create_and_test_invoice_verifactu_dict(inv_type, lines, extra_vals) return + + +class TestL10nEsAeatVerifactuQR(TestL10nEsAeatVerifactuBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def _get_required_qr_params(self): + """Helper to generate the required QR code parameters.""" + return { + "nif": self.invoice.company_id.partner_id._parse_aeat_vat_info()[2], + "numserie": self.invoice.name, + "fecha": self.invoice._change_date_format(self.invoice.invoice_date), + "importe": self.invoice.amount_total, + } + + def test_verifactu_qr_generation(self): + """ + Test the generation of the QR code image for the invoice. + """ + self.invoice.action_post() + qr_code = self.invoice.verifactu_qr + + self.assertTrue(qr_code, "QR code should be generated for the invoice.") + self.assertIsInstance(qr_code, bytes, "QR code should be in bytes format.") + + def test_verifactu_qr_url_format(self): + """ + Test the format of the generated QR URL to ensure it meets expected criteria. + """ + self.invoice.action_post() + qr_url = self.invoice.verifactu_qr_url + + self.assertTrue(qr_url, "QR URL should be generated for the invoice.") + + test_url = self.env.ref( + "l10n_es_aeat.aeat_tax_agency_spain" + ).verifactu_qr_base_url_test_address + self.assertTrue(test_url, "Test URL should not be empty.") + + parsed_url = urlparse(qr_url) + actual_params = parse_qs(parsed_url.query) + + expected_params = self._get_required_qr_params() + for key, expected_value in expected_params.items(): + self.assertIn( + key, actual_params, f"QR URL should contain the parameter: {key}" + ) + self.assertEqual( + actual_params[key][0], + str(expected_value), + f"QR URL parameter '{key}' should have value '{expected_value}', got '{actual_params[key][0]}' instead.", + ) + + def test_verifactu_qr_code_generation_on_draft(self): + """ + Ensure that the QR code is not generated for invoices in draft state. + """ + qr_code = self.invoice.verifactu_qr + self.assertFalse(qr_code, "QR code should not be generated for draft invoices.") + + def test_verifactu_qr_code_after_update(self): + """ + Test that the QR code is regenerated if the invoice details are updated. + """ + self.invoice.action_post() + original_qr_code = self.invoice.verifactu_qr + + self.invoice.button_cancel() + + self.invoice.button_draft() + + self.invoice.write( + { + "invoice_line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "account_id": self.account_expense.id, + "name": "Updated line", + "price_unit": 200, + "quantity": 1, + }, + ) + ] + } + ) + self.invoice.action_post() + + self.invoice.invalidate_model(["verifactu_qr_url", "verifactu_qr"]) + + updated_qr_code = self.invoice.verifactu_qr + + self.assertNotEqual( + original_qr_code, + updated_qr_code, + "QR code should be regenerated after invoice update.", + ) diff --git a/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml b/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml index 1f790731475..2c0ca0e5416 100644 --- a/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml +++ b/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml @@ -17,6 +17,13 @@ /> + + + +
diff --git a/l10n_es_aeat_verifactu/views/report_invoice.xml b/l10n_es_aeat_verifactu/views/report_invoice.xml new file mode 100644 index 00000000000..910cd124107 --- /dev/null +++ b/l10n_es_aeat_verifactu/views/report_invoice.xml @@ -0,0 +1,17 @@ + + + + From e75555708810701eb412be6410c0b6fb9fb80603 Mon Sep 17 00:00:00 2001 From: aritzolea-factorlibre <31987854+aritzolea@users.noreply.github.com> Date: Mon, 17 Feb 2025 09:15:28 +0100 Subject: [PATCH 07/15] [IMP] l10n_es_aeat_verifactu: Fixes on views, hash calculated on invoice post and refactor (#66) --- .../models/account_journal.py | 6 +++-- l10n_es_aeat_verifactu/models/account_move.py | 24 +++++++++++++------ .../models/verifactu_mixin.py | 18 +++----------- .../views/account_move_view.xml | 11 +++++---- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/l10n_es_aeat_verifactu/models/account_journal.py b/l10n_es_aeat_verifactu/models/account_journal.py index 781836c2b96..69ecf708cd3 100644 --- a/l10n_es_aeat_verifactu/models/account_journal.py +++ b/l10n_es_aeat_verifactu/models/account_journal.py @@ -37,8 +37,10 @@ def _compute_restrict_mode_hash_table(self): and record.company_id.country_code == "ES" and record.type == "sale" ): - record.restrict_mode_hash_table = True - record.restrict_mode_hash_table_readonly = True + record.write({ + "restrict_mode_hash_table": True, + "restrict_mode_hash_table_readonly": True + }) @api.model def check_hash_modification(self, country_code, journal_type, verifactu_enabled): diff --git a/l10n_es_aeat_verifactu/models/account_move.py b/l10n_es_aeat_verifactu/models/account_move.py index a4da671077d..4bd9273a794 100644 --- a/l10n_es_aeat_verifactu/models/account_move.py +++ b/l10n_es_aeat_verifactu/models/account_move.py @@ -4,6 +4,8 @@ import pytz from collections import OrderedDict +from datetime import datetime +from hashlib import sha256 from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -30,6 +32,7 @@ class AccountMove(models.Model): " of article 80 of LIVA for notifying to Vertifactu with the proper" " invoice type.", ) + verifactu_registration_date = fields.Datetime() @api.depends("move_type") def _compute_verifactu_refund_type(self): @@ -132,12 +135,8 @@ def _get_verifactu_previous_hash(self): def _get_verifactu_registration_date(self): # Date format must be ISO 8601 - """ - TODO - enviamos fecha creación, fecha factura o fecha actual? - """ return ( - pytz.utc.localize(self.create_date) + pytz.utc.localize(self.verifactu_registration_date) .astimezone() .isoformat(timespec="seconds") ) @@ -154,8 +153,8 @@ def _get_verifactu_hash_string(self): serialNumber = self._get_document_serial_number() expeditionDate = self._change_date_format(self._get_document_date()) documentType = self._get_verifactu_document_type() - amountTax = self._get_verifactu_amount_tax() - amountTotal = self._get_verifactu_amount_total() + amountTax = round(self._get_verifactu_amount_tax(), 2) + amountTotal = round(self._get_verifactu_amount_total(), 2) previousHash = self._get_verifactu_previous_hash() registrationDate = self._get_verifactu_registration_date() verifactu_hash_string = ( @@ -391,5 +390,16 @@ def _get_verifactu_qr_values(self): ] ) + def _post(self, soft=True): + verifactu_reg_date = datetime.now() + self.write({"verifactu_registration_date": verifactu_reg_date}) + res = super()._post(soft=soft) + for record in self: + verifactu_hash_values = record._get_verifactu_hash_string() + record.verifactu_hash_string = verifactu_hash_values + hash_string = sha256(verifactu_hash_values.encode("utf-8")) + record.verifactu_hash = hash_string.hexdigest().upper() + return res + def cancel_verifactu(self): raise NotImplementedError diff --git a/l10n_es_aeat_verifactu/models/verifactu_mixin.py b/l10n_es_aeat_verifactu/models/verifactu_mixin.py index 3520c8e5334..823873574e7 100644 --- a/l10n_es_aeat_verifactu/models/verifactu_mixin.py +++ b/l10n_es_aeat_verifactu/models/verifactu_mixin.py @@ -6,7 +6,6 @@ import io import json import logging -from hashlib import sha256 from urllib.parse import urlencode from requests import Session @@ -56,8 +55,8 @@ class VerifactuMixin(models.AbstractModel): string="Enable AEAT", compute="_compute_verifactu_enabled", ) - verifactu_hash_string = fields.Char(compute="_compute_verifactu_hash") - verifactu_hash = fields.Char(compute="_compute_verifactu_hash") + verifactu_hash_string = fields.Char(copy=False) + verifactu_hash = fields.Char(copy=False) verifactu_refund_type = fields.Selection( selection=[ # ('S', 'By substitution'), - en sii no está soportado, aquí igual? @@ -267,17 +266,6 @@ def _change_date_format(self, date): def _get_verifactu_hash_string(self): raise NotImplementedError - def _compute_verifactu_hash(self): - # TODO by the moment those fields are not stored, - # but they must be stored because are unalterable - # when invoice is sent to verifactu, because the hash depends - # on previous sent hash.. - for record in self: - verifactu_hash_values = record._get_verifactu_hash_string() - record.verifactu_hash_string = verifactu_hash_values - hash_string = sha256(verifactu_hash_values.encode("utf-8")) - record.verifactu_hash = hash_string.hexdigest().upper() - def _get_verifactu_document_type(self): raise NotImplementedError() @@ -479,7 +467,7 @@ def _compute_verifactu_registration_key(self): ( "verifactu_tax_key", "=", - "iva", + "01", ), ] verifactu_key_obj = self.env["aeat.verifactu.registration.keys"] diff --git a/l10n_es_aeat_verifactu/views/account_move_view.xml b/l10n_es_aeat_verifactu/views/account_move_view.xml index 410da260ac4..db71b1d66be 100644 --- a/l10n_es_aeat_verifactu/views/account_move_view.xml +++ b/l10n_es_aeat_verifactu/views/account_move_view.xml @@ -36,12 +36,13 @@ > - - + + - - - + + + Date: Fri, 21 Feb 2025 10:29:42 +0100 Subject: [PATCH 08/15] [IMP] l10n_es_aeat_verifactu: Adapt tests to last changes --- ...rifactu_out_invoice_s_iva21s_s_req52_dict.json | 2 +- ...ut_refund_s_iva10b_s_iva10b_s_iva21s_dict.json | 2 +- .../tests/test_10n_es_aeat_verifactu.py | 15 ++++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json b/l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json index 83b9f52390c..869e0d60ba8 100644 --- a/l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json +++ b/l10n_es_aeat_verifactu/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json @@ -3,7 +3,7 @@ "IDVersion": 1.0, "IDFactura": { "IDEmisorFactura": "G87846952", - "NumSerieFactura": "TEST001", + "NumSerieFactura": "TEST003", "FechaExpedicionFactura": "01-01-2024" }, "NombreRazonEmisor": "Spanish test company", diff --git a/l10n_es_aeat_verifactu/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json b/l10n_es_aeat_verifactu/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json index 92ea6ec68e0..33490e0508b 100644 --- a/l10n_es_aeat_verifactu/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json +++ b/l10n_es_aeat_verifactu/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json @@ -3,7 +3,7 @@ "IDVersion": 1.0, "IDFactura": { "IDEmisorFactura": "G87846952", - "NumSerieFactura": "TEST001", + "NumSerieFactura": "TEST002", "FechaExpedicionFactura": "01-01-2024" }, "NombreRazonEmisor": "Spanish test company", diff --git a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py index a0b50a61ee6..b83ffd90125 100644 --- a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py +++ b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py @@ -93,7 +93,7 @@ def test_verifactu_hash_code(self): self.assertEqual(hash_code, expected_hash) def _create_and_test_invoice_verifactu_dict( - self, inv_type, lines, extra_vals, module=None + self, name, inv_type, lines, extra_vals, module=None ): vals = [] tax_names = [] @@ -111,6 +111,7 @@ def _create_and_test_invoice_verifactu_dict( vals.append({"price_unit": line[0], "taxes": taxes}) return self._compare_verifactu_dict( "verifactu_{}_{}_dict.json".format(inv_type, "_".join(tax_names)), + name, inv_type, vals, extra_vals=extra_vals, @@ -118,14 +119,14 @@ def _create_and_test_invoice_verifactu_dict( ) def _compare_verifactu_dict( - self, json_file, inv_type, lines, extra_vals=None, module=None + self, json_file, name, inv_type, lines, extra_vals=None, module=None ): """Helper method for creating an invoice according arguments, and comparing the expected verifactu dict with . """ module = module or "l10n_es_aeat_verifactu" vals = { - "name": "TEST001", + "name": name, "partner_id": self.partner.id, "invoice_date": "2024-01-01", "move_type": inv_type, @@ -149,6 +150,7 @@ def _compare_verifactu_dict( if extra_vals: vals.update(extra_vals) invoice = self.env["account.move"].create(vals) + invoice.action_post() result_dict = invoice._get_verifactu_invoice_dict() result_dict["RegistroAlta"].pop("FechaHoraHusoGenRegistro") result_dict["RegistroAlta"].pop("TipoHuella") @@ -170,6 +172,7 @@ def setUpClass(cls): def test_get_verifactu_invoice_data(self): mapping = [ ( + "TEST001", "out_invoice", [(100, ["s_iva10b"]), (200, ["s_iva21s"])], { @@ -178,6 +181,7 @@ def test_get_verifactu_invoice_data(self): }, ), ( + "TEST002", "out_refund", [(100, ["s_iva10b"]), (100, ["s_iva10b"]), (200, ["s_iva21s"])], { @@ -186,6 +190,7 @@ def test_get_verifactu_invoice_data(self): }, ), ( + "TEST003", "out_invoice", [(200, ["s_iva21s", "s_req52"])], { @@ -194,8 +199,8 @@ def test_get_verifactu_invoice_data(self): }, ), ] - for inv_type, lines, extra_vals in mapping: - self._create_and_test_invoice_verifactu_dict(inv_type, lines, extra_vals) + for name, inv_type, lines, extra_vals in mapping: + self._create_and_test_invoice_verifactu_dict(name, inv_type, lines, extra_vals) return From 1198c01bfabb086202f7e7a8117dc901edcadfd4 Mon Sep 17 00:00:00 2001 From: Aritz Olea Date: Fri, 21 Feb 2025 10:35:55 +0100 Subject: [PATCH 09/15] [FIX] l10n_es_aeat_verifactu: do pre-commit stuff --- l10n_es_aeat_verifactu/README.rst | 1 + .../data/aeat_verifactu_tax_agency_data.xml | 8 +++++-- .../models/account_journal.py | 10 +++++---- l10n_es_aeat_verifactu/models/account_move.py | 3 ++- .../models/verifactu_mixin.py | 8 ++++--- .../static/description/index.html | 12 +++++------ .../tests/test_10n_es_aeat_verifactu.py | 9 +++++--- .../views/account_move_view.xml | 17 +++++++++++---- .../views/report_invoice.xml | 21 +++++++++++++------ 9 files changed, 59 insertions(+), 30 deletions(-) diff --git a/l10n_es_aeat_verifactu/README.rst b/l10n_es_aeat_verifactu/README.rst index f561f953ce5..a2ac5d58fcd 100644 --- a/l10n_es_aeat_verifactu/README.rst +++ b/l10n_es_aeat_verifactu/README.rst @@ -130,6 +130,7 @@ Contributors * Almudena de La Puente * Laura Cazorla * Andreu Orensanz +* Iván Antón Maintainers ~~~~~~~~~~~ diff --git a/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml b/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml index b3ecf8e7b81..c641d5df822 100644 --- a/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml +++ b/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml @@ -9,7 +9,11 @@ https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP - https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR - https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR + https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR + https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR diff --git a/l10n_es_aeat_verifactu/models/account_journal.py b/l10n_es_aeat_verifactu/models/account_journal.py index 69ecf708cd3..0c5ae479fa9 100644 --- a/l10n_es_aeat_verifactu/models/account_journal.py +++ b/l10n_es_aeat_verifactu/models/account_journal.py @@ -37,10 +37,12 @@ def _compute_restrict_mode_hash_table(self): and record.company_id.country_code == "ES" and record.type == "sale" ): - record.write({ - "restrict_mode_hash_table": True, - "restrict_mode_hash_table_readonly": True - }) + record.write( + { + "restrict_mode_hash_table": True, + "restrict_mode_hash_table_readonly": True, + } + ) @api.model def check_hash_modification(self, country_code, journal_type, verifactu_enabled): diff --git a/l10n_es_aeat_verifactu/models/account_move.py b/l10n_es_aeat_verifactu/models/account_move.py index 4bd9273a794..d0f2c6acd24 100644 --- a/l10n_es_aeat_verifactu/models/account_move.py +++ b/l10n_es_aeat_verifactu/models/account_move.py @@ -2,11 +2,12 @@ # Copyright 2024 Aures Tic - Jose Zambudio # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import pytz from collections import OrderedDict from datetime import datetime from hashlib import sha256 +import pytz + from odoo import _, api, fields, models from odoo.exceptions import UserError diff --git a/l10n_es_aeat_verifactu/models/verifactu_mixin.py b/l10n_es_aeat_verifactu/models/verifactu_mixin.py index 823873574e7..6238370e84e 100644 --- a/l10n_es_aeat_verifactu/models/verifactu_mixin.py +++ b/l10n_es_aeat_verifactu/models/verifactu_mixin.py @@ -115,7 +115,7 @@ def _compute_verifactu_macrodata(self): def _compute_verifactu_qr_url(self): """Returns the URL to be used in the QR code. A sample URL would be (urlencoded): https://prewww2.aeat.es/wlpl/TIKECONT/ValidarQR?nif=89890001K&numserie=12345678%26G33&fecha=01-01-2024&importe=241.4 - """ + """ # noqa: B950 for record in self: agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain") if record.company_id.verifactu_test: @@ -129,8 +129,10 @@ def _compute_verifactu_qr_url(self): for value in qr_values.values(): try: str(value).encode("ascii") - except UnicodeEncodeError: - raise UserError(_("QR URL value '{}' is not ASCII").format(value)) + except UnicodeEncodeError as uee: + raise UserError( + _("QR URL value '{}' is not ASCII").format(value) + ) from uee # Build QR URL qr_url = "{}?{}".format( diff --git a/l10n_es_aeat_verifactu/static/description/index.html b/l10n_es_aeat_verifactu/static/description/index.html index d1abdd6c295..e5719c62365 100644 --- a/l10n_es_aeat_verifactu/static/description/index.html +++ b/l10n_es_aeat_verifactu/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { @@ -480,14 +479,13 @@

Contributors

  • Almudena de La Puente <almudena@aurestic.es>
  • Laura Cazorla <laura.cazorla@forgeflow.com>
  • Andreu Orensanz <andreu.orensanz@forgeflow.com>
  • +
  • Iván Antón <ozono@ozonomultimedia.com>
  • Maintainers

    This module is maintained by the OCA.

    - -Odoo Community Association - +Odoo Community Association

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    diff --git a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py index b83ffd90125..dfca7d9b8d5 100644 --- a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py +++ b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py @@ -3,7 +3,7 @@ import json from hashlib import sha256 -from urllib.parse import urlparse, parse_qs +from urllib.parse import parse_qs, urlparse from odoo.modules.module import get_resource_path @@ -200,7 +200,9 @@ def test_get_verifactu_invoice_data(self): ), ] for name, inv_type, lines, extra_vals in mapping: - self._create_and_test_invoice_verifactu_dict(name, inv_type, lines, extra_vals) + self._create_and_test_invoice_verifactu_dict( + name, inv_type, lines, extra_vals + ) return @@ -253,7 +255,8 @@ def test_verifactu_qr_url_format(self): self.assertEqual( actual_params[key][0], str(expected_value), - f"QR URL parameter '{key}' should have value '{expected_value}', got '{actual_params[key][0]}' instead.", + f"QR URL parameter '{key}' should have value '{expected_value}', " + "got '{actual_params[key][0]}' instead.", ) def test_verifactu_qr_code_generation_on_draft(self): diff --git a/l10n_es_aeat_verifactu/views/account_move_view.xml b/l10n_es_aeat_verifactu/views/account_move_view.xml index db71b1d66be..b6ccd11c1d3 100644 --- a/l10n_es_aeat_verifactu/views/account_move_view.xml +++ b/l10n_es_aeat_verifactu/views/account_move_view.xml @@ -36,11 +36,20 @@ > - - + + - + diff --git a/l10n_es_aeat_verifactu/views/report_invoice.xml b/l10n_es_aeat_verifactu/views/report_invoice.xml index 910cd124107..2f4b2d7516a 100644 --- a/l10n_es_aeat_verifactu/views/report_invoice.xml +++ b/l10n_es_aeat_verifactu/views/report_invoice.xml @@ -1,14 +1,23 @@ -