19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:28 +01:00
parent ff721d030e
commit 7721452493
1826 changed files with 124775 additions and 274114 deletions

View file

@ -1,16 +1,16 @@
# Spain - TicketBAI
This module sends invoices and vendor bills to the "Diputaciones
Forales" of Araba/Álava, Bizkaia and Gipuzkoa.
This module sends invoices and vendor bills to the "Diputaciones
Forales" of Araba/Álava, Bizkaia and Gipuzkoa.
Invoices and bills get converted to XML and regularly sent to the
Basque government servers which provides them with a unique identifier.
A hash chain ensures the continuous nature of the invoice/bill
sequences. QR codes are added to emitted (sent/printed) invoices,
bills and tickets to allow anyone to check they have been declared.
Invoices and bills get converted to XML and regularly sent to the
Basque government servers which provides them with a unique identifier.
A hash chain ensures the continuous nature of the invoice/bill
sequences. QR codes are added to emitted (sent/printed) invoices,
bills and tickets to allow anyone to check they have been declared.
You need to configure your certificate and the tax agency.
You need to configure your certificate and the tax agency.
## Installation
@ -21,35 +21,15 @@ pip install odoo-bringout-oca-ocb-l10n_es_edi_tbai
## Dependencies
This addon depends on:
- l10n_es_edi_sii
## Manifest Information
- **Name**: Spain - TicketBAI
- **Version**: 1.0
- **Category**: Accounting/Localizations/EDI
- **License**: LGPL-3
- **Installable**: False
- l10n_es
- certificate
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `l10n_es_edi_tbai`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/l10n_es_edi_tbai
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md
This package preserves the original LGPL-3 license.

View file

