[IMP] l10n_es_edi_tbai: lroe vendor bill for Batuz

From the first of January, Ticketbai becomes required in
the region of Batuz (Bizkaia).

And we were missing a piece where also the vendor bills
need to be sent, similar to SII (but different)

For that, they use an LROE structure, which we also used
for sending customer invoices, but with the regular Ticketbai
XML in there (encoded in base64).

For vendor bills, it is however not the regular Ticketbai
XML and the chaining and signature are also not needed, but
the enveloping structure is built in the same way.

So, we activate Ticketbai by default on vendor bill journals,
but it will only be sending when the agency is Batuz.

Thanks to Moduon and Landoo for the extra push.

opw-3628750

closes odoo/odoo#146625

Signed-off-by: Quentin De Paoli <qdp@odoo.com>
This commit is contained in:
Josse Colpaert
2023-12-22 13:31:41 +00:00
parent fefea440c8
commit f6d2e211a6
5 changed files with 370 additions and 38 deletions
@@ -34,5 +34,105 @@
</FacturaEmitida>
</FacturasEmitidas>
</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"/>
<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"/>
</template>
<template id="template_LROE_240_inner_recibidas">
<Cabecera>
<Modelo>240</Modelo>
<Capitulo>2</Capitulo>
<Operacion t-out="'A00' if is_emission else 'AN0'"/>
<Version>1.0</Version>
<Ejercicio t-out="fiscal_year"/>
<ObligadoTributario>
<NIF t-out="sender_vat"/>
<ApellidosNombreRazonSocial t-out="sender.name"/>
</ObligadoTributario>
</Cabecera>
<FacturasRecibidas>
<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()"/>
<SerieFactura t-out="seq_and_num[0]"/>
<NumFactura t-out="seq_and_num[1]"/>
<FechaExpedicionFactura t-out="format_date(invoice.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>
</IDRecibida>
</t>
<t t-else="">
<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>
<t t-set="partner" t-value="recipient['partner']"/>
<ApellidosNombreRazonSocial t-out="partner.name"/>
</EmisorFacturaRecibida>
<CabeceraFactura>
<t t-set="seq_and_num" t-value="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(invoice.invoice_date)"/>
<FechaRecepcion t-out="format_date(invoice.date)"/>
<TipoFactura t-out="tipofactura"/>
<t t-if="is_refund">
<FacturaRectificativa>
<Codigo t-out="credit_note_code"/>
<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()"/>
<SerieFactura t-out="seq_and_num[0]"/>
<NumFactura t-out="seq_and_num[1]"/>
<FechaExpedicionFactura t-out="format_date(credit_note_invoice.invoice_date)"/>
</IDFacturaRectificadaSustituida>
</FacturasRectificadasSustituidas>
</t>
</CabeceraFactura>
<DatosFactura>
<DescripcionOperacion t-out="invoice.ref"/>
<Claves>
<IDClave t-foreach="regime_key" t-as="key">
<ClaveRegimenIvaOpTrascendencia t-out="key"/>
</IDClave>
</Claves>
<ImporteTotalFactura t-out="format_float(amount_total)"/>
</DatosFactura>
<IVA>
<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'"/>
<BaseImponible t-out="tax['base']"/>
<TipoImpositivo t-out="tax['rec'].amount"/>
<CuotaIVASoportada t-out="tax['tax']"/>
<CuotaIVADeducible t-out="tax['tax']"/>
</DetalleIVA>
</IVA>
</t>
</FacturaRecibida>
</FacturasRecibidas>
</template>
</data>
</odoo>
@@ -50,14 +50,12 @@ class AccountEdiFormat(models.Model):
if self.code != 'es_tbai':
return super()._is_compatible_with_journal(journal)
return journal.country_code == 'ES' and journal.type == 'sale'
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 \
or move.move_type not in ('out_invoice', 'out_refund'):
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 {
@@ -103,25 +101,29 @@ class AccountEdiFormat(models.Model):
if self.code != 'es_tbai':
return super()._post_invoice_edi(invoice)
# 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()
if chain_head and chain_head != invoice and not chain_head._l10n_es_tbai_is_in_chain():
raise UserError(f"TicketBAI: Cannot post invoice while chain head ({chain_head.name}) has not been posted")
if invoice.is_purchase_document():
inv_xml = False # For Ticketbai Batuz vendor bills, we get the values later as it does not need chaining, ...
# 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
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()
if chain_head and chain_head != invoice and not chain_head._l10n_es_tbai_is_in_chain():
raise UserError(f"TicketBAI: Cannot post invoice while chain head ({chain_head.name}) has not been posted")
# 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)
# 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
# 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()
# 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)
@@ -161,14 +163,17 @@ class AccountEdiFormat(models.Model):
if self.code != 'es_tbai':
return super()._cancel_invoice_edi(invoice)
# 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
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)
# 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)
@@ -206,7 +211,6 @@ class AccountEdiFormat(models.Model):
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:
@@ -215,8 +219,13 @@ class AccountEdiFormat(models.Model):
def _l10n_es_tbai_get_invoice_content_edi(self, invoice):
cancel = invoice.edi_state in ('to_cancel', 'cancelled')
xml_tree = self._get_l10n_es_tbai_invoice_xml(invoice, cancel)[invoice]['xml_file']
return etree.tostring(xml_tree)
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
@@ -547,17 +556,55 @@ class AccountEdiFormat(models.Model):
return response_success, message, response_xml
def _l10n_es_tbai_prepare_post_params_bi(self, env, agency, invoice, invoice_xml, cancel=False):
"""Web service parameters for Bizkaia."""
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_casilla_10_balance')._get_matching_tags()
mod_303_11 = self.env.ref('l10n_es.mod_303_casilla_11_balance')._get_matching_tags()
tax_tags = invoice.invoice_line_ids.tax_ids.repartition_line_ids.tag_ids
intracom = bool(tax_tags & (mod_303_10 + mod_303_11))
values['regime_key'] = ['09'] if intracom else ['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
values['tipofactura'] = 'F5' if invoice._l10n_es_is_dua() else '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,
'tbai_b64_list': [b64encode(etree.tostring(invoice_xml, encoding="UTF-8")).decode()],
'fiscal_year': str(invoice.date.year),
}
lroe_str = env['ir.qweb']._render('l10n_es_edi_tbai.template_LROE_240_main', lroe_values)
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)
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)
@@ -574,10 +621,10 @@ class AccountEdiFormat(models.Model):
'eus-bizkaia-n3-content-type': 'application/xml',
'eus-bizkaia-n3-data': json.dumps({
'con': 'LROE',
'apa': '1.1',
'apa': '1.1' if invoice.is_sale_document() else '2',
'inte': {
'nif': lroe_values['sender_vat'],
'nrs': sender.name,
'nrs': invoice.company_id.name,
},
'drs': {
'mode': '240',
+32 -1
View File
@@ -4,6 +4,7 @@
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
@@ -80,7 +81,8 @@ class AccountMove(models.Model):
@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() \
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
@@ -221,3 +223,32 @@ class AccountMove(models.Model):
self.l10n_es_tbai_cancel_xml = b64_doc
else:
self.l10n_es_tbai_post_xml = b64_doc
def _get_vendor_bill_tax_values(self):
self.ensure_one()
results = defaultdict(lambda: {'base_amount': 0.0, 'tax_amount': 0.0})
amount_total = 0.0
for line in self.line_ids.filtered(lambda l: l.display_type in ('product', 'tax')):
if any(t.l10n_es_type == 'ignore' for t in line.tax_ids) or line.tax_line_id.l10n_es_type == 'ignore':
continue
if line.tax_line_id.l10n_es_type != 'retencion':
amount_total += line.balance
for tax in line.tax_ids.filtered(lambda t: t.l10n_es_type not in ('recargo', 'retencion')):
results[tax]['base_amount'] += line.balance
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
if tax.l10n_es_bien_inversion:
code = "I" # Investment Goods
if tax.tax_scope == 'service':
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}
+108
View File
@@ -212,3 +212,111 @@ class TestEsEdiTbaiCommon(AccountEdiTestCommon):
<NumSerieDispositivo>___ignore___</NumSerieDispositivo>
</HuellaTBAI>
</T:AnulaTicketBai>""".encode("utf-8")
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>2022</Ejercicio>
<ObligadoTributario>
<NIF>09760433S</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<FacturasRecibidas>
<FacturaRecibida>
<EmisorFacturaRecibida>
<IDOtro>
<IDType>02</IDType>
<ID>BE0477472701</ID>
</IDOtro>
<ApellidosNombreRazonSocial>&amp;@àÁ$£€èêÈÊöÔÇç¡⅛™³</ApellidosNombreRazonSocial>
</EmisorFacturaRecibida>
<CabeceraFactura>
<SerieFactura>INVTEST</SerieFactura>
<NumFactura>01</NumFactura>
<FechaExpedicionFactura>01-01-2022</FechaExpedicionFactura>
<FechaRecepcion>01-01-2022</FechaRecepcion>
<TipoFactura>F1</TipoFactura>
</CabeceraFactura>
<DatosFactura>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>01</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
<ImporteTotalFactura>4840.00</ImporteTotalFactura>
</DatosFactura>
<IVA>
<DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>C</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>4000.0</BaseImponible>
<TipoImpositivo>21.0</TipoImpositivo>
<CuotaIVASoportada>840.0</CuotaIVASoportada>
<CuotaIVADeducible>840.0</CuotaIVADeducible>
</DetalleIVA>
</IVA>
</FacturaRecibida>
</FacturasRecibidas>
</lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion>"""
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>2022</Ejercicio>
<ObligadoTributario>
<NIF>09760433S</NIF>
<ApellidosNombreRazonSocial>EUS Company</ApellidosNombreRazonSocial>
</ObligadoTributario>
</Cabecera>
<FacturasRecibidas>
<FacturaRecibida>
<EmisorFacturaRecibida>
<NIF>F35999705</NIF>
<ApellidosNombreRazonSocial>partner_b</ApellidosNombreRazonSocial>
</EmisorFacturaRecibida>
<CabeceraFactura>
<SerieFactura>INVTEST</SerieFactura>
<NumFactura>01</NumFactura>
<FechaExpedicionFactura>01-01-2022</FechaExpedicionFactura>
<FechaRecepcion>01-01-2022</FechaRecepcion>
<TipoFactura>F1</TipoFactura>
</CabeceraFactura>
<DatosFactura>
<Claves>
<IDClave>
<ClaveRegimenIvaOpTrascendencia>09</ClaveRegimenIvaOpTrascendencia>
</IDClave>
</Claves>
<ImporteTotalFactura>12000.00</ImporteTotalFactura>
</DatosFactura>
<IVA>
<DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>C</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>4000.0</BaseImponible>
<TipoImpositivo>21.0</TipoImpositivo>
<CuotaIVASoportada>840.0</CuotaIVASoportada>
<CuotaIVADeducible>840.0</CuotaIVADeducible>
</DetalleIVA><DetalleIVA>
<CompraBienesCorrientesGastosBienesInversion>G</CompraBienesCorrientesGastosBienesInversion>
<InversionSujetoPasivo>N</InversionSujetoPasivo>
<BaseImponible>8000.0</BaseImponible>
<TipoImpositivo>21.0</TipoImpositivo>
<CuotaIVASoportada>1680.0</CuotaIVASoportada>
<CuotaIVADeducible>1680.0</CuotaIVADeducible>
</DetalleIVA>
</IVA>
</FacturaRecibida>
</FacturasRecibidas>
</lrpjframp:LROEPJ240FacturasRecibidasAltaModifPeticion>
"""
+47 -1
View File
@@ -33,16 +33,62 @@ 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 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']
xml_doc.remove(xml_doc.find("Signature", namespaces=NS_MAP))
xml_expected = etree.fromstring(super().L10N_ES_TBAI_SAMPLE_XML_POST)
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_in_post(self):
"""Test XML of vendor bill for LROE Batuz"""
with freeze_time(self.frozen_today):
self.in_invoice = self.env['account.move'].create({
'name': 'INV/01',
'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)],
})],
})
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)
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_in_ic_post(self):
"""Test XML of vendor bill for LROE Batuz intra-community"""
with freeze_time(self.frozen_today):
self.in_invoice = self.env['account.move'].create({
'name': 'INV/01',
'move_type': 'in_invoice',
'invoice_date': datetime.now(),
'partner_id': self.partner_b.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_ic_bc').ids)],
}), (0, 0, {
'product_id': self.product_b.id,
'price_unit': 2000.0,
'quantity': 5,
'discount': 20.0,
'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)
self.assertXmlTreeEqual(xml_doc, xml_expected)
def test_xml_tree_cancel(self):
self.out_invoice.l10n_es_tbai_post_xml = b64encode(b"""<TicketBAI>
<CabeceraFactura><FechaExpedicionFactura>01-01-2022</FechaExpedicionFactura></CabeceraFactura>