diff --git a/addons/l10n_es_edi_facturae/__init__.py b/addons/l10n_es_edi_facturae/__init__.py new file mode 100644 index 00000000000..cf432c87e49 --- /dev/null +++ b/addons/l10n_es_edi_facturae/__init__.py @@ -0,0 +1,39 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import csv + +from odoo.tools import file_open +from . import models +from . import wizard + +def _edit_tax_types(env, template_data): + """ + Applies all existing tax l10n_es_edi_facturae_tax_type field to their proper value if any link between tax and their template is found + """ + concerned_company_ids = [ + company.id + for company in env.companies + if company.chart_template and company.chart_template.startswith('es_') + ] + if not concerned_company_ids: + return + current_taxes = env['account.tax'].search([('company_id', 'in', concerned_company_ids)]) + if not current_taxes: + return + xmlid2tax = { + xml_id.split('.')[1].split('_', maxsplit=1)[1]: env['account.tax'].browse(record) + for record, xml_id in current_taxes.get_external_id().items() if xml_id + } + for xmlid, values in template_data.items(): + # Only update the tax_type fields + oldtax = xmlid2tax.get(xmlid) + if oldtax and oldtax.l10n_es_edi_facturae_tax_type != values.get('l10n_es_edi_facturae_tax_type'): + oldtax.l10n_es_edi_facturae_tax_type = values.get('l10n_es_edi_facturae_tax_type') + +def _l10n_es_edi_facturae_post_init_hook(env): + """ + We need to replace the existing spanish taxes following the template so the new fields are set properly + """ + if env['account.tax'].search_count([('country_id', '=', env.ref('base.es').id)], limit=1): + with file_open('l10n_es_edi_facturae/data/template/account.tax-es_common.csv') as template_file: + template_data = {record['id']: record['l10n_es_edi_facturae_tax_type'] for record in csv.DictReader(template_file)} + _edit_tax_types(env, template_data) diff --git a/addons/l10n_es_edi_facturae/__manifest__.py b/addons/l10n_es_edi_facturae/__manifest__.py new file mode 100644 index 00000000000..f62efff2920 --- /dev/null +++ b/addons/l10n_es_edi_facturae/__manifest__.py @@ -0,0 +1,42 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Spain - Facturae EDI', + 'version': '1.0', + 'category': 'Accounting/Localizations/EDI', + 'website': 'https://www.facturae.gob.es/face/Paginas/FACE.aspx', + 'description': """ + This module create the Facturae file required to send the invoices information to the General State Administrations. + It allows the export and signature of the signing of Facturae files. + The current version of Facturae supported is the 3.2.1 + + for more informations, see https://www.facturae.gob.es/face/Paginas/FACE.aspx + """, + 'depends': [ + 'l10n_es', + ], + 'data': [ + 'data/uom.uom.csv', + 'data/facturae_templates.xml', + 'data/signature_templates.xml', + + 'security/ir.model.access.csv', + 'security/l10n_es_edi_certificate.xml', + + 'views/l10n_es_edi_facturae_views.xml', + 'views/res_company_views.xml', + 'views/account_tax_views.xml', + 'views/uom_uom_views.xml', + 'views/account_menuitem.xml', + 'views/account_move_send_views.xml', + + 'wizard/account_move_reversal_view.xml', + ], + 'demo': [ + 'demo/l10n_es_edi_facturae_demo.xml', + ], + 'post_init_hook': '_l10n_es_edi_facturae_post_init_hook', + 'installable': True, + 'auto_install': True, + 'license': 'LGPL-3', +} diff --git a/addons/l10n_es_edi_facturae/data/facturae_templates.xml b/addons/l10n_es_edi_facturae/data/facturae_templates.xml new file mode 100644 index 00000000000..86a12aaa1df --- /dev/null +++ b/addons/l10n_es_edi_facturae/data/facturae_templates.xml @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/l10n_es_edi_facturae/data/signature_templates.xml b/addons/l10n_es_edi_facturae/data/signature_templates.xml new file mode 100644 index 00000000000..f6181637a19 --- /dev/null +++ b/addons/l10n_es_edi_facturae/data/signature_templates.xml @@ -0,0 +1,95 @@ + + + + + diff --git a/addons/l10n_es_edi_facturae/data/template/account.tax-es_common.csv b/addons/l10n_es_edi_facturae/data/template/account.tax-es_common.csv new file mode 100644 index 00000000000..2e095424d1c --- /dev/null +++ b/addons/l10n_es_edi_facturae/data/template/account.tax-es_common.csv @@ -0,0 +1,89 @@ +"id","l10n_es_edi_facturae_tax_type" +"l10n_es.account_tax_template_s_iva21b","01" +"l10n_es.account_tax_template_p_iva4_bc","01" +"l10n_es.account_tax_template_p_iva4_ibc","01" +"l10n_es.account_tax_template_p_iva4_sc","01" +"l10n_es.account_tax_template_p_iva4_sp_ex","01" +"l10n_es.account_tax_template_p_iva4_bi","01" +"l10n_es.account_tax_template_p_iva4_ibi","01" +"l10n_es.account_tax_template_p_iva10_bc","01" +"l10n_es.account_tax_template_p_iva10_ibc","01" +"l10n_es.account_tax_template_p_iva10_sc","01" +"l10n_es.account_tax_template_p_iva10_sp_ex","01" +"l10n_es.account_tax_template_p_iva10_bi","01" +"l10n_es.account_tax_template_p_iva10_ibi","01" +"l10n_es.account_tax_template_p_iva21_bc","01" +"l10n_es.account_tax_template_p_iva21_ibc","01" +"l10n_es.account_tax_template_p_iva21_sc","01" +"l10n_es.account_tax_template_p_iva21_sp_ex","01" +"l10n_es.account_tax_template_p_iva21_bi","01" +"l10n_es.account_tax_template_p_iva21_ibi","01" +"l10n_es.account_tax_template_s_iva4b","01" +"l10n_es.account_tax_template_s_iva0_e","01" +"l10n_es.account_tax_template_s_iva4s","01" +"l10n_es.account_tax_template_s_iva_e","01" +"l10n_es.account_tax_template_s_iva10b","01" +"l10n_es.account_tax_template_s_iva10s","01" +"l10n_es.account_tax_template_s_iva21s","01" +"l10n_es.account_tax_template_s_iva21isp","01" +"l10n_es.account_tax_template_p_iva4_ic_bc","01" +"l10n_es.account_tax_template_p_iva4_sp_in","01" +"l10n_es.account_tax_template_p_iva4_ic_bi","01" +"l10n_es.account_tax_template_p_iva10_ic_bc","01" +"l10n_es.account_tax_template_p_iva10_sp_in","01" +"l10n_es.account_tax_template_p_iva10_ic_bi","01" +"l10n_es.account_tax_template_p_iva21_ic_bc","01" +"l10n_es.account_tax_template_p_iva21_sp_in","01" +"l10n_es.account_tax_template_p_iva21_ic_bi","01" +"l10n_es.account_tax_template_s_iva0_ic","01" +"l10n_es.account_tax_template_s_iva0_sp_i","01" +"l10n_es.account_tax_template_p_iva0_ns_b","01" +"l10n_es.account_tax_template_p_iva0_ns","01" +"l10n_es.account_tax_template_s_iva_ns_b","01" +"l10n_es.account_tax_template_s_iva_ns","01" +"l10n_es.account_tax_template_s_req05","05" +"l10n_es.account_tax_template_s_req014","05" +"l10n_es.account_tax_template_s_req52","05" +"l10n_es.account_tax_template_p_req05","05" +"l10n_es.account_tax_template_p_req014","05" +"l10n_es.account_tax_template_p_req52","05" +"l10n_es.account_tax_template_s_iva0_isp","01" +"l10n_es.account_tax_template_s_irpf19a","04" +"l10n_es.account_tax_template_p_irpf19a","04" +"l10n_es.account_tax_template_s_iva0","01" +"l10n_es.account_tax_template_p_iva0_bc","01" +"l10n_es.account_tax_template_s_irpf195a","04" +"l10n_es.account_tax_template_p_irpf195a","04" +"l10n_es.account_tax_template_s_irpf20a","04" +"l10n_es.account_tax_template_p_irpf20a","04" +"l10n_es.account_tax_template_s_irpf21a","04" +"l10n_es.account_tax_template_p_irpf21a","04" +"l10n_es.account_tax_template_s_irpf21","04" +"l10n_es.account_tax_template_s_iva0_ns","01" +"l10n_es.account_tax_template_p_irpf21p","04" +"l10n_es.account_tax_template_s_irpf20","04" +"l10n_es.account_tax_template_p_irpf20","04" +"l10n_es.account_tax_template_s_irpf24","04" +"l10n_es.account_tax_template_p_irpf24","04" +"l10n_es.account_tax_template_s_irpf15","04" +"l10n_es.account_tax_template_p_irpf15","04" +"l10n_es.account_tax_template_s_irpf18","04" +"l10n_es.account_tax_template_p_irpf18","04" +"l10n_es.account_tax_template_s_irpf19","04" +"l10n_es.account_tax_template_p_irpf19","04" +"l10n_es.account_tax_template_s_irpf9","04" +"l10n_es.account_tax_template_p_irpf9","04" +"l10n_es.account_tax_template_s_irpf7","04" +"l10n_es.account_tax_template_p_irpf7","04" +"l10n_es.account_tax_template_s_irpf2","04" +"l10n_es.account_tax_template_p_irpf2","04" +"l10n_es.account_tax_template_s_irpf1","04" +"l10n_es.account_tax_template_p_irpf1","04" +"l10n_es.account_tax_template_p_iva4_isp","01" +"l10n_es.account_tax_template_p_iva4_isp_bi","01" +"l10n_es.account_tax_template_p_iva10_isp","01" +"l10n_es.account_tax_template_p_iva10_isp_bi","01" +"l10n_es.account_tax_template_p_iva21_isp","01" +"l10n_es.account_tax_template_p_iva21_isp_bi","01" +"l10n_es.account_tax_template_p_iva12_agr","01" +"l10n_es.account_tax_template_p_iva105_gan","01" diff --git a/addons/l10n_es_edi_facturae/data/uom.uom.csv b/addons/l10n_es_edi_facturae/data/uom.uom.csv new file mode 100644 index 00000000000..c0472627175 --- /dev/null +++ b/addons/l10n_es_edi_facturae/data/uom.uom.csv @@ -0,0 +1,14 @@ +"id","l10n_es_edi_facturae_uom_code" +uom.product_uom_unit,01 +uom.product_uom_dozen,18 +uom.product_uom_hour,02 +uom.product_uom_meter,25 +uom.product_uom_millimeter,26 +uom.product_uom_km,22 +uom.product_uom_cm,16 +uom.uom_square_meter,05 +uom.product_uom_litre,04 +uom.product_uom_cubic_meter,33 +uom.product_uom_kgm,03 +uom.product_uom_gram,21 +uom.product_uom_ton,05 diff --git a/addons/l10n_es_edi_facturae/demo/certificate_demo.pfx b/addons/l10n_es_edi_facturae/demo/certificate_demo.pfx new file mode 100644 index 00000000000..45265fe7130 Binary files /dev/null and b/addons/l10n_es_edi_facturae/demo/certificate_demo.pfx differ diff --git a/addons/l10n_es_edi_facturae/demo/l10n_es_edi_facturae_demo.xml b/addons/l10n_es_edi_facturae/demo/l10n_es_edi_facturae_demo.xml new file mode 100644 index 00000000000..fc1058a580a --- /dev/null +++ b/addons/l10n_es_edi_facturae/demo/l10n_es_edi_facturae_demo.xml @@ -0,0 +1,10 @@ + + + + + test + + + + + diff --git a/addons/l10n_es_edi_facturae/i18n/l10n_es_edi_facturae.pot b/addons/l10n_es_edi_facturae/i18n/l10n_es_edi_facturae.pot new file mode 100644 index 00000000000..18c8db4384b --- /dev/null +++ b/addons/l10n_es_edi_facturae/i18n/l10n_es_edi_facturae.pot @@ -0,0 +1,620 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_es_edi_facturae +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.3alpha1+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-12 10:00+0000\n" +"PO-Revision-Date: 2023-05-12 10:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.corrective_type +msgid "01" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.account_invoice_facturae_export +msgid "3.2.1" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__27 +msgid "6-Packs" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model,name:l10n_es_edi_facturae.model_account_move_reversal +msgid "Account Move Reversal" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model,name:l10n_es_edi_facturae.model_account_move_send +msgid "Account Move Send" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__10 +msgid "Bags" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__08 +msgid "Barrels" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__17 +msgid "Bins" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__12 +msgid "Bottles, non-protected, cylindrical" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__06 +msgid "Boxes" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__24 +msgid "Bunches" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__80 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__80 +msgid "Calculation of output quotas" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__81 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__81 +msgid "Calculation of withholding taxes" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__13 +msgid "Canisters" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__23 +msgid "Cans, rectangular" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__11 +msgid "Carboys, non-protected" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__19 +msgid "Cases" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__15 +msgid "Centiliters" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__16 +msgid "Centimeters" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.ui.menu,name:l10n_es_edi_facturae.menu_l10n_es_edi_facturae_root_certificates +msgid "Certificates" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.actions.act_window,name:l10n_es_edi_facturae.l10n_es_edi_facturae_certificate_action +msgid "Certificates for Facturae EDI invoices on Spain" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model,name:l10n_es_edi_facturae.model_res_company +msgid "Companies" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__company_id +msgid "Company" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model,name:l10n_es_edi_facturae.model_res_partner +msgid "Contact" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.actions.act_window,help:l10n_es_edi_facturae.l10n_es_edi_facturae_certificate_action +msgid "Create the first certificate" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__create_uid +msgid "Created by" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__create_date +msgid "Created on" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__33 +msgid "Cubic meter" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__date_end +msgid "Date End" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__date_start +msgid "Date Start" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__03 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__03 +msgid "Date of issue" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__13 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__13 +msgid "Date/Period to apply" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__20 +msgid "Demijohns, non-protected" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__display_name +msgid "Display Name" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__18 +msgid "Dozens" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__31 +msgid "Envelopes" +msgstr "" + +#. module: l10n_es_edi_facturae +#. odoo-python +#: code:addons/l10n_es_edi_facturae/models/account_move_send.py:0 +#, python-format +msgid "Errors occured while creating the EDI document (format: %s):" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_bank_statement_line__l10n_es_edi_facturae_xml_id +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_move__l10n_es_edi_facturae_xml_id +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_payment__l10n_es_edi_facturae_xml_id +msgid "Facturae Attachment" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model,name:l10n_es_edi_facturae.model_l10n_es_edi_facturae_certificate +msgid "Facturae Digital Certificate" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_res_company__l10n_es_edi_facturae_residence_type +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_res_partner__l10n_es_edi_facturae_residence_type +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_res_users__l10n_es_edi_facturae_residence_type +msgid "Facturae EDI Residency Type Code" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_res_company__l10n_es_edi_facturae_certificate_id +msgid "Facturae EDI signing certificate" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_bank_statement_line__l10n_es_edi_facturae_xml_file +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_move__l10n_es_edi_facturae_xml_file +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_payment__l10n_es_edi_facturae_xml_file +msgid "Facturae File" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.view_company_form_inherit_l10n_es_edi_facturae +msgid "Facturae signature certificate" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_move_send__l10n_es_edi_facturae_checkbox_xml +msgid "Generate Facturae edi file" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__21 +msgid "Grams" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__02 +msgid "Hours" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__id +msgid "ID" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__01 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__01 +msgid "Invoice number" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__02 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__02 +msgid "Invoice series" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__14 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__14 +msgid "Invoice type" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__08 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__08 +msgid "Issuer/Oblige Address" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__09 +msgid "Jerricans, cylindrical" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model,name:l10n_es_edi_facturae.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__03 +msgid "Kilograms" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__22 +msgid "Kilometers" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_move_send__l10n_es_edi_facturae_enable_xml +msgid "L10N Es Edi Facturae Enable Xml" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__write_date +msgid "Last Updated on" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__04 +msgid "Liters" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__25 +msgid "Meters" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__26 +msgid "Millimeters" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__04 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__04 +msgid "Name and surname/company name - Issuer" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__05 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__05 +msgid "Name and surname/company name - Recipient" +msgstr "" + +#. module: l10n_es_edi_facturae +#. odoo-python +#: code:addons/l10n_es_edi_facturae/models/account_move.py:0 +#, python-format +msgid "" +"No valid certificate found for this company, Facturae EDI file will not be " +"signed.\n" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__05 +msgid "Other" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__content +#: model:ir.model.fields,help:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__content +msgid "PFX Certificate" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__28 +msgid "Packages" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,help:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__password +msgid "Passphrase for the PFX certificate" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__password +msgid "Password" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__29 +msgid "Portions" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model,name:l10n_es_edi_facturae.model_uom_uom +msgid "Product Unit of Measure" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.view_account_move_reversal_inherit_l10n_es_edi_facturae +msgid "Reason" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__09 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__09 +msgid "Receiving Address" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.corrective_type +msgid "Rectificación modelo íntegro." +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__30 +msgid "Rolls" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__34 +msgid "Second" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__serial_number +msgid "Serial Number" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.ui.menu,name:l10n_es_edi_facturae.menu_l10n_es_edi_facturae_root +msgid "Spain Facturae EDI" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_uom_uom__l10n_es_edi_facturae_uom_code +msgid "Spanish EDI Units" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_bank_statement_line__l10n_es_edi_facturae_reason_code +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_move__l10n_es_edi_facturae_reason_code +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_move_reversal__l10n_es_edi_facturae_reason_code +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_payment__l10n_es_edi_facturae_reason_code +msgid "Spanish Facturae EDI Reason Code" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,field_description:l10n_es_edi_facturae.field_account_tax__l10n_es_edi_facturae_tax_type +msgid "Spanish Facturae EDI Tax Type" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.product_uom_categ_form_view_inherit_l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.product_uom_form_view_inherit_l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.product_uom_tree_view_inherit_l10n_es_edi_facturae +msgid "Spanish Facturae EDI type" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.view_tax_tree_inherit_l10n_es_edi_facturae +msgid "Spanish Tax Type" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__15 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__15 +msgid "Statutory letters" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model,name:l10n_es_edi_facturae.model_account_tax +msgid "Tax" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__06 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__06 +msgid "Tax identification Issuer/Oblige" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__07 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__07 +msgid "Tax identification Receiver" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__11 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__12 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__11 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__12 +msgid "Tax rate to be applied" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__16 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__16 +msgid "Taxable amount" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__82 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__82 +msgid "Taxable amount modified by return of containers/packaging" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__83 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__83 +msgid "Taxable income modified by discounts and allowances" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__84 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__84 +msgid "Taxable income modified by final, judicial or administrative ruling" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__85 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__85 +msgid "" +"Taxable income modified by unpaid tax assessments. Order of declaration of " +"bankruptcy" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__14 +msgid "Tetra Briks" +msgstr "" + +#. module: l10n_es_edi_facturae +#. odoo-python +#: code:addons/l10n_es_edi_facturae/models/l10n_es_edi_facturae_certificate.py:0 +#, python-format +msgid "The certificate is expired since %s" +msgstr "" + +#. module: l10n_es_edi_facturae +#. odoo-python +#: code:addons/l10n_es_edi_facturae/models/account_move.py:0 +#, python-format +msgid "The company needs a set tax identification number or VAT number" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,help:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__date_end +msgid "The date on which the certificate expires" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,help:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__date_start +msgid "The date on which the certificate starts to be valid" +msgstr "" + +#. module: l10n_es_edi_facturae +#. odoo-python +#: code:addons/l10n_es_edi_facturae/models/account_move.py:0 +#, python-format +msgid "The partner needs a set tax identification number or VAT number" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields,help:l10n_es_edi_facturae.field_l10n_es_edi_facturae_certificate__serial_number +msgid "The serial number to add to electronic documents" +msgstr "" + +#. module: l10n_es_edi_facturae +#. odoo-python +#: code:addons/l10n_es_edi_facturae/models/l10n_es_edi_facturae_certificate.py:0 +#, python-format +msgid "" +"There has been a problem with the certificate, some usual problems can be:\n" +"\t- The password given or the certificate are not valid.\n" +"\t- The certificate content is invalid." +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move__l10n_es_edi_facturae_reason_code__10 +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__account_move_reversal__l10n_es_edi_facturae_reason_code__10 +msgid "Transaction Details" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__07 +msgid "Trays, one layer no cover, plastic" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__32 +msgid "Tubs" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__01 +msgid "Units" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.l10n_es_edi_facturae_certificate_form +msgid "Validity" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model:ir.model.fields.selection,name:l10n_es_edi_facturae.selection__uom_uom__l10n_es_edi_facturae_uom_code__35 +msgid "Watt" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.template_xades_signature +msgid "not(ancestor-or-self::ds:Signature)" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.template_xades_signature +msgid "text/xml" +msgstr "" + +#. module: l10n_es_edi_facturae +#: model_terms:ir.ui.view,arch_db:l10n_es_edi_facturae.template_xades_signature +msgid "urn:oid:1.2.840.10003.5.109.10" +msgstr "" \ No newline at end of file diff --git a/addons/l10n_es_edi_facturae/models/__init__.py b/addons/l10n_es_edi_facturae/models/__init__.py new file mode 100644 index 00000000000..c69dff49fda --- /dev/null +++ b/addons/l10n_es_edi_facturae/models/__init__.py @@ -0,0 +1,7 @@ +from . import account_tax +from . import l10n_es_edi_facturae_certificate +from . import res_company +from . import res_partner +from . import uom_uom +from . import account_move +from . import account_move_send diff --git a/addons/l10n_es_edi_facturae/models/account_move.py b/addons/l10n_es_edi_facturae/models/account_move.py new file mode 100644 index 00000000000..7c96949d4b5 --- /dev/null +++ b/addons/l10n_es_edi_facturae/models/account_move.py @@ -0,0 +1,362 @@ +from collections import defaultdict + +from odoo import fields, models, api, _ +from odoo.exceptions import UserError +from odoo.tools import float_repr, date_utils +from odoo.tools.xml_utils import cleanup_xml_node + +PHONE_CLEAN_TABLE = str.maketrans({" ": None, "-": None, "(": None, ")": None, "+": None}) +COUNTRY_CODE_MAP = { + "BD": "BGD", "BE": "BEL", "BF": "BFA", "BG": "BGR", "BA": "BIH", "BB": "BRB", "WF": "WLF", "BL": "BLM", "BM": "BMU", + "BN": "BRN", "BO": "BOL", "BH": "BHR", "BI": "BDI", "BJ": "BEN", "BT": "BTN", "JM": "JAM", "BV": "BVT", "BW": "BWA", + "WS": "WSM", "BQ": "BES", "BR": "BRA", "BS": "BHS", "JE": "JEY", "BY": "BLR", "BZ": "BLZ", "RU": "RUS", "RW": "RWA", + "RS": "SRB", "TL": "TLS", "RE": "REU", "TM": "TKM", "TJ": "TJK", "RO": "ROU", "TK": "TKL", "GW": "GNB", "GU": "GUM", + "GT": "GTM", "GS": "SGS", "GR": "GRC", "GQ": "GNQ", "GP": "GLP", "JP": "JPN", "GY": "GUY", "GG": "GGY", "GF": "GUF", + "GE": "GEO", "GD": "GRD", "GB": "GBR", "GA": "GAB", "SV": "SLV", "GN": "GIN", "GM": "GMB", "GL": "GRL", "GI": "GIB", + "GH": "GHA", "OM": "OMN", "TN": "TUN", "JO": "JOR", "HR": "HRV", "HT": "HTI", "HU": "HUN", "HK": "HKG", "HN": "HND", + "HM": "HMD", "VE": "VEN", "PR": "PRI", "PS": "PSE", "PW": "PLW", "PT": "PRT", "SJ": "SJM", "PY": "PRY", "IQ": "IRQ", + "PA": "PAN", "PF": "PYF", "PG": "PNG", "PE": "PER", "PK": "PAK", "PH": "PHL", "PN": "PCN", "PL": "POL", "PM": "SPM", + "ZM": "ZMB", "EH": "ESH", "EE": "EST", "EG": "EGY", "ZA": "ZAF", "EC": "ECU", "IT": "ITA", "VN": "VNM", "SB": "SLB", + "ET": "ETH", "SO": "SOM", "ZW": "ZWE", "SA": "SAU", "ES": "ESP", "ER": "ERI", "ME": "MNE", "MD": "MDA", "MG": "MDG", + "MF": "MAF", "MA": "MAR", "MC": "MCO", "UZ": "UZB", "MM": "MMR", "ML": "MLI", "MO": "MAC", "MN": "MNG", "MH": "MHL", + "MK": "MKD", "MU": "MUS", "MT": "MLT", "MW": "MWI", "MV": "MDV", "MQ": "MTQ", "MP": "MNP", "MS": "MSR", "MR": "MRT", + "IM": "IMN", "UG": "UGA", "TZ": "TZA", "MY": "MYS", "MX": "MEX", "IL": "ISR", "FR": "FRA", "IO": "IOT", "SH": "SHN", + "FI": "FIN", "FJ": "FJI", "FK": "FLK", "FM": "FSM", "FO": "FRO", "NI": "NIC", "NL": "NLD", "NO": "NOR", "NA": "NAM", + "VU": "VUT", "NC": "NCL", "NE": "NER", "NF": "NFK", "NG": "NGA", "NZ": "NZL", "NP": "NPL", "NR": "NRU", "NU": "NIU", + "CK": "COK", "XK": "XKX", "CI": "CIV", "CH": "CHE", "CO": "COL", "CN": "CHN", "CM": "CMR", "CL": "CHL", "CC": "CCK", + "CA": "CAN", "CG": "COG", "CF": "CAF", "CD": "COD", "CZ": "CZE", "CY": "CYP", "CX": "CXR", "CR": "CRI", "CW": "CUW", + "CV": "CPV", "CU": "CUB", "SZ": "SWZ", "SY": "SYR", "SX": "SXM", "KG": "KGZ", "KE": "KEN", "SS": "SSD", "SR": "SUR", + "KI": "KIR", "KH": "KHM", "KN": "KNA", "KM": "COM", "ST": "STP", "SK": "SVK", "KR": "KOR", "SI": "SVN", "KP": "PRK", + "KW": "KWT", "SN": "SEN", "SM": "SMR", "SL": "SLE", "SC": "SYC", "KZ": "KAZ", "KY": "CYM", "SG": "SGP", "SE": "SWE", + "SD": "SDN", "DO": "DOM", "DM": "DMA", "DJ": "DJI", "DK": "DNK", "VG": "VGB", "DE": "DEU", "YE": "YEM", "DZ": "DZA", + "US": "USA", "UY": "URY", "YT": "MYT", "UM": "UMI", "LB": "LBN", "LC": "LCA", "LA": "LAO", "TV": "TUV", "TW": "TWN", + "TT": "TTO", "TR": "TUR", "LK": "LKA", "LI": "LIE", "LV": "LVA", "TO": "TON", "LT": "LTU", "LU": "LUX", "LR": "LBR", + "LS": "LSO", "TH": "THA", "TF": "ATF", "TG": "TGO", "TD": "TCD", "TC": "TCA", "LY": "LBY", "VA": "VAT", "VC": "VCT", + "AE": "ARE", "AD": "AND", "AG": "ATG", "AF": "AFG", "AI": "AIA", "VI": "VIR", "IS": "ISL", "IR": "IRN", "AM": "ARM", + "AL": "ALB", "AO": "AGO", "AQ": "ATA", "AS": "ASM", "AR": "ARG", "AU": "AUS", "AT": "AUT", "AW": "ABW", "IN": "IND", + "AX": "ALA", "AZ": "AZE", "IE": "IRL", "ID": "IDN", "UA": "UKR", "QA": "QAT", "MZ": "MOZ" +} +REVERSED_COUNTRY_CODE = {v: k for k, v in COUNTRY_CODE_MAP.items()} + +class AccountMove(models.Model): + _inherit = 'account.move' + + l10n_es_edi_facturae_xml_id = fields.Many2one( + comodel_name='ir.attachment', + string="Facturae Attachment", + compute=lambda self: self._compute_linked_attachment_id('l10n_es_edi_facturae_xml_id', 'l10n_es_edi_facturae_xml_file'), + depends=['l10n_es_edi_facturae_xml_file'] + ) + l10n_es_edi_facturae_xml_file = fields.Binary( + attachment=True, + string="Facturae File", + copy=False, + ) + l10n_es_edi_facturae_reason_code = fields.Selection( + selection=[ + ('01', 'Invoice number'), + ('02', 'Invoice series'), + ('03', 'Date of issue'), + ('04', 'Name and surname/company name - Issuer'), + ('05', 'Name and surname/company name - Recipient'), + ('06', 'Tax identification Issuer/Oblige'), + ('07', 'Tax identification Receiver'), + ('08', 'Issuer/Oblige Address'), + ('09', 'Receiving Address'), + ('10', 'Transaction Details'), + ('11', 'Tax rate to be applied'), + ('12', 'Tax rate to be applied'), + ('13', 'Date/Period to apply'), + ('14', 'Invoice type'), + ('15', 'Statutory letters'), + ('16', 'Taxable amount'), + ('80', 'Calculation of output quotas'), + ('81', 'Calculation of withholding taxes'), + ('82', 'Taxable amount modified by return of containers/packaging'), + ('83', 'Taxable income modified by discounts and allowances'), + ('84', 'Taxable income modified by final, judicial or administrative ruling'), + ('85', 'Taxable income modified by unpaid tax assessments. Order of declaration of bankruptcy'), + ], string='Spanish Facturae EDI Reason Code', default='10') + + def _l10n_es_edi_facturae_get_default_enable(self): + self.ensure_one() + return self.company_id.country_code == 'ES' and self.company_id.currency_id.name == 'EUR' + + def _l10n_es_edi_facturae_get_filename(self): + self.ensure_one() + return f'{self.name.replace("/", "_")}_facturae_signed.xml' + + def _l10n_es_edi_facturae_get_tax_period(self): + self.ensure_one() + if self.env['res.company'].fields_get(['account_tax_periodicity']): + period_start, period_end = self.company_id._get_tax_closing_period_boundaries(self.date) + else: + period_start = date_utils.start_of(self.date, 'month') + period_end = date_utils.end_of(self.date, 'month') + + return {'start':period_start, 'end':period_end} + + def _l10n_es_edi_facturae_get_refunded_invoices(self): + self.env['account.partial.reconcile'].flush_model() + invoices_refunded_mapping = {invoice.id: invoice.reversed_entry_id for invoice in self} + + queries = [] + for source_field, counterpart_field in (('debit', 'credit'), ('credit', 'debit')): + queries.append(f''' + SELECT + source_line.move_id AS source_move_id, + counterpart_line.move_id AS counterpart_move_id + FROM account_partial_reconcile part + JOIN account_move_line source_line ON source_line.id = part.{source_field}_move_id + JOIN account_move_line counterpart_line ON counterpart_line.id = part.{counterpart_field}_move_id + WHERE source_line.move_id IN %s AND counterpart_line.move_id != source_line.move_id + GROUP BY source_move_id, counterpart_move_id + ''') + self._cr.execute(' UNION ALL '.join(queries), [tuple(self.ids)] * 2) + payment_data = defaultdict(lambda: []) + for row in self._cr.dictfetchall(): + payment_data[row['source_move_id']].append(row) + + for invoice in self: + if not invoice.move_type.endswith('refund'): + # We only want to map refunds + continue + + for move_id in (record_dict['counterpart_move_id'] for record_dict in payment_data.get(invoice.id, [])): + invoices_refunded_mapping[invoice.id] = move_id + return invoices_refunded_mapping + + def _l10n_es_edi_facturae_get_corrective_data(self): + self.ensure_one() + if self.move_type.endswith('refund'): + refunded_invoice = self.env['account.move'].browse(self._l10n_es_edi_facturae_get_refunded_invoices()[self.id]) + tax_period = refunded_invoice._l10n_es_edi_facturae_get_tax_period() + + reason_code = self.l10n_es_edi_facturae_reason_code or '10' + reason_description = [label for code, label in self._fields['l10n_es_edi_facturae_reason_code'].selection + if code == reason_code][0] + return { + 'refunded_invoice_record': refunded_invoice, + 'ReasonCode': reason_code, + 'Reason': reason_description, + 'TaxPeriod': { + 'StartDate': tax_period.get('start'), + 'EndDate': tax_period.get('end'), + } + } + return {} + + @api.model + def _l10n_es_edi_facturae_convert_computed_tax_to_template(self, computed_tax_dict): + """ Helper to convert the tax dict from a _compute_taxes() into a dict usable in the template """ + tax = self.env["account.tax"].browse(computed_tax_dict["tax_id"]) + return { + "tax_record": tax, + "TaxRate": f'{abs(tax.amount):.3f}', + "TaxableBase": { + 'TotalAmount': computed_tax_dict["base_amount_currency"], + 'EquivalentInEuros': computed_tax_dict["base_amount"], + }, + "TaxAmount": { + "TotalAmount": abs(computed_tax_dict["tax_amount_currency"]), + "EquivalentInEuros": abs(computed_tax_dict["tax_amount"]), + }, + } + + def _l10n_es_edi_facturae_inv_lines_to_items(self, conversion_rate=None): + """ + Convert the invoice lines to a list of items required for the Facturae xml generation + + :param float conversion_rate: Conversion rate of the invoice, if needed + :return: A tuple containing the Face items, the taxes and the invoice totals data. + """ + self.ensure_one() + items = [] + totals = { + 'total_gross_amount': 0., + 'total_general_discounts': 0., + 'total_general_surcharges': 0., + 'total_taxes_withheld': 0., + 'total_tax_outputs': 0., + 'total_payments_on_account': 0., + 'amounts_withheld': 0., + } + taxes = [] + taxes_withheld = [] + for line in self.invoice_line_ids: + if line.display_type in {'line_section', 'line_note'}: + continue + invoice_line_values = {} + + price_before_discount = line.currency_id.round(line.price_subtotal / (1 - line.discount / 100.0)) + discount = max(0., (price_before_discount - line.price_subtotal)) + surcharge = abs(min(0., (price_before_discount - line.price_subtotal))) + totals['total_gross_amount'] += line.price_subtotal + totals['total_general_discounts'] += discount + totals['total_general_surcharges'] += surcharge + base_line = self.env['account.tax']._convert_to_tax_base_line_dict( + line, partner=line.partner_id, currency=line.currency_id, product=line.product_id, taxes=line.tax_ids, + price_unit=line.price_unit, quantity=line.quantity, discount=line.discount, account=line.account_id, + price_subtotal=line.price_subtotal, is_refund=line.is_refund, rate=conversion_rate + ) + + taxes_computed = self.env['account.tax']._compute_taxes([base_line]) + taxes_withheld_computed = [tax for tax in taxes_computed["tax_lines_to_add"] if tax["tax_amount"] < 0] + taxes_normal_computed = [tax for tax in taxes_computed["tax_lines_to_add"] if tax["tax_amount"] >= 0] + + taxes_output = [self._l10n_es_edi_facturae_convert_computed_tax_to_template(tax) for tax in taxes_normal_computed] + totals['total_tax_outputs'] += sum((abs(tax["tax_amount"]) for tax in taxes_normal_computed)) + + tax_withheld_output = [self._l10n_es_edi_facturae_convert_computed_tax_to_template(tax) for tax in taxes_withheld_computed] + totals['total_taxes_withheld'] += sum((abs(tax["tax_amount"]) for tax in taxes_withheld_computed)) + + invoice_line_values.update({ + 'ItemDescription': line.name, + 'Quantity': line.quantity, + 'UnitOfMeasure': line.product_uom_id.l10n_es_edi_facturae_uom_code, + 'UnitPriceWithoutTax': line.currency_id.round(price_before_discount / line.quantity if line.quantity else 0.), + 'TotalCost': price_before_discount, + 'DiscountsAndRebates': [{ + 'DiscountReason': '', + 'DiscountRate': f'{line.discount:.2f}', + 'DiscountAmount': discount + }, ] if discount != 0. else [], + 'Charges': [{ + 'ChargeReason': '', + 'ChargeRate': f'{max(0, -line.discount):.2f}', + 'ChargeAmount': surcharge, + }, ] if surcharge != 0. else [], + 'GrossAmount': line.price_subtotal, + 'TaxesOutputs': taxes_output, + 'TaxesWithheld': tax_withheld_output, + }) + items.append(invoice_line_values) + taxes += taxes_output + taxes_withheld += tax_withheld_output + return items, taxes, taxes_withheld, totals + + def _l10n_es_edi_facturae_export_facturae(self): + """ + Produce the Facturae XML data for the invoice. + + :return: (data needed to render the full template, data needed to render the signature template) + """ + self.ensure_one() + company = self.company_id + partner = self.commercial_partner_id + + if not company.vat: + raise UserError(_('The company needs a set tax identification number or VAT number')) + if not partner.vat: + raise UserError(_('The partner needs a set tax identification number or VAT number')) + if self.move_type == "entry": + return False + + eur_curr = self.env['res.currency'].search([('name', '=', 'EUR')]) + inv_curr = self.currency_id + legal_literals = self.narration.striptags() if self.narration else False + legal_literals = legal_literals.split(";") if legal_literals else False + + partner_name = {'firstname': 'UNKNOWN', 'surname': 'UNKNOWN', 'surname2': ''} + if not partner.is_company: + name_split = [part for part in partner.name.replace(', ', ' ').split(' ') if part] + if len(name_split) > 2: + partner_name['firstname'] = ' '.join(name_split[:-2]) + partner_name['surname'], partner_name['surname2'] = name_split[-2:] + elif len(name_split) == 2: + partner_name['firstname'] = ' '.join(name_split[:-1]) + partner_name['surname'] = name_split[-1] + + invoice_issuer_signature_type = 'supplier' if self.move_type == 'out_invoice' else 'customer' + need_conv = bool(inv_curr != eur_curr) + conversion_rate = abs(self.amount_total_in_currency_signed / self.amount_total_signed) if self.amount_total_signed else 0. + total_outst_am_in_currency = abs(self.amount_total_in_currency_signed) + total_outst_am = abs(self.amount_total_signed) + total_exec_am_in_currency = abs(self.amount_total_in_currency_signed) + total_exec_am = abs(self.amount_total_signed) + items, taxes, taxes_withheld, totals = self._l10n_es_edi_facturae_inv_lines_to_items(conversion_rate) + template_values = { + 'self_party': company.partner_id, + 'self_party_country_code': COUNTRY_CODE_MAP[company.country_id.code], + 'other_party': partner, + 'other_party_country_code': COUNTRY_CODE_MAP[partner.country_id.code], + 'other_party_phone': partner.phone.translate(PHONE_CLEAN_TABLE) if partner.phone else False, + 'other_party_name': partner_name, + 'is_outstanding': self.move_type.startswith('out_'), + 'float_repr': float_repr, + 'file_currency': inv_curr, + 'eur': eur_curr, + 'conversion_needed': need_conv, + 'refund_multiplier': -1 if self.move_type.endswith('refund') else 1, + + 'Modality': 'I', + 'BatchIdentifier': self.name, + 'InvoicesCount': 1, + 'TotalInvoicesAmount': { + 'TotalAmount': abs(self.amount_total_in_currency_signed), + 'EquivalentInEuros': abs(self.amount_total_signed), + }, + 'TotalOutstandingAmount': { + 'TotalAmount': abs(total_outst_am_in_currency), + 'EquivalentInEuros': abs(total_outst_am), + }, + 'TotalExecutableAmount': { + 'TotalAmount': total_exec_am_in_currency, + 'EquivalentInEuros': total_exec_am, + }, + 'InvoiceCurrencyCode': inv_curr.name, + 'Invoices': [{ + 'invoice_record': self, + 'invoice_currency': inv_curr, + 'InvoiceDocumentType': 'FC', + 'InvoiceClass': 'OO', + 'Corrective': self._l10n_es_edi_facturae_get_corrective_data(), + 'InvoiceIssueData': { + 'ExchangeRateDetails': need_conv, + 'ExchangeRate': f"{round(conversion_rate, 4):.4f}", + 'LanguageName': self._context.get('lang', 'en_US').split('_')[0], + }, + 'TaxOutputs': taxes, + 'TaxesWithheld': taxes_withheld, + 'TotalGrossAmount': totals['total_gross_amount'], + 'TotalGeneralDiscounts': totals['total_general_discounts'], + 'TotalGeneralSurcharges': totals['total_general_surcharges'], + 'TotalGrossAmountBeforeTaxes': totals['total_gross_amount'] - totals['total_general_discounts'] + totals['total_general_surcharges'], + 'TotalTaxOutputs': totals['total_tax_outputs'], + 'TotalTaxesWithheld': totals['total_taxes_withheld'], + 'PaymentsOnAccount': [], + 'TotalOutstandingAmount': total_outst_am_in_currency, + 'InvoiceTotal': abs(self.amount_total_in_currency_signed), + 'TotalPaymentsOnAccount': totals['total_payments_on_account'], + 'AmountsWithheld': { + 'WithholdingReason': '', + 'WithholdingRate': False, + 'WithholdingAmount': totals['amounts_withheld'], + } if totals['amounts_withheld'] else False, + 'TotalExecutableAmount': total_exec_am_in_currency, + 'Items': items, + 'PaymentDetails': [], + 'LegalLiterals': legal_literals, + }], + } + signature_values = {'SigningTime': '', 'SignerRole': invoice_issuer_signature_type, } + return template_values, signature_values + + def _l10n_es_edi_facturae_render_facturae(self): + """ + Produce the Facturae XML file for the invoice. + + :return: rendered xml file string. + :rtype: str + """ + self.ensure_one() + company = self.company_id + template_values, signature_values = self._l10n_es_edi_facturae_export_facturae() + xml_content = cleanup_xml_node(self.env['ir.qweb']._render('l10n_es_edi_facturae.account_invoice_facturae_export', template_values)) + certificate = self.env['l10n_es_edi_facturae.certificate'].search([("company_id", '=', company.id)], limit=1) + + try: + xml_content = certificate._sign_xml(xml_content, signature_values) + except ValueError: + raise UserError(_('No valid certificate found for this company, Facturae EDI file will not be signed.\n')) + return xml_content diff --git a/addons/l10n_es_edi_facturae/models/account_move_send.py b/addons/l10n_es_edi_facturae/models/account_move_send.py new file mode 100644 index 00000000000..bf2fc8b86bd --- /dev/null +++ b/addons/l10n_es_edi_facturae/models/account_move_send.py @@ -0,0 +1,86 @@ +from odoo import _, api, fields, models + +class AccountMoveSend(models.Model): + _inherit = 'account.move.send' + + l10n_es_edi_facturae_enable_xml = fields.Boolean(compute='_compute_send_mail_extra_fields') + l10n_es_edi_facturae_checkbox_xml = fields.Boolean( + string="Generate Facturae edi file", + default=True, + ) + + # ------------------------------------------------------------------------- + # COMPUTE METHODS + # ------------------------------------------------------------------------- + + def _compute_send_mail_extra_fields(self): + # EXTENDS 'account' + super()._compute_send_mail_extra_fields() + for wizard in self: + wizard.l10n_es_edi_facturae_enable_xml = any(move._l10n_es_edi_facturae_get_default_enable() for move in wizard.move_ids) + + @api.depends('l10n_es_edi_facturae_checkbox_xml') + def _compute_mail_attachments_widget(self): + # EXTENDS 'account' - add depends + super()._compute_mail_attachments_widget() + + # ------------------------------------------------------------------------- + # ATTACHMENTS + # ------------------------------------------------------------------------- + + @api.model + def _get_linked_attachments(self, move): + # EXTENDS 'account' + return super()._get_linked_attachments(move) + move.l10n_es_edi_facturae_xml_id + + def _get_placeholder_mail_attachments_data(self, move): + # EXTENDS 'account' + results = super()._get_placeholder_mail_attachments_data(move) + + if self.l10n_es_edi_facturae_enable_xml and self.l10n_es_edi_facturae_checkbox_xml: + filename = f'{move.name.replace("/", "_")}_facturae_signed.xml' + results.append({ + 'id': f'placeholder_{filename}', + 'name': filename, + 'mimetype': 'application/xml', + 'placeholder': True, + }) + + return results + + # ------------------------------------------------------------------------- + # BUSINESS ACTIONS + # ------------------------------------------------------------------------- + + def _prepare_document(self, invoice): + # EXTENDS 'account' + results = super()._prepare_document(invoice) + + if self.l10n_es_edi_facturae_checkbox_xml and invoice._l10n_es_edi_facturae_get_default_enable(): + try: + xml_content = invoice._l10n_es_edi_facturae_render_facturae(invoice) + except Exception as error: + return { + 'error': "".join((_("Errors occured while creating the EDI document (format: %s):", "Facturae"), + "\n", str(error))), + } + + results['l10n_es_edi_facturae_attachment_values'] = { + 'name': invoice._l10n_es_edi_facturae_get_filename(), + 'raw': xml_content, + 'mimetype': 'application/xml', + 'res_model': invoice._name, + 'res_id': invoice.id, + 'res_field': 'l10n_es_edi_facturae_xml_file', # Binary field + } + + return results + + def _link_document(self, invoice, prepared_data): + # EXTENDS 'account' + super()._link_document(invoice, prepared_data) + + attachment_vals = prepared_data.get('l10n_es_edi_facturae_attachment_values') + if attachment_vals: + self.env['ir.attachment'].create(attachment_vals) + invoice.invalidate_model(fnames=['l10n_es_edi_facturae_xml_id', 'l10n_es_edi_facturae_xml_file']) diff --git a/addons/l10n_es_edi_facturae/models/account_tax.py b/addons/l10n_es_edi_facturae/models/account_tax.py new file mode 100644 index 00000000000..2a4a1b23308 --- /dev/null +++ b/addons/l10n_es_edi_facturae/models/account_tax.py @@ -0,0 +1,37 @@ +from odoo import fields, models + + +class AccountTax(models.Model): + _inherit = 'account.tax' + + l10n_es_edi_facturae_tax_type = fields.Selection(( + ('01', 'Value-Added Tax'), + ('02', 'Taxes on production, services and imports in Ceuta and Melilla'), + ('03', 'IGIC: Canaries General Indirect Tax'), + ('04', 'IRPF: Personal Income Tax'), + ('05', 'Other'), + ('06', 'ITPAJD: Tax on wealth transfers and stamp duty'), + ('07', 'IE: Excise duties and consumption taxes'), + ('08', 'RA: Customs duties'), + ('09', 'IGTECM: Sales tax in Ceuta and Melilla'), + ('10', 'IECDPCAC: Excise duties on oil derivates in Canaries'), + ('11', 'IIIMAB: Tax on premises that affect the environment in the Balearic Islands'), + ('12', 'ICIO: Tax on construction, installation and works'), + ('13', 'IMVDN: Local tax on unoccupied homes in Navarre'), + ('14', 'IMSN: Local tax on building plots in Navarre'), + ('15', 'IMGSN: Local sumptuary tax in Navarre'), + ('16', 'IMPN: Local tax on advertising in Navarre'), + ('17', 'REIVA: Special VAT for travel agencies'), + ('18', 'REIGIC: Special IGIC: for travel agencies'), + ('19', 'REIPSI: Special IPSI for travel agencies'), + ('20', 'IPS: Insurance premiums Tax'), + ('21', 'SWUA: Surcharge for Winding Up Activity'), + ('22', 'IVPEE: Tax on the value of electricity generation'), + ('23', 'Tax on the production of spent nuclear fuel and radioactive waste from the generation of nuclear electric power'), + ('24', 'Tax on the storage of spent nuclear energy and radioactive waste in centralised facilities'), + ('25', 'IDEC: Tax on bank deposits'), + ('26', 'Excise duty applied to manufactured tobacco in Canaries'), + ('27', 'IGFEI: Tax on Fluorinated Greenhouse Gases'), + ('28', 'IRNR: Non-resident Income Tax'), + ('29', 'Corporation Tax'), + ), string='Spanish Facturae EDI Tax Type', default='01') diff --git a/addons/l10n_es_edi_facturae/models/l10n_es_edi_facturae_certificate.py b/addons/l10n_es_edi_facturae/models/l10n_es_edi_facturae_certificate.py new file mode 100644 index 00000000000..7f665573473 --- /dev/null +++ b/addons/l10n_es_edi_facturae/models/l10n_es_edi_facturae_certificate.py @@ -0,0 +1,109 @@ +import re +from base64 import b64decode, b64encode, encodebytes +from copy import deepcopy +from hashlib import sha1 + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.serialization import pkcs12 +from lxml import etree + +from odoo import _, api, fields, models +from odoo.addons.l10n_es_edi_facturae import xml_utils +from odoo.exceptions import UserError +from odoo.tools import cleanup_xml_node + +class Certificate(models.Model): + _name = 'l10n_es_edi_facturae.certificate' + _description = 'Facturae Digital Certificate' + _order = 'date_start desc, id desc' + _rec_name = 'serial_number' + + content = fields.Binary(string="PFX Certificate", required=True, help="PFX Certificate") + password = fields.Char(help="Passphrase for the PFX certificate") + serial_number = fields.Char(readonly=True, index=True, help="The serial number to add to electronic documents") + date_start = fields.Datetime(readonly=True, help="The date on which the certificate starts to be valid") + date_end = fields.Datetime(readonly=True, help="The date on which the certificate expires") + company_id = fields.Many2one(comodel_name='res.company', default=lambda self: self.env.company, required=True, readonly=True) + + def _decode_certificate(self): + """ + Return certificate data + + :return tuple: private_key, certificate + """ + self.ensure_one() + content, password = b64decode(self.with_context(bin_size=False).content), self.password.encode() + private_key, certificate, *_dummy = pkcs12.load_key_and_certificates(content, password, backend=default_backend()) + return private_key, certificate + + # ------------------------------------------------------------------------- + # LOW-LEVEL METHODS + # ------------------------------------------------------------------------- + + @api.model_create_multi + def create(self, vals_list): + certificates = super().create(vals_list) + for certificate in certificates: + try: + _key, certif = certificate._decode_certificate() + except ValueError: + raise UserError(_('There has been a problem with the certificate, some usual problems can be:\n' + '\t- The password given or the certificate are not valid.\n' + '\t- The certificate content is invalid.')) + if fields.datetime.now() > certif.not_valid_after: + raise UserError(_('The certificate is expired since %s', certif.not_valid_after)) + # Assign extracted values from the certificate + certificate.write({'serial_number': certif.serial_number, 'date_start': certif.not_valid_before, 'date_end': certif.not_valid_after}) + return certificates + + # ------------------------------------------------------------------------- + # BUSINESS METHODS # + # ------------------------------------------------------------------------- + def _sign_xml(self, edi_data, signature_data): + """ + Signs the given XML data with the certificate and private key. + + :param etree._Element edi_data: The XML data to sign. + :param dict signature_data: The signature data to use. + :return: The signed XML data string. + :rtype: str + """ + self.ensure_one() + if not (self.date_start < fields.Datetime.now() < self.date_end): + raise UserError('Facturae certificate date is not valid, its validity has probably expired') + root = deepcopy(edi_data) + cert_private, cert_public = self._decode_certificate() + public_key_numbers = cert_public.public_key().public_numbers() + rdns_pattern = re.compile(r'(?<=\()([A-Z]{1,2}=.*)(?=\))') + issuer = ', '.join(sorted(match.group() for match in (rdns_pattern.search(str(element)) for element in cert_public.issuer.rdns) if match)) + + # Identifiers + document_id = f"Document-{sha1(etree.tostring(edi_data)).hexdigest()}" + signature_id = f"Signature-{document_id}" + keyinfo_id = f"KeyInfo-{document_id}" + sigproperties_id = f"SignatureProperties-{document_id}" + + signature_data.update({ + 'document_id': document_id, + 'x509_certificate': encodebytes(cert_public.public_bytes(encoding=serialization.Encoding.DER)).decode(), + 'public_modulus': encodebytes(xml_utils._int_to_bytes(public_key_numbers.n)).decode(), + 'public_exponent': encodebytes(xml_utils._int_to_bytes(public_key_numbers.e)).decode(), + 'iso_now': fields.datetime.now().isoformat(), + 'keyinfo_id': keyinfo_id, + 'signature_id': signature_id, + 'sigproperties_id': sigproperties_id, + 'reference_uri': "Reference-" + document_id, + 'sigpolicy_url': "http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf", + 'sigpolicy_description': "Política de firma electrónica para facturación electrónica con formato Facturae", + 'sigcertif_digest': b64encode(cert_public.fingerprint(hashes.SHA256())).decode(), + 'x509_issuer_description': issuer, + 'x509_serial_number': cert_public.serial_number, + }) + signature = self.env['ir.qweb']._render('l10n_es_edi_facturae.template_xades_signature', signature_data) + signature = cleanup_xml_node(signature, remove_blank_nodes=False) + root.append(signature) + xml_utils._reference_digests(signature.find("ds:SignedInfo", namespaces=xml_utils.NS_MAP)) + xml_utils._fill_signature(signature, cert_private) + + return etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True) diff --git a/addons/l10n_es_edi_facturae/models/res_company.py b/addons/l10n_es_edi_facturae/models/res_company.py new file mode 100644 index 00000000000..27b2b6c40f4 --- /dev/null +++ b/addons/l10n_es_edi_facturae/models/res_company.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class Company(models.Model): + _inherit = 'res.company' + + l10n_es_edi_facturae_residence_type = fields.Char(string='Facturae EDI Residency Type Code', related='partner_id.l10n_es_edi_facturae_residence_type') + l10n_es_edi_facturae_certificate_id = fields.One2many(string='Facturae EDI signing certificate', + comodel_name='l10n_es_edi_facturae.certificate', inverse_name='company_id') diff --git a/addons/l10n_es_edi_facturae/models/res_partner.py b/addons/l10n_es_edi_facturae/models/res_partner.py new file mode 100644 index 00000000000..471fcce8be5 --- /dev/null +++ b/addons/l10n_es_edi_facturae/models/res_partner.py @@ -0,0 +1,20 @@ +from odoo import api, fields, models + + +class Partner(models.Model): + _inherit = 'res.partner' + + l10n_es_edi_facturae_residence_type = fields.Char(string='Facturae EDI Residency Type Code', + compute='_compute_l10n_es_edi_facturae_residence_type', store=False, readonly=True,) + + @api.depends('country_id') + def _compute_l10n_es_edi_facturae_residence_type(self): + eu_country_ids = self.env.ref('base.europe').country_ids.ids + for partner in self: + country = partner.country_id + if country.code == 'ES': + partner.l10n_es_edi_facturae_residence_type = 'R' + elif country.id in eu_country_ids: + partner.l10n_es_edi_facturae_residence_type = 'U' + else: + partner.l10n_es_edi_facturae_residence_type = 'E' diff --git a/addons/l10n_es_edi_facturae/models/uom_uom.py b/addons/l10n_es_edi_facturae/models/uom_uom.py new file mode 100644 index 00000000000..22b7f0b6821 --- /dev/null +++ b/addons/l10n_es_edi_facturae/models/uom_uom.py @@ -0,0 +1,44 @@ +from odoo import fields, models + + +class UoM(models.Model): + _inherit = 'uom.uom' + + l10n_es_edi_facturae_uom_code = fields.Selection( + selection=[ + ('01', 'Units'), + ('02', 'Hours'), + ('03', 'Kilograms'), + ('04', 'Liters'), + ('05', 'Other'), + ('06', 'Boxes'), + ('07', 'Trays, one layer no cover, plastic'), + ('08', 'Barrels'), + ('09', 'Jerricans, cylindrical'), + ('10', 'Bags'), + ('11', 'Carboys, non-protected'), + ('12', 'Bottles, non-protected, cylindrical'), + ('13', 'Canisters'), + ('14', 'Tetra Briks'), + ('15', 'Centiliters'), + ('16', 'Centimeters'), + ('17', 'Bins'), + ('18', 'Dozens'), + ('19', 'Cases'), + ('20', 'Demijohns, non-protected'), + ('21', 'Grams'), + ('22', 'Kilometers'), + ('23', 'Cans, rectangular'), + ('24', 'Bunches'), + ('25', 'Meters'), + ('26', 'Millimeters'), + ('27', '6-Packs'), + ('28', 'Packages'), + ('29', 'Portions'), + ('30', 'Rolls'), + ('31', 'Envelopes'), + ('32', 'Tubs'), + ('33', 'Cubic meter'), + ('34', 'Second'), + ('35', 'Watt'), + ], string='Spanish EDI Units', default="05", required=True) diff --git a/addons/l10n_es_edi_facturae/security/ir.model.access.csv b/addons/l10n_es_edi_facturae/security/ir.model.access.csv new file mode 100644 index 00000000000..43975f5496d --- /dev/null +++ b/addons/l10n_es_edi_facturae/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +l10n_es_edi_facturae.access_l10n_es_edi_facturae_certificate_manager,access_l10n_es_edi_facturae_certificate,l10n_es_edi_facturae.model_l10n_es_edi_facturae_certificate,account.group_account_manager,1,0,0,0 +l10n_es_edi_facturae.access_l10n_es_edi_facturae_certificate_system,access_l10n_es_edi_facturae_certificate,l10n_es_edi_facturae.model_l10n_es_edi_facturae_certificate,base.group_system,1,1,1,1 diff --git a/addons/l10n_es_edi_facturae/security/l10n_es_edi_certificate.xml b/addons/l10n_es_edi_facturae/security/l10n_es_edi_certificate.xml new file mode 100644 index 00000000000..7f8d43a7e8f --- /dev/null +++ b/addons/l10n_es_edi_facturae/security/l10n_es_edi_certificate.xml @@ -0,0 +1,10 @@ + + + + + Facturae Digital Certificate + + + [('company_id', 'in', user.company_ids.ids)] + + diff --git a/addons/l10n_es_edi_facturae/tests/__init__.py b/addons/l10n_es_edi_facturae/tests/__init__.py new file mode 100644 index 00000000000..d3a8ed6ec33 --- /dev/null +++ b/addons/l10n_es_edi_facturae/tests/__init__.py @@ -0,0 +1 @@ +from . import test_edi_xml diff --git a/addons/l10n_es_edi_facturae/tests/data/certificate_test.pfx b/addons/l10n_es_edi_facturae/tests/data/certificate_test.pfx new file mode 100644 index 00000000000..4c50110c2fd Binary files /dev/null and b/addons/l10n_es_edi_facturae/tests/data/certificate_test.pfx differ diff --git a/addons/l10n_es_edi_facturae/tests/data/expected_in_invoice_document.xml b/addons/l10n_es_edi_facturae/tests/data/expected_in_invoice_document.xml new file mode 100644 index 00000000000..00f358df4bc --- /dev/null +++ b/addons/l10n_es_edi_facturae/tests/data/expected_in_invoice_document.xml @@ -0,0 +1,358 @@ + + + + 3.2.1 + I + RE + + BILL/2023/01/0001 + 1 + + 2904.00 + + + 2904.00 + + + 2904.00 + + EUR + + + + + + F + U + BE0477472701 + + + UNKNOWN + UNKNOWN + +
Rue de Bruxelles, 15000
+ 5000 Namur + Belgium + BEL +
+
+
+ + + J + R + ES59962470K + + + company_1_data + company_1_data + +
C. de Embajadores, 68-116
+ 12345 + Madrid + Madrid + ESP +
+
+
+
+ + + + BILL/2023/01/0001 + FC + OO + + + 2023-01-01 + EUR + EUR + en + + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + 01 + 21.000 + + 200.00 + + + 42.00 + + + + 01 + 21.000 + + 900.00 + + + 189.00 + + + + 01 + 21.000 + + 1100.00 + + + 231.00 + + + + + 2400.00 + 100.00 + 100.00 + 2400.00 + 504.00 + 0.00 + 2904.00 + 2904.00 + 2904.00 + + + + product_a + 1.0 + 01 + 100.00 + 100.00 + 100.00 + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + + + product_a + 1.0 + 01 + 100.00 + 100.00 + 100.00 + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + + + product_a + 1.0 + 01 + 200.00 + 200.00 + 200.00 + + + 01 + 21.000 + + 200.00 + + + 42.00 + + + + + + product_a + 1.0 + 01 + 1000.00 + 1000.00 + + + 10.00 + 100.00 + + + 900.00 + + + 01 + 21.000 + + 900.00 + + + 189.00 + + + + + + product_a + 1.0 + 01 + 1000.00 + 1000.00 + + + 10.00 + 100.00 + + + 1100.00 + + + 01 + 21.000 + + 1100.00 + + + 231.00 + + + + + + + + + + + + + + + + + not(ancestor-or-self::ds:Signature) + + + + ___ignore___ + + + + UQH7ea0mtLoqLN/aM5mWssQ38fOK1bxnWT4fKQBYiSI= + + + + wwDtGZ3EpRH8tRKQ8lKgbpfDkE1Zmza9QHkjJDN1Nmw= + + + ___ignore___ + + + MIID8jCCAtqgAwIBAgIUYBl+61DsTkNyyUFGveskbaGP/SkwDQYJKoZIhvcNAQELBQAwgYoxCzAJ +BgNVBAYTAkJFMRIwEAYDVQQIDAlXYWxsb25uaWExEjAQBgNVBAcMCVJhbWlsbGllczENMAsGA1UE +CgwET2RvbzEMMAoGA1UECwwDUiZEMRgwFgYDVQQDDA9ydW5ib3Qub2Rvby5jb20xHDAaBgkqhkiG +9w0BCQEWDWluZm9Ab2Rvby5jb20wHhcNMjIwNDI2MDczMTUwWhcNMjMwNDI2MDczMTUwWjCBlTEL +MAkGA1UEBhMCQkUxEjAQBgNVBAgMCVdhbGxvbm5pYTESMBAGA1UEBwwJUmFtaWxsaWVzMRIwEAYD +VQQKDAlPZG9vIFMuQS4xGTAXBgNVBAsMEFImRCAtIEFjY291bnRpbmcxETAPBgNVBAMMCG9kb28u +Y29tMRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9kb28uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAtGUbyNJrA3yyOC01gIMR1A1rWIHCHD+afLCLCddBDZwC4Q034yFBuMqQFNWaxxLc +9s+wFbNZqu5D7rynN3qXvWJj40VZxygoV/09y71IhaspuAZ0rdzYsqjDmTJiPSrEl6iydF2eq6sJ +4J0ItCUFc2e88B25wz7esslAN249C20oQj5ZiGifR4P0CRQSmDy0SPYZGxTurVwtq6IsYbOqEw8/ +Wqx6ApOltperFX88DZCdbY59ZedqQrGQh2Z5Au7h/8nhmdbpz7Ixw2zocxVigR0ClNDpKKxvolX1 +virQpICkfWcAaWAYAMCL4KhvISclysD+5juDLpGCLHPtKxBXTQIDAQABo0MwQTAfBgNVHSMEGDAW +gBTmxEG4Ub7b5wRXqWea94Vs1hB13zAJBgNVHRMEAjAAMBMGA1UdEQQMMAqCCG9kb28uY29tMA0G +CSqGSIb3DQEBCwUAA4IBAQAFLvTlSELTVWRfIlGthoFOmkv8MrwJuo8y7qTmvsFXeA2hZtsPdNxx +iwAGPobiUO0GCEPUFKZ7NksQa7DAKGXfb5RSwfujDfEMAhgi00+wF1VIKeKqnGt4idzCr+2NtiQY +e9oK+Ee7o82llnNFwbWvRKx55lhHW3bwN6Tl31qUYbxacJyA9z5sQpe2zImWMVsm4g7OAjzg3Cn2 +0TFu+0XekB892n7DYh4gHiMWaMOVYmi/aEWLSqxMlbiHQr75FmPftozFvTxW26jU3ALhq+yXPVa4 +lg4r/h6HH5TbtSjYzTSJ4MvMyrECgIQYf0zsS4D6K/GvCZbu7LKHRwuC0fis + + + + tGUbyNJrA3yyOC01gIMR1A1rWIHCHD+afLCLCddBDZwC4Q034yFBuMqQFNWaxxLc9s+wFbNZqu5D +7rynN3qXvWJj40VZxygoV/09y71IhaspuAZ0rdzYsqjDmTJiPSrEl6iydF2eq6sJ4J0ItCUFc2e8 +8B25wz7esslAN249C20oQj5ZiGifR4P0CRQSmDy0SPYZGxTurVwtq6IsYbOqEw8/Wqx6ApOltper +FX88DZCdbY59ZedqQrGQh2Z5Au7h/8nhmdbpz7Ixw2zocxVigR0ClNDpKKxvolX1virQpICkfWcA +aWAYAMCL4KhvISclysD+5juDLpGCLHPtKxBXTQ== + AQAB + + + + + + + + 2023-01-01T00:00:00 + + + + + FHf2lN+5FALNa7JXV2+pByL92/dr8LeCVhpItYic5oA= + + + C=BE, CN=runbot.odoo.com, L=Ramillies, O=Odoo, OU=R&D, ST=Wallonnia + 548631688851000697209704649636588277530075594025 + + + + + + + http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf + Política de firma electrónica para facturación electrónica con formato Facturae + + + + a/vDoueJq9zxWGCFwdsXdlXtkFSEek4XVtmlQCEDaE8= + + + + http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf + + + + + + + + + + urn:oid:1.2.840.10003.5.109.10 + + + text/xml + + + + + + + +
diff --git a/addons/l10n_es_edi_facturae/tests/data/expected_refund_document.xml b/addons/l10n_es_edi_facturae/tests/data/expected_refund_document.xml new file mode 100644 index 00000000000..0ce979bf3c3 --- /dev/null +++ b/addons/l10n_es_edi_facturae/tests/data/expected_refund_document.xml @@ -0,0 +1,265 @@ + + + + 3.2.1 + I + EM + + RINV/2023/00001 + 1 + + -242.00 + + + -242.00 + + + -242.00 + + EUR + + + + + + J + R + ES59962470K + + + company_1_data + company_1_data + +
C. de Embajadores, 68-116
+ 12345 + Madrid + Madrid + ESP +
+
+
+ + + F + U + BE0477472701 + + + UNKNOWN + UNKNOWN + +
Rue de Bruxelles, 15000
+ 5000 Namur + Belgium + BEL +
+
+
+
+ + + + RINV/2023/00001 + FC + OO + + INV/2023/00001 + 01 + Invoice number + + 2023-01-01 + 2023-01-31 + + 01 + Rectificación modelo íntegro. + + + + 2023-01-01 + EUR + EUR + en + + + + 01 + 21.000 + + -100.00 + + + -21.00 + + + + 01 + 21.000 + + -100.00 + + + -21.00 + + + + + -200.00 + -200.00 + -42.00 + -0.00 + -242.00 + -242.00 + -242.00 + + + + product_a + 1.0 + 01 + -100.00 + -100.00 + -100.00 + + + 01 + 21.000 + + -100.00 + + + -21.00 + + + + + + product_a + 1.0 + 01 + -100.00 + -100.00 + -100.00 + + + 01 + 21.000 + + -100.00 + + + -21.00 + + + + + + + + + + + + + + + + + not(ancestor-or-self::ds:Signature) + + + + ___ignore___ + + + + UQH7ea0mtLoqLN/aM5mWssQ38fOK1bxnWT4fKQBYiSI= + + + + wwDtGZ3EpRH8tRKQ8lKgbpfDkE1Zmza9QHkjJDN1Nmw= + + + ___ignore___ + + + MIID8jCCAtqgAwIBAgIUYBl+61DsTkNyyUFGveskbaGP/SkwDQYJKoZIhvcNAQELBQAwgYoxCzAJ +BgNVBAYTAkJFMRIwEAYDVQQIDAlXYWxsb25uaWExEjAQBgNVBAcMCVJhbWlsbGllczENMAsGA1UE +CgwET2RvbzEMMAoGA1UECwwDUiZEMRgwFgYDVQQDDA9ydW5ib3Qub2Rvby5jb20xHDAaBgkqhkiG +9w0BCQEWDWluZm9Ab2Rvby5jb20wHhcNMjIwNDI2MDczMTUwWhcNMjMwNDI2MDczMTUwWjCBlTEL +MAkGA1UEBhMCQkUxEjAQBgNVBAgMCVdhbGxvbm5pYTESMBAGA1UEBwwJUmFtaWxsaWVzMRIwEAYD +VQQKDAlPZG9vIFMuQS4xGTAXBgNVBAsMEFImRCAtIEFjY291bnRpbmcxETAPBgNVBAMMCG9kb28u +Y29tMRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9kb28uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAtGUbyNJrA3yyOC01gIMR1A1rWIHCHD+afLCLCddBDZwC4Q034yFBuMqQFNWaxxLc +9s+wFbNZqu5D7rynN3qXvWJj40VZxygoV/09y71IhaspuAZ0rdzYsqjDmTJiPSrEl6iydF2eq6sJ +4J0ItCUFc2e88B25wz7esslAN249C20oQj5ZiGifR4P0CRQSmDy0SPYZGxTurVwtq6IsYbOqEw8/ +Wqx6ApOltperFX88DZCdbY59ZedqQrGQh2Z5Au7h/8nhmdbpz7Ixw2zocxVigR0ClNDpKKxvolX1 +virQpICkfWcAaWAYAMCL4KhvISclysD+5juDLpGCLHPtKxBXTQIDAQABo0MwQTAfBgNVHSMEGDAW +gBTmxEG4Ub7b5wRXqWea94Vs1hB13zAJBgNVHRMEAjAAMBMGA1UdEQQMMAqCCG9kb28uY29tMA0G +CSqGSIb3DQEBCwUAA4IBAQAFLvTlSELTVWRfIlGthoFOmkv8MrwJuo8y7qTmvsFXeA2hZtsPdNxx +iwAGPobiUO0GCEPUFKZ7NksQa7DAKGXfb5RSwfujDfEMAhgi00+wF1VIKeKqnGt4idzCr+2NtiQY +e9oK+Ee7o82llnNFwbWvRKx55lhHW3bwN6Tl31qUYbxacJyA9z5sQpe2zImWMVsm4g7OAjzg3Cn2 +0TFu+0XekB892n7DYh4gHiMWaMOVYmi/aEWLSqxMlbiHQr75FmPftozFvTxW26jU3ALhq+yXPVa4 +lg4r/h6HH5TbtSjYzTSJ4MvMyrECgIQYf0zsS4D6K/GvCZbu7LKHRwuC0fis + + + + tGUbyNJrA3yyOC01gIMR1A1rWIHCHD+afLCLCddBDZwC4Q034yFBuMqQFNWaxxLc9s+wFbNZqu5D +7rynN3qXvWJj40VZxygoV/09y71IhaspuAZ0rdzYsqjDmTJiPSrEl6iydF2eq6sJ4J0ItCUFc2e8 +8B25wz7esslAN249C20oQj5ZiGifR4P0CRQSmDy0SPYZGxTurVwtq6IsYbOqEw8/Wqx6ApOltper +FX88DZCdbY59ZedqQrGQh2Z5Au7h/8nhmdbpz7Ixw2zocxVigR0ClNDpKKxvolX1virQpICkfWcA +aWAYAMCL4KhvISclysD+5juDLpGCLHPtKxBXTQ== + AQAB + + + + + + + + 2023-01-01T00:00:00 + + + + + FHf2lN+5FALNa7JXV2+pByL92/dr8LeCVhpItYic5oA= + + + C=BE, CN=runbot.odoo.com, L=Ramillies, O=Odoo, OU=R&D, ST=Wallonnia + 548631688851000697209704649636588277530075594025 + + + + + + + http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf + Política de firma electrónica para facturación electrónica con formato Facturae + + + + a/vDoueJq9zxWGCFwdsXdlXtkFSEek4XVtmlQCEDaE8= + + + + http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf + + + + + + + + + + urn:oid:1.2.840.10003.5.109.10 + + + text/xml + + + + + + + +
diff --git a/addons/l10n_es_edi_facturae/tests/data/expected_signed_document.xml b/addons/l10n_es_edi_facturae/tests/data/expected_signed_document.xml new file mode 100644 index 00000000000..1388ca20a93 --- /dev/null +++ b/addons/l10n_es_edi_facturae/tests/data/expected_signed_document.xml @@ -0,0 +1,358 @@ + + + + 3.2.1 + I + EM + + INV/2023/00001 + 1 + + 2904.00 + + + 2904.00 + + + 2904.00 + + EUR + + + + + + J + R + ES59962470K + + + company_1_data + company_1_data + +
C. de Embajadores, 68-116
+ 12345 + Madrid + Madrid + ESP +
+
+
+ + + F + U + BE0477472701 + + + UNKNOWN + UNKNOWN + +
Rue de Bruxelles, 15000
+ 5000 Namur + Belgium + BEL +
+
+
+
+ + + + INV/2023/00001 + FC + OO + + + 2023-01-01 + EUR + EUR + en + + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + 01 + 21.000 + + 200.00 + + + 42.00 + + + + 01 + 21.000 + + 900.00 + + + 189.00 + + + + 01 + 21.000 + + 1100.00 + + + 231.00 + + + + + 2400.00 + 100.00 + 100.00 + 2400.00 + 504.00 + 0.00 + 2904.00 + 2904.00 + 2904.00 + + + + product_a + 1.0 + 01 + 100.00 + 100.00 + 100.00 + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + + + product_a + 1.0 + 01 + 100.00 + 100.00 + 100.00 + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + + + product_a + 1.0 + 01 + 200.00 + 200.00 + 200.00 + + + 01 + 21.000 + + 200.00 + + + 42.00 + + + + + + product_a + 1.0 + 01 + 1000.00 + 1000.00 + + + 10.00 + 100.00 + + + 900.00 + + + 01 + 21.000 + + 900.00 + + + 189.00 + + + + + + product_a + 1.0 + 01 + 1000.00 + 1000.00 + + + 10.00 + 100.00 + + + 1100.00 + + + 01 + 21.000 + + 1100.00 + + + 231.00 + + + + + + + + + + + + + + + + + not(ancestor-or-self::ds:Signature) + + + + ___ignore___ + + + + UQH7ea0mtLoqLN/aM5mWssQ38fOK1bxnWT4fKQBYiSI= + + + + wwDtGZ3EpRH8tRKQ8lKgbpfDkE1Zmza9QHkjJDN1Nmw= + + + ___ignore___ + + + MIID8jCCAtqgAwIBAgIUYBl+61DsTkNyyUFGveskbaGP/SkwDQYJKoZIhvcNAQELBQAwgYoxCzAJ +BgNVBAYTAkJFMRIwEAYDVQQIDAlXYWxsb25uaWExEjAQBgNVBAcMCVJhbWlsbGllczENMAsGA1UE +CgwET2RvbzEMMAoGA1UECwwDUiZEMRgwFgYDVQQDDA9ydW5ib3Qub2Rvby5jb20xHDAaBgkqhkiG +9w0BCQEWDWluZm9Ab2Rvby5jb20wHhcNMjIwNDI2MDczMTUwWhcNMjMwNDI2MDczMTUwWjCBlTEL +MAkGA1UEBhMCQkUxEjAQBgNVBAgMCVdhbGxvbm5pYTESMBAGA1UEBwwJUmFtaWxsaWVzMRIwEAYD +VQQKDAlPZG9vIFMuQS4xGTAXBgNVBAsMEFImRCAtIEFjY291bnRpbmcxETAPBgNVBAMMCG9kb28u +Y29tMRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9kb28uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAtGUbyNJrA3yyOC01gIMR1A1rWIHCHD+afLCLCddBDZwC4Q034yFBuMqQFNWaxxLc +9s+wFbNZqu5D7rynN3qXvWJj40VZxygoV/09y71IhaspuAZ0rdzYsqjDmTJiPSrEl6iydF2eq6sJ +4J0ItCUFc2e88B25wz7esslAN249C20oQj5ZiGifR4P0CRQSmDy0SPYZGxTurVwtq6IsYbOqEw8/ +Wqx6ApOltperFX88DZCdbY59ZedqQrGQh2Z5Au7h/8nhmdbpz7Ixw2zocxVigR0ClNDpKKxvolX1 +virQpICkfWcAaWAYAMCL4KhvISclysD+5juDLpGCLHPtKxBXTQIDAQABo0MwQTAfBgNVHSMEGDAW +gBTmxEG4Ub7b5wRXqWea94Vs1hB13zAJBgNVHRMEAjAAMBMGA1UdEQQMMAqCCG9kb28uY29tMA0G +CSqGSIb3DQEBCwUAA4IBAQAFLvTlSELTVWRfIlGthoFOmkv8MrwJuo8y7qTmvsFXeA2hZtsPdNxx +iwAGPobiUO0GCEPUFKZ7NksQa7DAKGXfb5RSwfujDfEMAhgi00+wF1VIKeKqnGt4idzCr+2NtiQY +e9oK+Ee7o82llnNFwbWvRKx55lhHW3bwN6Tl31qUYbxacJyA9z5sQpe2zImWMVsm4g7OAjzg3Cn2 +0TFu+0XekB892n7DYh4gHiMWaMOVYmi/aEWLSqxMlbiHQr75FmPftozFvTxW26jU3ALhq+yXPVa4 +lg4r/h6HH5TbtSjYzTSJ4MvMyrECgIQYf0zsS4D6K/GvCZbu7LKHRwuC0fis + + + + tGUbyNJrA3yyOC01gIMR1A1rWIHCHD+afLCLCddBDZwC4Q034yFBuMqQFNWaxxLc9s+wFbNZqu5D +7rynN3qXvWJj40VZxygoV/09y71IhaspuAZ0rdzYsqjDmTJiPSrEl6iydF2eq6sJ4J0ItCUFc2e8 +8B25wz7esslAN249C20oQj5ZiGifR4P0CRQSmDy0SPYZGxTurVwtq6IsYbOqEw8/Wqx6ApOltper +FX88DZCdbY59ZedqQrGQh2Z5Au7h/8nhmdbpz7Ixw2zocxVigR0ClNDpKKxvolX1virQpICkfWcA +aWAYAMCL4KhvISclysD+5juDLpGCLHPtKxBXTQ== + AQAB + + + + + + + + 2023-01-01T00:00:00 + + + + + FHf2lN+5FALNa7JXV2+pByL92/dr8LeCVhpItYic5oA= + + + C=BE, CN=runbot.odoo.com, L=Ramillies, O=Odoo, OU=R&D, ST=Wallonnia + 548631688851000697209704649636588277530075594025 + + + + + + + http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf + Política de firma electrónica para facturación electrónica con formato Facturae + + + + a/vDoueJq9zxWGCFwdsXdlXtkFSEek4XVtmlQCEDaE8= + + + + http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf + + + + + + + + + + urn:oid:1.2.840.10003.5.109.10 + + + text/xml + + + + + + + +
diff --git a/addons/l10n_es_edi_facturae/tests/data/expected_tax_withholding.xml b/addons/l10n_es_edi_facturae/tests/data/expected_tax_withholding.xml new file mode 100644 index 00000000000..ecd8f153e96 --- /dev/null +++ b/addons/l10n_es_edi_facturae/tests/data/expected_tax_withholding.xml @@ -0,0 +1,331 @@ + + + + 3.2.1 + I + EM + + INV/2023/00001 + 1 + + 400.00 + + + 400.00 + + + 400.00 + + EUR + + + + + + J + R + ES59962470K + + + company_1_data + company_1_data + +
C. de Embajadores, 68-116
+ 12345 + Madrid + Madrid + ESP +
+
+
+ + + F + U + BE0477472701 + + + UNKNOWN + UNKNOWN + +
Rue de Bruxelles, 15000
+ 5000 Namur + Belgium + BEL +
+
+
+
+ + + + INV/2023/00001 + FC + OO + + + 2023-01-01 + EUR + EUR + en + + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + 01 + 21.000 + + 200.00 + + + 42.00 + + + + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + 01 + 21.000 + + 200.00 + + + 42.00 + + + + + 400.00 + 400.00 + 84.00 + 84.00 + 400.00 + 400.00 + 400.00 + + + + product_a + 1.0 + 01 + 100.00 + 100.00 + 100.00 + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + + + product_a + 1.0 + 01 + 100.00 + 100.00 + 100.00 + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + + + 01 + 21.000 + + 100.00 + + + 21.00 + + + + + + product_a + 1.0 + 01 + 200.00 + 200.00 + 200.00 + + + 01 + 21.000 + + 200.00 + + + 42.00 + + + + + + 01 + 21.000 + + 200.00 + + + 42.00 + + + + + + + + + + + + + + + + + ___ignore___ + + + + ___ignore___ + + + + ___ignore___ + + + + ___ignore___ + + + ___ignore___ + + + ___ignore___ + + + + ___ignore___ + ___ignore___ + + + + + + + + ___ignore___ + + + + + ___ignore___ + + + ___ignore___ + ___ignore___ + + + + + + + ___ignore___ + ___ignore___ + + + + ___ignore___ + + + + ___ignore___ + + + + + + + + ___ignore___ + + ___ignore___ + ___ignore___ + + ___ignore___ + ___ignore___ + + + + + + +
diff --git a/addons/l10n_es_edi_facturae/tests/test_edi_xml.py b/addons/l10n_es_edi_facturae/tests/test_edi_xml.py new file mode 100644 index 00000000000..6b5e856a5b7 --- /dev/null +++ b/addons/l10n_es_edi_facturae/tests/test_edi_xml.py @@ -0,0 +1,222 @@ +import logging +import random +from base64 import b64encode +from datetime import datetime +from hashlib import sha1 +from unittest.mock import patch + +import lxml +from freezegun import freeze_time + +from odoo import Command +from odoo.addons.account_edi.tests.common import AccountEdiTestCommon +from odoo.exceptions import UserError +from odoo.tests import tagged +from odoo.tools import file_open + +_logger = logging.getLogger(__name__) + +@tagged('post_install_l10n', 'post_install', '-at_install') +class TestEdiFacturaeXmls(AccountEdiTestCommon): + @classmethod + def setUpClass(cls, chart_template_ref='es_full', edi_format_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref) + cls.frozen_today = datetime(year=2023, month=1, day=1, hour=0, minute=0, second=0) + + # ==== Companies ==== + cls.company_data['company'].write({ # -> PersonTypeCode 'J' + 'country_id': cls.env.ref('base.es').id, # -> ResidenceTypeCode 'R' + 'street': "C. de Embajadores, 68-116", + 'state_id': cls.env.ref('base.state_es_m').id, + 'city': "Madrid", + 'zip': "12345", + 'vat': 'ES59962470K', + }) + cls.company_data_2['company'].write({ # -> PersonTypeCode 'J' + 'country_id': cls.env.ref('base.us').id, # -> ResidenceTypeCode 'R' + }) + + # ==== Business ==== + cls.partner_a.write({ # -> PersonTypeCode 'F' + 'country_id': cls.env.ref('base.be').id, # -> ResidenceTypeCode 'U' + 'vat': 'BE0477472701', + 'city': "Namur", + 'street': "Rue de Bruxelles, 15000", + 'zip': "5000", + }) + + cls.password = "test" + + cls.certificate_module = "odoo.addons.l10n_es_edi_facturae.models.l10n_es_edi_facturae_certificate" + with freeze_time(cls.frozen_today), patch(f"{cls.certificate_module}.fields.datetime.now", lambda x=None: cls.frozen_today): + cls.certificate = cls.env["l10n_es_edi_facturae.certificate"].sudo().create({ + "content": b64encode(file_open('l10n_es_edi_facturae/tests/data/certificate_test.pfx', 'rb').read()), + "password": "test", + 'company_id': cls.company_data['company'].id, + }) + + cls.tax, cls.tax_2 = cls.env['account.tax'].create([{ + 'name': "IVA 21% (Bienes)", + 'company_id': cls.company_data['company'].id, + 'amount': 21.0, + 'price_include': False, + 'l10n_es_edi_facturae_tax_type': '01' + }, { + 'name': "IVA 21% (Bienes) Included", + 'company_id': cls.company_data['company'].id, + 'amount': 21.0, + 'price_include': True, + 'l10n_es_edi_facturae_tax_type': '01' + } + ]) + + cls.nsmap = { + 'ds': "http://www.w3.org/2000/09/xmldsig#", 'fac': "http://www.facturae.es/Facturae/2007/v3.1/Facturae", + 'xades': "http://uri.etsi.org/01903/v1.3.2#", 'xd': "http://www.w3.org/2000/09/xmldsig#", + } + + cls.maxDiff = None + + @classmethod + def create_invoice(cls, **kwargs): + return cls.env['account.move'].with_context(edi_test_mode=True).create({ + 'partner_id': cls.partner_a.id, + 'invoice_date': cls.frozen_today.isoformat(), + 'date': cls.frozen_today.isoformat(), + **kwargs, + 'invoice_line_ids': [ + Command.create({'product_id': cls.product_a.id, 'price_unit': 1000.0, **line_vals, }) + for line_vals in kwargs.get('invoice_line_ids', []) + ], + }) + + def test_generate_signed_xml(self, date=None): + random.seed(42) + date = date or self.frozen_today + # We need to patch dates and uuid to ensure the signature's consistency + with freeze_time(date), \ + patch(f"{self.certificate_module}.fields.datetime.now", lambda x=None: date), \ + patch(f"{self.certificate_module}.sha1", lambda x: sha1()): + invoice = self.create_invoice( + partner_id=self.partner_a.id, + move_type='out_invoice', + invoice_line_ids=[ + {'price_unit': 100.0, 'tax_ids': [self.tax.id]}, + {'price_unit': 100.0, 'tax_ids': [self.tax.id]}, + {'price_unit': 242.0, 'tax_ids': [self.tax_2.id]}, + {'price_unit': 1000.0, "discount": 10, "tax_ids": [self.tax.id]}, + {'price_unit': 1210.0, "discount": -10, "tax_ids": [self.tax_2.id]}, + ], + ) + invoice.action_post() + generated_file = invoice._l10n_es_edi_facturae_render_facturae() + self.assertTrue(generated_file) + + with file_open("l10n_es_edi_facturae/tests/data/expected_signed_document.xml", "rt") as f: + expected_xml = lxml.etree.fromstring(f.read().encode()) + self.assertXmlTreeEqual(lxml.etree.fromstring(generated_file), expected_xml) + + def test_cannot_generate_unsigned_xml(self): + """ Test that no valid certificate prevents a xml generation""" + self.certificate.unlink() + random.seed(42) + with freeze_time(self.frozen_today), \ + patch(f"{self.certificate_module}.fields.datetime.now", lambda x=None: self.frozen_today), \ + patch(f"{self.certificate_module}.sha1", lambda x: sha1()): + invoice = self.create_invoice(partner_id=self.partner_a.id, move_type='out_invoice', invoice_line_ids=[{'price_unit': 100.0, 'tax_ids': [self.tax.id]},],) + invoice.action_post() + self.assertRaises(UserError, invoice._l10n_es_edi_facturae_render_facturae) + + def test_tax_withheld(self): + with freeze_time(self.frozen_today), \ + patch(f"{self.certificate_module}.fields.datetime.now", lambda x=None: self.frozen_today), \ + patch(f"{self.certificate_module}.sha1", lambda x: sha1()): + witholding_taxes = self.env["account.tax"].create([{ + 'name': "IVA 21%", + 'company_id': self.company_data['company'].id, + 'amount': 21.0, + 'price_include': False, + 'l10n_es_edi_facturae_tax_type': '01' + }, { + 'name': "IVA 21% withholding", + 'company_id': self.company_data['company'].id, + 'amount': -21.0, + 'price_include': False, + 'l10n_es_edi_facturae_tax_type': '01' + }]) + + invoice = self.create_invoice( + partner_id=self.partner_a.id, + move_type='out_invoice', + invoice_line_ids=[ + {'price_unit': 100.0, 'tax_ids': witholding_taxes.ids}, + {'price_unit': 100.0, 'tax_ids': witholding_taxes.ids}, + {'price_unit': 200.0, 'tax_ids': witholding_taxes.ids}, + ], + ) + invoice.action_post() + generated_file = invoice._l10n_es_edi_facturae_render_facturae() + self.assertTrue(generated_file) + with file_open("l10n_es_edi_facturae/tests/data/expected_tax_withholding.xml", "rt") as f: + expected_xml = lxml.etree.fromstring(f.read().encode()) + self.assertXmlTreeEqual(lxml.etree.fromstring(generated_file), expected_xml) + + def test_in_invoice(self): + random.seed(42) + # We need to patch dates and uuid to ensure the signature's consistency + with freeze_time(self.frozen_today), \ + patch(f"{self.certificate_module}.fields.datetime.now", lambda x=None: self.frozen_today), \ + patch(f"{self.certificate_module}.sha1", lambda x: sha1()): + invoice = self.create_invoice( + partner_id=self.partner_a.id, + move_type='in_invoice', + invoice_line_ids=[ + {'price_unit': 100.0, 'tax_ids': [self.tax.id]}, + {'price_unit': 100.0, 'tax_ids': [self.tax.id]}, + {'price_unit': 242.0, 'tax_ids': [self.tax_2.id]}, + {'price_unit': 1000.0, "discount": 10, "tax_ids": [self.tax.id]}, + {'price_unit': 1000.0, "discount": -10, "tax_ids": [self.tax.id]}, + ], + ) + invoice.action_post() + generated_file = invoice._l10n_es_edi_facturae_render_facturae() + self.assertTrue(generated_file) + + with file_open("l10n_es_edi_facturae/tests/data/expected_in_invoice_document.xml", "rt") as f: + expected_xml = lxml.etree.fromstring(f.read().encode()) + self.assertXmlTreeEqual(lxml.etree.fromstring(generated_file), expected_xml) + + def test_refund_invoice(self): + random.seed(42) + # We need to patch dates and uuid to ensure the signature's consistency + + with freeze_time(self.frozen_today), \ + patch(f"{self.certificate_module}.fields.datetime.now", lambda x=None: self.frozen_today), \ + patch(f"{self.certificate_module}.fields.datetime.now", lambda x=None: self.frozen_today), \ + patch(f"{self.certificate_module}.sha1", lambda x: sha1()): + invoice = self.create_invoice( + partner_id=self.partner_a.id, + move_type='out_invoice', + invoice_line_ids=[ + {'price_unit': 100.0, 'tax_ids': [self.tax.id]}, + {'price_unit': 100.0, 'tax_ids': [self.tax.id]}, + ], + ) + invoice.action_post() + reversal_wizard = self.env['account.move.reversal'].create({ + 'move_ids': invoice.ids, + 'date_mode': 'custom', + 'journal_id': invoice.journal_id.id, + 'date': self.frozen_today, + 'refund_method': 'cancel', + 'company_id': self.company_data['company'].id, + 'l10n_es_edi_facturae_reason_code': '01' + }) + reversal_wizard.reverse_moves() + refund = reversal_wizard.new_move_ids + generated_file = refund._l10n_es_edi_facturae_render_facturae() + self.assertTrue(generated_file) + + with file_open("l10n_es_edi_facturae/tests/data/expected_refund_document.xml", "rt") as f: + expected_xml = lxml.etree.fromstring(f.read().encode()) + self.assertXmlTreeEqual(lxml.etree.fromstring(generated_file), expected_xml) diff --git a/addons/l10n_es_edi_facturae/views/account_menuitem.xml b/addons/l10n_es_edi_facturae/views/account_menuitem.xml new file mode 100644 index 00000000000..f4cf7427e8c --- /dev/null +++ b/addons/l10n_es_edi_facturae/views/account_menuitem.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/addons/l10n_es_edi_facturae/views/account_move_send_views.xml b/addons/l10n_es_edi_facturae/views/account_move_send_views.xml new file mode 100644 index 00000000000..67c0fec34e6 --- /dev/null +++ b/addons/l10n_es_edi_facturae/views/account_move_send_views.xml @@ -0,0 +1,21 @@ + + + + + account.move.send.form + account.move.send + + + + +
+ + + +
+
+
+
+ +
diff --git a/addons/l10n_es_edi_facturae/views/account_tax_views.xml b/addons/l10n_es_edi_facturae/views/account_tax_views.xml new file mode 100644 index 00000000000..88a64e0085a --- /dev/null +++ b/addons/l10n_es_edi_facturae/views/account_tax_views.xml @@ -0,0 +1,28 @@ + + + + + account.tax.tree.inherit.l10n_es_edi_facturae + + account.tax + + + + + + + + + + account.tax.form.inherit.l10n_es_edi_facturae + account.tax + + + + + + + + + diff --git a/addons/l10n_es_edi_facturae/views/l10n_es_edi_facturae_views.xml b/addons/l10n_es_edi_facturae/views/l10n_es_edi_facturae_views.xml new file mode 100644 index 00000000000..1c495297b6e --- /dev/null +++ b/addons/l10n_es_edi_facturae/views/l10n_es_edi_facturae_views.xml @@ -0,0 +1,47 @@ + + + + + l10n_es_edi_facturae.certificate.form + l10n_es_edi_facturae.certificate + +
+ + + + + + +
+
+
+ + + l10n_es_edi_facturae.certificate.tree + l10n_es_edi_facturae.certificate + + + + + + + + + + + Certificates for Facturae EDI invoices on Spain + l10n_es_edi_facturae.certificate + tree,form + + +