@ -6,30 +6,34 @@
{
'name': "Spain - TicketBAI",
'version': '1.0',
'version': '1.1',
'category': 'Accounting/Localizations/EDI',
'application': False,
'description': """
This module sends invoices and vendor bills to the "Diputaciones
Forales" of Araba/Álava, Bizkaia and Gipuzkoa.
This module sends invoices and vendor bills to the "Diputaciones
Forales" of Araba/Álava, Bizkaia and Gipuzkoa.
Invoices and bills get converted to XML and regularly sent to the
Basque government servers which provides them with a unique identifier.
A hash chain ensures the continuous nature of the invoice/bill
sequences. QR codes are added to emitted (sent/printed) invoices,
bills and tickets to allow anyone to check they have been declared.
Invoices and bills get converted to XML and regularly sent to the
Basque government servers which provides them with a unique identifier.
A hash chain ensures the continuous nature of the invoice/bill
sequences. QR codes are added to emitted (sent/printed) invoices,
bills and tickets to allow anyone to check they have been declared.
You need to configure your certificate and the tax agency.
You need to configure your certificate and the tax agency.
""",
'depends': [
'l10n_es_edi_sii',
'l10n_es',
'certificate',
],
'data': [
'data/account_edi_data.xml',
'data/template_invoice.xml',
'data/template_LROE_bizkaia.xml',
'data/ir_config_parameter.xml',
'security/ir.model.access.csv',
'security/l10n_es_edi_tbai_security.xml',
'views/account_move_view.xml',
'views/l10n_es_edi_tbai_certificate_views.xml',
'views/report_invoice.xml',
'views/res_config_settings_views.xml',
'views/res_company_views.xml',
@ -37,8 +41,10 @@
'wizards/account_move_reversal_views.xml',
],
'demo': [
'demo/demo_certificate.xml',
'demo/demo_res_partner.xml',
'demo/demo_company.xml',
],
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="edi_es_tbai" model="account.edi.format">
<field name="name">TicketBAI (ES)</field>
<field name="code">es_tbai</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="epigrafe_config" model="ir.config_parameter">
<field name="key">l10n_es_edi_tbai.epigrafe</field>
<field name="value" eval=""/>
</record>
</data>
</odoo>

View file

@ -0,0 +1,3 @@
-- disable_l10n_es_edi_integration
UPDATE res_company
SET l10n_es_tbai_test_env = true;

View file

@ -6,17 +6,29 @@
<template id="template_LROE_240_main">
<lrpjfecsgap:LROEPJ240FacturasEmitidasConSGAltaPeticion
xmlns:lrpjfecsgap="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PJ_240_1_1_FacturasEmitidas_ConSG_AltaPeticion_V1_0_2.xsd"
t-if="is_emission"
t-call="l10n_es_edi_tbai.template_LROE_240_inner"/>
t-if="is_emission and not freelancer">
<t t-call="l10n_es_edi_tbai.template_LROE_240_inner"/>
</lrpjfecsgap:LROEPJ240FacturasEmitidasConSGAltaPeticion>
<lrpficfcsgap:LROEPF140IngresosConFacturaConSGAltaPeticion
xmlns:lrpficfcsgap="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PF_140_1_1_Ingresos_ConfacturaConSG_AltaPeticion_V1_0_2.xsd"
t-elif="is_emission and freelancer">
<t t-call="l10n_es_edi_tbai.template_LROE_240_inner"/>
</lrpficfcsgap:LROEPF140IngresosConFacturaConSGAltaPeticion>
<lrpjfecsgap:LROEPJ240FacturasEmitidasConSGAnulacionPeticion
xmlns:lrpjfecsgap="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PJ_240_1_1_FacturasEmitidas_ConSG_AnulacionPeticion_V1_0_0.xsd"
t-else=""
t-call="l10n_es_edi_tbai.template_LROE_240_inner"/>
t-elif="not is_emission and not freelancer">
<t t-call="l10n_es_edi_tbai.template_LROE_240_inner"/>
</lrpjfecsgap:LROEPJ240FacturasEmitidasConSGAnulacionPeticion>
<lrpficfcsgap:LROEPF140IngresosConFacturaConSGAnulacionPeticion
xmlns:lrpficfcsgap="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PF_140_1_1_Ingresos_ConfacturaConSG_AnulacionPeticion_V1_0_0.xsd"
t-elif="not is_emission and freelancer">
<t t-call="l10n_es_edi_tbai.template_LROE_240_inner"/>
</lrpficfcsgap:LROEPF140IngresosConFacturaConSGAnulacionPeticion>
</template>
<template id="template_LROE_240_inner">
<template id="template_LROE_240_inner"> <!-- To be used for both 140 and 240 -->
<Cabecera>
<Modelo>240</Modelo>
<Modelo t-out="'140' if freelancer else '240'"/>
<Capitulo>1</Capitulo>
<Subcapitulo>1.1</Subcapitulo>
<Operacion t-out="'A00' if is_emission else 'AN0'"/>
@ -27,31 +39,51 @@
<ApellidosNombreRazonSocial t-out="sender.name"/>
</ObligadoTributario>
</Cabecera>
<FacturasEmitidas>
<FacturasEmitidas t-if="not freelancer">
<FacturaEmitida t-foreach="tbai_b64_list" t-as="tbai_b64">
<TicketBai t-if="is_emission" t-out="tbai_b64"/>
<AnulacionTicketBai t-else="" t-out="tbai_b64"/>
</FacturaEmitida>
</FacturasEmitidas>
<Ingresos t-if="freelancer">
<Ingreso t-foreach="tbai_b64_list" t-as="tbai_b64">
<TicketBai t-if="is_emission" t-out="tbai_b64"/>
<Renta t-if="is_emission"><DetalleRenta><Epigrafe t-out="epigrafe"/></DetalleRenta></Renta>
<AnulacionTicketBai t-else="" t-out="tbai_b64"/>
</Ingreso>
</Ingresos>
</template>
<template id="template_LROE_240_main_recibidas">
<lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion
xmlns:lrpjframp="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PJ_240_2_FacturasRecibidas_AltaModifPeticion_V1_0_1.xsd"
t-if="is_emission"
t-call="l10n_es_edi_tbai.template_LROE_240_inner_recibidas"/>
t-if="is_emission and not freelancer">
<t t-call="l10n_es_edi_tbai.template_LROE_240_inner_recibidas"/>
</lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion>
<lrpfgcfamp:LROEPF140GastosConFacturaAltaModifPeticion
xmlns:lrpfgcfamp="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PF_140_2_1_Gastos_Confactura_AltaModifPeticion_V1_0_2.xsd"
t-elif="is_emission and freelancer">
<t t-call="l10n_es_edi_tbai.template_LROE_240_inner_recibidas"/>
</lrpfgcfamp:LROEPF140GastosConFacturaAltaModifPeticion>
<lrpjfrap:LROEPJ240FacturasRecibidasAnulacionPeticion
xmlns:lrpjfrap="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PJ_240_2_FacturasRecibidas_AnulacionPeticion_V1_0_0.xsd"
t-else=""
t-call="l10n_es_edi_tbai.template_LROE_240_inner_recibidas"/>
t-elif="not is_emission and not freelancer">
<t t-call="l10n_es_edi_tbai.template_LROE_240_inner_recibidas"/>
</lrpjfrap:LROEPJ240FacturasRecibidasAnulacionPeticion>
<lrpfgcfap:LROEPF140GastosConFacturaAnulacionPeticion
xmlns:lrpfgcfap="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PF_140_2_1_Gastos_Confactura_AnulacionPeticion_V1_0_0.xsd"
t-elif="not is_emission and freelancer">
<t t-call="l10n_es_edi_tbai.template_LROE_240_inner_recibidas"/>
</lrpfgcfap:LROEPF140GastosConFacturaAnulacionPeticion>
</template>
<template id="template_LROE_240_inner_recibidas">
<Cabecera>
<Modelo>240</Modelo>
<Modelo t-out="'140' if freelancer else '240'"/>
<Capitulo>2</Capitulo>
<Operacion t-out="'A00' if is_emission else 'AN0'"/>
<Subcapitulo t-out="'2.1' if freelancer else None"/>
<Operacion t-out="'M00' if batuz_correction else 'A00' if is_emission else 'AN0'"/>
<Version>1.0</Version>
<Ejercicio t-out="fiscal_year"/>
<ObligadoTributario>
@ -59,14 +91,25 @@
<ApellidosNombreRazonSocial t-out="sender.name"/>
</ObligadoTributario>
</Cabecera>
<FacturasRecibidas>
<FacturasRecibidas t-if="not freelancer">
<FacturaRecibida>
<t t-if="not is_emission"> <!-- cancel case -->
<IDRecibida>
<t t-set="seq_and_num" t-value="invoice._get_l10n_es_tbai_sequence_and_number()"/>
<t t-call="l10n_es_edi_tbai.template_LROE_recibidas_common"/>
</FacturaRecibida>
</FacturasRecibidas>
<Gastos t-else="">
<Gasto>
<t t-call="l10n_es_edi_tbai.template_LROE_recibidas_common"/>
</Gasto>
</Gastos>
</template>
<template id="template_LROE_recibidas_common">
<t t-if="not is_emission"> <!-- cancel case -->
<IDRecibida t-if="not freelancer">
<t t-set="seq_and_num" t-value="doc._get_tbai_sequence_and_number()"/>
<SerieFactura t-out="seq_and_num[0]"/>
<NumFactura t-out="seq_and_num[1]"/>
<FechaExpedicionFactura t-out="format_date(invoice.invoice_date)"/>
<FechaExpedicionFactura t-out="format_date(invoice_date)"/>
<EmisorFacturaRecibida>
<NIF t-if="recipient.get('nif')" t-out="recipient['nif']"/>
<IDOtro t-else="">
@ -76,6 +119,20 @@
</IDOtro>
</EmisorFacturaRecibida>
</IDRecibida>
<IDGasto t-else="">
<t t-set="seq_and_num" t-value="doc._get_tbai_sequence_and_number_purchase()"/>
<SerieFactura t-out="seq_and_num[0]"/>
<NumFactura t-out="seq_and_num[1]"/>
<FechaExpedicionFactura t-out="format_date(invoice_date)"/>
<EmisorFacturaRecibida>
<NIF t-if="recipient.get('nif')" t-out="recipient['nif']"/>
<IDOtro t-else="">
<CodigoPais t-if="recipient.get('alt_id_country')" t-out="recipient['alt_id_country']"/>
<IDType t-out="recipient['alt_id_type']"/>
<ID t-out="recipient['alt_id_number']"/>
</IDOtro>
</EmisorFacturaRecibida>
</IDGasto>
</t>
<t t-else="">
<EmisorFacturaRecibida>
@ -89,20 +146,20 @@
<ApellidosNombreRazonSocial t-out="partner.name"/>
</EmisorFacturaRecibida>
<CabeceraFactura>
<t t-set="seq_and_num" t-value="invoice._get_l10n_es_tbai_sequence_and_number()"/>
<t t-set="seq_and_num" t-value="doc._get_tbai_sequence_and_number_purchase()"/>
<SerieFactura t-out="seq_and_num[0]"/>
<NumFactura t-out="seq_and_num[1]"/>
<FechaExpedicionFactura t-out="format_date(invoice.invoice_date)"/>
<FechaRecepcion t-out="format_date(invoice.date)"/>
<FechaExpedicionFactura t-out="format_date(invoice_date)"/>
<FechaRecepcion t-out="format_date(doc.date)"/>
<TipoFactura t-out="tipofactura"/>
<t t-if="is_refund">
<FacturaRectificativa>
<Codigo t-out="credit_note_code"/>
<Codigo t-out="refund_reason"/>
<Tipo>I</Tipo>
</FacturaRectificativa>
<FacturasRectificadasSustituidas t-if="credit_note_invoice">
<IDFacturaRectificadaSustituida>
<t t-set="seq_and_num" t-value="credit_note_invoice._get_l10n_es_tbai_sequence_and_number()"/>
<FacturasRectificadasSustituidas t-if="credit_note_invoices">
<IDFacturaRectificadaSustituida t-foreach="credit_note_invoices" t-as="credit_note_invoice">
<t t-set="seq_and_num" t-value="credit_note_invoice.l10n_es_tbai_post_document_id._get_tbai_sequence_and_number_purchase()"/>
<SerieFactura t-out="seq_and_num[0]"/>
<NumFactura t-out="seq_and_num[1]"/>
<FechaExpedicionFactura t-out="format_date(credit_note_invoice.invoice_date)"/>
@ -111,7 +168,7 @@
</t>
</CabeceraFactura>
<DatosFactura>
<DescripcionOperacion t-out="invoice.ref"/>
<DescripcionOperacion t-out="ref"/>
<Claves>
<IDClave t-foreach="regime_key" t-as="key">
@ -120,7 +177,7 @@
</Claves>
<ImporteTotalFactura t-out="format_float(amount_total)"/>
</DatosFactura>
<IVA>
<IVA t-if="not freelancer">
<DetalleIVA t-foreach="iva_values" t-as="tax">
<CompraBienesCorrientesGastosBienesInversion t-out="tax['code']"/>
<InversionSujetoPasivo t-out="'N' if tax['rec'].l10n_es_type != 'sujeto_isp' else 'S'"/>
@ -136,9 +193,17 @@
</t>
</DetalleIVA>
</IVA>
<RentaIVA t-elif="freelancer">
<DetalleRentaIVA t-foreach="iva_values" t-as="tax">
<Epigrafe t-out="epigrafe"/>
<InversionSujetoPasivo t-out="'N' if tax['rec'].l10n_es_type != 'sujeto_isp' else 'S'"/>
<BaseImponible t-out="format_float(tax['base'])"/>
<TipoImpositivo t-out="tax['rec'].amount"/>
<CuotaIVASoportada t-out="format_float(tax['tax'])"/>
<CuotaIVADeducible t-out="format_float(tax['tax']) if tax['rec'].l10n_es_type != 'no_deducible' else '0.00'"/>
</DetalleRentaIVA>
</RentaIVA>
</t>
</FacturaRecibida>
</FacturasRecibidas>
</template>
</data>
</odoo>

View file

@ -17,22 +17,26 @@
<Cabecera>
<IDVersionTBAI t-out="tbai_version"/>
</Cabecera>
<Sujetos t-if="is_emission">
<t t-call="l10n_es_edi_tbai.template_invoice_sujetos"/>
</Sujetos>
<Factura t-if="is_emission">
<t t-call="l10n_es_edi_tbai.template_invoice_factura"/>
</Factura>
<IDFactura t-if="not is_emission">
<t t-call="l10n_es_edi_tbai.template_invoice_sujetos"/>
<t t-call="l10n_es_edi_tbai.template_invoice_factura"/>
</IDFactura>
<t t-if="is_emission">
<Sujetos>
<t t-call="l10n_es_edi_tbai.template_invoice_sujetos"/>
</Sujetos>
<Factura>
<t t-call="l10n_es_edi_tbai.template_invoice_factura"/>
</Factura>
</t>
<t t-else="">
<IDFactura>
<t t-call="l10n_es_edi_tbai.template_invoice_sujetos"/>
<t t-call="l10n_es_edi_tbai.template_invoice_factura"/>
</IDFactura>
</t>
<HuellaTBAI>
<EncadenamientoFacturaAnterior t-if="chain_prev_invoice">
<t t-set="seq_and_num" t-value="chain_prev_invoice._get_l10n_es_tbai_sequence_and_number()"/>
<EncadenamientoFacturaAnterior t-if="chain_prev_document">
<t t-set="seq_and_num" t-value="chain_prev_document._get_tbai_sequence_and_number()"/>
<SerieFacturaAnterior t-out="seq_and_num[0]"/>
<NumFacturaAnterior t-out="seq_and_num[1]"/>
<t t-set="sig_and_date" t-value="chain_prev_invoice._get_l10n_es_tbai_signature_and_date()"/>
<t t-set="sig_and_date" t-value="chain_prev_document._get_tbai_signature_and_date()"/>
<FechaExpedicionFacturaAnterior t-out="format_date(sig_and_date[1])"/>
<SignatureValueFirmaFacturaAnterior t-out="sig_and_date[0][:100]"/>
</EncadenamientoFacturaAnterior>
@ -72,9 +76,8 @@
</template>
<template id="template_invoice_factura">
<t t-set="is_simplified" t-value="invoice._is_l10n_es_tbai_simplified()"/>
<CabeceraFactura>
<t t-set="seq_and_num" t-value="invoice._get_l10n_es_tbai_sequence_and_number()"/>
<t t-set="seq_and_num" t-value="doc._get_tbai_sequence_and_number()"/>
<SerieFactura t-out="seq_and_num[0]"/>
<NumFactura t-out="seq_and_num[1]"/>
<t t-if="is_emission">
@ -82,11 +85,11 @@
<HoraExpedicionFactura t-out="format_time(datetime_now)"/>
<FacturaSimplificada t-out="'S' if is_simplified else 'N'"/>
</t>
<FechaExpedicionFactura t-else="" t-out="format_date(invoice._get_l10n_es_tbai_signature_and_date()[1])"/>
<t t-if="is_refund">
<FechaExpedicionFactura t-else="" t-out="format_date(post_doc._get_tbai_signature_and_date()[1])"/>
<t t-if="is_refund and is_emission">
<FacturaEmitidaSustitucionSimplificada t-out="'S' if (is_simplified and recipient) else 'N'"/>
<FacturaRectificativa>
<Codigo t-out="credit_note_code"/>
<Codigo t-out="refund_reason"/>
<Tipo>I</Tipo>
<!-- NOTE: could also allow credit note Tipo 'S' (optional, tipo I already supported by SII)
<ImporteRectificacionSustitutiva>
@ -96,28 +99,28 @@
</FacturaRectificativa>
<FacturasRectificadasSustituidas>
<IDFacturaRectificadaSustituida>
<t t-set="seq_and_num" t-value="credit_note_invoice._get_l10n_es_tbai_sequence_and_number()"/>
<SerieFactura t-out="seq_and_num[0]"/>
<NumFactura t-out="seq_and_num[1]"/>
<FechaExpedicionFactura t-out="format_date(credit_note_invoice.l10n_es_tbai_post_xml and credit_note_invoice._get_l10n_es_tbai_signature_and_date()[1] or credit_note_invoice.invoice_date)"/>
<!-- NOTE: could support issuing a single credit note for multiple invoices (optional) -->
<SerieFactura t-out="refunded_serie"/>
<NumFactura t-out="refunded_num"/>
<FechaExpedicionFactura t-out="format_date(refunded_date)"/>
</IDFacturaRectificadaSustituida>
</FacturasRectificadasSustituidas>
</t>
</CabeceraFactura>
<DatosFactura t-if="is_emission">
<DescripcionFactura t-out="invoice.invoice_origin and invoice.invoice_origin[:250] or 'manual'"/>
<FechaOperacion t-if="delivery_date" t-out="format_date(delivery_date)"/>
<DescripcionFactura t-out="origin"/>
<DetallesFactura>
<IDDetalleFactura t-foreach="invoice_lines" t-as="line_values">
<t t-set="line" t-value="line_values['line']"/>
<DescripcionDetalle t-out="line_values['description']"/>
<Cantidad t-out="format_float(line.quantity)"/>
<ImporteUnitario t-out="format_float(line_values['unit_price'])"/>
<Descuento t-out="format_float(line_values['discount'])"/>
<ImporteTotal t-out="format_float(line_values['total'])"/>
<IDDetalleFactura t-foreach="base_lines" t-as="base_line">
<DescripcionDetalle t-out="base_line['description']"/>
<Cantidad t-out="format_float(base_line['quantity'], precision_digits=8)"/>
<ImporteUnitario t-out="format_float(base_line['gross_price_unit'], precision_digits=8)"/>
<Descuento t-out="format_float(base_line['discount_amount'], precision_digits=8)"/>
<ImporteTotal t-out="format_float(base_line['price_total'], precision_digits=8)"/>
</IDDetalleFactura>
</DetallesFactura>
<ImporteTotalFactura t-out="format_float(amount_total)"/>
<RetencionSoportada t-if="amount_retention != 0.0" t-out="format_float(amount_retention)"/>
<ImporteTotalFactura t-out="format_float(total_amount)"/>
<RetencionSoportada t-if="total_retention" t-out="format_float(-total_retention)"/>
<!-- <BaseImponibleACoste/> NOTE (only applicable with ClaveRegimenIvaOpTrascendencia 06, not supported yet) -->
<Claves>
<IDClave t-foreach="regime_key" t-as="key">

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="demo_certificate" model="certificate.certificate">
<field name="name">Demo TBAI certificate</field>
<field name="content" type="base64" file="l10n_es_edi_tbai/demo/certificates/gipuzkoa_Iz3np32024.p12"/>
<field name="pkcs12_password">Iz3np32024</field>
<field name="scope">tbai</field>
<field name="company_id" ref="base.demo_company_es"/>
</record>
</odoo>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="l10n_es.demo_company_es" model="res.company">
<record id="base.demo_company_es" model="res.company">
<field name="l10n_es_tbai_tax_agency">gipuzkoa</field>
</record>
</odoo>

View file

@ -4,18 +4,17 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0+e\n"
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-28 11:10+0000\n"
"PO-Revision-Date: 2025-01-28 11:13+0000\n"
"Last-Translator: Jairo Llopis <jairo@moduon.team>\n"
"POT-Creation-Date: 2025-12-30 19:06+0000\n"
"PO-Revision-Date: 2025-01-13 15:08+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.4.4\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.template_LROE_240_inner
@ -29,19 +28,9 @@ msgid "1.1"
msgstr "1.1"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.template_LROE_240_inner
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.template_LROE_240_inner_recibidas
msgid "240"
msgstr "240"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid ""
"<span class=\"o_form_label\">Registro de Libros connection "
"SII/TicketBAI</span>"
msgstr ""
"<span class=\"o_form_label\">Conexión con el Registro de Libros mediante "
"SII/TicketBAI</span>"
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__l10n_es_edi_tbai_document__state__accepted
msgid "Accepted"
msgstr "Aceptado"
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_account_move_reversal
@ -49,15 +38,14 @@ msgid "Account Move Reversal"
msgstr "Reversión de movimiento de cuenta"
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_ir_attachment
msgid "Attachment"
msgstr "Archivo adjunto"
#: model:ir.model,name:l10n_es_edi_tbai.model_account_move_send
msgid "Account Move Send"
msgstr "Enviar asiento contable"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_refund_reason
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_refund_reason
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move_reversal__l10n_es_tbai_refund_reason
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_refund_reason
msgid ""
"BOE-A-1992-28740. Ley 37/1992, de 28 de diciembre, del Impuesto sobre el "
"Valor Añadido. Artículo 80. Modificación de la base imponible."
@ -66,42 +54,116 @@ msgstr ""
"Valor Añadido. Artículo 80. Modificación de la base imponible."
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_cancel_xml
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_cancel_xml
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_cancel_xml
msgid "Cancellation XML"
msgstr "XML de cancelación"
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
msgid ""
"Be careful if you modified this vendor bill, because the official version is "
"still the previous one sent. "
msgstr ""
"Ten cuidado aquí si modificaste esta factura del proveedor, porque la "
"versión oficial sigue siendo la precedente que se envió."
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_cancel_xml
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_cancel_xml
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_cancel_xml
msgid ""
"Cancellation XML sent to TicketBAI. Kept if accepted or no response "
"(timeout), cleared otherwise."
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move__l10n_es_tbai_state__cancelled
msgid "Cancelled"
msgstr "Cancelado"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
msgid "Cannot send an entry that is not posted to TicketBAI."
msgstr ""
"XML de cancelación enviado a TicketBAI. Se mantiene si se acepta o no se "
"obtiene respuesta (tiempo de espera), de lo contrario, se borra."
"No es posible enviar a través de TicketBAI un asiento o registro sin "
"contabilizar."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
msgid "Cannot send this entry as it is already being processed."
msgstr "No es posible enviar este registro porque ya está en proceso."
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_certificate_certificate
msgid "Certificate"
msgstr "Certificado"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_certificate_id
msgid "Certificate (TicketBAI)"
msgstr "Certificado (TicketBAI)"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_certificate_certificate__scope
msgid "Certificate scope"
msgstr "Alcance del certificado"
#. module: l10n_es_edi_tbai
#: model:ir.ui.menu,name:l10n_es_edi_tbai.menu_l10n_es_edi_tbai_certificates
msgid "Certificates"
msgstr "Certificados"
#. module: l10n_es_edi_tbai
#: model:ir.actions.act_window,name:l10n_es_edi_tbai.l10n_es_edi_tbai_certificate_action
msgid "Certificates for EDI TicketBAI invoices on Spain"
msgstr "Certificados para las facturas EDI TicketBAI en España"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__chain_index
msgid "Chain Index"
msgstr "Índice de secuencia"
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_res_company
msgid "Companies"
msgstr "Compañías"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__company_id
msgid "Company"
msgstr "Compañía"
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_res_config_settings
msgid "Config Settings"
msgstr "Ajustes de configuración"
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_account_edi_format
msgid "EDI format"
msgstr "Formato EDI"
#: model_terms:ir.actions.act_window,help:l10n_es_edi_tbai.l10n_es_edi_tbai_certificate_action
msgid "Create the first certificate"
msgstr "Crear el primer certificado"
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_account_edi_document
msgid "Electronic Document for an account.move"
msgstr "Documento electrónico de un account.move"
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__create_uid
msgid "Created by"
msgstr "Creado por"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__create_date
msgid "Created on"
msgstr "Creado el"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__date
msgid "Date"
msgstr "Fecha"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_line__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_reversal__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_send__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_certificate_certificate__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_config_settings__display_name
msgid "Display Name"
msgstr "Nombre mostrado"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move_send.py:0
msgid "Error when sending the invoice to TicketBAI:"
msgstr "Se ha producido un error al enviar la factura a TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__res_company__l10n_es_tbai_tax_agency__araba
@ -118,47 +180,87 @@ msgstr "Hacienda Foral de Bizkaia"
msgid "Hacienda Foral de Gipuzkoa"
msgstr "Diputación Foral de Gipuzkoa"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_line__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_reversal__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_send__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_certificate_certificate__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_config_settings__id
msgid "ID"
msgstr "ID"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"In case of a foreign customer, you need to configure the tax scope on taxes:\n"
"In case of a foreign customer, you need to configure the tax scope on "
"taxes:\n"
"%s"
msgstr ""
"En el caso de un cliente extranjero, es necesario configurar el ámbito fiscal en impuestos:\n"
"En el caso de un cliente extranjero, es necesario configurar el ámbito "
"fiscal en impuestos:\n"
"%s"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"In order to use Ticketbai Batuz for freelancers, you will need to configure "
"the Epigrafe or Main Activity. In this version, you need to go in debug "
"mode to Settings > Technical > System Parameters and set the parameter "
"'l10n_es_edi_tbai.epigrafe'to your epigrafe number. You can find them in %s"
msgstr ""
"Para utilizar TicketBAI Batuz para autónomos, necesitará configurar el "
"epígrafe o actividad principal. En esta versión, debe ir al modo "
"desarrolladorAjustes > Técnico > Parámetros del sistema y configurar los "
"parámetros 'l10n_es_edi_tbai.epigrafe' con su número de epígrafe. Puede "
"encontrar la lista de epígrafes en: %s"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_reversed_ids
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_reversed_ids
msgid ""
"In the case where a vendor refund has multiple original invoices, you can "
"set them here. "
msgstr ""
"Para las facturas rectificativas de un proveedor que tenga diversas facturas "
"reembolsadas, puede configurarlas aquí."
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_refund_reason
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_refund_reason
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_reversal__l10n_es_tbai_refund_reason
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_refund_reason
msgid "Invoice Refund Reason Code (TicketBai)"
msgstr "Código de motivo de reembolso de la factura (TicketBai)"
msgstr "Código de motivo de reembolso de la factura (TicketBAI)"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_chain_index
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_chain_index
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_chain_index
msgid ""
"Invoice index in chain, set if and only if an in-chain XML was submitted and"
" did not error"
"Invoice index in chain, set if and only if an in-chain XML was submitted and "
"did not error"
msgstr ""
"Índice de facturas en cadena que se establece si y solo si se envió un XML y"
" no se produjo un error."
"Índice de facturas en cadena que se establece si y solo si se envió un XML y "
"no se produjo un error."
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__is_cancel
msgid "Is Cancel"
msgstr "Está cancelado"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_reversal__l10n_es_tbai_is_required
msgid "Is TicketBai required for this reversal"
msgstr "¿Se necesita TicketBai para realizar una reversión?"
msgstr "¿Se necesita TicketBAI para realizar una reversión?"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_is_required
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_is_required
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_is_required
msgid "Is the Basque EDI (TicketBAI) needed ?"
msgstr "¿Se necesita el EDI vasco (TicketBai)?"
msgstr "¿Se necesita el EDI vasco (TicketBAI)?"
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_account_move
@ -166,30 +268,81 @@ msgid "Journal Entry"
msgstr "Asiento contable"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Licence NIF"
msgstr "Licencia de NIF"
#: model:ir.model,name:l10n_es_edi_tbai.model_account_move_line
msgid "Journal Item"
msgstr "Apunte contable"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_cancel_document_id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_cancel_document_id
msgid "L10N Es Tbai Cancel Document"
msgstr "Cancelar documento L10N Es Tbai"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_certificate_ids
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_config_settings__l10n_es_tbai_certificate_ids
msgid "L10N Es Tbai Certificate"
msgstr "Certificado L10N Es Tbai"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_is_enabled
msgid "L10N Es Tbai Is Enabled"
msgstr "L10N Es Tbai activado"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_post_document_id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_post_document_id
msgid "L10N Es Tbai Post Document"
msgstr "Publicar documento L10N Es Tbai"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__write_uid
msgid "Last Updated by"
msgstr "Última actualización por"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__write_date
msgid "Last Updated on"
msgstr "Última actualización el"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
msgid "Licence NIF"
msgstr "Licencia NIF"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Licence number"
msgstr "Número de licencia"
#. module: l10n_es_edi_tbai
#: model:ir.ui.menu,name:l10n_es_edi_tbai.menu_l10n_es_edi_tbai_license
msgid "Licenses (TicketBAI)"
msgstr "Licencias (TicketBai)"
msgid "Licenses"
msgstr "Licencias (TicketBAI)"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid "Manage certificates (TicketBAI)"
msgstr "Gestionar certificados (TicketBAI)"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__name
msgid "Name"
msgstr "Nombre"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
msgid "No XML response received from LROE."
msgstr "No se ha recibido respuesta XML de LROE."
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "No XML response received."
msgstr "No se ha recibido respuesta XML."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "No certificate found"
msgstr "No se ha encontrado ningún certificado"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
@ -198,43 +351,49 @@ msgstr ""
"No se ha seleccionado ninguna agencia tributaria: TicketBai no activado."
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_l10n_es_edi_certificate
msgid "Personal Digital Certificate"
msgstr "Certificado digital de persona física"
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"No valid certificate found for this company, TicketBAI file will not be "
"signed.\n"
msgstr ""
"No se ha encontrado ningún certificado válido para esta empresa, el archivo "
"TicketBAI no se podrá firmar.\n"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Please configure the Tax ID on your company for TicketBAI."
msgstr "Configure el NIF en su compañía para TicketBai."
msgstr "Configure el NIF en su compañía par TicketBai."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
msgid "Please configure the certificate for TicketBAI/SII."
msgstr "Configure el certificado para TicketBai/SII."
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Please configure the certificate for TicketBAI."
msgstr "Por favor, configure el certificado para TicketBAI/SII."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Please specify a tax agency on your company for TicketBAI."
msgstr "Especifique una agencia tributaria para su compañía en TicketBai."
msgstr "Especifique una agencia tributaria para su compañía en TicketBAI."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Production license"
msgstr "Licencia de producción"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid "Production mode: EDI data is sent to the official agency servers."
msgstr "Modo producción: los datos EDI se envían a los servidores oficiales."
#. module: l10n_es_edi_tbai
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move__l10n_es_tbai_refund_reason__r1
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move_reversal__l10n_es_tbai_refund_reason__r1
msgid "R1: Art. 80.1, 80.2, 80.6 and rights founded error"
msgstr "R1: Art. 80.1, 80.2, 80.6 y por error fundado de derecho"
msgstr "R1: Art. 80.1, 80.2, 80.6 y por derechos fundados en el error"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move__l10n_es_tbai_refund_reason__r2
@ -262,69 +421,111 @@ msgstr "R5: Factura rectificativa en facturas simplificadas"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Refund reason cannot be R5 for non-simplified invoices (TicketBAI)"
msgstr ""
"El motivo de reembolso no puede ser R5 para facturas no simplificadas "
"(TicketBai)"
"(TicketBAI)"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Refund reason must be R5 for simplified invoices (TicketBAI)"
msgstr ""
"El motivo de reembolso debe ser R5 para facturas simplificadas (TicketBai)"
"El motivo de reembolso debe ser R5 para facturas simplificadas (TicketBAI)"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Refund reason must be specified (TicketBAI)"
msgstr "Se debe especificar el motivo del reembolso (TicketBai)"
msgstr "Se debe especificar el motivo del reembolso (TicketBAI)"
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_account_resequence_wizard
msgid "Remake the sequence of Journal Entries."
msgstr "Remake the sequence of Journal Entries."
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_reversed_ids
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_reversed_ids
msgid "Refunded Vendor Bills"
msgstr "Facturas de proveedores reembolsadas"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid "Registro de Libros connection TicketBAI"
msgstr "Registro de Libros conexión TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__l10n_es_edi_tbai_document__state__rejected
msgid "Rejected"
msgstr "Rechazado"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.view_move_form_inherit_l10n_es_edi_tbai
msgid "Resend to TicketBAI"
msgstr "Reenviar a TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__response_message
msgid "Response Message"
msgstr "Mensaje de respuesta"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/wizards/account_move_reversal.py:0
msgid "Reversals mixing invoices with and without TicketBAI are not allowed."
msgstr "No se permiten reversiones que mezclen facturas con y sin TicketBAI."
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.view_move_form_inherit_l10n_es_edi_tbai
msgid "Send Bill to TicketBAI"
msgstr "Envíe facturas a TicketBAI"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move_send.py:0
msgid "Send the e-invoice to the Basque Government."
msgstr "Envíe la factura electrónica al Gobierno Vasco"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move__l10n_es_tbai_state__sent
msgid "Sent"
msgstr "Enviado"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Software name"
msgstr "Nombre del software"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Software version"
msgstr "Versión del software"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_post_xml
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_post_xml
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_post_xml
msgid "Submission XML"
msgstr "XML de envío"
#: model:ir.ui.menu,name:l10n_es_edi_tbai.menu_l10n_es_edi_tbai_root
msgid "Spain TicketBAI"
msgstr "TicketBAI España"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_post_xml
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_post_xml
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_post_xml
msgid ""
"Submission XML sent to TicketBAI. Kept if accepted or no response (timeout),"
" cleared otherwise."
msgstr ""
"XML enviado a TicketBai. Se mantiene si se acepta o no se obtiene respuesta "
"(tiempo de espera), de lo contrario, se borra."
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__certificate_certificate__scope__tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.certificate_certificate_view_search
msgid "TBAI"
msgstr "TBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_test_env
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_config_settings__l10n_es_tbai_test_env
msgid "TBAI Test Mode"
msgstr "Modo de prueba TBAI"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.certificate_certificate_view_search
msgid "TBAI certificates"
msgstr "Certificados TBAI"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.template_invoice_bundle
msgid "TEST-DEVICE-001"
msgstr "TEST-DEVICE-001"
msgstr "PRUEBA-DISPOSITIVO-001"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_tax_agency
@ -334,83 +535,135 @@ msgstr "Agencia tributaria para TBAI"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid "Tax agency selected: TicketBAI is activated."
msgstr "Agencia tributaria seleccionada: TicketBai está activado."
msgid "Tax agency selected: invoices will be sent by TicketBAI."
msgstr "Agencia tributaria seleccionada: las facturas las enviará TicketBAI."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Test license (Araba)"
msgstr "Licencia de prueba (Araba)"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Test license (Bizkaia)"
msgstr "Licencia de prueba (Bizkaia)"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Test license (Gipuzkoa)"
msgstr "Licencia de prueba (Gipuzkoa)"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid ""
"Test mode: EDI data is sent to separate test servers and is not considered "
"official."
msgstr ""
"Modo de prueba: los datos EDI se envían a servidores de prueba distintos y "
"no se consideran oficiales."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"There should be at least one tax set on each line in order to send to "
"TicketBAI."
msgstr ""
"Debería haber, como mínimo, un impuesto definido en cada línea para enviarlo "
"a TicketBAI."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
msgid "This entry has already been posted."
msgstr "Este asiento ya se ha publicado."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move_send.py:0
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.view_move_form_inherit_l10n_es_edi_tbai
msgid "TicketBAI"
msgstr "TicketBAI"
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.view_move_form_inherit_l10n_es_edi_tbai
msgid "TicketBAI Cancel"
msgstr "Cancelar TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_cancel_file
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_cancel_file
msgid "TicketBAI Cancel File"
msgstr "Cancelar archivo TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_cancel_file_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_cancel_file_name
msgid "TicketBAI Cancel File Name"
msgstr "Cancelar nombre de archivo TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_l10n_es_edi_tbai_document
msgid "TicketBAI Document"
msgstr "Documento TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_post_file_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_post_file_name
msgid "TicketBAI Post Attachment Name"
msgstr "Nombre del archivo adjunto de TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_post_file
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_post_file
msgid "TicketBAI Post File"
msgstr "Archivo de publicación de TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_chain_index
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_chain_index
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_chain_index
msgid "TicketBAI chain index"
msgstr "Índice de TicketBai en cadena"
msgstr "Índice de secuencia de TicketBAI"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "TicketBAI is not configured"
msgstr "TicketBai no está configurado"
msgstr "TicketBAI no está configurado"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_license_html
msgid "TicketBAI license"
msgstr "Licencia de TicketBai"
msgstr "Licencia de TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_is_required
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_is_required
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_is_required
msgid "TicketBAI required"
msgstr "Se requiere TicketBai"
msgstr "Se requiere TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_state
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_state
msgid "TicketBAI status"
msgstr "Estado de TicketBAI"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
msgid "TicketBAI: Cannot post a refund without source documents"
msgstr "TicketBAI: No se puede publicar un reembolso sin documentos de origen"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"TicketBAI: Cannot post a reversal move if its source documents (%s) have not"
" been posted"
"TicketBAI: Cannot post a reversal document while the source document has not "
"been posted"
msgstr ""
"TicketBAI: No se puede publicar un asiento de reversión mientras no se hayan"
" publicado sus documentos de origen (%s)"
"TicketBAI: No es posible publicar un documento de reversión mientras el "
"documento original no haya sido publicado"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"TicketBAI: Cannot post invoice while chain head (%s) has not been posted"
msgstr ""
@ -422,30 +675,49 @@ msgstr ""
msgid "TicketBai account.move chain sequence"
msgstr "Secuencia en cadena del account.move de TicketBai"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move__l10n_es_tbai_state__to_send
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__l10n_es_edi_tbai_document__state__to_send
msgid "To Send"
msgstr "Por enviar"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_test_env
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_res_config_settings__l10n_es_tbai_test_env
msgid "Use the test environment for TicketBAI"
msgstr "Utilizar entorno de prueba para TicketBAI"
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__xml_attachment_id
msgid "XML Attachment"
msgstr "Adjunto XML"
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
#, python-format
msgid "You cannot delete a move that has a TicketBAI chain id."
msgstr "No puede eliminar un asiento que tiene un ID en cadena de TicketBai."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
#, python-format
msgid ""
"You cannot reset to draft an entry that has been posted to TicketBAI's chain"
msgstr ""
"No puede restablecer a borrador un asiento que se ha publicado en la cadena "
"de TicketBai."
"No puede reestablecer a borrador un asiento que se publicó en la cadena de "
"TicketBai."
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
msgid ""
"You need to fill in the Reference field as the invoice number from your "
"vendor."
msgstr ""
"Debe rellenar el campo de referencia con el número de factura de su "
"proveedor."
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__state
msgid "status"
msgstr "estado"

View file

@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0+e\n"
"Project-Id-Version: Odoo Server 19.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-28 11:10+0000\n"
"PO-Revision-Date: 2025-01-28 11:10+0000\n"
"POT-Creation-Date: 2025-12-30 19:06+0000\n"
"PO-Revision-Date: 2025-12-30 19:06+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@ -27,16 +27,8 @@ msgid "1.1"
msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.template_LROE_240_inner
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.template_LROE_240_inner_recibidas
msgid "240"
msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid ""
"<span class=\"o_form_label\">Registro de Libros connection "
"SII/TicketBAI</span>"
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__l10n_es_edi_tbai_document__state__accepted
msgid "Accepted"
msgstr ""
#. module: l10n_es_edi_tbai
@ -45,34 +37,72 @@ msgid "Account Move Reversal"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_ir_attachment
msgid "Attachment"
#: model:ir.model,name:l10n_es_edi_tbai.model_account_move_send
msgid "Account Move Send"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_refund_reason
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_refund_reason
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move_reversal__l10n_es_tbai_refund_reason
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_refund_reason
msgid ""
"BOE-A-1992-28740. Ley 37/1992, de 28 de diciembre, del Impuesto sobre el "
"Valor Añadido. Artículo 80. Modificación de la base imponible."
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_cancel_xml
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_cancel_xml
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_cancel_xml
msgid "Cancellation XML"
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
msgid ""
"Be careful if you modified this vendor bill, because the official version is"
" still the previous one sent. "
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_cancel_xml
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_cancel_xml
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_cancel_xml
msgid ""
"Cancellation XML sent to TicketBAI. Kept if accepted or no response "
"(timeout), cleared otherwise."
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move__l10n_es_tbai_state__cancelled
msgid "Cancelled"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
msgid "Cannot send an entry that is not posted to TicketBAI."
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
msgid "Cannot send this entry as it is already being processed."
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_certificate_certificate
msgid "Certificate"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_certificate_id
msgid "Certificate (TicketBAI)"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_certificate_certificate__scope
msgid "Certificate scope"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.ui.menu,name:l10n_es_edi_tbai.menu_l10n_es_edi_tbai_certificates
msgid "Certificates"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.actions.act_window,name:l10n_es_edi_tbai.l10n_es_edi_tbai_certificate_action
msgid "Certificates for EDI TicketBAI invoices on Spain"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__chain_index
msgid "Chain Index"
msgstr ""
#. module: l10n_es_edi_tbai
@ -80,19 +110,52 @@ msgstr ""
msgid "Companies"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__company_id
msgid "Company"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_res_config_settings
msgid "Config Settings"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_account_edi_format
msgid "EDI format"
#: model_terms:ir.actions.act_window,help:l10n_es_edi_tbai.l10n_es_edi_tbai_certificate_action
msgid "Create the first certificate"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_account_edi_document
msgid "Electronic Document for an account.move"
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__create_uid
msgid "Created by"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__create_date
msgid "Created on"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__date
msgid "Date"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_line__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_reversal__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_send__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_certificate_certificate__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__display_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_config_settings__display_name
msgid "Display Name"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move_send.py:0
msgid "Error when sending the invoice to TicketBAI:"
msgstr ""
#. module: l10n_es_edi_tbai
@ -110,32 +173,64 @@ msgstr ""
msgid "Hacienda Foral de Gipuzkoa"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_line__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_reversal__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_send__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_certificate_certificate__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_config_settings__id
msgid "ID"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"In case of a foreign customer, you need to configure the tax scope on taxes:\n"
"%s"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"In order to use Ticketbai Batuz for freelancers, you will need to configure "
"the Epigrafe or Main Activity. In this version, you need to go in debug "
"mode to Settings > Technical > System Parameters and set the parameter "
"'l10n_es_edi_tbai.epigrafe'to your epigrafe number. You can find them in %s"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_reversed_ids
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_reversed_ids
msgid ""
"In the case where a vendor refund has multiple original invoices, you can "
"set them here. "
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_refund_reason
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_refund_reason
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_reversal__l10n_es_tbai_refund_reason
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_refund_reason
msgid "Invoice Refund Reason Code (TicketBai)"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_chain_index
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_chain_index
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_chain_index
msgid ""
"Invoice index in chain, set if and only if an in-chain XML was submitted and"
" did not error"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__is_cancel
msgid "Is Cancel"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move_reversal__l10n_es_tbai_is_required
msgid "Is TicketBai required for this reversal"
@ -144,7 +239,6 @@ msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_is_required
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_is_required
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_is_required
msgid "Is the Basque EDI (TicketBAI) needed ?"
msgstr ""
@ -153,30 +247,81 @@ msgstr ""
msgid "Journal Entry"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_account_move_line
msgid "Journal Item"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_cancel_document_id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_cancel_document_id
msgid "L10N Es Tbai Cancel Document"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_certificate_ids
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_config_settings__l10n_es_tbai_certificate_ids
msgid "L10N Es Tbai Certificate"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_is_enabled
msgid "L10N Es Tbai Is Enabled"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_post_document_id
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_post_document_id
msgid "L10N Es Tbai Post Document"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__write_uid
msgid "Last Updated by"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__write_date
msgid "Last Updated on"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Licence NIF"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Licence number"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.ui.menu,name:l10n_es_edi_tbai.menu_l10n_es_edi_tbai_license
msgid "Licenses (TicketBAI)"
msgid "Licenses"
msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid "Manage certificates (TicketBAI)"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__name
msgid "Name"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
msgid "No XML response received from LROE."
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "No XML response received."
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "No certificate found"
msgstr ""
#. module: l10n_es_edi_tbai
@ -185,38 +330,42 @@ msgid "No tax agency selected: TicketBAI not activated."
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_l10n_es_edi_certificate
msgid "Personal Digital Certificate"
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"No valid certificate found for this company, TicketBAI file will not be "
"signed.\n"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Please configure the Tax ID on your company for TicketBAI."
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
msgid "Please configure the certificate for TicketBAI/SII."
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Please configure the certificate for TicketBAI."
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Please specify a tax agency on your company for TicketBAI."
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Production license"
msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid "Production mode: EDI data is sent to the official agency servers."
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move__l10n_es_tbai_refund_reason__r1
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move_reversal__l10n_es_tbai_refund_reason__r1
@ -249,58 +398,102 @@ msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Refund reason cannot be R5 for non-simplified invoices (TicketBAI)"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Refund reason must be R5 for simplified invoices (TicketBAI)"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid "Refund reason must be specified (TicketBAI)"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_account_resequence_wizard
msgid "Remake the sequence of Journal Entries."
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_reversed_ids
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_reversed_ids
msgid "Refunded Vendor Bills"
msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid "Registro de Libros connection TicketBAI"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__l10n_es_edi_tbai_document__state__rejected
msgid "Rejected"
msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.view_move_form_inherit_l10n_es_edi_tbai
msgid "Resend to TicketBAI"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__response_message
msgid "Response Message"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/wizards/account_move_reversal.py:0
msgid "Reversals mixing invoices with and without TicketBAI are not allowed."
msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.view_move_form_inherit_l10n_es_edi_tbai
msgid "Send Bill to TicketBAI"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move_send.py:0
msgid "Send the e-invoice to the Basque Government."
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move__l10n_es_tbai_state__sent
msgid "Sent"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Software name"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Software version"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_post_xml
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_post_xml
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_post_xml
msgid "Submission XML"
#: model:ir.ui.menu,name:l10n_es_edi_tbai.menu_l10n_es_edi_tbai_root
msgid "Spain TicketBAI"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_post_xml
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_post_xml
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_post_xml
msgid ""
"Submission XML sent to TicketBAI. Kept if accepted or no response (timeout),"
" cleared otherwise."
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__certificate_certificate__scope__tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.certificate_certificate_view_search
msgid "TBAI"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_test_env
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_res_config_settings__l10n_es_tbai_test_env
msgid "TBAI Test Mode"
msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.certificate_certificate_view_search
msgid "TBAI certificates"
msgstr ""
#. module: l10n_es_edi_tbai
@ -316,46 +509,98 @@ msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid "Tax agency selected: TicketBAI is activated."
msgid "Tax agency selected: invoices will be sent by TicketBAI."
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Test license (Araba)"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Test license (Bizkaia)"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "Test license (Gipuzkoa)"
msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.res_config_settings_view_form
msgid ""
"Test mode: EDI data is sent to separate test servers and is not considered "
"official."
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"There should be at least one tax set on each line in order to send to "
"TicketBAI."
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
msgid "This entry has already been posted."
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move_send.py:0
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.view_move_form_inherit_l10n_es_edi_tbai
msgid "TicketBAI"
msgstr ""
#. module: l10n_es_edi_tbai
#: model_terms:ir.ui.view,arch_db:l10n_es_edi_tbai.view_move_form_inherit_l10n_es_edi_tbai
msgid "TicketBAI Cancel"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_cancel_file
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_cancel_file
msgid "TicketBAI Cancel File"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_cancel_file_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_cancel_file_name
msgid "TicketBAI Cancel File Name"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model,name:l10n_es_edi_tbai.model_l10n_es_edi_tbai_document
msgid "TicketBAI Document"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_post_file_name
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_post_file_name
msgid "TicketBAI Post Attachment Name"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_post_file
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_post_file
msgid "TicketBAI Post File"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_chain_index
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_chain_index
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_chain_index
msgid "TicketBAI chain index"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/res_company.py:0
#, python-format
msgid "TicketBAI is not configured"
msgstr ""
@ -367,30 +612,26 @@ msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_is_required
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_is_required
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_payment__l10n_es_tbai_is_required
msgid "TicketBAI required"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
msgid "TicketBAI: Cannot post a refund without source documents"
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_bank_statement_line__l10n_es_tbai_state
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_account_move__l10n_es_tbai_state
msgid "TicketBAI status"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"TicketBAI: Cannot post a reversal move if its source documents (%s) have not"
"TicketBAI: Cannot post a reversal document while the source document has not"
" been posted"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py:0
msgid ""
"TicketBAI: Cannot post invoice while chain head (%s) has not been posted"
msgstr ""
@ -400,26 +641,45 @@ msgstr ""
msgid "TicketBai account.move chain sequence"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__account_move__l10n_es_tbai_state__to_send
#: model:ir.model.fields.selection,name:l10n_es_edi_tbai.selection__l10n_es_edi_tbai_document__state__to_send
msgid "To Send"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_res_company__l10n_es_tbai_test_env
#: model:ir.model.fields,help:l10n_es_edi_tbai.field_res_config_settings__l10n_es_tbai_test_env
msgid "Use the test environment for TicketBAI"
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__xml_attachment_id
msgid "XML Attachment"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
#, python-format
msgid "You cannot delete a move that has a TicketBAI chain id."
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
#, python-format
msgid ""
"You cannot reset to draft an entry that has been posted to TicketBAI's chain"
msgstr ""
#. module: l10n_es_edi_tbai
#. odoo-python
#: code:addons/l10n_es_edi_tbai/models/account_edi_format.py:0
#, python-format
#: code:addons/l10n_es_edi_tbai/models/account_move.py:0
msgid ""
"You need to fill in the Reference field as the invoice number from your "
"vendor."
msgstr ""
#. module: l10n_es_edi_tbai
#: model:ir.model.fields,field_description:l10n_es_edi_tbai.field_l10n_es_edi_tbai_document__state
msgid "status"
msgstr ""

View file

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_edi_document
from . import account_edi_format
from . import account_move
from . import ir_attachment
from . import l10n_es_edi_tbai_certificate
from . import account_move_line
from . import certificate
from . import account_move_send
from . import l10n_es_edi_tbai_agencies
from . import l10n_es_edi_tbai_document
from . import res_company
from . import res_config_settings
from . import xml_utils
from . import l10n_es_edi_tbai_agencies

View file

@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountEdiDocument(models.Model):
_inherit = 'account.edi.document'
def _prepare_jobs(self):
"""
If there is a job to process that may already be part of the chain (posted invoice that timeout'ed),
Re-places it at the beginning of the list.
"""
# EXTENDS account_edi
jobs = super()._prepare_jobs()
if len(jobs) > 1:
move_first_index = 0
for index, job in enumerate(jobs):
documents = job['documents']
if any(d.edi_format_id.code == 'es_tbai' and d.state == 'to_send' and d.move_id.l10n_es_tbai_chain_index for d in documents):
move_first_index = index
break
jobs = [jobs[move_first_index]] + jobs[:move_first_index] + jobs[move_first_index + 1:]
return jobs

View file

@ -1,755 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import gzip
import json
from base64 import b64encode
from datetime import datetime
from re import sub as regex_sub
from uuid import uuid4
from markupsafe import Markup, escape
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.x509.oid import NameOID
from lxml import etree
from pytz import timezone
from requests.exceptions import RequestException
from odoo import _, models, release
from odoo.addons.l10n_es_edi_sii.models.account_edi_format import PatchedHTTPAdapter
from odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_agencies import get_key
from odoo.addons.l10n_es_edi_tbai.models.xml_utils import (
NS_MAP, bytes_as_block, calculate_references_digests,
cleanup_xml_signature, fill_signature, int_as_bytes)
from odoo.exceptions import UserError, ValidationError
from odoo.tools import get_lang
from odoo.tools.float_utils import float_repr
from odoo.tools.xml_utils import cleanup_xml_node, validate_xml_from_attachment
class AccountEdiFormat(models.Model):
_inherit = 'account.edi.format'
# -------------------------------------------------------------------------
# OVERRIDES & EXTENSIONS
# -------------------------------------------------------------------------
def _needs_web_services(self):
# EXTENDS account_edi
return self.code == 'es_tbai' or super()._needs_web_services()
def _is_enabled_by_default_on_journal(self, journal):
""" Disable SII by default on a new journal when tbai is installed"""
if self.code != 'es_sii':
return super()._is_enabled_by_default_on_journal(journal)
return False
def _is_compatible_with_journal(self, journal):
# EXTENDS account_edi
if self.code != 'es_tbai':
return super()._is_compatible_with_journal(journal)
return journal.country_code == 'ES' and journal.type in ('sale', 'purchase')
def _get_move_applicability(self, move):
# EXTENDS account_edi
self.ensure_one()
if self.code != 'es_tbai' or move.country_code != 'ES' or not move.l10n_es_tbai_is_required:
return super()._get_move_applicability(move)
return {
'post': self._l10n_es_tbai_post_invoice_edi,
'cancel': self._l10n_es_tbai_cancel_invoice_edi,
'edi_content': self._l10n_es_tbai_get_invoice_content_edi,
}
def _check_move_configuration(self, invoice):
# EXTENDS account_edi
errors = super()._check_move_configuration(invoice)
if self.code != 'es_tbai' or invoice.country_code != 'ES':
return errors
if invoice.is_purchase_document() and not invoice.ref:
errors.append(_("You need to fill in the Reference field as the invoice number from your vendor."))
# Ensure a certificate is available.
if not invoice.company_id.l10n_es_edi_certificate_id:
errors.append(_("Please configure the certificate for TicketBAI/SII."))
# Ensure a tax agency is available.
if not invoice.company_id.mapped('l10n_es_tbai_tax_agency')[0]:
errors.append(_("Please specify a tax agency on your company for TicketBAI."))
# Ensure a vat is available.
if not invoice.company_id.vat:
errors.append(_("Please configure the Tax ID on your company for TicketBAI."))
# Check the refund reason
if invoice.move_type == 'out_refund':
if not invoice.l10n_es_tbai_refund_reason:
raise ValidationError(_('Refund reason must be specified (TicketBAI)'))
if invoice._is_l10n_es_tbai_simplified():
if invoice.l10n_es_tbai_refund_reason != 'R5':
raise ValidationError(_('Refund reason must be R5 for simplified invoices (TicketBAI)'))
else:
if invoice.l10n_es_tbai_refund_reason == 'R5':
raise ValidationError(_('Refund reason cannot be R5 for non-simplified invoices (TicketBAI)'))
return errors
def _l10n_es_tbai_refunded_invoices(self, invoice):
return invoice.reversed_entry_id
def _l10n_es_tbai_post_invoice_edi(self, invoice):
# EXTENDS account_edi
if self.code != 'es_tbai':
return super()._post_invoice_edi(invoice)
if invoice.is_purchase_document():
inv_xml = False # For Ticketbai Batuz vendor bills, we get the values later as it does not need chaining, ...
else:
# Chain integrity check: chain head must have been REALLY posted (not timeout'ed)
# - If called from a cron, then the re-ordering of jobs should prevent this from triggering
# - If called manually, then the user will see this error pop up when it triggers
chain_head = invoice.company_id._get_l10n_es_tbai_last_posted_invoice()
error_msg = ''
if chain_head and chain_head != invoice and not chain_head._l10n_es_tbai_is_in_chain():
error_msg = _("TicketBAI: Cannot post invoice while chain head (%s) has not been posted", chain_head.name)
if invoice.move_type == 'out_refund':
refunded_invoices = self._l10n_es_tbai_refunded_invoices(invoice)
if not refunded_invoices:
error_msg = _("TicketBAI: Cannot post a refund without source documents")
else:
invalid_refunds = refunded_invoices.filtered(lambda inv:
not inv._l10n_es_tbai_is_in_chain()
and inv.edi_document_ids.filtered(lambda d: d.edi_format_id.code == 'es_tbai') # avoid imported ones
)
if invalid_refunds:
error_msg = _(
"TicketBAI: Cannot post a reversal move if its source documents (%s) have not been posted",
', '.join(invalid_refunds.mapped('name'))
)
# Tax configuration check: In case of foreign customer we need the tax scope to be set
com_partner = invoice.commercial_partner_id
if (com_partner.country_id.code not in ('ES', False) or (com_partner.vat or '').startswith("ESN")) and\
invoice.line_ids.tax_ids.filtered(lambda t: not t.tax_scope):
error_msg = _(
"In case of a foreign customer, you need to configure the tax scope on taxes:\n%s",
"\n".join(invoice.line_ids.tax_ids.mapped('name'))
)
if error_msg:
return {
invoice: {
'error': error_msg,
'blocking_level': 'error',
}
}
# Generate the XML values.
inv_dict = self._get_l10n_es_tbai_invoice_xml(invoice)
if 'error' in inv_dict[invoice]:
return inv_dict # XSD validation failed, return result dict
# Store the XML as attachment to ensure it is never lost (even in case of timeout error)
inv_xml = inv_dict[invoice]['xml_file']
invoice._update_l10n_es_tbai_submitted_xml(xml_doc=inv_xml, cancel=False)
# Assign unique 'chain index' from dedicated sequence
if not invoice.l10n_es_tbai_chain_index:
invoice.l10n_es_tbai_chain_index = invoice.company_id._get_l10n_es_tbai_next_chain_index()
# Call the web service and get response
res = self._l10n_es_tbai_post_to_web_service(invoice, inv_xml)
# SUCCESS
if res[invoice].get('success'):
# Create attachment
attachment = self.env['ir.attachment'].create({
'name': invoice.name + '_post.xml',
'datas': invoice.l10n_es_tbai_post_xml,
'mimetype': 'application/xml',
'res_id': invoice.id,
'res_model': 'account.move',
})
# Post attachment to chatter and save it as EDI document
test_suffix = '(test mode)' if invoice.company_id.l10n_es_edi_test_env else ''
invoice.with_context(no_new_invoice=True).message_post(
body=Markup("<pre>TicketBAI: posted emission XML {test_suffix}\n{message}</pre>").format(
test_suffix=test_suffix, message=res[invoice]['message']
),
attachment_ids=[attachment.id],
)
res[invoice]['attachment'] = attachment
# FAILURE
# NOTE: 'warning' means timeout so absolutely keep the XML and chain index
elif res[invoice].get('blocking_level') == 'error':
invoice._update_l10n_es_tbai_submitted_xml(xml_doc=None, cancel=False) # deletes XML
# delete index (avoids re-trying same XML and chaining off of it)
invoice.l10n_es_tbai_chain_index = False
return res
def _l10n_es_tbai_cancel_invoice_edi(self, invoice):
# EXTENDS account_edi
if self.code != 'es_tbai':
return super()._cancel_invoice_edi(invoice)
if invoice.is_purchase_document():
cancel_xml = False # Batuz specific
else:
# Generate the XML values.
cancel_dict = self._get_l10n_es_tbai_invoice_xml(invoice, cancel=True)
if 'error' in cancel_dict[invoice]:
return cancel_dict # XSD validation failed, return result dict
# Store the XML as attachment to ensure it is never lost (even in case of timeout error)
cancel_xml = cancel_dict[invoice]['xml_file']
invoice._update_l10n_es_tbai_submitted_xml(xml_doc=cancel_xml, cancel=True)
# Call the web service and get response
res = self._l10n_es_tbai_post_to_web_service(invoice, cancel_xml, cancel=True)
# SUCCESS
if res[invoice].get('success'):
# Create attachment
attachment = self.env['ir.attachment'].create({
'name': invoice.name + '_cancel.xml',
'datas': invoice.l10n_es_tbai_cancel_xml,
'mimetype': 'application/xml',
'res_id': invoice.id,
'res_model': 'account.move',
})
# Post attachment to chatter
test_suffix = '(test mode)' if invoice.company_id.l10n_es_edi_test_env else ''
invoice.with_context(no_new_invoice=True).message_post(
body=Markup("<pre>TicketBAI: posted cancellation XML {test_suffix}\n{message}</pre>").format(
test_suffix=test_suffix, message=res[invoice]['message']
),
attachment_ids=[attachment.id],
)
# FAILURE
# NOTE: 'warning' means timeout so absolutely keep the XML and chain index
elif res[invoice].get('blocking_level') == 'error':
invoice._update_l10n_es_tbai_submitted_xml(xml_doc=None, cancel=True) # will need to be re-created
return res
# -------------------------------------------------------------------------
# XML DOCUMENT
# -------------------------------------------------------------------------
def _l10n_es_tbai_validate_xml_with_xsd(self, xml_doc, cancel, tax_agency):
xsd_name = get_key(tax_agency, 'xsd_name')['cancel' if cancel else 'post']
try:
validate_xml_from_attachment(self.env, xml_doc, xsd_name, prefix='l10n_es_edi_tbai')
except UserError as e:
return {'error': escape(str(e)), 'blocking_level': 'error'}
return {}
def _l10n_es_tbai_get_invoice_content_edi(self, invoice):
cancel = invoice.edi_state in ('to_cancel', 'cancelled')
if invoice.is_purchase_document():
lroe_values = self._l10n_es_tbai_prepare_values_bi(invoice, False, cancel=cancel)
xml_str = self.env['ir.qweb']._render('l10n_es_edi_tbai.template_LROE_240_main_recibidas', lroe_values).encode()
else:
xml_tree = self._get_l10n_es_tbai_invoice_xml(invoice, cancel)[invoice]['xml_file']
xml_str = etree.tostring(xml_tree)
return xml_str
def _get_l10n_es_tbai_invoice_xml(self, invoice, cancel=False):
# If previously generated XML was posted and not rejected (success or timeout), reuse it
doc = invoice._get_l10n_es_tbai_submitted_xml(cancel)
if doc is not None:
return {invoice: {'xml_file': doc}}
# Otherwise, generate a new XML
values = {
**invoice.company_id._get_l10n_es_tbai_license_dict(),
**self._l10n_es_tbai_get_header_values(invoice),
**self._l10n_es_tbai_get_subject_values(invoice, cancel),
**self._l10n_es_tbai_get_invoice_values(invoice, cancel),
**self._l10n_es_tbai_get_trail_values(invoice, cancel),
'is_emission': not cancel,
'datetime_now': datetime.now(tz=timezone('Europe/Madrid')),
'format_date': lambda d: datetime.strftime(d, '%d-%m-%Y'),
'format_time': lambda d: datetime.strftime(d, '%H:%M:%S'),
'format_float': lambda f: float_repr(f, precision_digits=2),
}
template_name = 'l10n_es_edi_tbai.template_invoice_main' + ('_cancel' if cancel else '_post')
xml_str = self.env['ir.qweb']._render(template_name, values)
xml_doc = cleanup_xml_node(xml_str, remove_blank_nodes=False)
xml_doc = self._l10n_es_tbai_sign_invoice(invoice, xml_doc)
res = {invoice: {'xml_file': xml_doc}}
# Optional check using the XSD
res[invoice].update(self._l10n_es_tbai_validate_xml_with_xsd(xml_doc, cancel, invoice.company_id.l10n_es_tbai_tax_agency))
return res
def _l10n_es_tbai_get_header_values(self, invoice):
return {
'tbai_version': self.L10N_ES_TBAI_VERSION,
'odoo_version': release.version,
}
def _l10n_es_tbai_get_subject_values(self, invoice, cancel):
# === SENDER (EMISOR) ===
sender = invoice.company_id
values = {
'sender_vat': sender.vat[2:] if sender.vat.startswith('ES') else sender.vat,
'sender': sender,
}
if cancel:
return values # cancellation invoices do not specify recipients (they stay the same)
# NOTE: TicketBai supports simplified invoices WITH recipients but we don't for now (we should for POS)
# NOTE: TicketBAI credit notes for simplified invoices are ALWAYS simplified BUT can have a recipient even if invoice doesn't
if invoice._is_l10n_es_tbai_simplified():
return values # do not set 'recipient' unless there is an actual recipient (used as condition in template)
# === RECIPIENTS (DESTINATARIOS) ===
nif = False
alt_id_country = False
partner = invoice.commercial_partner_id
alt_id_number = partner.vat or 'NO_DISPONIBLE'
alt_id_type = ""
if (not partner.country_id or partner.country_id.code == 'ES') and partner.vat:
# ES partner with VAT.
nif = partner.vat[2:] if partner.vat.startswith('ES') else partner.vat
elif partner.country_id.code in self.env.ref('base.europe').country_ids.mapped('code'):
# European partner
alt_id_type = '02'
else:
# Non-european partner
if partner.vat:
alt_id_type = '04'
else:
alt_id_type = '06'
if partner.country_id:
alt_id_country = partner.country_id.code
values_dest = {
'nif': nif,
'alt_id_country': alt_id_country,
'alt_id_number': alt_id_number,
'alt_id_type': alt_id_type,
'partner': partner,
'partner_address': ', '.join(filter(None, [partner.street, partner.street2, partner.city])),
}
values.update({
'recipient': values_dest,
})
return values
def _l10n_es_tbai_get_invoice_values(self, invoice, cancel):
# Header
values = {'invoice': invoice}
if cancel:
return values
# Credit notes (factura rectificativa)
# NOTE values below would have to be adapted for purchase invoices (Bizkaia LROE)
values['is_refund'] = invoice.move_type == 'out_refund'
if values['is_refund']:
values['credit_note_code'] = invoice.l10n_es_tbai_refund_reason
values['credit_note_invoice'] = invoice.reversed_entry_id
# Lines (detalle)
refund_sign = (1 if values['is_refund'] else -1)
invoice_lines = []
for line in invoice.invoice_line_ids.filtered(lambda line: line.display_type not in ('line_section', 'line_note')):
if line.discount == 100.0:
inverse_currency_rate = abs(line.move_id.amount_total_signed / line.move_id.amount_total) if line.move_id.amount_total else 1
balance_before_discount = - line.price_unit * line.quantity * inverse_currency_rate
else:
balance_before_discount = line.balance / (1 - line.discount / 100)
discount = (balance_before_discount - line.balance)
line_price_total = self._l10n_es_tbai_get_invoice_line_price_total(line)
if not any([t.l10n_es_type == 'sujeto_isp' for t in line.tax_ids]):
total = line_price_total * abs(line.balance / line.amount_currency if line.amount_currency != 0 else 1) * -refund_sign
else:
total = abs(line.balance) * -refund_sign * (-1 if line_price_total < 0 else 1)
invoice_lines.append({
'line': line,
'discount': -discount,
'unit_price': -(line.balance + discount) / line.quantity if line.quantity else 0.0,
'total': total,
'description': regex_sub(r'[^0-9a-zA-Z ]', '', line.name or '')[:250]
})
values['invoice_lines'] = invoice_lines
# Tax details (desglose)
importe_total, desglose, amount_retention = self._l10n_es_tbai_get_importe_desglose(invoice)
values['amount_total'] = importe_total
values['invoice_info'] = desglose
values['amount_retention'] = amount_retention * refund_sign if amount_retention != 0.0 else 0.0
# Regime codes (ClaveRegimenEspecialOTrascendencia)
# NOTE there's 11 more codes to implement, also there can be up to 3 in total
# See https://www.gipuzkoa.eus/documents/2456431/13761128/Anexo+I.pdf/2ab0116c-25b4-f16a-440e-c299952d683d
com_partner = invoice.commercial_partner_id
# If an invoice line contains an OSS tax, the invoice is considered as an OSS operation
is_oss = self._has_oss_taxes(invoice)
if is_oss:
values['regime_key'] = ['17']
elif not com_partner.country_id or com_partner.country_id.code in self.env.ref('base.europe').country_ids.mapped('code'):
values['regime_key'] = ['01']
else:
values['regime_key'] = ['02']
values['nosujeto_causa'] = 'IE' if is_oss else 'RL'
return values
def _l10n_es_tbai_get_invoice_line_price_total(self, invoice_line):
price_total = invoice_line.price_total
retention_tax_lines = invoice_line.tax_ids.filtered(lambda t: t.l10n_es_type == "retencion")
if retention_tax_lines:
line_discount_price_unit = invoice_line.price_unit * (1 - (invoice_line.discount / 100.0))
tax_lines_no_retention = invoice_line.tax_ids - retention_tax_lines
if tax_lines_no_retention:
taxes_res = tax_lines_no_retention.compute_all(line_discount_price_unit,
quantity=invoice_line.quantity,
currency=invoice_line.currency_id,
product=invoice_line.product_id,
partner=invoice_line.move_id.partner_id,
is_refund=invoice_line.is_refund)
price_total = taxes_res['total_included']
return price_total
def _l10n_es_tbai_get_importe_desglose(self, invoice):
com_partner = invoice.commercial_partner_id
sign = -1 if invoice.move_type in ('out_refund', 'in_refund') else 1
if (com_partner.country_id.code in ('ES', False) and not (com_partner.vat or '').startswith("ESN")) \
or invoice._is_l10n_es_tbai_simplified():
tax_details_info_vals = self._l10n_es_edi_get_invoices_tax_details_info(invoice)
tax_amount_retention = tax_details_info_vals['tax_amount_retention']
desglose = {'DesgloseFactura': tax_details_info_vals['tax_details_info']}
desglose['DesgloseFactura'].update({'S1': tax_details_info_vals['S1_list'],
'S2': tax_details_info_vals['S2_list']})
importe_total = round(sign * (
tax_details_info_vals['tax_details']['base_amount']
+ tax_details_info_vals['tax_details']['tax_amount']
- tax_amount_retention
), 2)
else:
tax_details_info_service_vals = self._l10n_es_edi_get_invoices_tax_details_info(
invoice,
filter_invl_to_apply=lambda x: any(t.tax_scope == 'service' for t in x.tax_ids)
)
tax_details_info_consu_vals = self._l10n_es_edi_get_invoices_tax_details_info(
invoice,
filter_invl_to_apply=lambda x: any(t.tax_scope == 'consu' for t in x.tax_ids)
)
service_retention = tax_details_info_service_vals['tax_amount_retention']
consu_retention = tax_details_info_consu_vals['tax_amount_retention']
desglose = {}
if tax_details_info_service_vals['tax_details_info']:
desglose.setdefault('DesgloseTipoOperacion', {})
desglose['DesgloseTipoOperacion']['PrestacionServicios'] = tax_details_info_service_vals['tax_details_info']
desglose['DesgloseTipoOperacion']['PrestacionServicios'].update(
{'S1': tax_details_info_service_vals['S1_list'],
'S2': tax_details_info_service_vals['S2_list']})
if tax_details_info_consu_vals['tax_details_info']:
desglose.setdefault('DesgloseTipoOperacion', {})
desglose['DesgloseTipoOperacion']['Entrega'] = tax_details_info_consu_vals['tax_details_info']
desglose['DesgloseTipoOperacion']['Entrega'].update(
{'S1': tax_details_info_consu_vals['S1_list'],
'S2': tax_details_info_consu_vals['S2_list']})
importe_total = round(sign * (
tax_details_info_service_vals['tax_details']['base_amount']
+ tax_details_info_service_vals['tax_details']['tax_amount']
- service_retention
+ tax_details_info_consu_vals['tax_details']['base_amount']
+ tax_details_info_consu_vals['tax_details']['tax_amount']
- consu_retention
), 2)
tax_amount_retention = service_retention + consu_retention
return importe_total, desglose, tax_amount_retention
def _l10n_es_tbai_get_trail_values(self, invoice, cancel):
prev_invoice = invoice.company_id._get_l10n_es_tbai_last_posted_invoice(invoice)
# NOTE: assumtion that last posted == previous works because XML is generated on post
if prev_invoice and not cancel:
return {
'chain_prev_invoice': prev_invoice
}
else:
return {}
def _l10n_es_tbai_sign_invoice(self, invoice, xml_root):
company = invoice.company_id
cert_private, cert_public = (
company.l10n_es_edi_certificate_id.sudo()._get_key_pair()
)
public_key = cert_public.public_key()
# Identifiers
document_id = "Document-" + str(uuid4())
signature_id = "Signature-" + document_id
keyinfo_id = "KeyInfo-" + document_id
sigproperties_id = "SignatureProperties-" + document_id
# Render digital signature scaffold from QWeb
common_name = cert_public.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
org_unit = cert_public.issuer.get_attributes_for_oid(NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value
org_name = cert_public.issuer.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)[0].value
country_name = cert_public.issuer.get_attributes_for_oid(NameOID.COUNTRY_NAME)[0].value
values = {
'dsig': {
'document_id': document_id,
'x509_certificate': bytes_as_block(cert_public.public_bytes(encoding=serialization.Encoding.DER)),
'public_modulus': bytes_as_block(int_as_bytes(public_key.public_numbers().n)),
'public_exponent': bytes_as_block(int_as_bytes(public_key.public_numbers().e)),
'iso_now': datetime.now().isoformat(),
'keyinfo_id': keyinfo_id,
'signature_id': signature_id,
'sigproperties_id': sigproperties_id,
'reference_uri': "Reference-" + document_id,
'sigpolicy_url': get_key(company.l10n_es_tbai_tax_agency, 'sigpolicy_url'),
'sigpolicy_digest': get_key(company.l10n_es_tbai_tax_agency, 'sigpolicy_digest'),
'sigcertif_digest': b64encode(cert_public.fingerprint(hashes.SHA256())).decode(),
'x509_issuer_description': 'CN={}, OU={}, O={}, C={}'.format(common_name, org_unit, org_name, country_name),
'x509_serial_number': cert_public.serial_number,
}
}
xml_sig_str = self.env['ir.qweb']._render('l10n_es_edi_tbai.template_digital_signature', values)
xml_sig = cleanup_xml_signature(xml_sig_str)
# Complete document with signature template
xml_root.append(xml_sig)
# Compute digest values for references
calculate_references_digests(xml_sig.find("SignedInfo", namespaces=NS_MAP))
# Sign (writes into SignatureValue)
fill_signature(xml_sig, cert_private)
return xml_root
# -------------------------------------------------------------------------
# WEB SERVICE CALLS
# -------------------------------------------------------------------------
def _l10n_es_tbai_post_to_web_service(self, invoice, invoice_xml, cancel=False):
company = invoice.company_id
try:
# Call the web service, retrieve and parse response
success, message, response_xml = self._l10n_es_tbai_post_to_agency(
self.env, company.l10n_es_tbai_tax_agency, invoice, invoice_xml, cancel)
except (ValueError, RequestException) as e:
# In case of timeout / request exception, return warning
return {invoice: {
'error': str(e),
'blocking_level': 'warning',
'response': None,
}}
if success:
return {invoice: {
'success': True,
'message': message,
'response': response_xml,
}}
else:
return {invoice: {
'error': message,
'blocking_level': 'error',
'response': response_xml,
}}
# -------------------------------------------------------------------------
# WEB SERVICE METHODS
# -------------------------------------------------------------------------
# Provides helper methods for interacting with the Basque country's TicketBai servers.
L10N_ES_TBAI_VERSION = 1.2
def _l10n_es_tbai_post_to_agency(self, env, agency, invoice, invoice_xml, cancel=False):
if agency in ('araba', 'gipuzkoa'):
post_method, process_method = self._l10n_es_tbai_prepare_post_params_ar_gi, self._l10n_es_tbai_process_post_response_ar_gi
elif agency == 'bizkaia':
post_method, process_method = self._l10n_es_tbai_prepare_post_params_bi, self._l10n_es_tbai_process_post_response_bi
params = post_method(env, agency, invoice, invoice_xml, cancel)
response = self._l10n_es_tbai_send_request_to_agency(timeout=10, **params)
return process_method(env, response)
def _l10n_es_tbai_send_request_to_agency(self, *args, **kwargs):
session = requests.Session()
session.cert = kwargs.pop('pkcs12_data')
session.mount("https://", PatchedHTTPAdapter())
return session.request('post', *args, **kwargs)
def _l10n_es_tbai_prepare_post_params_ar_gi(self, env, agency, invoice, invoice_xml, cancel=False):
"""Web service parameters for Araba and Gipuzkoa."""
company = invoice.company_id
return {
'url': get_key(agency, 'cancel_url_' if cancel else 'post_url_', company.l10n_es_edi_test_env),
'headers': {"Content-Type": "application/xml; charset=utf-8"},
'pkcs12_data': company.l10n_es_edi_certificate_id,
'data': etree.tostring(invoice_xml, encoding='UTF-8'),
}
def _l10n_es_tbai_process_post_response_ar_gi(self, env, response):
"""Government response processing for Araba and Gipuzkoa."""
try:
response_xml = etree.fromstring(response.content)
except etree.XMLSyntaxError as e:
return False, e, None
# Error management
message = ''
already_received = False
# Get message in basque if env is in basque
msg_node_name = 'Azalpena' if get_lang(env).code == 'eu_ES' else 'Descripcion'
for xml_res_node in response_xml.findall(r'.//ResultadosValidacion'):
message_code = xml_res_node.find('Codigo').text
message += message_code + ": " + xml_res_node.find(msg_node_name).text + "\n"
if message_code in ('005', '019'):
already_received = True # error codes 5/19 mean XML was already received with that sequence
response_code = int(response_xml.find(r'.//Estado').text)
response_success = (response_code == 0) or already_received
return response_success, message, response_xml
def _l10n_es_tbai_get_in_invoice_values_batuz(self, invoice):
""" For the vendor bills for Bizkaia, the structure is different than the regular Ticketbai XML (LROE)"""
values = {
**self._l10n_es_tbai_get_subject_values(invoice, False),
**self._l10n_es_tbai_get_header_values(invoice),
**invoice._get_vendor_bill_tax_values(),
'invoice': invoice,
'datetime_now': datetime.now(tz=timezone('Europe/Madrid')),
'format_date': lambda d: datetime.strftime(d, '%d-%m-%Y'),
'format_time': lambda d: datetime.strftime(d, '%H:%M:%S'),
'format_float': lambda f: float_repr(f, precision_digits=2),
}
# Check if intracom
mod_303_10 = self.env.ref('l10n_es.mod_303_10')
mod_303_11 = self.env.ref('l10n_es.mod_303_11')
tax_tags = invoice.invoice_line_ids.tax_ids.invoice_repartition_line_ids.tag_ids
intracom = bool(tax_tags & (mod_303_10 + mod_303_11))
# Special regime for agriculture, livestock and fishing https://sede.agenciatributaria.gob.es/Sede/iva/regimenes-tributacion-iva/regimen-especial-agricultura-ganaderia-pesca.html
reagyp = invoice.invoice_line_ids.tax_ids.filtered(lambda t: t.l10n_es_type == 'sujeto_agricultura')
if intracom:
values['regime_key'] = ['09']
elif reagyp:
values['regime_key'] = ['02']
else:
values['regime_key'] = ['01']
# Credit notes (factura rectificativa)
values['is_refund'] = invoice.move_type == 'in_refund'
if values['is_refund']:
values['credit_note_code'] = invoice.l10n_es_tbai_refund_reason
values['credit_note_invoice'] = invoice.reversed_entry_id
if reagyp:
values['tipofactura'] = 'F6'
else:
values['tipofactura'] = 'F1'
return values
def _l10n_es_tbai_prepare_values_bi(self, invoice, invoice_xml, cancel=False):
sender = invoice.company_id
lroe_values = {
'is_emission': not cancel,
'sender': sender,
'sender_vat': sender.vat[2:] if sender.vat.startswith('ES') else sender.vat,
'fiscal_year': str(invoice.date.year),
}
if invoice.is_sale_document():
lroe_values.update({'tbai_b64_list': [b64encode(etree.tostring(invoice_xml, encoding="UTF-8")).decode()]})
else:
lroe_values.update(self._l10n_es_tbai_get_in_invoice_values_batuz(invoice))
return lroe_values
def _l10n_es_tbai_prepare_post_params_bi(self, env, agency, invoice, invoice_xml, cancel=False):
"""Web service parameters for Bizkaia."""
lroe_values = self._l10n_es_tbai_prepare_values_bi(invoice, invoice_xml, cancel=cancel)
if invoice.is_purchase_document():
lroe_str = env['ir.qweb']._render('l10n_es_edi_tbai.template_LROE_240_main_recibidas', lroe_values)
if cancel:
invoice.l10n_es_tbai_cancel_xml = b64encode(lroe_str.encode())
else:
invoice.l10n_es_tbai_post_xml = b64encode(lroe_str.encode())
else:
lroe_str = env['ir.qweb']._render('l10n_es_edi_tbai.template_LROE_240_main', lroe_values)
lroe_xml = cleanup_xml_node(lroe_str)
lroe_str = etree.tostring(lroe_xml, encoding="UTF-8")
lroe_bytes = gzip.compress(lroe_str)
company = invoice.company_id
return {
'url': get_key(agency, 'cancel_url_' if cancel else 'post_url_', company.l10n_es_edi_test_env),
'headers': {
'Accept-Encoding': 'gzip',
'Content-Encoding': 'gzip',
'Content-Length': str(len(lroe_str)),
'Content-Type': 'application/octet-stream',
'eus-bizkaia-n3-version': '1.0',
'eus-bizkaia-n3-content-type': 'application/xml',
'eus-bizkaia-n3-data': json.dumps({
'con': 'LROE',
'apa': '1.1' if invoice.is_sale_document() else '2',
'inte': {
'nif': lroe_values['sender_vat'],
'nrs': invoice.company_id.name,
},
'drs': {
'mode': '240',
# NOTE: modelo 140 for freelancers (in/out invoices)
# modelo 240 for legal entities (lots of account moves ?)
'ejer': str(invoice.date.year),
}
}),
},
'pkcs12_data': invoice.company_id.l10n_es_edi_certificate_id,
'data': lroe_bytes,
}
def _l10n_es_tbai_process_post_response_bi(self, env, response):
"""Government response processing for Bizkaia."""
# GLOBAL STATUS (LROE)
response_messages = []
response_success = True
if response.headers['eus-bizkaia-n3-tipo-respuesta'] != "Correcto":
code = response.headers['eus-bizkaia-n3-codigo-respuesta']
response_messages.append(code + ': ' + response.headers['eus-bizkaia-n3-mensaje-respuesta'])
response_success = False
response_data = response.content
response_xml = None
if response_data:
try:
response_xml = etree.fromstring(response_data)
except etree.XMLSyntaxError as e:
response_success = False
response_messages.append(str(e))
else:
response_success = False
response_messages.append(_('No XML response received from LROE.'))
# INVOICE STATUS (only one in batch)
# Get message in basque if env is in basque
if response_xml is not None:
msg_node_name = 'DescripcionErrorRegistro' + ('EU' if get_lang(env).code == 'eu_ES' else 'ES')
invoice_success = response_xml.find(r'.//EstadoRegistro').text == "Correcto"
if not invoice_success:
invoice_code = response_xml.find(r'.//CodigoErrorRegistro').text
if invoice_code == "B4_2000003": # already received
invoice_success = True
response_messages.append(invoice_code + ": " + (response_xml.find(rf'.//{msg_node_name}').text or ''))
return response_success and invoice_success, '<br/>'.join(response_messages), response_xml

View file

@ -1,92 +1,115 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from base64 import b64decode, b64encode
from datetime import datetime
from re import sub as regex_sub
from collections import defaultdict
from lxml import etree
from odoo import _, api, fields, models
from odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_agencies import get_key
from odoo.exceptions import UserError
from markupsafe import Markup
L10N_ES_TBAI_CRC8_TABLE = [
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
from odoo import _, api, fields, models
from odoo.exceptions import LockError, UserError
TBAI_REFUND_REASONS = [
('R1', "R1: Art. 80.1, 80.2, 80.6 and rights founded error"),
('R2', "R2: Art. 80.3"),
('R3', "R3: Art. 80.4"),
('R4', "R4: Art. 80 - other"),
('R5', "R5: Factura rectificativa en facturas simplificadas"),
]
class AccountMove(models.Model):
_inherit = 'account.move'
# Stored fields
l10n_es_tbai_state = fields.Selection([
('to_send', 'To Send'),
('sent', 'Sent'),
('cancelled', 'Cancelled'),
],
string='TicketBAI status',
compute='_compute_l10n_es_tbai_state',
)
l10n_es_tbai_chain_index = fields.Integer(
string="TicketBAI chain index",
help="Invoice index in chain, set if and only if an in-chain XML was submitted and did not error",
copy=False, readonly=True,
related='l10n_es_tbai_post_document_id.chain_index',
)
# Stored XML Binaries
l10n_es_tbai_post_xml = fields.Binary(
attachment=True, readonly=True, copy=False,
string="Submission XML",
help="Submission XML sent to TicketBAI. Kept if accepted or no response (timeout), cleared otherwise.",
l10n_es_tbai_post_document_id = fields.Many2one(
comodel_name='l10n_es_edi_tbai.document',
readonly=True,
copy=False,
)
l10n_es_tbai_cancel_xml = fields.Binary(
attachment=True, readonly=True, copy=False,
string="Cancellation XML",
help="Cancellation XML sent to TicketBAI. Kept if accepted or no response (timeout), cleared otherwise.",
l10n_es_tbai_cancel_document_id = fields.Many2one(
comodel_name='l10n_es_edi_tbai.document',
readonly=True,
copy=False,
)
l10n_es_tbai_post_file = fields.Binary(
string="TicketBAI Post File",
related='l10n_es_tbai_post_document_id.xml_attachment_id.datas',
)
l10n_es_tbai_post_file_name = fields.Char(
string="TicketBAI Post Attachment Name",
related="l10n_es_tbai_post_document_id.xml_attachment_id.name",
)
l10n_es_tbai_cancel_file = fields.Binary(
string="TicketBAI Cancel File",
related='l10n_es_tbai_cancel_document_id.xml_attachment_id.datas',
)
l10n_es_tbai_cancel_file_name = fields.Char(
string="TicketBAI Cancel File Name",
related='l10n_es_tbai_cancel_document_id.xml_attachment_id.name',
)
# Non-stored fields
l10n_es_tbai_is_required = fields.Boolean(
string="TicketBAI required",
help="Is the Basque EDI (TicketBAI) needed ?",
compute="_compute_l10n_es_tbai_is_required",
compute='_compute_l10n_es_tbai_is_required',
)
# Optional fields
l10n_es_tbai_refund_reason = fields.Selection(
selection=[
('R1', "R1: Art. 80.1, 80.2, 80.6 and rights founded error"),
('R2', "R2: Art. 80.3"),
('R3', "R3: Art. 80.4"),
('R4', "R4: Art. 80 - other"),
('R5', "R5: Factura rectificativa en facturas simplificadas"),
],
selection=TBAI_REFUND_REASONS,
string="Invoice Refund Reason Code (TicketBai)",
help="BOE-A-1992-28740. Ley 37/1992, de 28 de diciembre, del Impuesto sobre el "
"Valor Añadido. Artículo 80. Modificación de la base imponible.",
copy=False,
)
l10n_es_tbai_reversed_ids = fields.Many2many(
'account.move', 'account_move_tbai_reversed_moves', 'refund_id', 'reversed_move_id',
string="Refunded Vendor Bills",
domain="[('move_type', '=', 'in_invoice'), ('commercial_partner_id', '=', commercial_partner_id)]",
help="In the case where a vendor refund has multiple original invoices, you can set them here. ",
)
# -------------------------------------------------------------------------
# API-DECORATED & EXTENDED METHODS
# -------------------------------------------------------------------------
@api.depends('l10n_es_tbai_post_document_id.state', 'l10n_es_tbai_cancel_document_id.state')
def _compute_l10n_es_tbai_state(self):
for move in self:
state = 'to_send' if move.l10n_es_tbai_is_required else None
if move.l10n_es_tbai_post_document_id and move.l10n_es_tbai_post_document_id.state == 'accepted':
state = 'sent'
if move.l10n_es_tbai_cancel_document_id and move.l10n_es_tbai_cancel_document_id.state == 'accepted':
state = 'cancelled'
move.l10n_es_tbai_state = state
@api.depends('move_type', 'company_id')
def _compute_l10n_es_tbai_is_required(self):
for move in self:
move.l10n_es_tbai_is_required = (move.is_sale_document() or move.is_purchase_document() and move.company_id.l10n_es_tbai_tax_agency == 'bizkaia'
and not any(t.l10n_es_type == 'ignore' for t in move.invoice_line_ids.tax_ids))\
and move.country_code == 'ES' \
and move.company_id.l10n_es_tbai_tax_agency
move.l10n_es_tbai_is_required = (
move.company_id.l10n_es_tbai_is_enabled
and (
move.is_sale_document()
or move.is_purchase_document() and move.company_id.l10n_es_tbai_tax_agency == 'bizkaia'
)
and any(not line._l10n_es_tbai_is_ignored() for line in move.invoice_line_ids)
)
@api.depends('state', 'edi_document_ids.state')
@api.depends('l10n_es_tbai_post_document_id.chain_index')
def _compute_show_reset_to_draft_button(self):
# EXTENDS account_edi account.move
super()._compute_show_reset_to_draft_button()
@ -98,156 +121,238 @@ class AccountMove(models.Model):
def button_draft(self):
# EXTENDS account account.move
for move in self:
if move.l10n_es_tbai_chain_index and not move.edi_state == 'cancelled':
if move.l10n_es_tbai_chain_index and move.l10n_es_tbai_state != 'cancelled':
# NOTE this last condition (state is cancelled) is there because
# _postprocess_cancel_edi_results calls button_draft before
# calling button_cancel. Draft button does not appear for user.
# button_cancel calls button_draft.
# Draft button does not appear for user.
raise UserError(_("You cannot reset to draft an entry that has been posted to TicketBAI's chain"))
super().button_draft()
@api.ondelete(at_uninstall=False)
def _l10n_es_tbai_unlink_except_in_chain(self):
# Prevent deleting moves that are part of the TicketBAI chain
if not self._context.get('force_delete') and any(m.l10n_es_tbai_chain_index for m in self):
if not self.env.context.get('force_delete') and any(m.l10n_es_tbai_chain_index for m in self):
raise UserError(_('You cannot delete a move that has a TicketBAI chain id.'))
# -------------------------------------------------------------------------
# HELPER METHODS
# -------------------------------------------------------------------------
def _l10n_es_tbai_is_in_chain(self):
"""
True iff invoice has been posted to the chain and confirmed by govt.
Note that cancelled invoices remain part of the chain.
"""
tbai_doc_ids = self.edi_document_ids.filtered(lambda d: d.edi_format_id.code == 'es_tbai')
return self.l10n_es_tbai_is_required \
and len(tbai_doc_ids) > 0 \
and not any(tbai_doc_ids.filtered(lambda d: d.state == 'to_send'))
def _l10n_es_tbai_check_can_send(self):
# Ensure the move is posted
if self.state != 'posted':
return _("Cannot send an entry that is not posted to TicketBAI.")
if self.l10n_es_tbai_state in ('sent', 'cancelled') and not self.env.context.get('batuz_correction'):
return _("This entry has already been posted.")
if self.company_id.l10n_es_tbai_tax_agency == 'bizkaia' and self.is_purchase_document() and not self.ref:
return _("You need to fill in the Reference field as the invoice number from your vendor.")
def _get_l10n_es_tbai_sequence_and_number(self):
"""Get the TicketBAI sequence a number values for this invoice."""
self.ensure_one()
if self.is_purchase_document(): # Batuz
# Check if we are cancelling or not
doc = self.env['account.edi.document'].search([('state', '=', 'to_cancel'),
('edi_format_id.code', '=', 'es_tbai')], limit=1)
if doc and self.l10n_es_tbai_post_xml:
vals = self._get_l10n_es_tbai_values_from_xml({
'sequence': './/CabeceraFactura/SerieFactura',
'number': './/CabeceraFactura/NumFactura',
})
if vals['sequence'] and vals['number']:
return vals['sequence'], vals['number']
number = self.ref
sequence = "TEST" if self.company_id.l10n_es_edi_test_env else ""
else:
sequence = self.sequence_prefix.rstrip('/')
def _l10n_es_tbai_get_attachment_name(self, cancel=False):
return self.name + ('_post.xml' if not cancel else '_cancel.xml')
# NOTE non-decimal characters should not appear in the number
seq_length = self._get_sequence_format_param(self.name)[1]['seq_length']
number = f"{self.sequence_number:0{seq_length}d}"
sequence = regex_sub(r"[^0-9A-Za-z.\_\-\/]", "", sequence) # remove forbidden characters
sequence = regex_sub(r"\s+", " ", sequence) # no more than one consecutive whitespace allowed
# NOTE (optional) not recommended to use chars out of ([0123456789ABCDEFGHJKLMNPQRSTUVXYZ.\_\-\/ ])
sequence += "TEST" if self.company_id.l10n_es_edi_test_env else ""
return sequence, number
def _get_l10n_es_tbai_signature_and_date(self):
"""
Get the TicketBAI signature and registration date for this invoice.
Values are read directly from the 'post' XMLs submitted to the government \
(the 'cancel' XML is ignored).
The registration date is the date the invoice was registered into the govt's TicketBAI servers.
"""
self.ensure_one()
vals = self._get_l10n_es_tbai_values_from_xml({
'signature': r'.//{http://www.w3.org/2000/09/xmldsig#}SignatureValue',
'registration_date': r'.//CabeceraFactura//FechaExpedicionFactura'
def _l10n_es_tbai_create_edi_document(self, cancel=False):
name = self.name
if self.is_purchase_document():
name = self.ref
return self.env['l10n_es_edi_tbai.document'].sudo().create({
'name': name,
'date': self.date,
'company_id': self.company_id.id,
'is_cancel': cancel,
})
# RFC2045 - Base64 Content-Transfer-Encoding (page 25)
# Any characters outside of the base64 alphabet are to be ignored in base64-encoded data.
signature = vals['signature'].replace("\n", "")
registration_date = datetime.strptime(vals['registration_date'], '%d-%m-%Y')
return signature, registration_date
def _get_l10n_es_tbai_id(self):
"""Get the TicketBAI ID (TBAID) as defined in the TicketBAI doc."""
def _l10n_es_tbai_post_document_in_chatter(self, message, cancel=False):
test_suffix = '(test mode)' if self.company_id.l10n_es_tbai_test_env else ''
self.message_post(
body=Markup("<pre>TicketBAI: posted {document_type} XML {test_suffix}\n{message}</pre>").format(
document_type='emission' if not cancel else 'cancellation',
test_suffix=test_suffix,
message=message,
),
attachment_ids=[self.l10n_es_tbai_post_document_id.xml_attachment_id.id] if not cancel else [self.l10n_es_tbai_cancel_document_id.xml_attachment_id.id],
)
def _l10n_es_tbai_lock_move(self):
""" Acquire a write lock on the invoices in self. """
self.ensure_one()
if not self._l10n_es_tbai_is_in_chain():
return ''
try:
self.lock_for_update()
except LockError:
raise UserError(_('Cannot send this entry as it is already being processed.'))
signature, registration_date = self._get_l10n_es_tbai_signature_and_date()
company = self.company_id
tbai_id_no_crc = '-'.join([
'TBAI',
str(company.vat[2:] if company.vat.startswith('ES') else company.vat),
datetime.strftime(registration_date, '%d%m%y'),
signature[:13],
'' # CRC
])
return tbai_id_no_crc + self._l10n_es_edi_tbai_crc8(tbai_id_no_crc)
# -------------------------------------------------------------------------
# WEB SERVICE CALLS
# -------------------------------------------------------------------------
def _get_l10n_es_tbai_qr(self):
"""Returns the URL for the invoice's QR code. We can not use url_encode because it escapes / e.g."""
def l10n_es_tbai_resend_bill(self):
self.ensure_one()
if not self._l10n_es_tbai_is_in_chain():
return ''
self.l10n_es_tbai_post_document_id = False
if error := self.with_context(batuz_correction=True)._l10n_es_tbai_post():
error = error + "\n\n" + _("Be careful if you modified this vendor bill, "
"because the official version is still the previous one sent. ")
raise UserError(error) # This way, we rollback when rejected and the old accepted document is kept
company = self.company_id
sequence, number = self._get_l10n_es_tbai_sequence_and_number()
tbai_qr_no_crc = get_key(company.l10n_es_tbai_tax_agency, 'qr_url_', company.l10n_es_edi_test_env) + '?' + '&'.join([
'id=' + self._get_l10n_es_tbai_id(),
's=' + sequence,
'nf=' + number,
'i=' + self._get_l10n_es_tbai_values_from_xml({'importe': r'.//ImporteTotalFactura'})['importe']
])
qr_url = tbai_qr_no_crc + '&cr=' + self._l10n_es_edi_tbai_crc8(tbai_qr_no_crc)
return qr_url
def l10n_es_tbai_send_bill(self):
for bill in self:
error = bill._l10n_es_tbai_post()
if self.env['account.move.send']._can_commit():
self.env.cr.commit()
if error:
raise UserError(error)
def _l10n_es_edi_tbai_crc8(self, data):
crc = 0x0
for c in data:
crc = L10N_ES_TBAI_CRC8_TABLE[(crc ^ ord(c)) & 0xFF]
return '{:03d}'.format(crc & 0xFF)
def l10n_es_tbai_cancel(self):
for invoice in self:
invoice._l10n_es_tbai_lock_move()
def _get_l10n_es_tbai_values_from_xml(self, xpaths):
"""
This function reads values directly from the 'post' XML submitted to the government \
(the 'cancel' XML is ignored).
"""
res = dict.fromkeys(xpaths, '')
doc_xml = self._get_l10n_es_tbai_submitted_xml()
if doc_xml is None:
return res
for key, value in xpaths.items():
res[key] = doc_xml.find(value).text
return res
if invoice.l10n_es_tbai_cancel_document_id and invoice.l10n_es_tbai_cancel_document_id.state == 'rejected':
invoice.l10n_es_tbai_cancel_document_id.sudo().unlink()
def _get_l10n_es_tbai_submitted_xml(self, cancel=False):
"""Returns the XML object representing the post or cancel document."""
if not invoice.l10n_es_tbai_cancel_document_id:
invoice.l10n_es_tbai_cancel_document_id = invoice._l10n_es_tbai_create_edi_document(cancel=True)
edi_document = invoice.l10n_es_tbai_cancel_document_id
error = edi_document._post_to_web_service(invoice._l10n_es_tbai_get_values(cancel=True))
if error:
raise UserError(error)
if edi_document.state == 'accepted':
invoice.button_cancel()
invoice._l10n_es_tbai_post_document_in_chatter(edi_document.response_message, cancel=True)
if self.env['account.move.send']._can_commit():
self.env.cr.commit()
if edi_document.state != 'accepted':
raise UserError(edi_document.response_message)
def _l10n_es_tbai_post(self):
self.ensure_one()
self = self.with_context(bin_size=False)
doc = self.l10n_es_tbai_cancel_xml if cancel else self.l10n_es_tbai_post_xml
if not doc:
return None
return etree.fromstring(b64decode(doc))
def _update_l10n_es_tbai_submitted_xml(self, xml_doc, cancel):
"""Updates the binary data of the post or cancel document, from its XML object."""
# Avoid the move to be sent if it is being modified by a parallel transaction (for example reset to draft)
# It will also avoid the move to be sent by different parallel transactions
self._l10n_es_tbai_lock_move()
error = self._l10n_es_tbai_check_can_send()
if error:
return error
if self.l10n_es_tbai_post_document_id and self.l10n_es_tbai_post_document_id.state == 'rejected':
self.l10n_es_tbai_post_document_id.sudo().unlink()
if not self.l10n_es_tbai_post_document_id:
self.l10n_es_tbai_post_document_id = self._l10n_es_tbai_create_edi_document()
edi_document = self.l10n_es_tbai_post_document_id
error = edi_document._post_to_web_service(self._l10n_es_tbai_get_values())
if error:
return error
if edi_document.state == 'accepted':
self._l10n_es_tbai_post_document_in_chatter(edi_document.response_message)
return
# Return the error message if the xml document was not accepted
return edi_document.response_message
# -------------------------------------------------------------------------
# XML DOCUMENT
# -------------------------------------------------------------------------
def _l10n_es_tbai_get_values(self, cancel=False):
values = {
'is_sale': self.is_sale_document(),
'partner': self.commercial_partner_id,
'is_simplified': self.l10n_es_is_simplified,
'delivery_date': self.delivery_date if self.delivery_date != fields.Datetime.today() else None,
**self._l10n_es_tbai_get_attachment_values(cancel),
}
if values['is_sale']:
values.update(self._l10n_es_tbai_get_invoice_values(cancel=cancel))
elif self.company_id.l10n_es_tbai_tax_agency == 'bizkaia':
values.update(self._l10n_es_tbai_get_vendor_bill_values_batuz())
return values
def _l10n_es_tbai_get_attachment_values(self, cancel=False):
return {
'attachment_name': self._l10n_es_tbai_get_attachment_name(cancel=cancel),
'res_model': 'account.move',
'res_id': self.id,
}
def _l10n_es_tbai_get_invoice_values(self, cancel=False):
self.ensure_one()
b64_doc = b'' if xml_doc is None else b64encode(etree.tostring(xml_doc, encoding='UTF-8'))
if cancel:
self.l10n_es_tbai_cancel_xml = b64_doc
base_amls = self.line_ids.filtered(lambda x: x.display_type == 'product')
base_lines = [self._prepare_product_base_line_for_taxes_computation(x) for x in base_amls]
for base_line in base_lines:
base_line['name'] = base_line['record'].name
tax_amls = self.line_ids.filtered('tax_repartition_line_id')
tax_lines = [self._prepare_tax_line_for_taxes_computation(x) for x in tax_amls]
self.env['l10n_es_edi_tbai.document']._add_base_lines_tax_amounts(base_lines, self.company_id, tax_lines=tax_lines)
for base_line in base_lines:
sign = base_line['is_refund'] and -1 or 1
base_line['gross_price_unit'] = sign * base_line['gross_price_unit']
base_line['discount_amount'] = sign * base_line['discount_amount']
base_line['price_total'] = sign * base_line['price_total']
taxes = self.invoice_line_ids.tax_ids.flatten_taxes_hierarchy()
is_oss = any(tax._l10n_es_get_regime_code() == '17' for tax in taxes)
return {
**self._l10n_es_tbai_get_credit_note_values(),
'origin': self.invoice_origin and self.invoice_origin[:250] or 'manual',
'taxes': taxes,
'rate': abs(self.amount_total / self.amount_total_signed) if self.amount_total else 1,
'base_lines': base_lines,
'nosujeto_causa': 'IE' if is_oss else 'RL',
**({'post_doc': self.l10n_es_tbai_post_document_id} if cancel else {}),
}
def _l10n_es_tbai_get_credit_note_values(self):
return {
'is_refund': self.move_type == 'out_refund',
'refund_reason': self.l10n_es_tbai_refund_reason,
'refunded_doc': self.reversed_entry_id.l10n_es_tbai_post_document_id,
'refunded_doc_invoice_date': self.reversed_entry_id.invoice_date if self.reversed_entry_id else False,
'refunded_name': self.reversed_entry_id.name if self.reversed_entry_id else False,
}
def _l10n_es_tbai_get_vendor_bill_values_batuz(self):
""" For the vendor bills for Bizkaia, the structure is different than the regular Ticketbai XML (LROE)"""
values = {
'ref': self.ref,
'is_refund': self.move_type == 'in_refund',
'invoice_date': self.invoice_date,
**self._l10n_es_tbai_get_vendor_bill_tax_values(),
}
# Check if intracom
mod_303_10 = self.env.ref('l10n_es.mod_303_casilla_10_balance')._get_matching_tags()
mod_303_11 = self.env.ref('l10n_es.mod_303_casilla_11_balance')._get_matching_tags()
tax_tags = self.invoice_line_ids.tax_ids.flatten_taxes_hierarchy().repartition_line_ids.tag_ids
intracom = bool(tax_tags & (mod_303_10 + mod_303_11))
reagyp = self.invoice_line_ids.tax_ids.filtered(lambda t: t.l10n_es_type == 'sujeto_agricultura')
if intracom:
values['regime_key'] = ['09']
elif reagyp:
values['regime_key'] = ['19']
else:
self.l10n_es_tbai_post_xml = b64_doc
values['regime_key'] = ['01']
# Credit notes (factura rectificativa)
if values['is_refund']:
values['refund_reason'] = self.l10n_es_tbai_refund_reason
values['credit_note_invoices'] = self.reversed_entry_id | self.l10n_es_tbai_reversed_ids
if reagyp:
values['tipofactura'] = 'F6'
elif self._l10n_es_is_dua():
values['tipofactura'] = 'F5'
else:
values['tipofactura'] = 'F1'
return values
def _is_l10n_es_tbai_simplified(self):
return self.commercial_partner_id == self.env.ref("l10n_es_edi_sii.partner_simplified")
def _get_vendor_bill_tax_values(self):
def _l10n_es_tbai_get_vendor_bill_tax_values(self):
self.ensure_one()
results = defaultdict(lambda: {'base_amount': 0.0, 'tax_amount': 0.0})
amount_total = 0.0
@ -259,20 +364,24 @@ class AccountMove(models.Model):
for tax in line.tax_ids.filtered(lambda t: t.l10n_es_type not in ('recargo', 'retencion')):
results[tax]['base_amount'] += line.balance
tax = line.tax_line_id
if (tax and tax.l10n_es_type not in ('recargo', 'retencion') and
if ((tax := line.tax_line_id) and tax.l10n_es_type not in ('recargo', 'retencion') and
line.tax_repartition_line_id.factor_percent != -100.0):
results[tax]['tax_amount'] += line.balance
iva_values = []
for tax in results:
code = "C" # Bienes Corrientes
code = "C" # Bienes Corrientes
if tax.l10n_es_bien_inversion:
code = "I" # Investment Goods
code = "I" # Investment Goods
if tax.tax_scope == 'service':
code = 'G' # Gastos
code = 'G' # Gastos
iva_values.append({'base': results[tax]['base_amount'],
'code': code,
'tax': results[tax]['tax_amount'],
'rec': tax})
return {'iva_values': iva_values,
'amount_total': amount_total}
def _refunds_origin_required(self):
if self.l10n_es_tbai_is_required:
return True
return super()._refunds_origin_required()

View file

@ -0,0 +1,10 @@
from odoo import models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
def _l10n_es_tbai_is_ignored(self):
self.ensure_one()
return 'ignore' in self.tax_ids.mapped('l10n_es_type')

View file

@ -0,0 +1,63 @@
from odoo import _, api, models
class AccountMoveSend(models.AbstractModel):
_inherit = 'account.move.send'
@api.model
def _is_tbai_applicable(self, move):
return move.l10n_es_tbai_is_required and move.l10n_es_tbai_state == 'to_send'
def _get_all_extra_edis(self) -> dict:
# EXTENDS 'account'
res = super()._get_all_extra_edis()
res.update({'es_tbai': {'label': _("TicketBAI"), 'is_applicable': self._is_tbai_applicable, 'help': _('Send the e-invoice to the Basque Government.')}})
return res
# -------------------------------------------------------------------------
# ATTACHMENTS
# -------------------------------------------------------------------------
def _get_invoice_extra_attachments(self, move):
# EXTENDS 'account'
return super()._get_invoice_extra_attachments(move) + move.l10n_es_tbai_post_document_id.xml_attachment_id
def _get_placeholder_mail_attachments_data(self, move, invoice_edi_format=None, extra_edis=None, pdf_report=None):
# EXTENDS 'account'
results = super()._get_placeholder_mail_attachments_data(move, invoice_edi_format=invoice_edi_format, extra_edis=extra_edis, pdf_report=pdf_report)
if (
not move.l10n_es_tbai_post_document_id.xml_attachment_id
and 'es_tbai' in extra_edis
):
filename = move._l10n_es_tbai_get_attachment_name()
results.append({
'id': f'placeholder_{filename}',
'name': filename,
'mimetype': 'application/xml',
'placeholder': True,
})
return results
# -------------------------------------------------------------------------
# SENDING METHODS
# -------------------------------------------------------------------------
def _call_web_service_before_invoice_pdf_render(self, invoices_data):
# EXTENDS 'account'
super()._call_web_service_before_invoice_pdf_render(invoices_data)
for invoice, invoice_data in invoices_data.items():
if 'es_tbai' in invoice_data['extra_edis']:
error = invoice._l10n_es_tbai_post()
if error:
invoice_data['error'] = {
'error_title': _("Error when sending the invoice to TicketBAI:"),
'errors': [error],
}
if self._can_commit():
self.env.cr.commit()

View file

@ -0,0 +1,27 @@
import base64
from cryptography import x509
from odoo import fields, models
class CertificateCertificate(models.Model):
_inherit = 'certificate.certificate'
scope = fields.Selection(
selection_add=[
('tbai', 'TBAI')
],
)
def _l10n_es_edi_tbai_get_issuer(self):
self.ensure_one()
cert = x509.load_pem_x509_certificate(base64.b64decode(self.pem_certificate))
common_name = cert.issuer.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0].value
org_unit = cert.issuer.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value
org_name = cert.issuer.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATION_NAME)[0].value
country_name = cert.issuer.get_attributes_for_oid(x509.oid.NameOID.COUNTRY_NAME)[0].value
return f'CN={common_name}, OU={org_unit}, O={org_name}, C={country_name}'

View file

@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, api
from odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_agencies import get_key
from odoo.tools import xml_utils
class IrAttachment(models.Model):
_inherit = 'ir.attachment'
@api.model
def action_download_xsd_files(self):
"""
Downloads the TicketBAI XSD validation files if they don't already exist, for the active tax agency.
"""
xml_utils.load_xsd_files_from_url(
self.env, 'https://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd', 'xmldsig-core-schema.xsd',
xsd_name_prefix='l10n_es_edi_tbai')
for agency in ['gipuzkoa', 'araba', 'bizkaia']:
urls = get_key(agency, 'xsd_url')
names = get_key(agency, 'xsd_name')
# For Bizkaia, one url per XSD (post/cancel)
if isinstance(urls, dict):
for move_type in ('post', 'cancel'):
xml_utils.load_xsd_files_from_url(
self.env, urls[move_type], names[move_type],
xsd_name_prefix='l10n_es_edi_tbai',
)
# For other agencies, single url to zip file (only keep the desired names)
else:
xml_utils.load_xsd_files_from_url(
self.env, urls, # NOTE: file_name discarded when XSDs bundled in ZIPs
xsd_name_prefix='l10n_es_edi_tbai',
xsd_names_filter=list(names.values()),
)
return super().action_download_xsd_files()

View file

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from base64 import b64decode
from odoo import models
from odoo.addons.account.tools.certificate import load_key_and_certificates
class Certificate(models.Model):
_inherit = 'l10n_es_edi.certificate'
# -------------------------------------------------------------------------
# HELPERS
# -------------------------------------------------------------------------
def _get_key_pair(self):
self.ensure_one()
if not self.password:
return None, None
private_key, certificate = load_key_and_certificates(
b64decode(self.with_context(bin_size=False).content), # Without bin_size=False, size is returned instead of content
self.password.encode(),
)
return private_key, certificate

View file

@ -0,0 +1,875 @@
import gzip
import json
import re
import base64
from datetime import datetime
from uuid import uuid4
import requests
from lxml import etree
from pytz import timezone
from requests.exceptions import RequestException
from odoo import _, api, fields, models, release
from odoo.addons.certificate.tools import CertificateAdapter
from odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_agencies import get_key
from odoo.addons.l10n_es_edi_tbai.models.xml_utils import (
NS_MAP,
calculate_references_digests,
canonicalize_node,
cleanup_xml_signature,
)
from odoo.exceptions import UserError
from odoo.tools import get_lang
from odoo.tools.float_utils import float_repr, float_round
from odoo.tools.xml_utils import cleanup_xml_node
CRC8_TABLE = [
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
]
class L10n_Es_Edi_TbaiDocument(models.Model):
_name = 'l10n_es_edi_tbai.document'
_description = 'TicketBAI Document'
name = fields.Char(
required=True,
readonly=True,
)
date = fields.Date(
required=True,
readonly=True,
)
xml_attachment_id = fields.Many2one(
comodel_name='ir.attachment',
string="XML Attachment",
copy=False,
readonly=True,
)
company_id = fields.Many2one(
'res.company',
required=True,
)
state = fields.Selection([
('to_send', "To Send"),
('accepted', "Accepted"),
('rejected', "Rejected"),
],
string="status",
default='to_send',
copy=False,
readonly=True,
)
chain_index = fields.Integer(
copy=False,
readonly=True,
)
response_message = fields.Text(
copy=False,
readonly=True,
)
is_cancel = fields.Boolean(
default=False,
readonly=True,
)
# -------------------------------------------------------------------------
# HELPER METHODS
# -------------------------------------------------------------------------
def _is_in_chain(self):
"""True iff the document has been posted to the chain and confirmed by govt."""
return self.chain_index and self.state == 'accepted'
def _check_can_post(self, values):
# Ensure a certificate is available.
if not self.company_id.l10n_es_tbai_certificate_id:
return _("Please configure the certificate for TicketBAI.")
# Ensure a tax agency is available.
if not self.company_id.l10n_es_tbai_tax_agency:
return _("Please specify a tax agency on your company for TicketBAI.")
# Ensure a vat is available.
if not self.company_id.vat:
return _("Please configure the Tax ID on your company for TicketBAI.")
if self.company_id.l10n_es_tbai_tax_agency == 'bizkaia' and self.company_id._l10n_es_freelancer() and not self.env['ir.config_parameter'].sudo().get_param('l10n_es_edi_tbai.epigrafe', False):
return _("In order to use Ticketbai Batuz for freelancers, you will need to configure the "
"Epigrafe or Main Activity. In this version, you need to go in debug mode to "
"Settings > Technical > System Parameters and set the parameter 'l10n_es_edi_tbai.epigrafe'"
"to your epigrafe number. You can find them in %s",
"https://www.batuz.eus/fitxategiak/batuz/lroe/batuz_lroe_lista_epigrafes_v1_0_3.xlsx")
if values['is_sale'] and not self.is_cancel:
if any(not base_line['tax_ids'] for base_line in values['base_lines']):
return self.env._("There should be at least one tax set on each line in order to send to TicketBAI.")
# Chain integrity check: chain head must have been REALLY posted
chain_head_doc = self.company_id._get_l10n_es_tbai_last_chained_document()
if chain_head_doc and chain_head_doc != self and chain_head_doc.state != 'accepted':
return _("TicketBAI: Cannot post invoice while chain head (%s) has not been posted", chain_head_doc.name)
# Tax configuration check: In case of foreign customer we need the tax scope to be set
if values['partner'] and values['partner']._l10n_es_is_foreign() and values['taxes'].filtered(lambda t: not t.tax_scope):
return _(
"In case of a foreign customer, you need to configure the tax scope on taxes:\n%s",
"\n".join(values['taxes'].mapped('name'))
)
if values['is_refund']:
refunded_doc = values['refunded_doc']
refund_reason = values['refund_reason']
refunded_doc_invoice_date = values['refunded_doc_invoice_date']
is_simplified = values['is_simplified']
if not refunded_doc or refunded_doc.state == 'to_send':
invoice_sent_before_original = True
if not refunded_doc and refunded_doc_invoice_date:
domain = [('date', '<', refunded_doc_invoice_date),
('company_id', '=', self.company_id.id),
('chain_index', '!=', 0)]
invoice_sent_before_original = self.search(domain, order="date", limit=1)
if invoice_sent_before_original: # No error if the original invoice was imported from a previous system
return _("TicketBAI: Cannot post a reversal document while the source document has not been posted")
if not refund_reason:
return _('Refund reason must be specified (TicketBAI)')
if is_simplified and refund_reason != 'R5':
return _('Refund reason must be R5 for simplified invoices (TicketBAI)')
if not is_simplified and refund_reason == 'R5':
return _('Refund reason cannot be R5 for non-simplified invoices (TicketBAI)')
# -------------------------------------------------------------------------
# WEB SERVICE CALLS
# -------------------------------------------------------------------------
def _post_to_web_service(self, values):
self.ensure_one()
error = self._check_can_post(values)
if error:
return error
if not self.xml_attachment_id:
self._generate_xml(values)
if (
not self.chain_index
and not self.is_cancel
and values['is_sale']
):
# Assign unique 'chain index' from dedicated sequence
self.sudo().chain_index = self.company_id._get_l10n_es_tbai_next_chain_index()
try:
# Call the web service, retrieve and parse response
success, response_msgs = self._post_to_agency(self.env, values['is_sale'])
except (RequestException) as e:
# In case of timeout / request exception
self.sudo().response_message = e
return
self.sudo().response_message = '\n'.join(response_msgs)
if success:
self.sudo().state = 'accepted'
else:
self.sudo().state = 'rejected'
self.sudo().chain_index = 0
def _post_to_agency(self, env, is_sale):
def _send_request_to_agency(*args, **kwargs):
session = requests.Session()
session.cert = kwargs.pop('pkcs12_data')
session.mount("https://", CertificateAdapter())
response = session.request('post', *args, **kwargs)
response.raise_for_status()
response_xml = None
error = None
if response.content:
try:
response_xml = etree.fromstring(response.content)
except etree.XMLSyntaxError as e:
error = str(e)
else:
error = self.env._('No XML response received.')
return response.headers, response_xml, [error] if error else []
if self.company_id.l10n_es_tbai_tax_agency in ('araba', 'gipuzkoa'):
params = self._prepare_post_params_ar_gi()
_response_headers, response_xml, errors = _send_request_to_agency(timeout=10, **params)
if errors:
return False, errors
return self._process_post_response_xml_ar_gi(env, response_xml)
elif self.company_id.l10n_es_tbai_tax_agency == 'bizkaia':
params = self._prepare_post_params_bi(is_sale)
response_headers, response_xml, errors = _send_request_to_agency(timeout=10, **params)
if response_headers['eus-bizkaia-n3-tipo-respuesta'] != "Correcto":
error_code = response_headers['eus-bizkaia-n3-codigo-respuesta']
error_msg = response_headers['eus-bizkaia-n3-mensaje-respuesta']
errors.append(error_code + ": " + error_msg)
success, errors_add = self._process_post_response_xml_bi(env, response_xml)
errors += errors_add
return success, errors
def _prepare_post_params_ar_gi(self):
"""Web service parameters for Araba and Gipuzkoa."""
company = self.company_id
return {
'url': get_key(self.company_id.l10n_es_tbai_tax_agency, 'cancel_url_' if self.is_cancel else 'post_url_', company.l10n_es_tbai_test_env),
'headers': {"Content-Type": "application/xml; charset=utf-8"},
'pkcs12_data': company.l10n_es_tbai_certificate_id,
'data': self.xml_attachment_id.raw,
}
@api.model
def _process_post_response_xml_ar_gi(self, env, response_xml):
"""Government response processing for Araba and Gipuzkoa."""
success = int(response_xml.findtext('.//Estado')) == 0
response_msgs = []
# Get message in basque if env is in basque
msg_node_name = 'Azalpena' if get_lang(env).code == 'eu_ES' else 'Descripcion'
for res_node in response_xml.findall('.//ResultadosValidacion'):
msg_code = res_node.findtext('Codigo')
response_msgs.append(msg_code + ": " + res_node.findtext(msg_node_name))
if msg_code in ('005', '019'):
success = True # error codes 5/19 mean XML was already received with that sequence
return success, response_msgs
def _prepare_post_params_bi(self, is_sale):
"""Web service parameters for Bizkaia."""
company = self.company_id
freelancer = company._l10n_es_freelancer()
if is_sale:
xml_to_send = self._generate_final_xml_bi(freelancer=freelancer)
lroe_str = etree.tostring(xml_to_send)
else:
lroe_str = self.xml_attachment_id.raw
lroe_bytes = gzip.compress(lroe_str)
return {
'url': get_key(company.l10n_es_tbai_tax_agency, 'cancel_url_' if self.is_cancel else 'post_url_', company.l10n_es_tbai_test_env),
'headers': {
'Accept-Encoding': 'gzip',
'Content-Encoding': 'gzip',
'Content-Length': str(len(lroe_str)),
'Content-Type': 'application/octet-stream',
'eus-bizkaia-n3-version': '1.0',
'eus-bizkaia-n3-content-type': 'application/xml',
'eus-bizkaia-n3-data': json.dumps({
'con': 'LROE',
'apa': '2.1' if freelancer and not is_sale else '1.1' if is_sale else '2',
'inte': {
'nif': company.vat[2:] if company.vat.startswith('ES') else company.vat,
'nrs': company.name,
},
'drs': {
'mode': '140' if freelancer else '240',
'ejer': str(self.date.year),
}
}),
},
'pkcs12_data': company.l10n_es_tbai_certificate_id,
'data': lroe_bytes,
}
def _generate_final_xml_bi(self, freelancer=False):
sender = self.company_id
lroe_values = {
'is_emission': not self.is_cancel,
'sender': sender,
'sender_vat': sender.vat[2:] if sender.vat.startswith('ES') else sender.vat,
'fiscal_year': str(self.date.year),
'freelancer': freelancer,
'epigrafe': self.env['ir.config_parameter'].sudo().get_param('l10n_es_edi_tbai.epigrafe', '')
}
lroe_values.update({'tbai_b64_list': [base64.b64encode(self.xml_attachment_id.raw).decode()]})
lroe_str = self.env['ir.qweb']._render('l10n_es_edi_tbai.template_LROE_240_main', lroe_values)
lroe_xml = cleanup_xml_node(lroe_str)
return lroe_xml
@api.model
def _process_post_response_xml_bi(self, env, response_xml):
"""Government response processing for Bizkaia."""
if response_xml is None:
return False, []
success = response_xml.findtext('.//EstadoRegistro') == "Correcto"
if success:
return True, []
error_code = response_xml.findtext('.//CodigoErrorRegistro')
# Get message in basque if env is in basque
error_msg_node_name = 'DescripcionErrorRegistro' + ('EU' if get_lang(env).code == 'eu_ES' else 'ES')
error_msg = error_code + ": " + response_xml.findtext(f'.//{error_msg_node_name}', '')
if error_code == "B4_2000003": # already received
success = True
return success, [error_msg]
# -------------------------------------------------------------------------
# XML
# -------------------------------------------------------------------------
L10N_ES_TBAI_VERSION = 1.2
def _generate_xml(self, values):
self.ensure_one()
def format_float(value, precision_digits=2):
rounded_value = float_round(value, precision_digits=precision_digits)
return float_repr(rounded_value, precision_digits=precision_digits)
values.update({
'doc': self,
**self._get_header_values(),
**self._get_sender_values(),
**(self._get_recipient_values(values['partner'], values["is_simplified"]) if values['partner'] and not self.is_cancel or not values['is_sale'] else {}),
'datetime_now': datetime.now(tz=timezone('Europe/Madrid')),
'format_date': lambda d: datetime.strftime(d, '%d-%m-%Y'),
'format_time': lambda d: datetime.strftime(d, '%H:%M:%S'),
'format_float': format_float,
})
xml_doc = None
if values['is_sale']:
values.update({
'is_emission': not self.is_cancel,
**self.company_id._get_l10n_es_tbai_license_dict(),
**(self._get_sale_values(values) if not self.is_cancel else {}),
})
xml_doc = self._generate_sale_document_xml(values)
elif self.company_id.l10n_es_tbai_tax_agency == 'bizkaia':
company = self.company_id
freelancer = company._l10n_es_freelancer()
values.update({'freelancer': freelancer})
xml_doc = self._generate_purchase_document_xml_bi(values)
if xml_doc is not None:
self.sudo().xml_attachment_id = self.env['ir.attachment'].create({
'name': values['attachment_name'],
'raw': etree.tostring(xml_doc, encoding='UTF-8'),
'type': 'binary',
'res_model': values['res_model'],
'res_id': values['res_id'],
})
@api.model
def _get_header_values(self):
return {
'tbai_version': self.L10N_ES_TBAI_VERSION,
'odoo_version': release.version,
}
@api.model
def _get_sender_values(self):
sender = self.company_id
return {
'sender_vat': sender.vat[2:] if sender.vat.startswith('ES') else sender.vat,
'sender': sender,
}
def _get_recipient_values(self, partner, is_simplified=False):
# TicketBAI accept recipient data for simplified invoices,
# but only if the partner has a VAT number
if is_simplified and not partner.vat:
return {}
recipient_values = {
'partner': partner,
'partner_address': ', '.join(filter(None, [partner.street, partner.street2, partner.city])),
'alt_id_number': partner.vat or 'NO_DISPONIBLE',
}
if not partner._l10n_es_is_foreign() and partner.vat:
recipient_values['nif'] = partner.vat[2:] if partner.vat.startswith('ES') else partner.vat
elif partner.country_id and 'EU' in partner.country_id.country_group_codes:
recipient_values['alt_id_type'] = '02'
else:
recipient_values['alt_id_type'] = '04' if partner.vat else '06'
recipient_values['alt_id_country'] = partner.country_id.code if partner.country_id else None
return {'recipient': recipient_values}
def _get_refunded_values(self, values):
if not values.get('is_refund'):
return {}
refunded_doc = values['refunded_doc']
refunded_name = values['refunded_name']
if refunded_doc:
sequence, number = refunded_doc._get_tbai_sequence_and_number()
else:
sequence, number = self._get_tbai_seq_from_name(refunded_name)
return {
'refunded_serie': sequence,
'refunded_num': number,
'refunded_date': values['refunded_doc_invoice_date'],
}
def _get_sale_values(self, values):
sale_values = {
'chain_prev_document': self.company_id._get_l10n_es_tbai_last_chained_document(),
**self._get_regime_code_value(values['taxes'], values['is_simplified']),
**self._get_refunded_values(values),
}
# Regime key override for Canarias/Ceuta/Melilla and no_sujeto_loc
if values['partner'] and values['partner'].country_id.code == 'ES' and values['partner'].state_id.code in ('TF', 'GC', 'CE', 'ME'):
if any(t.l10n_es_type == 'no_sujeto_loc' for t in values['taxes']):
sale_values.update({'regime_key': ['08']})
if not values['partner'] or not values['partner']._l10n_es_is_foreign() or values["is_simplified"]:
sale_values.update(**self._get_importe_desglose_es_partner(values['base_lines'], values['is_refund']))
else:
sale_values.update(**self._get_importe_desglose_foreign_partner(values['base_lines'], values['is_refund']))
return sale_values
def _get_regime_code_value(self, taxes, is_simplified):
return {'regime_key': [taxes._l10n_es_get_regime_code()]}
@api.model
def _add_base_lines_tax_amounts(self, base_lines, company, tax_lines=None):
AccountTax = self.env['account.tax']
AccountTax._add_tax_details_in_base_lines(base_lines, company)
AccountTax._round_base_lines_tax_details(base_lines, company, tax_lines=tax_lines)
for base_line in base_lines:
discount = base_line['discount']
price_unit = base_line['price_unit'] / base_line['rate'] if base_line['rate'] else 0.0
quantity = base_line['quantity']
price_subtotal = base_line['price_subtotal'] = base_line['tax_details']['raw_total_excluded']
base_line['price_total'] = base_line['tax_details']['raw_total_included']
for tax_data in base_line['tax_details']['taxes_data']:
if tax_data['tax'].l10n_es_type == 'retencion':
base_line['price_total'] -= tax_data['tax_amount']
if discount == 100.0:
gross_price_subtotal_before_discount = price_unit * quantity
else:
gross_price_subtotal_before_discount = price_subtotal / (1 - discount / 100.0)
base_line['gross_price_subtotal'] = gross_price_subtotal_before_discount
base_line['discount_amount'] = gross_price_subtotal_before_discount - price_subtotal
base_line['description'] = re.sub(r'[^0-9a-zA-Z ]+', '', base_line['name'] or base_line['product_id'].display_name or '')[:250]
if quantity:
base_line['gross_price_unit'] = gross_price_subtotal_before_discount / quantity
else:
base_line['gross_price_unit'] = 0.0
@api.model
def _build_tax_details_info(self, values_list):
sujeta_no_sujeta = {}
sujeto = []
sujeto_isp = []
encountered_l10n_es_type = set()
for values in values_list:
grouping_key = values['grouping_key']
if not grouping_key:
continue
l10n_es_type = grouping_key['l10n_es_type']
sign = grouping_key['is_refund'] and -1 or 1
encountered_l10n_es_type.add(l10n_es_type)
if l10n_es_type in ('sujeto', 'sujeto_isp'):
tax_info = {
'TipoImpositivo': grouping_key['applied_tax_amount'],
'BaseImponible': sign * float_round(values['base_amount'], 2),
'CuotaRepercutida': sign * float_round(values['tax_amount'], 2),
}
sujeta_no_sujeta\
.setdefault('Sujeta', {})\
.setdefault('NoExenta', {})\
.setdefault('DesgloseIVA', {'DetalleIVA': []})['DetalleIVA']\
.append(tax_info)
if l10n_es_type == 'sujeto':
sujeto.append(tax_info)
else:
sujeto_isp.append(tax_info)
elif l10n_es_type == 'exento':
sujeta_no_sujeta\
.setdefault('Sujeta', {})\
.setdefault('Exenta', {'DetalleExenta': []})['DetalleExenta']\
.append({
'BaseImponible': sign * float_round(values['base_amount'], 2),
'CausaExencion': grouping_key['l10n_es_exempt_reason'],
})
elif l10n_es_type == 'recargo':
detalle_iva = sujeta_no_sujeta\
.get('Sujeta', {})\
.get('NoExenta', {})\
.get('DesgloseIVA', {})\
.get('DetalleIVA')
if detalle_iva:
detalle_iva[-1]['CuotaRecargoEquivalencia'] = sign * float_round(values['tax_amount'], 2)
detalle_iva[-1]['TipoRecargoEquivalencia'] = sign * grouping_key['applied_tax_amount']
elif l10n_es_type == 'no_sujeto':
no_sujeta = sujeta_no_sujeta.setdefault('NoSujeta', {})
no_sujeta.setdefault('ImportePorArticulos7_14_Otros', 0.0)
no_sujeta['ImportePorArticulos7_14_Otros'] += sign * float_round(values['base_amount'], 2)
elif l10n_es_type == 'no_sujeto_loc':
no_sujeta = sujeta_no_sujeta.setdefault('NoSujeta', {})
no_sujeta.setdefault('ImporteTAIReglasLocalizacion', 0.0)
no_sujeta['ImporteTAIReglasLocalizacion'] += sign * float_round(values['base_amount'], 2)
if 'sujeto' in encountered_l10n_es_type and 'sujeto_isp' not in encountered_l10n_es_type:
sujeta_no_sujeta['Sujeta']['NoExenta']['TipoNoExenta'] = 'S2'
elif 'sujeto' not in encountered_l10n_es_type and 'sujeto_isp' in encountered_l10n_es_type:
sujeta_no_sujeta['Sujeta']['NoExenta']['TipoNoExenta'] = 'S1'
elif 'sujeto' in encountered_l10n_es_type and 'sujeto_isp' in encountered_l10n_es_type:
sujeta_no_sujeta['Sujeta']['NoExenta']['TipoNoExenta'] = 'S3'
return {
'sujeta_no_sujeta': sujeta_no_sujeta,
'sujeto': sujeto,
'sujeto_isp': sujeto_isp,
}
@api.model
def _get_importe_desglose_es_partner(self, base_lines, is_refund):
AccountTax = self.env['account.tax']
def tax_details_info_grouping_function(base_line, tax_data):
if not tax_data:
return None
tax = tax_data['tax']
return {
'applied_tax_amount': tax.amount,
'l10n_es_type': tax.l10n_es_type,
'l10n_es_exempt_reason': tax.l10n_es_exempt_reason if tax.l10n_es_type == 'exento' else False,
'l10n_es_bien_inversion': tax.l10n_es_bien_inversion,
'is_reverse_charge': tax_data['is_reverse_charge'],
'tax_scope': tax.tax_scope,
'is_refund': base_line['is_refund'],
}
base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(base_lines, tax_details_info_grouping_function)
values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
tax_details_info = self._build_tax_details_info(values_per_grouping_key.values())
invoice_info = {
'DesgloseFactura': {
**tax_details_info['sujeta_no_sujeta'],
'S1': tax_details_info['sujeto'],
'S2': tax_details_info['sujeto_isp'],
},
}
total_amount = 0.0
total_retention = 0.0
for values in values_per_grouping_key.values():
if values['grouping_key'] and values['grouping_key']['l10n_es_type'] == 'retencion':
total_retention += values['tax_amount']
else:
total_amount += values['tax_amount']
# Aggregate the base lines again (with no grouping) to add the base amount to the total.
def totals_grouping_function(base_line, tax_data):
return True if tax_data else None
base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(base_lines, totals_grouping_function)
values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
for values in values_per_grouping_key.values():
total_amount += values['base_amount']
if is_refund:
total_amount = -total_amount
total_retention = -total_retention
return {
'invoice_info': invoice_info,
'total_amount': total_amount,
'total_retention': total_retention,
}
@api.model
def _get_importe_desglose_foreign_partner(self, base_lines, is_refund):
AccountTax = self.env['account.tax']
def tax_details_info_grouping_function(base_line, tax_data):
if not tax_data:
return None
tax = tax_data['tax']
return {
'applied_tax_amount': tax.amount,
'l10n_es_type': tax.l10n_es_type,
'l10n_es_exempt_reason': tax.l10n_es_exempt_reason if tax.l10n_es_type == 'exento' else False,
'l10n_es_bien_inversion': tax.l10n_es_bien_inversion,
'is_reverse_charge': tax_data['is_reverse_charge'],
'tax_scope': tax.tax_scope,
'is_refund': base_line['is_refund'],
}
base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(base_lines, tax_details_info_grouping_function)
values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
invoice_info = {}
for scope, target_key in (('service', 'PrestacionServicios'), ('consu', 'Entrega')):
service_values_list = [
values
for values in values_per_grouping_key.values()
if values['grouping_key'] and values['grouping_key']['tax_scope'] == scope
]
if service_values_list:
tax_details_info = self._build_tax_details_info(service_values_list)
invoice_info.setdefault('DesgloseTipoOperacion', {})[target_key] = {
**tax_details_info['sujeta_no_sujeta'],
'S1': tax_details_info['sujeto'],
'S2': tax_details_info['sujeto_isp'],
}
total_amount = 0.0
total_retention = 0.0
for values in values_per_grouping_key.values():
if values['grouping_key'] and values['grouping_key']['l10n_es_type'] == 'retencion':
total_retention += values['tax_amount']
else:
total_amount += values['tax_amount']
# Aggregate the base lines again (with no grouping) to add the base amount to the total.
def totals_grouping_function(base_line, tax_data):
return True if tax_data else None
base_lines_aggregated_values = AccountTax._aggregate_base_lines_tax_details(base_lines, totals_grouping_function)
values_per_grouping_key = AccountTax._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
for values in values_per_grouping_key.values():
total_amount += values['base_amount']
if is_refund:
total_amount = -total_amount
total_retention = -total_retention
return {
'invoice_info': invoice_info,
'total_amount': total_amount,
'total_retention': total_retention,
}
def _generate_sale_document_xml(self, values):
template_name = 'l10n_es_edi_tbai.template_invoice_main' + ('_cancel' if self.is_cancel else '_post')
xml_str = self.env['ir.qweb']._render(template_name, values)
xml_doc = cleanup_xml_node(xml_str, remove_blank_nodes=False)
try:
xml_doc = self._sign_sale_document(xml_doc)
except ValueError:
raise UserError(_('No valid certificate found for this company, TicketBAI file will not be signed.\n'))
return xml_doc
def _sign_sale_document(self, xml_root):
self.ensure_one()
company = self.company_id
certificate_sudo = company.sudo().l10n_es_tbai_certificate_id
if not certificate_sudo:
raise UserError(_('No certificate found'))
# Identifiers
document_id = "Document-" + str(uuid4())
signature_id = "Signature-" + document_id
keyinfo_id = "KeyInfo-" + document_id
sigproperties_id = "SignatureProperties-" + document_id
# Render digital signature scaffold from QWeb
e, n = certificate_sudo._get_public_key_numbers_bytes()
issuer = certificate_sudo._l10n_es_edi_tbai_get_issuer()
values = {
'dsig': {
'document_id': document_id,
'x509_certificate': base64.encodebytes(base64.b64decode(certificate_sudo._get_der_certificate_bytes())).decode(),
'public_modulus': n.decode(),
'public_exponent': e.decode(),
'iso_now': datetime.now().isoformat(),
'keyinfo_id': keyinfo_id,
'signature_id': signature_id,
'sigproperties_id': sigproperties_id,
'reference_uri': "Reference-" + document_id,
'sigpolicy_url': get_key(company.l10n_es_tbai_tax_agency, 'sigpolicy_url'),
'sigpolicy_digest': get_key(company.l10n_es_tbai_tax_agency, 'sigpolicy_digest'),
'sigcertif_digest': certificate_sudo._get_fingerprint_bytes(formatting='base64').decode(),
'x509_issuer_description': issuer,
'x509_serial_number': int(certificate_sudo.serial_number),
}
}
xml_sig_str = self.env['ir.qweb']._render('l10n_es_edi_tbai.template_digital_signature', values)
xml_sig = cleanup_xml_signature(xml_sig_str)
# Complete document with signature template
xml_root.append(xml_sig)
# Compute digest values for references
calculate_references_digests(xml_sig.find("SignedInfo", namespaces=NS_MAP))
# Sign (writes into SignatureValue)
signed_info_xml = xml_sig.find('SignedInfo', namespaces=NS_MAP)
xml_sig.find('SignatureValue', namespaces=NS_MAP).text = certificate_sudo._sign(canonicalize_node(signed_info_xml)).decode()
return xml_root
def _generate_purchase_document_xml_bi(self, values):
sender = self.company_id
lroe_values = {
'is_emission': not self.is_cancel,
'sender': sender,
'sender_vat': sender.vat[2:] if sender.vat.startswith('ES') else sender.vat,
'fiscal_year': str(self.date.year),
'epigrafe': self.env['ir.config_parameter'].sudo().get_param('l10n_es_edi_tbai.epigrafe', ''),
'batuz_correction': self.env.context.get('batuz_correction'),
}
lroe_values.update(values)
lroe_str = self.env['ir.qweb']._render('l10n_es_edi_tbai.template_LROE_240_main_recibidas', lroe_values)
lroe_xml = cleanup_xml_node(lroe_str)
return lroe_xml
# -------------------------------------------------------------------------
# SIGNATURE AND QR CODE
# -------------------------------------------------------------------------
@api.model
def _get_tbai_sequence_and_number_purchase(self):
''' Get the numbers in the case of vendor bills of Bizkaia'''
self.ensure_one()
original_vendor_bill = self.env['account.move'].search([('l10n_es_tbai_post_document_id', '=', self.id)],
limit=1)
if original_vendor_bill and self.is_cancel: # Normally it should be is_cancel in this case
vals = original_vendor_bill.l10n_es_tbai_post_document_id._get_values_from_xml({
'sequence': './/CabeceraFactura/SerieFactura',
'number': './/CabeceraFactura/NumFactura',
})
if vals['sequence'] and vals['number']:
return vals['sequence'], vals['number']
sequence = "TEST" if self.company_id.l10n_es_tbai_test_env else ""
return sequence, self.name
@api.model
def _get_tbai_seq_from_name(self, name):
matching = list(re.finditer(r'\d+', name))[-1]
sequence_prefix = name[:matching.start()]
sequence_number = int(matching.group())
# NOTE non-decimal characters should not appear in the number
seq_length = self.env['sequence.mixin']._get_sequence_format_param(name)[1]['seq_length']
number = f"{sequence_number:0{seq_length}d}"
sequence = sequence_prefix.rstrip('/')
sequence = re.sub(r"[^0-9A-Za-z.\_\-\/]+", "", sequence) # remove forbidden characters
sequence = re.sub(r"\s+", " ", sequence) # no more than one consecutive whitespace allowed
# NOTE (optional) not recommended to use chars out of ([0123456789ABCDEFGHJKLMNPQRSTUVXYZ.\_\-\/ ])
sequence += "TEST" if self.company_id.l10n_es_tbai_test_env else ""
return sequence[-20:], number
def _get_tbai_sequence_and_number(self):
"""Get the TicketBAI sequence a number values for this invoice."""
self.ensure_one()
return self._get_tbai_seq_from_name(self.name)
def _get_tbai_signature_and_date(self):
"""
Get the TicketBAI signature and registration date for this document.
Should only be called for a "post" document (is_cancel==False).
The registration date is the date the document was registered into the govt's TicketBAI servers.
"""
self.ensure_one()
vals = self._get_values_from_xml({
'signature': './/{http://www.w3.org/2000/09/xmldsig#}SignatureValue',
'registration_date': './/CabeceraFactura//FechaExpedicionFactura'
})
# RFC2045 - Base64 Content-Transfer-Encoding (page 25)
# Any characters outside of the base64 alphabet are to be ignored in base64-encoded data.
signature = vals['signature'].replace("\n", "")
registration_date = datetime.strptime(vals['registration_date'], '%d-%m-%Y')
return signature, registration_date
def _get_tbai_id(self):
"""Get the TicketBAI ID (TBAID) as defined in the TicketBAI doc."""
self.ensure_one()
if not self._is_in_chain():
return ''
signature, registration_date = self._get_tbai_signature_and_date()
company = self.company_id
tbai_id_no_crc = '-'.join([
'TBAI',
str(company.vat[2:] if company.vat.startswith('ES') else company.vat),
datetime.strftime(registration_date, '%d%m%y'),
signature[:13],
'' # CRC
])
return tbai_id_no_crc + self._get_crc8(tbai_id_no_crc)
def _get_tbai_qr(self):
"""Returns the URL for the document's QR code. We can not use url_encode because it escapes / e.g."""
self.ensure_one()
if not self._is_in_chain():
return ''
company = self.company_id
sequence, number = self._get_tbai_sequence_and_number()
tbai_qr_no_crc = get_key(company.l10n_es_tbai_tax_agency, 'qr_url_', company.l10n_es_tbai_test_env) + '?' + '&'.join([
'id=' + self._get_tbai_id(),
's=' + sequence,
'nf=' + number,
'i=' + self._get_values_from_xml({'importe': './/ImporteTotalFactura'})['importe']
])
qr_url = tbai_qr_no_crc + '&cr=' + self._get_crc8(tbai_qr_no_crc)
return qr_url
def _get_crc8(self, data):
crc = 0x0
for c in data:
crc = CRC8_TABLE[(crc ^ ord(c)) & 0xFF]
return f'{crc & 0xFF:03d}'
def _get_values_from_xml(self, xpaths):
"""This function reads values directly from the 'post' XML submitted to the government"""
res = dict.fromkeys(xpaths, '')
doc_xml = self._get_xml()
if doc_xml is None:
return res
for key, value in xpaths.items():
res[key] = doc_xml.find(value).text
return res
def _get_xml(self):
"""Returns the XML object representing the document."""
self.ensure_one()
doc = self.xml_attachment_id
if not doc:
return None
return etree.fromstring(doc.raw.decode('utf-8'))

View file

@ -2,34 +2,38 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import markupsafe
from odoo import _, api, fields, models, release
import re
from odoo import api, fields, models, release
from odoo.tools import LazyTranslate
_lt = LazyTranslate(__name__)
# === TBAI license values ===
L10N_ES_TBAI_LICENSE_DICT = {
'production': {
'license_name': _('Production license'), # all agencies
'license_name': _lt('Production license'), # all agencies
'license_number': 'TBAIGI5A266A7CCDE1EC',
'license_nif': 'N0251909H',
'software_name': 'Odoo SA',
'software_version': release.version,
},
'araba': {
'license_name': _('Test license (Araba)'),
'license_name': _lt('Test license (Araba)'),
'license_number': 'TBAIARbjjMClHKH00849',
'license_nif': 'N0251909H',
'software_name': 'Odoo SA',
'software_version': release.version,
},
'bizkaia': {
'license_name': _('Test license (Bizkaia)'),
'license_name': _lt('Test license (Bizkaia)'),
'license_number': 'TBAIBI00000000PRUEBA',
'license_nif': 'A99800005',
'software_name': 'SOFTWARE GARANTE TICKETBAI PRUEBA',
'software_version': '1.0',
},
'gipuzkoa': {
'license_name': _('Test license (Gipuzkoa)'),
'license_name': _lt('Test license (Gipuzkoa)'),
'license_number': 'TBAIGIPRE00000000965',
'license_nif': 'N0251909H',
'software_name': 'Odoo SA',
@ -37,9 +41,23 @@ L10N_ES_TBAI_LICENSE_DICT = {
},
}
class ResCompany(models.Model):
_inherit = 'res.company'
l10n_es_tbai_certificate_id = fields.Many2one(
string="Certificate (TicketBAI)",
store=True,
readonly=False,
comodel_name='certificate.certificate',
compute="_compute_l10n_es_tbai_certificate",
)
l10n_es_tbai_certificate_ids = fields.One2many(
comodel_name='certificate.certificate',
inverse_name='company_id',
domain=[('scope', '=', 'tbai')],
)
# === TBAI config ===
l10n_es_tbai_tax_agency = fields.Selection(
string="Tax Agency for TBAI",
@ -62,16 +80,41 @@ class ResCompany(models.Model):
copy=False,
)
@api.depends('country_id', 'l10n_es_edi_test_env', 'l10n_es_tbai_tax_agency')
l10n_es_tbai_test_env = fields.Boolean(
string="TBAI Test Mode",
help="Use the test environment for TicketBAI",
default=True,
)
l10n_es_tbai_is_enabled = fields.Boolean(compute='_compute_l10n_es_tbai_is_enabled')
@api.depends('country_id', 'l10n_es_tbai_tax_agency')
def _compute_l10n_es_tbai_is_enabled(self):
for company in self:
company.l10n_es_tbai_is_enabled = company.country_code == 'ES' and company.l10n_es_tbai_tax_agency
@api.depends('country_id', 'l10n_es_tbai_certificate_ids')
def _compute_l10n_es_tbai_certificate(self):
for company in self:
if company.country_code == 'ES':
company.l10n_es_tbai_certificate_id = self.env['certificate.certificate'].search(
[('company_id', '=', company.id), ('is_valid', '=', True), ('scope', '=', 'tbai')],
order='date_end desc',
limit=1,
)
else:
company.l10n_es_tbai_certificate_id = False
@api.depends('country_id', 'l10n_es_tbai_test_env', 'l10n_es_tbai_tax_agency')
def _compute_l10n_es_tbai_license_html(self):
for company in self:
license_dict = company._get_l10n_es_tbai_license_dict()
if license_dict:
license_dict.update({
'tr_nif': _('Licence NIF'),
'tr_number': _('Licence number'),
'tr_name': _('Software name'),
'tr_version': _('Software version')
'tr_nif': self.env._('Licence NIF'),
'tr_number': self.env._('Licence number'),
'tr_name': self.env._('Software name'),
'tr_version': self.env._('Software version')
})
company.l10n_es_tbai_license_html = markupsafe.Markup('''
<strong>{license_name}</strong><br/>
@ -83,16 +126,17 @@ class ResCompany(models.Model):
</p>''').format(**license_dict)
else:
company.l10n_es_tbai_license_html = markupsafe.Markup('''
<strong>{tr_no_license}</strong>''').format(tr_no_license=_('TicketBAI is not configured'))
<strong>{tr_no_license}</strong>''').format(tr_no_license=self.env._('TicketBAI is not configured'))
def _get_l10n_es_tbai_license_dict(self):
self.ensure_one()
if self.country_code == 'ES' and self.l10n_es_tbai_tax_agency:
if self.l10n_es_edi_test_env: # test env: each agency has its test license
if self.l10n_es_tbai_is_enabled:
if self.l10n_es_tbai_test_env: # test env: each agency has its test license
license_key = self.l10n_es_tbai_tax_agency
else: # production env: only one license
license_key = 'production'
return L10N_ES_TBAI_LICENSE_DICT[license_key]
license = L10N_ES_TBAI_LICENSE_DICT[license_key]
return dict(license, license_name=str(license["license_name"])) # force translation
else:
return {}
@ -107,18 +151,18 @@ class ResCompany(models.Model):
})
return self.l10n_es_tbai_chain_sequence_id.next_by_id()
def _get_l10n_es_tbai_last_posted_invoice(self, being_posted=False):
def _get_l10n_es_tbai_last_chained_document(self):
"""
Returns the last invoice posted to this company's chain.
That invoice may have been received by the govt or not (eg. in case of a timeout).
Only upon confirmed reception/refusal of that invoice can another one be posted.
:param being_posted: next invoice to be posted on the chain, ignored in search domain
Returns the last tbai document posted to this company's chain.
That tbai document may have been received by the govt or not (eg. in case of a timeout).
Only upon confirmed reception/refusal of that tbai document can another one be posted.
"""
domain = [
('l10n_es_tbai_chain_index', '!=', 0),
('chain_index', '!=', 0),
('company_id', '=', self.id)
]
if being_posted:
domain.append(('l10n_es_tbai_chain_index', '!=', being_posted.l10n_es_tbai_chain_index))
# NOTE: being_posted may not have a chain index at all (if being posted for the first time)
return self.env['account.move'].search(domain, limit=1, order='l10n_es_tbai_chain_index desc')
return self.env['l10n_es_edi_tbai.document'].search(domain, limit=1, order='chain_index desc')
def _l10n_es_freelancer(self):
self.ensure_one()
return self.vat and re.fullmatch(r"(ES)?(\d{8}[A-Z]|[X-Z].*)", self.vat) or False

View file

@ -7,4 +7,6 @@ from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
l10n_es_tbai_certificate_ids = fields.One2many(related='company_id.l10n_es_tbai_certificate_ids', readonly=False)
l10n_es_tbai_tax_agency = fields.Selection(related='company_id.l10n_es_tbai_tax_agency', readonly=False)
l10n_es_tbai_test_env = fields.Boolean(related='company_id.l10n_es_tbai_test_env', readonly=False)

View file

@ -3,10 +3,8 @@
import hashlib
import re
from base64 import b64encode, encodebytes
from base64 import b64encode
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from lxml import etree
from odoo.tools.xml_utils import cleanup_xml_node
@ -15,6 +13,7 @@ from odoo.tools.xml_utils import cleanup_xml_node
NS_MAP = {'': 'http://www.w3.org/2000/09/xmldsig#'} # default namespace matches signature's `ds:``
def canonicalize_node(node):
"""
Returns the canonical (C14N 1.0, without comments, non exclusive) representation of node.
@ -25,6 +24,7 @@ def canonicalize_node(node):
node = etree.fromstring(node) if isinstance(node, str) else node
return etree.tostring(node, method='c14n', with_comments=False, exclusive=False)
def cleanup_xml_signature(xml_sig):
"""
Cleanups the content of the provided string representation of an XML signature.
@ -41,6 +41,7 @@ def cleanup_xml_signature(xml_sig):
elem.tail = '' # removes line feed and whitespace after the tag
return sig_elem
def get_uri(uri, reference, base_uri):
"""
Returns the content within `reference` that is identified by `uri`.
@ -74,6 +75,7 @@ def get_uri(uri, reference, base_uri):
raise Exception(f"URI {uri!r} not found")
def calculate_references_digests(node, base_uri=''):
"""
Processes the references from node and computes their digest values as specified in
@ -84,35 +86,3 @@ def calculate_references_digests(node, base_uri=''):
ref_node = get_uri(reference.get('URI', ''), reference, base_uri)
hash_digest = hashlib.new('sha256', ref_node).digest()
reference.find('DigestValue', namespaces=NS_MAP).text = b64encode(hash_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('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('SignatureValue', namespaces=NS_MAP).text =\
bytes_as_block(signature)
def int_as_bytes(number):
"""
Converts an integer to an ASCII/UTF-8 byte string (with no leading zeroes).
"""
return number.to_bytes((number.bit_length() + 7) // 8, byteorder='big')
def bytes_as_block(string):
"""
Returns the passed string modified to include a line feed every `length` characters.
It may be recommended to keep length under 76:
https://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#rf-maxLength
https://www.ietf.org/rfc/rfc2045.txt
"""
return encodebytes(string).decode()

View file

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_l10n_es_edi_tbai_document_readonly,access_l10n_es_edi_tbai_document,l10n_es_edi_tbai.model_l10n_es_edi_tbai_document,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_l10n_es_edi_tbai_document_readonly access_l10n_es_edi_tbai_document l10n_es_edi_tbai.model_l10n_es_edi_tbai_document base.group_user 1 0 0 0

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="tbai_document_comp_rule" model="ir.rule">
<field name="name">TicketBAI Document multi-company</field>
<field name="model_id" ref="model_l10n_es_edi_tbai_document"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
</odoo>

View file

@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_edi_tbai_send_bill_bizkaia
from . import test_edi_tbai_send_invoice_bizkaia
from . import test_edi_tbai_send_invoice
from . import test_edi_tbai_user_errors
from . import test_edi_web_services
from . import test_edi_xml
from . import test_resequence
from . import test_move_reversal

View file

@ -2,18 +2,25 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from datetime import datetime
import requests
from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
from odoo.tools import misc
from pytz import timezone
from datetime import date, datetime
from unittest.mock import Mock
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.tools import file_open
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.account.tests.test_account_move_send import TestAccountMoveSendCommon
class TestEsEdiTbaiCommon(AccountEdiTestCommon):
class TestEsEdiTbaiCommon(TestAccountMoveSendCommon):
@classmethod
def setUpClass(cls, chart_template_ref='l10n_es.account_chart_template_full', edi_format_ref='l10n_es_edi_tbai.edi_es_tbai'):
super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
@AccountTestInvoicingCommon.setup_country('es')
def setUpClass(cls):
super().setUpClass()
cls.frozen_today = datetime(year=2025, month=1, day=1, hour=0, minute=0, second=0, tzinfo=timezone('utc'))
@ -24,13 +31,10 @@ class TestEsEdiTbaiCommon(AccountEdiTestCommon):
cls.company_data['company'].write({
'name': 'EUS Company',
'country_id': cls.env.ref('base.es').id,
'state_id': cls.env.ref('base.state_es_ss').id,
'vat': 'ES09760433S',
'l10n_es_edi_test_env': True,
'vat': 'ESA12345674',
'l10n_es_tbai_test_env': True,
})
cls.certificate = None
cls._set_tax_agency('gipuzkoa')
# ==== Business ====
@ -41,16 +45,13 @@ class TestEsEdiTbaiCommon(AccountEdiTestCommon):
'country_id': cls.env.ref('base.be').id,
'street': 'Rue Sans Souci 1',
'zip': 93071,
'invoice_edi_format': False,
})
cls.partner_b.write({
'vat': 'ESF35999705',
})
cls.product_t = cls.env["product.product"].create(
{"name": "Test product"})
cls.partner_t = cls.env["res.partner"].create({"name": "Test partner", "vat": "ESF35999705"})
@classmethod
def _set_tax_agency(cls, agency):
if agency == "araba":
@ -65,14 +66,20 @@ class TestEsEdiTbaiCommon(AccountEdiTestCommon):
else:
raise ValueError("Unknown tax agency: " + agency)
cls.certificate = cls.env['l10n_es_edi.certificate'].sudo().create({
'content': base64.encodebytes(
misc.file_open("l10n_es_edi_tbai/demo/certificates/" + cert_name, 'rb').read()),
'password': cert_password,
cls.certificate = cls.env['certificate.certificate'].create({
'name': 'Test ES TBAI certificate',
'content': base64.b64encode(
file_open("l10n_es_edi_tbai/demo/certificates/" + cert_name, 'rb').read()),
'pkcs12_password': cert_password,
'scope': 'tbai',
'company_id': cls.company_data['company'].id,
})
cls.company_data['company'].sudo().write({
# Prevent certificate expiration in tests
cls.certificate.date_end = fields.Datetime.now() + relativedelta(days=2)
cls.company_data['company'].write({
'l10n_es_tbai_tax_agency': agency,
'l10n_es_edi_certificate_id': cls.certificate.id,
'l10n_es_tbai_certificate_id': cls.certificate.id,
})
@classmethod
@ -82,7 +89,7 @@ class TestEsEdiTbaiCommon(AccountEdiTestCommon):
:param trailing_xml_id: The trailing tax's xml id.
:return: An account.tax record
"""
return cls.env.ref(f'l10n_es.{cls.env.company.id}_account_tax_template_{trailing_xml_id}')
return cls.env.ref(f'account.{cls.env.company.id}_account_tax_template_{trailing_xml_id}')
@classmethod
def create_invoice(cls, **kwargs):
@ -99,373 +106,129 @@ class TestEsEdiTbaiCommon(AccountEdiTestCommon):
}) for line_vals in kwargs.get('invoice_line_ids', [])],
})
L10N_ES_TBAI_SAMPLE_XML_POST = """<?xml version='1.0' encoding='UTF-8'?>
<T:TicketBai xmlns:etsi="http://uri.etsi.org/01903/v1.3.2#" xmlns:T="urn:ticketbai:emision" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<Cabecera>
<IDVersionTBAI>1.2</IDVersionTBAI>
</Cabecera>
<Sujetos>
<Emisor>
<NIF>___ignore___</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</Emisor>
<Destinatarios>
<IDDestinatario>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@&#224;&#193;$&#163;&#8364;&#232;&#234;&#200;&#202;&#246;&#212;&#199;&#231;&#161;&#8539;&#8482;&#179;</ApellidosNombreRazonSocial>
<CodigoPostal>___ignore___</CodigoPostal>
<Direccion>___ignore___</Direccion>
</IDDestinatario>
</Destinatarios>
<VariosDestinatarios>N</VariosDestinatarios>
<EmitidaPorTercerosODestinatario>N</EmitidaPorTercerosODestinatario>
</Sujetos>
<Factura>
<CabeceraFactura>
<SerieFactura>INVTEST</SerieFactura>
<NumFactura>01</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<HoraExpedicionFactura>___ignore___</HoraExpedicionFactura>
<FacturaSimplificada>N</FacturaSimplificada>
</CabeceraFactura>
<DatosFactura>
<DescripcionFactura>manual</DescripcionFactura>
<DetallesFactura>
<IDDetalleFactura>
<DescripcionDetalle>producta</DescripcionDetalle>
<Cantidad>5.00</Cantidad>
<ImporteUnitario>1000.00</ImporteUnitario>
<Descuento>1000.00</Descuento>
<ImporteTotal>4840.00</ImporteTotal>
</IDDetalleFactura>
</DetallesFactura>
<ImporteTotalFactura>4840.00</ImporteTotalFactura>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
</DatosFactura>
<TipoDesglose>
<DesgloseTipoOperacion>
<Entrega>
<Sujeta>
<NoExenta>
<DetalleNoExenta>
<TipoNoExenta>S1</TipoNoExenta>
<DesgloseIVA>
<DetalleIVA>
<BaseImponible>4000.00</BaseImponible>
<TipoImpositivo>21.00</TipoImpositivo>
<CuotaImpuesto>840.00</CuotaImpuesto>
</DetalleIVA>
</DesgloseIVA>
</DetalleNoExenta>
</NoExenta>
</Sujeta>
</Entrega>
</DesgloseTipoOperacion>
</TipoDesglose>
</Factura>
<HuellaTBAI>
<Software>
<LicenciaTBAI>___ignore___</LicenciaTBAI>
<EntidadDesarrolladora>
<NIF>___ignore___</NIF>
</EntidadDesarrolladora>
<Nombre>___ignore___</Nombre>
<Version>___ignore___</Version>
</Software>
<NumSerieDispositivo>___ignore___</NumSerieDispositivo>
</HuellaTBAI>
</T:TicketBai>
""".encode("utf-8")
@classmethod
def _create_posted_invoice(cls):
out_invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'invoice_date': date(2025, 1, 1),
'partner_id': cls.partner_a.id,
'invoice_line_ids': [(0, 0, {
'product_id': cls.product_a.id,
'price_unit': 1000.0,
'quantity': 5,
'discount': 20.0,
'tax_ids': [(6, 0, cls._get_tax_by_xml_id('s_iva21b').ids)],
})],
})
out_invoice.action_post()
return out_invoice
L10N_ES_TBAI_SAMPLE_XML_CANCEL = """<T:AnulaTicketBai xmlns:T="urn:ticketbai:anulacion">
<Cabecera>
<IDVersionTBAI>1.2</IDVersionTBAI>
</Cabecera>
<IDFactura>
<Emisor>
<NIF>09760433S</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</Emisor>
<CabeceraFactura>
<SerieFactura>INVTEST</SerieFactura>
<NumFactura>01</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
</CabeceraFactura>
</IDFactura>
<HuellaTBAI>
<Software>
<LicenciaTBAI>___ignore___</LicenciaTBAI>
<EntidadDesarrolladora>
<NIF>___ignore___</NIF>
</EntidadDesarrolladora>
<Nombre>___ignore___</Nombre>
<Version>___ignore___</Version>
</Software>
<NumSerieDispositivo>___ignore___</NumSerieDispositivo>
</HuellaTBAI>
</T:AnulaTicketBai>""".encode("utf-8")
@classmethod
def _get_invoice_send_wizard(cls, invoice):
out_invoice_send_wizard = cls.env['account.move.send.wizard']\
.with_context(active_model='account.move', active_ids=invoice.ids)\
.create({'sending_methods': []})
return out_invoice_send_wizard
L10N_ES_TBAI_SAMPLE_XML_POST_IN = """
<lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion xmlns:lrpjframp="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PJ_240_2_FacturasRecibidas_AltaModifPeticion_V1_0_1.xsd">
<Cabecera>
<Modelo>240</Modelo>
<Capitulo>2</Capitulo>
<Operacion>A00</Operacion>
<Version>1.0</Version>
<Ejercicio>2025</Ejercicio>
<ObligadoTributario>
<NIF>09760433S</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<FacturasRecibidas>
<FacturaRecibida>
<EmisorFacturaRecibida>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@àÁ$£èêÈÊöÔÇ硳</ApellidosNombreRazonSocial>
</EmisorFacturaRecibida>
<CabeceraFactura>
<SerieFactura>TEST</SerieFactura>
<NumFactura>INV/5234</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<FechaRecepcion>01-01-2025</FechaRecepcion>
<TipoFactura>F1</TipoFactura>
</CabeceraFactura>
<DatosFactura>
<DescripcionOperacion>INV/5234</DescripcionOperacion>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
<ImporteTotalFactura>4840.00</ImporteTotalFactura>
</DatosFactura>
<IVA>
<DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>C</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>4000.00</BaseImponible>
<TipoImpositivo>21.0</TipoImpositivo>
<CuotaIVASoportada>840.00</CuotaIVASoportada>
<CuotaIVADeducible>840.00</CuotaIVADeducible>
</DetalleIVA>
</IVA>
</FacturaRecibida>
</FacturasRecibidas>
</lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion>"""
@classmethod
def _create_posted_bill(cls):
bill = cls.env['account.move'].create({
'move_type': 'in_invoice',
'invoice_date': date.today(),
'partner_id': cls.partner_a.id,
'ref': "INV123",
'invoice_line_ids': [(0, 0, {
'product_id': cls.product_a.id,
'price_unit': 1000.0,
'quantity': 5,
'discount': 20.0,
'tax_ids': [(6, 0, cls._get_tax_by_xml_id('p_iva21_bc').ids)],
})],
})
bill.action_post()
return bill
L10N_ES_TBAI_SAMPLE_XML_POST_IN_ND = """
<lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion xmlns:lrpjframp="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PJ_240_2_FacturasRecibidas_AltaModifPeticion_V1_0_1.xsd">
<Cabecera>
<Modelo>240</Modelo>
<Capitulo>2</Capitulo>
<Operacion>A00</Operacion>
<Version>1.0</Version>
<Ejercicio>2025</Ejercicio>
<ObligadoTributario>
<NIF>09760433S</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<FacturasRecibidas>
<FacturaRecibida>
<EmisorFacturaRecibida>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@àÁ$£èêÈÊöÔÇ硳</ApellidosNombreRazonSocial>
</EmisorFacturaRecibida>
<CabeceraFactura>
<SerieFactura>TEST</SerieFactura>
<NumFactura>INV/5234</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<FechaRecepcion>01-01-2025</FechaRecepcion>
<TipoFactura>F1</TipoFactura>
</CabeceraFactura>
<DatosFactura>
<DescripcionOperacion>INV/5234</DescripcionOperacion>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
<ImporteTotalFactura>1100.00</ImporteTotalFactura>
</DatosFactura>
<IVA>
<DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>C</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>1000.00</BaseImponible>
<TipoImpositivo>10.0</TipoImpositivo>
<CuotaIVASoportada>100.00</CuotaIVASoportada>
<CuotaIVADeducible>0.00</CuotaIVADeducible>
</DetalleIVA>
</IVA>
</FacturaRecibida>
</FacturasRecibidas>
</lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion>"""
@classmethod
def _get_sample_xml(cls, filename):
with file_open(f'l10n_es_edi_tbai/tests/document_xmls/{filename}', 'rb') as file:
content = file.read()
return content
L10N_ES_TBAI_SAMPLE_XML_POST_IN_IC = """
<lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion xmlns:lrpjframp="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PJ_240_2_FacturasRecibidas_AltaModifPeticion_V1_0_1.xsd">
<Cabecera>
<Modelo>240</Modelo>
<Capitulo>2</Capitulo>
<Operacion>A00</Operacion>
<Version>1.0</Version>
<Ejercicio>2025</Ejercicio>
<ObligadoTributario>
<NIF>09760433S</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<FacturasRecibidas>
<FacturaRecibida>
<EmisorFacturaRecibida>
<NIF>F35999705</NIF>
<ApellidosNombreRazonSocial>partner_b</ApellidosNombreRazonSocial>
</EmisorFacturaRecibida>
<CabeceraFactura>
<SerieFactura>TEST</SerieFactura>
<NumFactura>INV/5234</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<FechaRecepcion>01-01-2025</FechaRecepcion>
<TipoFactura>F1</TipoFactura>
</CabeceraFactura>
<DatosFactura>
<DescripcionOperacion>INV/5234</DescripcionOperacion>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>09</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
<ImporteTotalFactura>12000.00</ImporteTotalFactura>
</DatosFactura>
<IVA>
<DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>C</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>4000.00</BaseImponible>
<TipoImpositivo>21.0</TipoImpositivo>
<CuotaIVASoportada>840.00</CuotaIVASoportada>
<CuotaIVADeducible>840.00</CuotaIVADeducible>
</DetalleIVA><DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>G</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>8000.00</BaseImponible>
<TipoImpositivo>21.0</TipoImpositivo>
<CuotaIVASoportada>1680.00</CuotaIVASoportada>
<CuotaIVADeducible>1680.00</CuotaIVADeducible>
</DetalleIVA>
</IVA>
</FacturaRecibida>
</FacturasRecibidas>
</lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion>
"""
@classmethod
def _get_response_xml(cls, filename):
with file_open(f'l10n_es_edi_tbai/tests/response_xmls/{filename}', 'rb') as file:
content = file.read()
return content
L10N_ES_TBAI_CREDIT_NOTE_XML_POST = """<?xml version='1.0'?>
<T:TicketBai xmlns:etsi="http://uri.etsi.org/01903/v1.3.2#" xmlns:T="urn:ticketbai:emision" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<Cabecera>
<IDVersionTBAI>1.2</IDVersionTBAI>
</Cabecera>
<Sujetos>
<Emisor>
<NIF>___ignore___</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</Emisor>
<Destinatarios>
<IDDestinatario>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@&#224;&#193;$&#163;&#8364;&#232;&#234;&#200;&#202;&#246;&#212;&#199;&#231;&#161;&#8539;&#8482;&#179;</ApellidosNombreRazonSocial>
<CodigoPostal>___ignore___</CodigoPostal>
<Direccion>___ignore___</Direccion>
</IDDestinatario>
</Destinatarios>
<VariosDestinatarios>N</VariosDestinatarios>
<EmitidaPorTercerosODestinatario>N</EmitidaPorTercerosODestinatario>
</Sujetos>
<Factura>
<CabeceraFactura>
<SerieFactura>___ignore___</SerieFactura>
<NumFactura>00001</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<HoraExpedicionFactura>___ignore___</HoraExpedicionFactura>
<FacturaSimplificada>N</FacturaSimplificada>
<FacturaEmitidaSustitucionSimplificada>N</FacturaEmitidaSustitucionSimplificada>
<FacturaRectificativa>
<Codigo>R1</Codigo>
<Tipo>I</Tipo>
</FacturaRectificativa>
<FacturasRectificadasSustituidas>
<IDFacturaRectificadaSustituida>
<SerieFactura>INVTEST</SerieFactura>
<NumFactura>01</NumFactura>
<FechaExpedicionFactura>___ignore___</FechaExpedicionFactura>
</IDFacturaRectificadaSustituida>
</FacturasRectificadasSustituidas>
</CabeceraFactura>
<DatosFactura>
<DescripcionFactura>manual</DescripcionFactura>
<DetallesFactura>
<IDDetalleFactura>
<DescripcionDetalle>producta</DescripcionDetalle>
<Cantidad>5.00</Cantidad>
<ImporteUnitario>-1000.00</ImporteUnitario>
<Descuento>-1000.00</Descuento>
<ImporteTotal>-4840.00</ImporteTotal>
</IDDetalleFactura>
</DetallesFactura>
<ImporteTotalFactura>-4840.00</ImporteTotalFactura>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
</DatosFactura>
<TipoDesglose>
<DesgloseTipoOperacion>
<Entrega>
<Sujeta>
<NoExenta>
<DetalleNoExenta>
<TipoNoExenta>S1</TipoNoExenta>
<DesgloseIVA>
<DetalleIVA>
<BaseImponible>-4000.00</BaseImponible>
<TipoImpositivo>21.00</TipoImpositivo>
<CuotaImpuesto>-840.00</CuotaImpuesto>
</DetalleIVA>
</DesgloseIVA>
</DetalleNoExenta>
</NoExenta>
</Sujeta>
</Entrega>
</DesgloseTipoOperacion>
</TipoDesglose>
</Factura>
<HuellaTBAI>
<Software>
<LicenciaTBAI>___ignore___</LicenciaTBAI>
<EntidadDesarrolladora>
<NIF>___ignore___</NIF>
</EntidadDesarrolladora>
<Nombre>___ignore___</Nombre>
<Version>___ignore___</Version>
</Software>
<NumSerieDispositivo>___ignore___</NumSerieDispositivo>
</HuellaTBAI>
</T:TicketBai>
"""
def create_mock_response(content, headers=None):
mock_response = Mock(spec=requests.Response)
mock_response.content = content
mock_response.headers = headers or {}
return mock_response
class TestEsEdiTbaiCommonGipuzkoa(TestEsEdiTbaiCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.mock_response_post_invoice_success = create_mock_response(cls._get_response_xml('post_invoice_success_gi.xml'))
cls.mock_response_cancel_invoice_success = create_mock_response(cls._get_response_xml('cancel_invoice_success_gi.xml'))
cls.mock_response_failure = create_mock_response(cls._get_response_xml('post_or_cancel_invoice_failure_gi.xml'))
cls.mock_request_error = requests.exceptions.RequestException("A request exception")
class TestEsEdiTbaiCommonBizkaia(TestEsEdiTbaiCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.mock_response_post_invoice_success = create_mock_response(
cls._get_response_xml('post_invoice_success_bi.xml'),
cls.RESPONSE_HEADERS_SUCCESS
)
cls.mock_response_cancel_invoice_success = create_mock_response(
cls._get_response_xml('cancel_invoice_success_bi.xml'),
cls.RESPONSE_HEADERS_SUCCESS
)
cls.mock_response_post_invoice_failure = create_mock_response(
cls._get_response_xml('post_invoice_failure_bi.xml'),
cls.RESPONSE_HEADERS_FAILURE
)
cls.mock_response_cancel_invoice_failure = create_mock_response(
cls._get_response_xml('cancel_invoice_failure_bi.xml'),
cls.RESPONSE_HEADERS_FAILURE
)
cls.mock_response_post_bill_success = create_mock_response(
cls._get_response_xml('post_bill_success_bi.xml'),
cls.RESPONSE_HEADERS_SUCCESS
)
cls.mock_response_cancel_bill_success = create_mock_response(
cls._get_response_xml('cancel_bill_success_bi.xml'),
cls.RESPONSE_HEADERS_SUCCESS
)
cls.mock_response_post_bill_failure = create_mock_response(
None,
cls.RESPONSE_HEADERS_FAILURE
)
cls.mock_response_cancel_bill_failure = create_mock_response(
cls._get_response_xml('cancel_bill_failure_bi.xml'),
cls.RESPONSE_HEADERS_FAILURE
)
cls.mock_request_error = requests.exceptions.RequestException("A request exception")
cls.company.l10n_es_tbai_tax_agency = 'bizkaia'
RESPONSE_HEADERS_SUCCESS = {
'eus-bizkaia-n3-tipo-respuesta': 'Correcto',
'eus-bizkaia-n3-codigo-respuesta': '',
}
RESPONSE_HEADERS_FAILURE = {
'eus-bizkaia-n3-tipo-respuesta': 'Incorrecto',
'eus-bizkaia-n3-codigo-respuesta': 'B4_1000002',
'eus-bizkaia-n3-mensaje-respuesta': 'An error msg.',
}

View file

@ -0,0 +1,27 @@
<T:AnulaTicketBai xmlns:T="urn:ticketbai:anulacion">
<Cabecera>
<IDVersionTBAI>1.2</IDVersionTBAI>
</Cabecera>
<IDFactura>
<Emisor>
<NIF>A12345674</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</Emisor>
<CabeceraFactura>
<SerieFactura>INVTEST</SerieFactura>
<NumFactura>01</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
</CabeceraFactura>
</IDFactura>
<HuellaTBAI>
<Software>
<LicenciaTBAI>___ignore___</LicenciaTBAI>
<EntidadDesarrolladora>
<NIF>___ignore___</NIF>
</EntidadDesarrolladora>
<Nombre>___ignore___</Nombre>
<Version>___ignore___</Version>
</Software>
<NumSerieDispositivo>___ignore___</NumSerieDispositivo>
</HuellaTBAI>
</T:AnulaTicketBai>

View file

@ -0,0 +1,84 @@
<?xml version='1.0'?>
<T:TicketBai xmlns:etsi="http://uri.etsi.org/01903/v1.3.2#" xmlns:T="urn:ticketbai:emision" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<Cabecera>
<IDVersionTBAI>1.2</IDVersionTBAI>
</Cabecera>
<Sujetos>
<Emisor>
<NIF>___ignore___</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</Emisor>
<Destinatarios>
<IDDestinatario>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@&#224;&#193;$&#163;&#8364;&#232;&#234;&#200;&#202;&#246;&#212;&#199;&#231;&#161;&#8539;&#8482;&#179;</ApellidosNombreRazonSocial>
<CodigoPostal>___ignore___</CodigoPostal>
<Direccion>___ignore___</Direccion>
</IDDestinatario>
</Destinatarios>
<VariosDestinatarios>N</VariosDestinatarios>
<EmitidaPorTercerosODestinatario>N</EmitidaPorTercerosODestinatario>
</Sujetos>
<Factura>
<CabeceraFactura>
<SerieFactura>INVTEST</SerieFactura>
<NumFactura>01</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<HoraExpedicionFactura>___ignore___</HoraExpedicionFactura>
<FacturaSimplificada>N</FacturaSimplificada>
</CabeceraFactura>
<DatosFactura>
<FechaOperacion>01-01-2025</FechaOperacion>
<DescripcionFactura>manual</DescripcionFactura>
<DetallesFactura>
<IDDetalleFactura>
<DescripcionDetalle>producta</DescripcionDetalle>
<Cantidad>5.00000000</Cantidad>
<ImporteUnitario>1000.00000000</ImporteUnitario>
<Descuento>1000.00000000</Descuento>
<ImporteTotal>4840.00000000</ImporteTotal>
</IDDetalleFactura>
</DetallesFactura>
<ImporteTotalFactura>4840.00</ImporteTotalFactura>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
</DatosFactura>
<TipoDesglose>
<DesgloseTipoOperacion>
<Entrega>
<Sujeta>
<NoExenta>
<DetalleNoExenta>
<TipoNoExenta>S1</TipoNoExenta>
<DesgloseIVA>
<DetalleIVA>
<BaseImponible>4000.00</BaseImponible>
<TipoImpositivo>21.00</TipoImpositivo>
<CuotaImpuesto>840.00</CuotaImpuesto>
</DetalleIVA>
</DesgloseIVA>
</DetalleNoExenta>
</NoExenta>
</Sujeta>
</Entrega>
</DesgloseTipoOperacion>
</TipoDesglose>
</Factura>
<HuellaTBAI>
<Software>
<LicenciaTBAI>___ignore___</LicenciaTBAI>
<EntidadDesarrolladora>
<NIF>___ignore___</NIF>
</EntidadDesarrolladora>
<Nombre>___ignore___</Nombre>
<Version>___ignore___</Version>
</Software>
<NumSerieDispositivo>___ignore___</NumSerieDispositivo>
</HuellaTBAI>
</T:TicketBai>

View file

@ -0,0 +1,83 @@
<?xml version='1.0' encoding='UTF-8'?>
<T:TicketBai xmlns:etsi="http://uri.etsi.org/01903/v1.3.2#" xmlns:T="urn:ticketbai:emision" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<Cabecera>
<IDVersionTBAI>1.2</IDVersionTBAI>
</Cabecera>
<Sujetos>
<Emisor>
<NIF>___ignore___</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</Emisor>
<Destinatarios>
<IDDestinatario>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@&#224;&#193;$&#163;&#8364;&#232;&#234;&#200;&#202;&#246;&#212;&#199;&#231;&#161;&#8539;&#8482;&#179;</ApellidosNombreRazonSocial>
<CodigoPostal>___ignore___</CodigoPostal>
<Direccion>___ignore___</Direccion>
</IDDestinatario>
</Destinatarios>
<VariosDestinatarios>N</VariosDestinatarios>
<EmitidaPorTercerosODestinatario>N</EmitidaPorTercerosODestinatario>
</Sujetos>
<Factura>
<CabeceraFactura>
<SerieFactura>INVTEST</SerieFactura>
<NumFactura>01</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<HoraExpedicionFactura>___ignore___</HoraExpedicionFactura>
<FacturaSimplificada>N</FacturaSimplificada>
</CabeceraFactura>
<DatosFactura>
<DescripcionFactura>manual</DescripcionFactura>
<DetallesFactura>
<IDDetalleFactura>
<DescripcionDetalle>producta</DescripcionDetalle>
<Cantidad>5.00000000</Cantidad>
<ImporteUnitario>1000.00000000</ImporteUnitario>
<Descuento>1000.00000000</Descuento>
<ImporteTotal>4840.00000000</ImporteTotal>
</IDDetalleFactura>
</DetallesFactura>
<ImporteTotalFactura>4840.00</ImporteTotalFactura>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
</DatosFactura>
<TipoDesglose>
<DesgloseTipoOperacion>
<Entrega>
<Sujeta>
<NoExenta>
<DetalleNoExenta>
<TipoNoExenta>S1</TipoNoExenta>
<DesgloseIVA>
<DetalleIVA>
<BaseImponible>4000.00</BaseImponible>
<TipoImpositivo>21.00</TipoImpositivo>
<CuotaImpuesto>840.00</CuotaImpuesto>
</DetalleIVA>
</DesgloseIVA>
</DetalleNoExenta>
</NoExenta>
</Sujeta>
</Entrega>
</DesgloseTipoOperacion>
</TipoDesglose>
</Factura>
<HuellaTBAI>
<Software>
<LicenciaTBAI>___ignore___</LicenciaTBAI>
<EntidadDesarrolladora>
<NIF>___ignore___</NIF>
</EntidadDesarrolladora>
<Nombre>___ignore___</Nombre>
<Version>___ignore___</Version>
</Software>
<NumSerieDispositivo>___ignore___</NumSerieDispositivo>
</HuellaTBAI>
</T:TicketBai>

View file

@ -0,0 +1,50 @@
<lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion xmlns:lrpjframp="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PJ_240_2_FacturasRecibidas_AltaModifPeticion_V1_0_1.xsd">
<Cabecera>
<Modelo>240</Modelo>
<Capitulo>2</Capitulo>
<Operacion>A00</Operacion>
<Version>1.0</Version>
<Ejercicio>2025</Ejercicio>
<ObligadoTributario>
<NIF>A12345674</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<FacturasRecibidas>
<FacturaRecibida>
<EmisorFacturaRecibida>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@àÁ$£€èêÈÊöÔÇç¡⅛™³</ApellidosNombreRazonSocial>
</EmisorFacturaRecibida>
<CabeceraFactura>
<SerieFactura>TEST</SerieFactura>
<NumFactura>INV/5234</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<FechaRecepcion>01-01-2025</FechaRecepcion>
<TipoFactura>F1</TipoFactura>
</CabeceraFactura>
<DatosFactura>
<DescripcionOperacion>INV/5234</DescripcionOperacion>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
<ImporteTotalFactura>4840.00</ImporteTotalFactura>
</DatosFactura>
<IVA>
<DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>C</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>4000.00</BaseImponible>
<TipoImpositivo>21.0</TipoImpositivo>
<CuotaIVASoportada>840.00</CuotaIVASoportada>
<CuotaIVADeducible>840.00</CuotaIVADeducible>
</DetalleIVA>
</IVA>
</FacturaRecibida>
</FacturasRecibidas>
</lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion>

View file

@ -0,0 +1,51 @@
<lrpfgcfamp:LROEPF140GastosConFacturaAltaModifPeticion xmlns:lrpfgcfamp="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PF_140_2_1_Gastos_Confactura_AltaModifPeticion_V1_0_2.xsd">
<Cabecera>
<Modelo>140</Modelo>
<Capitulo>2</Capitulo>
<Subcapitulo>2.1</Subcapitulo>
<Operacion>A00</Operacion>
<Version>1.0</Version>
<Ejercicio>2025</Ejercicio>
<ObligadoTributario>
<NIF>09760433S</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<Gastos>
<Gasto>
<EmisorFacturaRecibida>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@&#224;&#193;$&#163;&#8364;&#232;&#234;&#200;&#202;&#246;&#212;&#199;&#231;&#161;&#8539;&#8482;&#179;</ApellidosNombreRazonSocial>
</EmisorFacturaRecibida>
<CabeceraFactura>
<SerieFactura>TEST</SerieFactura>
<NumFactura>INV/5234</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<FechaRecepcion>01-01-2025</FechaRecepcion>
<TipoFactura>F1</TipoFactura>
</CabeceraFactura>
<DatosFactura>
<DescripcionOperacion>INV/5234</DescripcionOperacion>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
<ImporteTotalFactura>4840.00</ImporteTotalFactura>
</DatosFactura>
<RentaIVA>
<DetalleRentaIVA>
<Epigrafe>102100</Epigrafe>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>4000.00</BaseImponible>
<TipoImpositivo>21.0</TipoImpositivo>
<CuotaIVASoportada>840.00</CuotaIVASoportada>
<CuotaIVADeducible>840.00</CuotaIVADeducible>
</DetalleRentaIVA>
</RentaIVA>
</Gasto>
</Gastos>
</lrpfgcfamp:LROEPF140GastosConFacturaAltaModifPeticion>

View file

@ -0,0 +1,55 @@
<lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion xmlns:lrpjframp="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PJ_240_2_FacturasRecibidas_AltaModifPeticion_V1_0_1.xsd">
<Cabecera>
<Modelo>240</Modelo>
<Capitulo>2</Capitulo>
<Operacion>A00</Operacion>
<Version>1.0</Version>
<Ejercicio>2025</Ejercicio>
<ObligadoTributario>
<NIF>A12345674</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<FacturasRecibidas>
<FacturaRecibida>
<EmisorFacturaRecibida>
<NIF>F35999705</NIF>
<ApellidosNombreRazonSocial>partner_b</ApellidosNombreRazonSocial>
</EmisorFacturaRecibida>
<CabeceraFactura>
<SerieFactura>TEST</SerieFactura>
<NumFactura>INV/5234</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<FechaRecepcion>01-01-2025</FechaRecepcion>
<TipoFactura>F1</TipoFactura>
</CabeceraFactura>
<DatosFactura>
<DescripcionOperacion>INV/5234</DescripcionOperacion>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>09</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
<ImporteTotalFactura>12000.00</ImporteTotalFactura>
</DatosFactura>
<IVA>
<DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>C</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>4000.00</BaseImponible>
<TipoImpositivo>21.0</TipoImpositivo>
<CuotaIVASoportada>840.00</CuotaIVASoportada>
<CuotaIVADeducible>840.00</CuotaIVADeducible>
</DetalleIVA>
<DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>G</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>8000.00</BaseImponible>
<TipoImpositivo>21.0</TipoImpositivo>
<CuotaIVASoportada>1680.00</CuotaIVASoportada>
<CuotaIVADeducible>1680.00</CuotaIVADeducible>
</DetalleIVA>
</IVA>
</FacturaRecibida>
</FacturasRecibidas>
</lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion>

View file

@ -0,0 +1,50 @@
<lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion xmlns:lrpjframp="https://www.batuz.eus/fitxategiak/batuz/LROE/esquemas/LROE_PJ_240_2_FacturasRecibidas_AltaModifPeticion_V1_0_1.xsd">
<Cabecera>
<Modelo>240</Modelo>
<Capitulo>2</Capitulo>
<Operacion>A00</Operacion>
<Version>1.0</Version>
<Ejercicio>2025</Ejercicio>
<ObligadoTributario>
<NIF>A12345674</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<FacturasRecibidas>
<FacturaRecibida>
<EmisorFacturaRecibida>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@àÁ$£€èêÈÊöÔÇç¡⅛™³</ApellidosNombreRazonSocial>
</EmisorFacturaRecibida>
<CabeceraFactura>
<SerieFactura>TEST</SerieFactura>
<NumFactura>INV/5234</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<FechaRecepcion>01-01-2025</FechaRecepcion>
<TipoFactura>F1</TipoFactura>
</CabeceraFactura>
<DatosFactura>
<DescripcionOperacion>INV/5234</DescripcionOperacion>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
<ImporteTotalFactura>1100.00</ImporteTotalFactura>
</DatosFactura>
<IVA>
<DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>C</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>1000.00</BaseImponible>
<TipoImpositivo>10.0</TipoImpositivo>
<CuotaIVASoportada>100.00</CuotaIVASoportada>
<CuotaIVADeducible>0.00</CuotaIVADeducible>
</DetalleIVA>
</IVA>
</FacturaRecibida>
</FacturasRecibidas>
</lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion>

View file

@ -0,0 +1,94 @@
<?xml version='1.0' encoding='UTF-8'?>
<T:TicketBai xmlns:etsi="http://uri.etsi.org/01903/v1.3.2#" xmlns:T="urn:ticketbai:emision" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<Cabecera>
<IDVersionTBAI>1.2</IDVersionTBAI>
</Cabecera>
<Sujetos>
<Emisor>
<NIF>___ignore___</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</Emisor>
<Destinatarios>
<IDDestinatario>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@&#224;&#193;$&#163;&#8364;&#232;&#234;&#200;&#202;&#246;&#212;&#199;&#231;&#161;&#8539;&#8482;&#179;</ApellidosNombreRazonSocial>
<CodigoPostal>___ignore___</CodigoPostal>
<Direccion>___ignore___</Direccion>
</IDDestinatario>
</Destinatarios>
<VariosDestinatarios>N</VariosDestinatarios>
<EmitidaPorTercerosODestinatario>N</EmitidaPorTercerosODestinatario>
</Sujetos>
<Factura>
<CabeceraFactura>
<SerieFactura>RINV/2020TEST</SerieFactura>
<NumFactura>00001</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
<HoraExpedicionFactura>___ignore___</HoraExpedicionFactura>
<FacturaSimplificada>N</FacturaSimplificada>
<FacturaEmitidaSustitucionSimplificada>N</FacturaEmitidaSustitucionSimplificada>
<FacturaRectificativa>
<Tipo>I</Tipo>
</FacturaRectificativa>
<FacturasRectificadasSustituidas>
<IDFacturaRectificadaSustituida>
<SerieFactura>INVTEST</SerieFactura>
<NumFactura>01</NumFactura>
<FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura>
</IDFacturaRectificadaSustituida>
</FacturasRectificadasSustituidas>
</CabeceraFactura>
<DatosFactura>
<DescripcionFactura>manual</DescripcionFactura>
<DetallesFactura>
<IDDetalleFactura>
<DescripcionDetalle>producta</DescripcionDetalle>
<Cantidad>5.00000000</Cantidad>
<ImporteUnitario>-1000.00000000</ImporteUnitario>
<Descuento>-1000.00000000</Descuento>
<ImporteTotal>-4840.00000000</ImporteTotal>
</IDDetalleFactura>
</DetallesFactura>
<ImporteTotalFactura>-4840.00</ImporteTotalFactura>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
</DatosFactura>
<TipoDesglose>
<DesgloseTipoOperacion>
<Entrega>
<Sujeta>
<NoExenta>
<DetalleNoExenta>
<TipoNoExenta>S1</TipoNoExenta>
<DesgloseIVA>
<DetalleIVA>
<BaseImponible>-4000.00</BaseImponible>
<TipoImpositivo>21.00</TipoImpositivo>
<CuotaImpuesto>-840.00</CuotaImpuesto>
</DetalleIVA>
</DesgloseIVA>
</DetalleNoExenta>
</NoExenta>
</Sujeta>
</Entrega>
</DesgloseTipoOperacion>
</TipoDesglose>
</Factura>
<HuellaTBAI>
<Software>
<LicenciaTBAI>___ignore___</LicenciaTBAI>
<EntidadDesarrolladora>
<NIF>___ignore___</NIF>
</EntidadDesarrolladora>
<Nombre>___ignore___</Nombre>
<Version>___ignore___</Version>
</Software>
<NumSerieDispositivo>___ignore___</NumSerieDispositivo>
</HuellaTBAI>
</T:TicketBai>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:LROEPJ240FacturasRecibidasAnulacionRespuesta xmlns:ns2="xxx">
<Cabecera>
<Modelo>XXX</Modelo>
<Capitulo>XXX</Capitulo>
<Operacion>XXX</Operacion>
<Version>XXX</Version>
<Ejercicio>XXX</Ejercicio>
<ObligadoTributario>
<NIF>XXX</NIF>
<ApellidosNombreRazonSocial>XXX</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<Registros>
<Registro>
<IDRecibida>
<NumFactura>XXX</NumFactura>
<FechaExpedicionFactura>XXX</FechaExpedicionFactura>
<EmisorFacturaRecibida>
<NIF>XXX</NIF>
</EmisorFacturaRecibida>
</IDRecibida>
<SituacionRegistro>
<EstadoRegistro>Incorrecto</EstadoRegistro>
<CodigoErrorRegistro>B4_2000004</CodigoErrorRegistro>
<DescripcionErrorRegistroES>Error description in Spanish.</DescripcionErrorRegistroES>
<DescripcionErrorRegistroEU>Error description in Basque.</DescripcionErrorRegistroEU>
</SituacionRegistro>
</Registro>
</Registros>
</ns2:LROEPJ240FacturasRecibidasAnulacionRespuesta>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:LROEPJ240FacturasRecibidasAnulacionRespuesta xmlns:ns2="xxx">
<Cabecera>
<Modelo>XXX</Modelo>
<Capitulo>XXX</Capitulo>
<Operacion>XXX</Operacion>
<Version>XXX</Version>
<Ejercicio>XXX</Ejercicio>
<ObligadoTributario>
<NIF>XXX</NIF>
<ApellidosNombreRazonSocial>XXX</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<DatosPresentacion>
<FechaPresentacion>XXX</FechaPresentacion>
<NIFPresentador>XXX</NIFPresentador>
</DatosPresentacion>
<Registros>
<Registro>
<IDRecibida>
<SerieFactura>XXX</SerieFactura>
<NumFactura>XXX</NumFactura>
<FechaExpedicionFactura>XXX</FechaExpedicionFactura>
<EmisorFacturaRecibida>
<NIF>XXX</NIF>
</EmisorFacturaRecibida>
</IDRecibida>
<SituacionRegistro>
<EstadoRegistro>Correcto</EstadoRegistro>
</SituacionRegistro>
</Registro>
</Registros>
</ns2:LROEPJ240FacturasRecibidasAnulacionRespuesta>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:LROEPJ240FacturasEmitidasConSGAnulacionRespuesta xmlns:ns2="xxx">
<Cabecera>
<Modelo>XXX</Modelo>
<Capitulo>XXX</Capitulo>
<Subcapitulo>XXX</Subcapitulo>
<Operacion>XXX</Operacion>
<Version>XXX</Version>
<Ejercicio>XXX</Ejercicio>
<ObligadoTributario>
<NIF>XXX</NIF>
<ApellidosNombreRazonSocial>XXX</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<Registros>
<Registro>
<Identificador>
<AnulacionTicketBai>XXXX</AnulacionTicketBai>
</Identificador>
<SituacionRegistro>
<EstadoRegistro>Incorrecto</EstadoRegistro>
<CodigoErrorRegistro>B4_2000001</CodigoErrorRegistro>
<DescripcionErrorRegistroES>Error description in Spanish.</DescripcionErrorRegistroES>
<DescripcionErrorRegistroEU>Error description in Basque.</DescripcionErrorRegistroEU>
</SituacionRegistro>
</Registro>
</Registros>
</ns2:LROEPJ240FacturasEmitidasConSGAnulacionRespuesta>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:LROEPJ240FacturasEmitidasConSGAnulacionRespuesta xmlns:ns2="xxx">
<Cabecera>
<Modelo>XXX</Modelo>
<Capitulo>XXX</Capitulo>
<Subcapitulo>XXX</Subcapitulo>
<Operacion>XXX</Operacion>
<Version>XXX</Version>
<Ejercicio>XXX</Ejercicio>
<ObligadoTributario>
<NIF>XXX</NIF>
<ApellidosNombreRazonSocial>XXX</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<DatosPresentacion>
<FechaPresentacion>XXX</FechaPresentacion>
<NIFPresentador>XXX</NIFPresentador>
</DatosPresentacion>
<Registros>
<Registro>
<Identificador>
<IDFactura>
<SerieFactura>XXX</SerieFactura>
<NumFactura>XXX</NumFactura>
<FechaExpedicionFactura>XXX</FechaExpedicionFactura>
</IDFactura>
</Identificador>
<SituacionRegistro>
<EstadoRegistro>Correcto</EstadoRegistro>
</SituacionRegistro>
</Registro>
</Registros>
</ns2:LROEPJ240FacturasEmitidasConSGAnulacionRespuesta>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:TicketBaiResponse xmlns:ns2="urn:ticketbai:emision">
<Salida>
<IdentificadorTBAI>XXX</IdentificadorTBAI>
<FechaRecepcion>XXX</FechaRecepcion>
<Estado>00</Estado>
<Descripcion>XXX</Descripcion>
<Azalpena>XXX</Azalpena>
<ResultadosValidacion>
<Codigo>1234</Codigo>
<Descripcion>Explanation in Spanish</Descripcion>
<Azalpena>Explanation in Basque</Azalpena>
</ResultadosValidacion>
<CSV>XXX</CSV>
</Salida>
</ns2:TicketBaiResponse>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:LROEPJ240FacturasRecibidasAltaModifRespuesta xmlns:ns2="xxx">
<Cabecera>
<Modelo>XXX</Modelo>
<Capitulo>XXX</Capitulo>
<Operacion>XXX</Operacion>
<Version>XXX</Version>
<Ejercicio>XXX</Ejercicio>
<ObligadoTributario>
<NIF>XXX</NIF>
<ApellidosNombreRazonSocial>XXX</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<DatosPresentacion>
<FechaPresentacion>XXX</FechaPresentacion>
<NIFPresentador>XXX</NIFPresentador>
</DatosPresentacion>
<Registros>
<Registro>
<IDRecibida>
<SerieFactura>XXX</SerieFactura>
<NumFactura>XXX</NumFactura>
<FechaExpedicionFactura>XXX</FechaExpedicionFactura>
<EmisorFacturaRecibida>
<NIF>XXX</NIF>
</EmisorFacturaRecibida>
</IDRecibida>
<SituacionRegistro>
<EstadoRegistro>Correcto</EstadoRegistro>
</SituacionRegistro>
</Registro>
</Registros>
</ns2:LROEPJ240FacturasRecibidasAltaModifRespuesta>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:LROEPJ240FacturasEmitidasConSGAltaRespuesta xmlns:ns2="xxx">
<Cabecera>
<Modelo>XXX</Modelo>
<Capitulo>XXX</Capitulo>
<Subcapitulo>XXX</Subcapitulo>
<Operacion>XXX</Operacion>
<Version>XXX</Version>
<Ejercicio>XXX</Ejercicio>
<ObligadoTributario>
<NIF>XXX</NIF>
<ApellidosNombreRazonSocial>XXX</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<Registros>
<Registro>
<Identificador>
<TicketBai>XXX</TicketBai>
</Identificador>
<SituacionRegistro>
<EstadoRegistro>Incorrecto</EstadoRegistro>
<CodigoErrorRegistro>B4_2000001</CodigoErrorRegistro>
<DescripcionErrorRegistroES>Error description in Spanish.</DescripcionErrorRegistroES>
<DescripcionErrorRegistroEU>Error description in Basque.</DescripcionErrorRegistroEU>
</SituacionRegistro>
</Registro>
</Registros>
</ns2:LROEPJ240FacturasEmitidasConSGAltaRespuesta>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:LROEPJ240FacturasEmitidasConSGAltaRespuesta xmlns:ns2="xxx">
<Cabecera>
<Modelo>XXX</Modelo>
<Capitulo>XXX</Capitulo>
<Subcapitulo>XXX</Subcapitulo>
<Operacion>XXX</Operacion>
<Version>XXX</Version>
<Ejercicio>XXX</Ejercicio>
<ObligadoTributario>
<NIF>XXX</NIF>
<ApellidosNombreRazonSocial>XXX</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<DatosPresentacion>
<FechaPresentacion>XXX</FechaPresentacion>
<NIFPresentador>XXX</NIFPresentador>
</DatosPresentacion>
<Registros>
<Registro>
<Identificador>
<IDFactura>
<SerieFactura>XXX</SerieFactura>
<NumFactura>XXX</NumFactura>
<FechaExpedicionFactura>XXX</FechaExpedicionFactura>
</IDFactura>
</Identificador>
<SituacionRegistro>
<EstadoRegistro>Correcto</EstadoRegistro>
</SituacionRegistro>
</Registro>
</Registros>
</ns2:LROEPJ240FacturasEmitidasConSGAltaRespuesta>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:TicketBaiResponse xmlns:ns2="urn:ticketbai:emision">
<Salida>
<IdentificadorTBAI>XXX</IdentificadorTBAI>
<FechaRecepcion>XXX</FechaRecepcion>
<Estado>00</Estado>
<Descripcion>XXX</Descripcion>
<Azalpena>XXX</Azalpena>
<ResultadosValidacion>
<Codigo>1234</Codigo>
<Descripcion>Explanation in Spanish</Descripcion>
<Azalpena>Explanation in Basque</Azalpena>
</ResultadosValidacion>
<CSV>XXX</CSV>
</Salida>
</ns2:TicketBaiResponse>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:TicketBaiResponse xmlns:ns2="urn:ticketbai:emision">
<Salida>
<FechaRecepcion>XXX</FechaRecepcion>
<Estado>01</Estado>
<Descripcion>XXX</Descripcion>
<Azalpena>XXX</Azalpena>
<ResultadosValidacion>
<Codigo>002</Codigo>
<Descripcion>Error in Spanish.</Descripcion>
<Azalpena>Error in Basque.</Azalpena>
</ResultadosValidacion>
</Salida>
</ns2:TicketBaiResponse>

View file

@ -0,0 +1,77 @@
from unittest.mock import patch
from odoo.exceptions import UserError
from odoo.tests import tagged
from .common import TestEsEdiTbaiCommonBizkaia
@tagged('post_install', '-at_install', 'post_install_l10n')
class TestSendBillEdiBizkaia(TestEsEdiTbaiCommonBizkaia):
def test_post_and_cancel_bill_tbai_success(self):
bill = self._create_posted_bill()
self.assertEqual(bill.l10n_es_tbai_state, 'to_send')
self.assertFalse(bill.l10n_es_tbai_chain_index)
self.assertFalse(bill.l10n_es_tbai_post_document_id.xml_attachment_id)
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_bill_success,
):
bill.l10n_es_tbai_send_bill()
self.assertEqual(bill.l10n_es_tbai_state, 'sent')
# No chain index for vendor bills
self.assertFalse(bill.l10n_es_tbai_chain_index)
self.assertTrue(bill.l10n_es_tbai_post_document_id.xml_attachment_id)
self.assertEqual(bill.state, 'posted')
self.assertFalse(bill.l10n_es_tbai_cancel_document_id.xml_attachment_id)
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_cancel_bill_success,
):
bill.l10n_es_tbai_cancel()
self.assertEqual(bill.l10n_es_tbai_state, 'cancelled')
self.assertEqual(bill.state, 'cancel')
self.assertTrue(bill.l10n_es_tbai_cancel_document_id.xml_attachment_id)
def test_post_bill_tbai_failure(self):
bill = self._create_posted_bill()
with self.assertRaises(UserError):
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_bill_failure,
):
bill.l10n_es_tbai_send_bill()
def test_cancel_bill_tbai_failure(self):
bill = self._create_posted_bill()
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_bill_success,
):
bill.l10n_es_tbai_send_bill()
with self.assertRaises(UserError):
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_cancel_bill_failure,
):
bill.l10n_es_tbai_cancel()
def test_post_bill_tbai_request_error(self):
bill = self._create_posted_bill()
with self.assertRaises(UserError):
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
side_effect=self.mock_request_error,
):
bill.l10n_es_tbai_send_bill()

View file

@ -0,0 +1,225 @@
from unittest.mock import patch
from odoo.exceptions import UserError
from odoo.tests import tagged
from .common import TestEsEdiTbaiCommonGipuzkoa
import base64
from lxml import etree
@tagged('post_install', '-at_install', 'post_install_l10n')
class TestSendAndPrintEdiGipuzkoa(TestEsEdiTbaiCommonGipuzkoa):
def test_post_and_cancel_invoice_tbai_success(self):
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
self.assertEqual(invoice.l10n_es_tbai_state, 'to_send')
self.assertFalse(invoice.l10n_es_tbai_chain_index)
self.assertFalse(invoice.l10n_es_tbai_post_document_id.xml_attachment_id)
# Post with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_send_wizard.action_send_and_print()
self.assertEqual(invoice.l10n_es_tbai_state, 'sent')
self.assertTrue(invoice.l10n_es_tbai_chain_index)
self.assertEqual(invoice.l10n_es_tbai_post_document_id.state, 'accepted')
self.assertTrue(invoice.l10n_es_tbai_post_document_id.xml_attachment_id)
self.assertEqual(invoice.state, 'posted')
self.assertFalse(invoice.l10n_es_tbai_cancel_document_id.xml_attachment_id)
# Cancel with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_cancel_invoice_success,
):
invoice.l10n_es_tbai_cancel()
self.assertEqual(invoice.l10n_es_tbai_state, 'cancelled')
self.assertEqual(invoice.l10n_es_tbai_cancel_document_id.state, 'accepted')
self.assertTrue(invoice.l10n_es_tbai_cancel_document_id.xml_attachment_id)
self.assertEqual(invoice.state, 'cancel')
def test_post_invoice_tbai_failure(self):
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
# Post with error
# In a non-test environment, the changes would be commited before raising the UserError,
# here we have to catch it in order to keep them.
try:
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_failure,
):
invoice_send_wizard.action_send_and_print()
raise AssertionError("A UserError should have been raised.")
except UserError:
self.assertEqual(invoice.l10n_es_tbai_state, 'to_send')
self.assertFalse(invoice.l10n_es_tbai_chain_index)
self.assertEqual(invoice.l10n_es_tbai_post_document_id.state, 'rejected')
self.assertTrue(invoice.l10n_es_tbai_post_document_id.xml_attachment_id)
failed_document_id = invoice.l10n_es_tbai_post_document_id.id
# Post with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_send_wizard.action_send_and_print()
self.assertNotEqual(invoice.l10n_es_tbai_post_document_id.id, failed_document_id)
self.assertEqual(invoice.l10n_es_tbai_state, 'sent')
self.assertTrue(invoice.l10n_es_tbai_chain_index)
self.assertEqual(invoice.l10n_es_tbai_post_document_id.state, 'accepted')
self.assertTrue(invoice.l10n_es_tbai_post_document_id.xml_attachment_id)
def test_cancel_invoice_tbai_failure(self):
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
# Post with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_send_wizard.action_send_and_print()
# Cancel with error
try:
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_failure,
):
invoice.l10n_es_tbai_cancel()
raise AssertionError("A UserError should have been raised.")
except UserError:
self.assertEqual(invoice.l10n_es_tbai_state, 'sent')
self.assertEqual(invoice.l10n_es_tbai_cancel_document_id.state, 'rejected')
self.assertTrue(invoice.l10n_es_tbai_cancel_document_id.xml_attachment_id)
failed_document_id = invoice.l10n_es_tbai_cancel_document_id.id
# Cancel with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_cancel_invoice_success,
):
invoice.l10n_es_tbai_cancel()
self.assertNotEqual(invoice.l10n_es_tbai_cancel_document_id.id, failed_document_id)
self.assertEqual(invoice.l10n_es_tbai_state, 'cancelled')
self.assertEqual(invoice.l10n_es_tbai_cancel_document_id.state, 'accepted')
self.assertTrue(invoice.l10n_es_tbai_cancel_document_id.xml_attachment_id)
def test_post_invoice_tbai_request_error(self):
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
# Post with request error
try:
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
side_effect=self.mock_request_error,
):
invoice_send_wizard.action_send_and_print()
raise AssertionError("A UserError should have been raised.")
except UserError:
self.assertEqual(invoice.l10n_es_tbai_state, 'to_send')
self.assertTrue(invoice.l10n_es_tbai_chain_index)
self.assertEqual(invoice.l10n_es_tbai_post_document_id.state, 'to_send')
self.assertTrue(invoice.l10n_es_tbai_post_document_id.xml_attachment_id)
pending_document_id = invoice.l10n_es_tbai_post_document_id.id
chain_index = invoice.l10n_es_tbai_chain_index
# Post with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_send_wizard.action_send_and_print()
self.assertEqual(invoice.l10n_es_tbai_post_document_id.id, pending_document_id)
self.assertEqual(invoice.l10n_es_tbai_chain_index, chain_index)
self.assertEqual(invoice.l10n_es_tbai_state, 'sent')
self.assertEqual(invoice.l10n_es_tbai_post_document_id.state, 'accepted')
self.assertTrue(invoice.l10n_es_tbai_post_document_id.xml_attachment_id)
def test_cancel_invoice_request_error(self):
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
# Post with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_send_wizard.action_send_and_print()
# Cancel with request error
try:
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
side_effect=self.mock_request_error,
):
invoice.l10n_es_tbai_cancel()
raise AssertionError("A UserError should have been raised.")
except UserError:
self.assertEqual(invoice.l10n_es_tbai_state, 'sent')
self.assertEqual(invoice.l10n_es_tbai_cancel_document_id.state, 'to_send')
self.assertTrue(invoice.l10n_es_tbai_cancel_document_id.xml_attachment_id)
pending_document_id = invoice.l10n_es_tbai_cancel_document_id.id
# Cancel with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_cancel_invoice_success,
):
invoice.l10n_es_tbai_cancel()
self.assertEqual(invoice.l10n_es_tbai_cancel_document_id.id, pending_document_id)
self.assertEqual(invoice.l10n_es_tbai_state, 'cancelled')
self.assertEqual(invoice.l10n_es_tbai_cancel_document_id.state, 'accepted')
self.assertTrue(invoice.l10n_es_tbai_cancel_document_id.xml_attachment_id)
def test_tbai_credit_note_importe_total(self):
invoice = self._create_posted_invoice()
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
self._get_invoice_send_wizard(invoice).action_send_and_print()
reversal = self.env['account.move.reversal'].with_context(
active_model="account.move", active_ids=invoice.ids
).create({
'journal_id': invoice.journal_id.id,
'l10n_es_tbai_refund_reason': 'R4',
})
credit_note = self.env['account.move'].browse(reversal.refund_moves()['res_id'])
credit_note.action_post()
self._get_invoice_send_wizard(credit_note).action_send_and_print()
tbai_xml = base64.b64decode(credit_note['l10n_es_tbai_post_file']).decode()
value = etree.fromstring(tbai_xml).findtext(".//ImporteTotalFactura")
self.assertEqual(value, '-4840.00')

View file

@ -0,0 +1,127 @@
from unittest.mock import patch
from odoo.exceptions import UserError
from odoo.tests import tagged
from .common import TestEsEdiTbaiCommonBizkaia
@tagged('post_install', '-at_install', 'post_install_l10n')
class TestSendAndPrintEdiBizkaia(TestEsEdiTbaiCommonBizkaia):
"""
All the ticketbai document logic is tested with TestSendAndPrintEdiGipuzkoa,
as Bizkaia and Gipuzokoa only differs by the requests,
only the request logic is tested here.
"""
def test_post_and_cancel_invoice_tbai_success(self):
self.company_data['company'].l10n_es_tbai_tax_agency = 'bizkaia'
self.company_data['company'].vat = '09760433S'
self.env['ir.config_parameter'].sudo().set_param('l10n_es_edi_tbai.epigrafe', '102100')
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
# Post with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_send_wizard.action_send_and_print()
self.assertEqual(invoice.l10n_es_tbai_state, 'sent')
# Cancel with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_cancel_invoice_success,
):
invoice.l10n_es_tbai_cancel()
self.assertEqual(invoice.l10n_es_tbai_state, 'cancelled')
def test_post_invoice_tbai_failure(self):
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
# Post with error
try:
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_failure,
):
invoice_send_wizard.action_send_and_print()
raise AssertionError("A UserError should have been raised.")
except UserError:
self.assertEqual(invoice.l10n_es_tbai_state, 'to_send')
# Post with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_send_wizard.action_send_and_print()
self.assertEqual(invoice.l10n_es_tbai_state, 'sent')
def test_cancel_invoice_tbai_failure(self):
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
# Post with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_send_wizard.action_send_and_print()
# Cancel with error
try:
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_cancel_invoice_failure,
):
invoice.l10n_es_tbai_cancel()
raise AssertionError("A UserError should have been raised.")
except UserError:
self.assertEqual(invoice.l10n_es_tbai_state, 'sent')
def test_post_invoice_tbai_request_error(self):
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
# Post with request error
try:
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
side_effect=self.mock_request_error,
):
invoice_send_wizard.action_send_and_print()
raise AssertionError("A UserError should have been raised.")
except UserError:
self.assertEqual(invoice.l10n_es_tbai_state, 'to_send')
def test_cancel_invoice_request_error(self):
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
# Post with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_send_wizard.action_send_and_print()
# Cancel with request error
try:
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
side_effect=self.mock_request_error,
):
invoice.l10n_es_tbai_cancel()
raise AssertionError("A UserError should have been raised.")
except UserError:
self.assertEqual(invoice.l10n_es_tbai_state, 'sent')

View file

@ -0,0 +1,172 @@
from unittest.mock import patch
from odoo import Command
from odoo.exceptions import UserError
from odoo.tests import tagged
from .common import TestEsEdiTbaiCommonGipuzkoa
@tagged('post_install', '-at_install', 'post_install_l10n')
class TestTbaiUserErrors(TestEsEdiTbaiCommonGipuzkoa):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.invoice_to_send = cls._create_posted_invoice()
cls.invoice_send_wizard = cls._get_invoice_send_wizard(cls.invoice_to_send)
cls.tbai_error_msg = "Error when sending the invoice to TicketBAI:\n- "
def test_no_certificate(self):
self.invoice_to_send.company_id.l10n_es_tbai_certificate_id = False
with self.assertRaises(UserError) as e:
self.invoice_send_wizard.action_send_and_print()
self.assertEqual(str(e.exception), self.tbai_error_msg + "Please configure the certificate for TicketBAI.")
def test_no_tax_agency(self):
self.invoice_to_send.company_id.l10n_es_tbai_tax_agency = False
with self.assertRaises(UserError) as e:
self.invoice_send_wizard.action_send_and_print()
self.assertEqual(str(e.exception), self.tbai_error_msg + "Please specify a tax agency on your company for TicketBAI.")
def test_no_company_vat(self):
self.invoice_to_send.company_id.vat = False
with self.assertRaises(UserError) as e:
self.invoice_send_wizard.action_send_and_print()
self.assertEqual(str(e.exception), self.tbai_error_msg + "Please configure the Tax ID on your company for TicketBAI.")
def test_no_tax_on_line(self):
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'invoice_date': '2025-01-01',
'partner_id': self.partner_a.id,
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'price_unit': 1000.0,
'quantity': 1,
'tax_ids': self._get_tax_by_xml_id('s_iva21b').ids,
}),
Command.create({
'product_id': self.product_b.id,
'price_unit': 50.0,
'quantity': 1,
'tax_ids': False,
}),
],
})
invoice.action_post()
with self.assertRaises(UserError) as e:
self._get_invoice_send_wizard(invoice).action_send_and_print()
self.assertEqual(str(e.exception), self.tbai_error_msg + "There should be at least one tax set on each line in order to send to TicketBAI.")
def test_pending_invoice(self):
first_invoice = self._create_posted_invoice()
first_invoice_send_wizard = self._get_invoice_send_wizard(first_invoice)
second_invoice = self._create_posted_invoice()
second_invoice_send_wizard = self._get_invoice_send_wizard(second_invoice)
# Post first with request error
try:
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
side_effect=self.mock_request_error,
):
first_invoice_send_wizard.action_send_and_print()
raise AssertionError("A UserError should have been raised.")
except UserError:
self.assertEqual(first_invoice.l10n_es_tbai_state, 'to_send')
self.assertTrue(first_invoice.l10n_es_tbai_chain_index)
# Post second raises an error as first is pending
try:
second_invoice_send_wizard.action_send_and_print()
raise AssertionError("A UserError should have been raised.")
except UserError as e:
self.assertEqual(str(e), self.tbai_error_msg + f"TicketBAI: Cannot post invoice while chain head ({first_invoice.name}) has not been posted")
self.assertEqual(second_invoice.l10n_es_tbai_state, 'to_send')
self.assertFalse(second_invoice.l10n_es_tbai_chain_index)
self.assertEqual(second_invoice.l10n_es_tbai_post_document_id.state, 'to_send')
# Post first with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
first_invoice_send_wizard.action_send_and_print()
# Can now post second with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
second_invoice_send_wizard.action_send_and_print()
self.assertEqual(first_invoice.l10n_es_tbai_state, 'sent')
self.assertEqual(second_invoice.l10n_es_tbai_state, 'sent')
self.assertGreater(second_invoice.l10n_es_tbai_chain_index, first_invoice.l10n_es_tbai_chain_index)
def test_post_tbai_credit_note_before_reversed_invoice(self):
# We send a first invoice, so the first invoice sent won't be an invoice imported from a previous system
invoice_already_sent = self.create_invoice(invoice_line_ids=[{
'quantity': 5,
'discount': 20.0,
'tax_ids': [(6, 0, self._get_tax_by_xml_id('s_iva21b').ids)],
}])
invoice_already_sent.invoice_date = "2020-01-01"
invoice_already_sent.action_post()
invoice_already_sent_wizard = self._get_invoice_send_wizard(invoice_already_sent)
# Can now post second with success
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_already_sent_wizard.action_send_and_print()
move_reversal = (
self.env['account.move.reversal']
.with_context(active_model="account.move", active_ids=self.invoice_to_send.ids)
.create({
'journal_id': self.invoice_to_send.journal_id.id,
'l10n_es_tbai_refund_reason': 'R4',
})
)
credit_note_id = move_reversal.refund_moves()['res_id']
credit_note = self.env['account.move'].browse(credit_note_id)
credit_note.action_post()
credit_note_send_wizard = self._get_invoice_send_wizard(credit_note)
try:
credit_note_send_wizard.action_send_and_print()
raise AssertionError("A UserError should have been raised.")
except UserError as e:
self.assertEqual(str(e), self.tbai_error_msg + "TicketBAI: Cannot post a reversal document while the source document has not been posted")
# Post the source invoice
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
self.invoice_send_wizard.action_send_and_print()
# It is now possible to post the credit note
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
credit_note_send_wizard.action_send_and_print()
self.assertEqual(self.invoice_to_send.l10n_es_tbai_state, 'sent')
self.assertEqual(credit_note.l10n_es_tbai_state, 'sent')

View file

@ -13,11 +13,8 @@ from .common import TestEsEdiTbaiCommon
class TestEdiTbaiWebServices(TestEsEdiTbaiCommon):
@classmethod
def setUpClass(cls, chart_template_ref='l10n_es.account_chart_template_full', edi_format_ref='l10n_es_edi_tbai.edi_es_tbai'):
super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
# Operations tested here should be available to a billing user
cls.env.user.groups_id = cls.env.ref("account.group_account_invoice")
def setUpClass(cls):
super().setUpClass()
# Invoice name are tracked by the web-services so this constant tries to get a new unique invoice name at each
# execution.
@ -58,34 +55,11 @@ class TestEdiTbaiWebServices(TestEsEdiTbaiCommon):
def test_edi_gipuzkoa(self):
self._set_tax_agency('gipuzkoa')
self.moves.action_process_edi_web_services(with_commit=False)
generated_files = self._process_documents_web_services(self.moves, {'es_tbai'})
self.assertTrue(generated_files)
self.assertRecordValues(self.out_invoice, [{'edi_state': 'sent'}])
def test_edi_cancellation(self):
self._set_tax_agency("gipuzkoa")
# Post the invoices
self.moves.action_process_edi_web_services(with_commit=False)
generated_files = self._process_documents_web_services(self.moves, {"es_tbai"})
self.assertTrue(generated_files)
self.assertRecordValues(
self.moves,
[
{"edi_state": "sent", "state": "posted"},
{"edi_state": False, "state": "posted"},
],
)
# Cancel the invoices
self.moves.invalidate_recordset(["l10n_es_tbai_post_xml"])
self.moves.button_cancel_posted_moves()
self.moves.action_process_edi_web_services(with_commit=False)
generated_files = self._process_documents_web_services(self.moves, {"es_tbai"})
# self.assertTrue(generated_files)
self.assertRecordValues(
self.moves,
[
{"edi_state": "cancelled", "state": "cancel"},
{"edi_state": False, "state": "cancel"},
],
)
self._get_invoice_send_wizard(self.out_invoice).action_send_and_print()
self.assertEqual(self.out_invoice.l10n_es_tbai_state, 'sent')
self.assertTrue(self.out_invoice.l10n_es_tbai_post_document_id.xml_attachment_id)
self.in_invoice.l10n_es_tbai_send_bill()
self.assertEqual(self.in_invoice.l10n_es_tbai_state, 'sent')
self.assertTrue(self.in_invoice.l10n_es_tbai_post_document_id.xml_attachment_id)

View file

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from base64 import b64encode
from datetime import datetime
from datetime import datetime, date
from freezegun import freeze_time
from lxml import etree
@ -23,7 +22,7 @@ class TestEdiTbaiXmls(TestEsEdiTbaiCommon):
cls.out_invoice = cls.env['account.move'].create({
'name': 'INV/01',
'move_type': 'out_invoice',
'invoice_date': datetime.now(),
'invoice_date': date(2025, 1, 1),
'partner_id': cls.partner_a.id,
'invoice_line_ids': [(0, 0, {
'product_id': cls.product_a.id,
@ -33,14 +32,45 @@ class TestEdiTbaiXmls(TestEsEdiTbaiCommon):
'tax_ids': [(6, 0, cls._get_tax_by_xml_id('s_iva21b').ids)],
})],
})
cls.edi_format = cls.env.ref('l10n_es_edi_tbai.edi_es_tbai')
def create_total_refund(self):
move_reversal = self.env['account.move.reversal'].with_context(
active_model="account.move",
active_ids=self.out_invoice.ids
).create({
'date': '2020-02-01',
'reason': 'no reason',
'journal_id': self.out_invoice.journal_id.id,
})
reversal = move_reversal.refund_moves()
reverse_move = self.env['account.move'].browse(reversal['res_id'])
reverse_move.action_post()
return reverse_move
def test_xml_tree_post(self):
"""Test of Customer Invoice XML"""
with freeze_time(self.frozen_today):
xml_doc = self.edi_format._get_l10n_es_tbai_invoice_xml(self.out_invoice, cancel=False)[self.out_invoice]['xml_file']
edi_document = self.out_invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(self.out_invoice._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_doc.remove(xml_doc.find("Signature", namespaces=NS_MAP))
xml_expected = etree.fromstring(super().L10N_ES_TBAI_SAMPLE_XML_POST)
xml_expected = etree.fromstring(super()._get_sample_xml('xml_post.xml'))
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_post_refund(self):
"""Test of Customer Invoice XML"""
with freeze_time(self.frozen_today):
edi_document = self.out_invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(self.out_invoice._l10n_es_tbai_get_values(cancel=False))
self.out_invoice.action_post()
self.out_invoice.l10n_es_tbai_post_document_id = edi_document.id
refund = self.create_total_refund()
edi_document = refund._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(refund._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_doc.remove(xml_doc.find("Signature", namespaces=NS_MAP))
xml_expected = etree.fromstring(super()._get_sample_xml('xml_post_refund.xml'))
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_post_generic_sequence(self):
@ -48,10 +78,13 @@ class TestEdiTbaiXmls(TestEsEdiTbaiCommon):
with freeze_time(self.frozen_today):
invoice = self.out_invoice.copy({
'name': 'INV01',
'invoice_date': date(2025, 1, 1),
})
xml_doc = self.edi_format._get_l10n_es_tbai_invoice_xml(invoice, cancel=False)[invoice]['xml_file']
edi_document = invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(invoice._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_doc.remove(xml_doc.find("Signature", namespaces=NS_MAP))
xml_expected = etree.fromstring(super().L10N_ES_TBAI_SAMPLE_XML_POST)
xml_expected = etree.fromstring(super()._get_sample_xml('xml_post.xml'))
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_post_multicurrency(self):
@ -90,25 +123,27 @@ class TestEdiTbaiXmls(TestEsEdiTbaiCommon):
})
with freeze_time(self.frozen_today):
xml_doc = self.edi_format._get_l10n_es_tbai_invoice_xml(invoice, cancel=False)[invoice]['xml_file']
edi_document = invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(invoice._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_doc.remove(xml_doc.find("Signature", namespaces=NS_MAP))
xml_expected_base = etree.fromstring(super().L10N_ES_TBAI_SAMPLE_XML_POST)
xml_expected_base = etree.fromstring(super()._get_sample_xml('xml_post.xml'))
xpath = """
<xpath expr="//DetallesFactura" position="replace">
<DetallesFactura>
<IDDetalleFactura>
<DescripcionDetalle>producta</DescripcionDetalle>
<Cantidad>5.00</Cantidad>
<ImporteUnitario>246.00</ImporteUnitario>
<Descuento>246.00</Descuento>
<ImporteTotal>1190.64</ImporteTotal>
<Cantidad>5.00000000</Cantidad>
<ImporteUnitario>246.00000000</ImporteUnitario>
<Descuento>246.00000000</Descuento>
<ImporteTotal>1190.64000000</ImporteTotal>
</IDDetalleFactura>
<IDDetalleFactura>
<DescripcionDetalle>producta</DescripcionDetalle>
<Cantidad>5.00</Cantidad>
<ImporteUnitario>246.00</ImporteUnitario>
<Descuento>1230.00</Descuento>
<ImporteTotal>0.00</ImporteTotal>
<Cantidad>5.00000000</Cantidad>
<ImporteUnitario>246.00000000</ImporteUnitario>
<Descuento>1230.00000000</Descuento>
<ImporteTotal>0.00000000</ImporteTotal>
</IDDetalleFactura>
</DetallesFactura>
</xpath>
@ -131,9 +166,11 @@ class TestEdiTbaiXmls(TestEsEdiTbaiCommon):
def test_xml_tree_post_retention(self):
self.out_invoice.invoice_line_ids.tax_ids = [(4, self._get_tax_by_xml_id('s_irpf15').id)]
with freeze_time(self.frozen_today):
xml_doc = self.edi_format._get_l10n_es_tbai_invoice_xml(self.out_invoice, cancel=False)[self.out_invoice]['xml_file']
edi_document = self.out_invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(self.out_invoice._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_doc.remove(xml_doc.find("Signature", namespaces=NS_MAP))
xml_expected_base = etree.fromstring(super().L10N_ES_TBAI_SAMPLE_XML_POST)
xml_expected_base = etree.fromstring(super()._get_sample_xml('xml_post.xml'))
xpath = """
<xpath expr="//ImporteTotalFactura" position="after">
<RetencionSoportada>600.00</RetencionSoportada>
@ -142,8 +179,29 @@ class TestEdiTbaiXmls(TestEsEdiTbaiCommon):
xml_expected = self.with_applied_xpath(xml_expected_base, xpath)
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_post_multitax(self):
self.out_invoice.invoice_line_ids.tax_ids = [self._get_tax_by_xml_id('s_req52').id, self._get_tax_by_xml_id('s_iva21b').id]
with freeze_time(self.frozen_today):
edi_document = self.out_invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(self.out_invoice._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_doc.remove(xml_doc.find("Signature", namespaces=NS_MAP))
xml_expected_base = etree.fromstring(super()._get_sample_xml('xml_post.xml'))
xpath = """
<xpath expr="//ImporteTotal" position="replace">
<ImporteTotal>5048.00000000</ImporteTotal>
</xpath>
<xpath expr="//ImporteTotalFactura" position="replace">
<ImporteTotalFactura>5048.00</ImporteTotalFactura>
</xpath>
"""
xml_expected = self.with_applied_xpath(xml_expected_base, xpath)
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_in_post(self):
"""Test XML of vendor bill for LROE Batuz"""
self.company_data['company'].l10n_es_tbai_tax_agency = 'bizkaia'
with freeze_time(self.frozen_today):
self.in_invoice = self.env['account.move'].create({
'name': 'INV/01',
@ -159,12 +217,43 @@ class TestEdiTbaiXmls(TestEsEdiTbaiCommon):
'tax_ids': [(6, 0, self._get_tax_by_xml_id('p_iva21_bc').ids)],
})],
})
xml_doc = etree.fromstring(self.edi_format._l10n_es_tbai_get_invoice_content_edi(self.in_invoice))
xml_expected = etree.fromstring(super().L10N_ES_TBAI_SAMPLE_XML_POST_IN)
edi_document = self.in_invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(self.in_invoice._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_expected = etree.fromstring(super()._get_sample_xml('xml_post_in.xml'))
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_in_140_post(self):
"""Test XML of vendor bill for LROE Batuz autonomos (modelo 140)"""
self.company_data['company'].l10n_es_tbai_tax_agency = 'bizkaia'
self.company_data['company'].vat = '09760433S'
self.env['ir.config_parameter'].sudo().set_param('l10n_es_edi_tbai.epigrafe', '102100')
with freeze_time(self.frozen_today):
self.in_invoice = self.env['account.move'].create({
'name': 'INV/01',
'ref': 'INV/5234',
'move_type': 'in_invoice',
'invoice_date': datetime.now(),
'partner_id': self.partner_a.id,
'invoice_line_ids': [(0, 0, {
'product_id': self.product_a.id,
'price_unit': 1000.0,
'quantity': 5,
'discount': 20.0,
'tax_ids': [(6, 0, self._get_tax_by_xml_id('p_iva21_bc').ids)],
})],
})
edi_document = self.in_invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(self.in_invoice._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_expected = etree.fromstring(super()._get_sample_xml('xml_post_in_140.xml'))
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_no_deducible_tax(self):
"""Test XML of vendor bill with non deductible tax"""
self.company_data['company'].l10n_es_tbai_tax_agency = 'bizkaia'
with freeze_time(self.frozen_today):
self.in_invoice = self.env['account.move'].create({
'name': 'INV/01',
@ -179,12 +268,16 @@ class TestEdiTbaiXmls(TestEsEdiTbaiCommon):
'tax_ids': [(6, 0, self._get_tax_by_xml_id('p_iva10_nd').ids)],
})],
})
xml_doc = etree.fromstring(self.edi_format._l10n_es_tbai_get_invoice_content_edi(self.in_invoice))
xml_expected = etree.fromstring(super().L10N_ES_TBAI_SAMPLE_XML_POST_IN_ND)
edi_document = self.in_invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(self.in_invoice._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_expected = etree.fromstring(super()._get_sample_xml('xml_post_in_nd.xml'))
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_in_ic_post(self):
"""Test XML of vendor bill for LROE Batuz intra-community"""
self.company_data['company'].l10n_es_tbai_tax_agency = 'bizkaia'
with freeze_time(self.frozen_today):
self.in_invoice = self.env['account.move'].create({
'name': 'INV/01',
@ -206,38 +299,47 @@ class TestEdiTbaiXmls(TestEsEdiTbaiCommon):
'tax_ids': [(6, 0, self._get_tax_by_xml_id('p_iva21_sp_in').ids)],
})],
})
xml_doc = etree.fromstring(self.edi_format._l10n_es_tbai_get_invoice_content_edi(self.in_invoice))
xml_expected = etree.fromstring(super().L10N_ES_TBAI_SAMPLE_XML_POST_IN_IC)
edi_document = self.in_invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(self.in_invoice._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_expected = etree.fromstring(super()._get_sample_xml('xml_post_in_ic.xml'))
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_cancel(self):
self.out_invoice.l10n_es_tbai_post_xml = b64encode(b"""<TicketBAI>
post_xml = b"""<TicketBAI>
<CabeceraFactura><FechaExpedicionFactura>01-01-2025</FechaExpedicionFactura></CabeceraFactura>
<ds:SignatureValue xmlns:ds="http://www.w3.org/2000/09/xmldsig#">TEXT</ds:SignatureValue>
</TicketBAI>""") # hack to set out_invoice's registration date
xml_doc = self.edi_format._get_l10n_es_tbai_invoice_xml(self.out_invoice, cancel=True)[self.out_invoice]['xml_file']
</TicketBAI>""" # hack to set out_invoice's registration date
post_edi_document = self.out_invoice._l10n_es_tbai_create_edi_document()
post_xml_attachment = self.env['ir.attachment'].create({
'name': self.out_invoice._l10n_es_tbai_get_attachment_name(cancel=True),
'raw': post_xml,
'type': 'binary',
'res_model': 'account.move',
'res_id': self.out_invoice.id,
'res_field': 'xml_attachment_id',
})
post_edi_document.xml_attachment_id = post_xml_attachment
self.out_invoice.l10n_es_tbai_post_document_id = post_edi_document
cancel_edi_document = self.out_invoice._l10n_es_tbai_create_edi_document(cancel=True)
cancel_edi_document._generate_xml(self.out_invoice._l10n_es_tbai_get_values(cancel=True))
xml_doc = cancel_edi_document._get_xml()
xml_doc.remove(xml_doc.find("Signature", namespaces=NS_MAP))
xml_expected = etree.fromstring(super().L10N_ES_TBAI_SAMPLE_XML_CANCEL)
xml_expected = etree.fromstring(super()._get_sample_xml('xml_cancel.xml'))
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_credit_note_post(self):
"""Test of Customer Credit Note XML"""
self.out_invoice.action_post()
move_reversal = self.env['account.move.reversal'].with_context(
active_model="account.move",
active_ids=self.out_invoice.ids
).create({
'date': str(self.out_invoice.invoice_date),
'reason': 'no reason',
'l10n_es_tbai_refund_reason': 'R1',
'refund_method': 'cancel',
'journal_id': self.out_invoice.journal_id.id,
})
reversal = move_reversal.reverse_moves()
reverse_move = self.env['account.move'].browse(reversal['res_id'])
def test_xml_tree_fecha_operacion(self):
"""
Test that when the invoice_date and delivery date are in the past but are the same
FechaOperacion appears in the xml since it is still different from
the date the xml is generated (FechaExpedicionFactura)
"""
self.out_invoice.delivery_date = self.out_invoice.invoice_date
with freeze_time(self.frozen_today):
xml_doc = self.edi_format._get_l10n_es_tbai_invoice_xml(reverse_move, cancel=False)[reverse_move]['xml_file']
edi_document = self.out_invoice._l10n_es_tbai_create_edi_document(cancel=False)
edi_document._generate_xml(self.out_invoice._l10n_es_tbai_get_values(cancel=False))
xml_doc = edi_document._get_xml()
xml_doc.remove(xml_doc.find("Signature", namespaces=NS_MAP))
xml_expected = etree.fromstring(super().L10N_ES_TBAI_CREDIT_NOTE_XML_POST)
xml_expected = etree.fromstring(super()._get_sample_xml('xml_fecha_operacion.xml'))
self.assertXmlTreeEqual(xml_doc, xml_expected)

View file

@ -0,0 +1,48 @@
from unittest.mock import patch
from odoo.tests import tagged
from .common import TestEsEdiTbaiCommonGipuzkoa
@tagged('post_install', '-at_install', 'post_install_l10n')
class TestSendAndPrintEdiGipuzkoa(TestEsEdiTbaiCommonGipuzkoa):
def test_post_and_cancel_tbai_credit_note(self):
invoice = self._create_posted_invoice()
invoice_send_wizard = self._get_invoice_send_wizard(invoice)
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
invoice_send_wizard.action_send_and_print()
move_reversal = self.env['account.move.reversal']\
.with_context(active_model="account.move", active_ids=invoice.ids)\
.create({
'journal_id': invoice.journal_id.id,
'l10n_es_tbai_refund_reason': 'R4',
})
credit_note_id = move_reversal.refund_moves()['res_id']
credit_note = self.env['account.move'].browse(credit_note_id)
credit_note.action_post()
self.assertEqual(credit_note.l10n_es_tbai_refund_reason, 'R4')
send_wizard = self._get_invoice_send_wizard(credit_note)
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_post_invoice_success,
):
send_wizard.action_send_and_print()
self.assertEqual(credit_note.l10n_es_tbai_state, 'sent')
with patch(
'odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_document.requests.Session.request',
return_value=self.mock_response_cancel_invoice_success,
):
credit_note.l10n_es_tbai_cancel()
self.assertEqual(credit_note.l10n_es_tbai_state, 'cancelled')

View file

@ -1,13 +0,0 @@
from odoo.addons.l10n_es_edi_sii.tests import test_resequence
class TestResequenceTbai(test_resequence.TestResequenceSII):
@classmethod
def setUpClass(
cls,
chart_template_ref="l10n_es.account_chart_template_full",
edi_format_ref="l10n_es_edi_sii.edi_es_sii",
):
super().setUpClass(
chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref
)

View file

@ -6,17 +6,52 @@
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@id='other_tab_group']/group[last()]" position='after'>
<group id="ticketbai_group" string="TicketBAI" attrs="{'invisible': [('l10n_es_tbai_is_required', '=', False)]}">
<field name="l10n_es_tbai_is_required" invisible="1"/>
<field name="l10n_es_tbai_chain_index" groups="base.group_no_one"/>
<field name="l10n_es_tbai_refund_reason"
attrs="{
'readonly': [('state', '!=', 'draft')],
'invisible': [('move_type', 'not in', ('in_refund', 'out_refund'))]
}"/>
<field name="reversed_entry_id" attrs="{'invisible': [('move_type', '!=', 'in_refund')]}"/>
</group>
<xpath expr="//header" position="inside">
<field name="l10n_es_tbai_is_required" invisible="1"/>
<field name="l10n_es_tbai_post_document_id" invisible="1"/>
<button
string="Send Bill to TicketBAI"
name="l10n_es_tbai_send_bill"
type="object"
invisible="not l10n_es_tbai_is_required or move_type not in ('in_invoice', 'in_refund') or state != 'posted' or l10n_es_tbai_state != 'to_send'"
/>
<button
string="Resend to TicketBAI"
name="l10n_es_tbai_resend_bill"
type="object"
invisible="not l10n_es_tbai_is_required or move_type not in ('in_invoice', 'in_refund') or state != 'posted' or l10n_es_tbai_state != 'sent'"
/>
<button
string="TicketBAI Cancel"
name="l10n_es_tbai_cancel"
type="object"
invisible="l10n_es_tbai_state != 'sent'"
/>
</xpath>
<xpath expr="//group[@id='header_right_group']" position='inside'>
<field
name="l10n_es_tbai_refund_reason"
invisible="move_type not in ('in_refund', 'out_refund') or not l10n_es_tbai_is_required"
readonly="state != 'draft'"
/>
</xpath>
<xpath expr="//page[@id='other_tab_entry']" position='after'>
<page
id="ticketbai_tab"
string="TicketBAI"
invisible="not l10n_es_tbai_is_required or not l10n_es_tbai_post_document_id"
>
<group>
<field name="l10n_es_tbai_state"/>
<field name="l10n_es_tbai_chain_index" groups="base.group_no_one"/>
<field name="l10n_es_tbai_post_file_name" invisible="1"/>
<field name="l10n_es_tbai_post_file" widget="binary" filename="l10n_es_tbai_post_file_name"/>
<field name="l10n_es_tbai_cancel_file_name" invisible="1"/>
<field name="l10n_es_tbai_cancel_file" widget="binary" filename="l10n_es_tbai_cancel_file_name"/>
<field name="l10n_es_tbai_reversed_ids" invisible="move_type != 'in_refund'" widget="many2many_tags"/>
<field name="reversed_entry_id" invisible="move_type != 'in_refund'"/>
</group>
</page>
</xpath>
</field>
</record>

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<record id="certificate_certificate_view_search" model="ir.ui.view">
<field name="name">certificate_certificate_view_search.inherit.l10n_es_edi_tbai</field>
<field name="model">certificate.certificate</field>
<field name="inherit_id" ref="certificate.certificate_certificate_view_search"/>
<field name="arch" type="xml">
<filter name="scope_general" position="after">
<filter string="TBAI" name="scope_tbai" domain="[('scope','=','tbai')]" help="TBAI certificates"/>
</filter>
</field>
</record>
<record id="l10n_es_edi_tbai_certificate_action" model="ir.actions.act_window">
<field name="name">Certificates for EDI TicketBAI invoices on Spain</field>
<field name="res_model">certificate.certificate</field>
<field name="view_mode">list,form</field>
<field name="context">{'scope': 'tbai', 'search_default_scope_tbai': 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">Create the first certificate</p>
</field>
</record>
<record id="certificate_certificate_view_form" model="ir.ui.view">
<field name="name">certificate_certificate_view_form.inherit.l10n_es_edi_tbai</field>
<field name="model">certificate.certificate</field>
<field name="inherit_id" ref="certificate.certificate_certificate_view_form"/>
<field name="arch" type="xml">
<field name="scope" position="attributes">
<attribute name="invisible">False</attribute>
</field>
</field>
</record>
<menuitem id="menu_l10n_es_edi_tbai_root"
name="Spain TicketBAI"
sequence="110"
groups="account.group_account_manager"
parent="account.menu_finance_configuration">
<menuitem id="menu_l10n_es_edi_tbai_certificates"
name="Certificates"
action="l10n_es_edi_tbai_certificate_action"
sequence="100"
groups="account.group_account_manager"/>
</menuitem>
</data>
</odoo>

View file

@ -2,10 +2,10 @@
<odoo>
<template id="l10n_es_tbai_external_layout_standard" inherit_id="account.report_invoice_document">
<xpath expr="//div[@id='qrcode']" position="after">
<div name="l10n_es_tbai_qrcode" t-if="o._l10n_es_tbai_is_in_chain()" style="page-break-inside: avoid">
<div t-out="o._get_l10n_es_tbai_id()"/>
<div name="l10n_es_tbai_qrcode" t-if="o.l10n_es_tbai_state == 'sent'" style="page-break-inside: avoid">
<div t-out="o.l10n_es_tbai_post_document_id._get_tbai_id()"/>
<img
t-att-src="'/report/barcode/?barcode_type=%s&amp;value=%s&amp;width=%s&amp;height=%s&amp;barLevel=%s'%('QR', quote_plus(o._get_l10n_es_tbai_qr()), 125, 125, 'M')"/>
t-att-src="'/report/barcode/?barcode_type=%s&amp;value=%s&amp;width=%s&amp;height=%s&amp;barLevel=%s&amp;quiet=%s'%('QR', quote_plus(o.l10n_es_tbai_post_document_id._get_tbai_qr()), 125, 125, 'M', 0)"/>
<!-- NOTE: Sizes assume a 90 dpi resolution to meet requirements (between 30 and 40 mm) -->
</div>

View file

@ -8,7 +8,7 @@
<xpath expr="//page[@name='general_info']/group/group[last()]" position="after">
<group>
<field name="l10n_es_tbai_license_html" type="object"
attrs="{'invisible': [('country_code', '!=', 'ES')]}"/>
invisible="country_code != 'ES'"/>
</group>
</xpath>
</field>
@ -16,10 +16,10 @@
<!-- TicketBAI specifies there needs to be a menu link to display the license information -->
<menuitem id="menu_l10n_es_edi_tbai_license"
name="Licenses (TicketBAI)"
name="Licenses"
action="base.action_res_company_form"
sequence="90"
parent="l10n_es_edi_sii.menu_l10n_es_edi_root"
parent="menu_l10n_es_edi_tbai_root"
groups="account.group_account_manager">
</menuitem>
</odoo>

View file

@ -3,21 +3,43 @@
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.l10n.es</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
<field name="inherit_id" ref="l10n_es.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@data-key='account']/div[@name='spain_localization']//div[hasclass('o_setting_right_pane')]/span[hasclass('o_form_label')]" position="replace">
<span class="o_form_label">Registro de Libros connection SII/TicketBAI</span>
<xpath expr="//block[@name='spain_localization']" position="attributes">
<attribute name="invisible">country_code != 'ES'</attribute>
</xpath>
<xpath expr="//div[@data-key='account']/div[@name='spain_localization']//div[hasclass('o_setting_right_pane')]//div[hasclass('mt16')]/*" position="before">
<label for="l10n_es_tbai_tax_agency" class="o_light_label"/>
<field name="l10n_es_tbai_tax_agency"/>
<div class="text-muted" attrs="{'invisible': [('l10n_es_tbai_tax_agency', '!=', False)]}">
No tax agency selected: TicketBAI not activated.
</div>
<div class="text-muted" attrs="{'invisible': [('l10n_es_tbai_tax_agency', '=', False)]}">
Tax agency selected: TicketBAI is activated.
</div>
<br/>
<xpath expr="//block[@name='spain_localization']" position="inside">
<!-- Invisible fields -->
<field name="l10n_es_tbai_certificate_ids" invisible="1"/>
<setting string="Registro de Libros connection TicketBAI" company_dependent="1">
<div class="content-group">
<div class="mt16">
<label for="l10n_es_tbai_tax_agency" class="o_light_label"/>
<field name="l10n_es_tbai_tax_agency"/>
<div class="text-muted" invisible="l10n_es_tbai_tax_agency">
No tax agency selected: TicketBAI not activated.
</div>
<div class="text-muted" invisible="not l10n_es_tbai_tax_agency">
Tax agency selected: invoices will be sent by TicketBAI.
</div>
<br/>
<div class="o_row">
<label for="l10n_es_tbai_test_env" class="o_light_label"/>
<field name="l10n_es_tbai_test_env"/>
</div>
<div class="text-muted" invisible="not l10n_es_tbai_test_env">
Test mode: EDI data is sent to separate test servers and is not considered official.
</div>
<div class="text-muted" invisible="l10n_es_tbai_test_env">
Production mode: EDI data is sent to the official agency servers.
</div>
<br/>
<div>
<button name="%(l10n_es_edi_tbai_certificate_action)d" type="action" class="oe_link">Manage certificates (TicketBAI)</button>
</div>
</div>
</div>
</setting>
</xpath>
</field>
</record>

View file

@ -1,5 +1,2 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move_reversal
from . import account_resequence_wizard

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
from odoo.addons.l10n_es_edi_tbai.models.account_move import TBAI_REFUND_REASONS
from odoo.exceptions import UserError
@ -13,13 +14,7 @@ class AccountMoveReversal(models.TransientModel):
)
l10n_es_tbai_refund_reason = fields.Selection(
selection=[
('R1', "R1: Art. 80.1, 80.2, 80.6 and rights founded error"),
('R2', "R2: Art. 80.3"),
('R3', "R3: Art. 80.4"),
('R4', "R4: Art. 80 - other"),
('R5', "R5: Factura rectificativa en facturas simplificadas"),
],
selection=TBAI_REFUND_REASONS,
string="Invoice Refund Reason Code (TicketBai)",
help="BOE-A-1992-28740. Ley 37/1992, de 28 de diciembre, del Impuesto sobre el "
"Valor Añadido. Artículo 80. Modificación de la base imponible.",
@ -30,13 +25,13 @@ class AccountMoveReversal(models.TransientModel):
for wizard in self:
moves_tbai_required = set(m.l10n_es_tbai_is_required for m in wizard.move_ids)
if len(moves_tbai_required) > 1:
raise UserError("Reversals mixing invoices with and without TicketBAI are not allowed.")
raise UserError(self.env._("Reversals mixing invoices with and without TicketBAI are not allowed."))
wizard.l10n_es_tbai_is_required = moves_tbai_required.pop()
def _prepare_default_reversal(self, move):
# OVERRIDE
values = super()._prepare_default_reversal(move)
if move.company_id.country_id.code == "ES" and move.l10n_es_tbai_is_required:
if move.l10n_es_tbai_is_required:
values.update({
'l10n_es_tbai_refund_reason': self.l10n_es_tbai_refund_reason,
})

View file

@ -7,7 +7,7 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='journal_id']" position="after">
<field name="l10n_es_tbai_is_required" invisible="1"/>
<field attrs="{'invisible': [('l10n_es_tbai_is_required', '=', False)]}" name="l10n_es_tbai_refund_reason" widget="selection"/>
<field invisible="not l10n_es_tbai_is_required" name="l10n_es_tbai_refund_reason" widget="selection"/>
</xpath>
</field>
</record>

View file

@ -1,14 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountResequenceWizard(models.TransientModel):
_inherit = "account.resequence.wizard"
def _frozen_edi_documents(self):
docs = super()._frozen_edi_documents()
# TicketBAI/Batuz vendor bills are sent with ref, so they can be resequenced
return docs.filtered(
lambda doc: doc.edi_format_id.code != "es_tbai"
or doc.move_id.is_sale_document()
)

View file

@ -1,12 +1,15 @@
[project]
name = "odoo-bringout-oca-ocb-l10n_es_edi_tbai"
version = "16.0.0"
description = "Spain - TicketBAI - Odoo addon"
description = "Spain - TicketBAI -
Odoo addon
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-l10n_es_edi_sii>=16.0.0",
"odoo-bringout-oca-ocb-l10n_es>=19.0.0",
"TODO_MAP-certificate>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -16,7 +19,7 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]