mirror of
https://github.com/bringout/oca-ocb-l10n_europe.git
synced 2026-04-27 04:01:59 +02:00
19.0 vanilla
This commit is contained in:
parent
ff721d030e
commit
7721452493
1826 changed files with 124775 additions and 274114 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- disable_l10n_es_edi_integration
|
||||
UPDATE res_company
|
||||
SET l10n_es_tbai_test_env = true;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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()
|
||||
|
|
@ -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}'
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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'))
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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.',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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>
|
||||
|
|
@ -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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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>
|
||||
|
|
@ -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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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>
|
||||
|
|
@ -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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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>
|
||||
|
|
@ -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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
@ -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')
|
||||
|
|
@ -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')
|
||||
|
|
@ -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')
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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&value=%s&width=%s&height=%s&barLevel=%s'%('QR', quote_plus(o._get_l10n_es_tbai_qr()), 125, 125, 'M')"/>
|
||||
t-att-src="'/report/barcode/?barcode_type=%s&value=%s&width=%s&height=%s&barLevel=%s&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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue