[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:
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>&@àÁ$£€èêÈÊöÔÇç¡⅛™³</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>
|
||||
"""
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user