Create the first certificate

+
+
+
+
diff --git a/addons/l10n_es_edi_facturae/views/res_company_views.xml b/addons/l10n_es_edi_facturae/views/res_company_views.xml new file mode 100644 index 00000000000..86be985179d --- /dev/null +++ b/addons/l10n_es_edi_facturae/views/res_company_views.xml @@ -0,0 +1,14 @@ + + + + res.company.form.inherit.l10n_es_edi_facturae + + res.company + + + + + + + diff --git a/addons/l10n_es_edi_facturae/views/uom_uom_views.xml b/addons/l10n_es_edi_facturae/views/uom_uom_views.xml new file mode 100644 index 00000000000..91d60a317d5 --- /dev/null +++ b/addons/l10n_es_edi_facturae/views/uom_uom_views.xml @@ -0,0 +1,33 @@ + + + + uom.uom.tree.inherit.l10n_es_edi_facturae + uom.uom + + + + + + + + + uom.uom.form.inherit.l10n_es_edi_facturae + uom.uom + + + + + + + + + uom.category.form.inherit.l10n_es_edi_facturae + uom.category + + + + + + + + diff --git a/addons/l10n_es_edi_facturae/wizard/__init__.py b/addons/l10n_es_edi_facturae/wizard/__init__.py new file mode 100644 index 00000000000..715d1bd6df0 --- /dev/null +++ b/addons/l10n_es_edi_facturae/wizard/__init__.py @@ -0,0 +1 @@ +from . import account_move_reversal diff --git a/addons/l10n_es_edi_facturae/wizard/account_move_reversal.py b/addons/l10n_es_edi_facturae/wizard/account_move_reversal.py new file mode 100644 index 00000000000..21fc498602a --- /dev/null +++ b/addons/l10n_es_edi_facturae/wizard/account_move_reversal.py @@ -0,0 +1,37 @@ +from odoo import models, fields + + +class AccountMoveReversal(models.TransientModel): + _inherit = 'account.move.reversal' + + l10n_es_edi_facturae_reason_code = fields.Selection( + selection=[ + ('01', 'Invoice number'), + ('02', 'Invoice series'), + ('03', 'Date of issue'), + ('04', 'Name and surname/company name - Issuer'), + ('05', 'Name and surname/company name - Recipient'), + ('06', 'Tax identification Issuer/Oblige'), + ('07', 'Tax identification Receiver'), + ('08', 'Issuer/Oblige Address'), + ('09', 'Receiving Address'), + ('10', 'Transaction Details'), + ('11', 'Tax rate to be applied'), + ('12', 'Tax rate to be applied'), + ('13', 'Date/Period to apply'), + ('14', 'Invoice type'), + ('15', 'Statutory letters'), + ('16', 'Taxable amount'), + ('80', 'Calculation of output quotas'), + ('81', 'Calculation of withholding taxes'), + ('82', 'Taxable amount modified by return of containers/packaging'), + ('83', 'Taxable income modified by discounts and allowances'), + ('84', 'Taxable income modified by final, judicial or administrative ruling'), + ('85', 'Taxable income modified by unpaid tax assessments. Order of declaration of bankruptcy'), + ], string='Spanish Facturae EDI Reason Code', default='10') + + def reverse_moves(self): + # Extends account_account + res = super(AccountMoveReversal, self).reverse_moves() + self.new_move_ids.l10n_es_edi_facturae_reason_code = self.l10n_es_edi_facturae_reason_code + return res diff --git a/addons/l10n_es_edi_facturae/wizard/account_move_reversal_view.xml b/addons/l10n_es_edi_facturae/wizard/account_move_reversal_view.xml new file mode 100644 index 00000000000..65f0361151c --- /dev/null +++ b/addons/l10n_es_edi_facturae/wizard/account_move_reversal_view.xml @@ -0,0 +1,30 @@ + + + + + + account.tax.tree.inherit.l10n_es_edi_facturae + + account.tax + + + + + + + + + account.move.reversal.form.inherit.l10n_es_edi_facturae + + account.move.reversal + + + + + + + + + + diff --git a/addons/l10n_es_edi_facturae/xml_utils.py b/addons/l10n_es_edi_facturae/xml_utils.py new file mode 100644 index 00000000000..4400f70cb32 --- /dev/null +++ b/addons/l10n_es_edi_facturae/xml_utils.py @@ -0,0 +1,82 @@ +import base64 +import hashlib +from base64 import b64encode +from copy import deepcopy + +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding +from lxml import etree + +from odoo.exceptions import UserError + +NS_MAP = {'ds': "http://www.w3.org/2000/09/xmldsig#"} + + +def _canonicalize_node(node): + """ + Returns the canonical (C14N 1.0, without comments, non exclusive) representation of node. + Speficied in: https://www.w3.org/TR/2001/REC-xml-c14n-20010315 + Required for computing digests and signatures. + Returns an UTF-8 encoded bytes string. + """ + + return etree.tostring(node, method="c14n", with_comments=False, exclusive=False) + + +def _get_uri(uri, reference, base_uri=""): + """ + Returns the content within `reference` that is identified by `uri`. + Canonicalization is used to convert node reference to an octet stream. + - URIs starting with # are same-document references + https://www.w3.org/TR/xmldsig-core/#sec-URI + - Empty URIs point to the whole document tree, without the signature + https://www.w3.org/TR/xmldsig-core/#sec-EnvelopedSignature + Returns an UTF-8 encoded bytes string. + """ + node = deepcopy(reference.getroottree()) + if uri == base_uri: + # Base URI: whole document, without signature (default is empty URI) + for signature in node.xpath('/ds:Signature', namespaces=NS_MAP): + node = signature.getparent() + node.remove(signature) + return _canonicalize_node(node) + + if uri.startswith("#"): + path = "//*[@*[local-name() = '{}' ]=$uri]" + results = node.xpath(path.format("Id"), uri=uri.lstrip("#")) # case-sensitive 'Id' + if len(results) == 1: + return _canonicalize_node(results[0]) + if len(results) > 1: + raise UserError(f"Ambiguous reference URI {uri} resolved to {len(results)} nodes") + + raise UserError(f'URI {uri} not found') + + +def _reference_digests(node, base_uri=""): + """ + Processes the references from node and computes their digest values as specified in + https://www.w3.org/TR/xmldsig-core/#sec-DigestMethod + https://www.w3.org/TR/xmldsig-core/#sec-DigestValue + """ + for reference in node.findall("ds:Reference", namespaces=NS_MAP): + ref_node = _get_uri(reference.get("URI", ""), reference, base_uri=base_uri) + lib = hashlib.new("sha256", ref_node) + reference.find("ds:DigestValue", namespaces=NS_MAP).text = b64encode(lib.digest()) + + +def _fill_signature(node, private_key): + """ + Uses private_key to sign the SignedInfo sub-node of `node`, as specified in: + https://www.w3.org/TR/xmldsig-core/#sec-SignatureValue + https://www.w3.org/TR/xmldsig-core/#sec-SignedInfo + """ + signed_info_xml = node.find("ds:SignedInfo", namespaces=NS_MAP) + + # During signature generation, the digest is computed over the canonical form of the document + signature = private_key.sign(_canonicalize_node(signed_info_xml), padding.PKCS1v15(), hashes.SHA256()) + node.find("ds:SignatureValue", namespaces=NS_MAP).text = base64.encodebytes(signature) + + +def _int_to_bytes(number): + """ Converts an integer to a byte string (in smallest big-endian form). """ + return number.to_bytes((number.bit_length() + 7) // 8, byteorder='big')