[FIX] l10n_it_edi: use euros in all cases

The invoice line and tax line sections of the Italian edi should be
reported in euros. Due to this, the line.balance is used instead of the
line.price_subtotal when calculating the PrezzoTotale' (price_subtotal)
of the line. The amount is then made negative if the line is
representing a completed downpayment, or if it is a negative line on a
reverse charge refund.

The unit price is calculated mostly the same way (using the new
price_subtotal value). If the line has a discount of 100% then the unit
price of the line is computed from the line.price_unit, by converting it
to euros using the _convert method on the invoice currency.

The exchange rate and the original currency / original currency amount
are listed on the lines using he 'AltriDatiGestionali' elements in the
xml.

A mistake with the way a reverse charge invoice was calculated has been
corrected too. Before reverse charge was determined by the document type
being 'TD16', 'TD17' or 'TD18'. Where instead it should be 'TD17',
'TD18' or 'TD19'.

A typo (inovice -> invoice) has also be corrected in the tests.

closes odoo/odoo#98534

Ticket-id: 2952018
Signed-off-by: Josse Colpaert <jco@odoo.com>
Signed-off-by: Daniel Kosky (dako) <dako@odoo.com>
This commit is contained in:
dbkosky
2022-08-22 16:48:51 +02:00
committed by dbkosky
parent 58fb7c1df1
commit 2d65713286
3 changed files with 71 additions and 33 deletions
@@ -25,6 +25,16 @@
<AliquotaIVA t-if="line.tax_ids.amount_type == 'percent'" t-esc="format_numbers(line.tax_ids.amount)"/>
<AliquotaIVA t-elif="line.tax_ids.amount_type != 'percent'" t-esc="'0.00'"/>
<Natura t-if="line.tax_ids.l10n_it_has_exoneration" t-esc="line.tax_ids.l10n_it_kind_exoneration"/>
<AltriDatiGestionali t-if="conversion_rate">
<TipoDato>Currency</TipoDato>
<RiferimentoTesto t-esc="format_alphanumeric(record.currency_id.name)"/>
<RiferimentoNumero t-esc="'%.06f' % line.price_subtotal"/>
</AltriDatiGestionali>
<AltriDatiGestionali t-if="conversion_rate">
<TipoDato>Exch.Rate</TipoDato>
<RiferimentoNumero t-esc="conversion_rate"/>
<RiferimentoData t-esc="format_date(record.invoice_date)"/>
</AltriDatiGestionali>
</DettaglioLinee>
</template>
+60 -32
View File
@@ -80,56 +80,73 @@ class AccountMove(models.Model):
"""
return len(self.commercial_partner_id.l10n_it_pa_index or '') == 6
def _l10n_it_edi_prepare_fatturapa_line_details(self, rc_refund=False):
def _l10n_it_edi_prepare_fatturapa_line_details(self, reverse_charge_refund=False, is_downpayment=False, convert_to_euros=True):
""" Returns a list of dictionaries passed to the template for the invoice lines (DettaglioLinee)
"""
invoice_lines = []
lines = self.invoice_line_ids.filtered(lambda l: not l.display_type in ('line_note', 'line_section'))
for num, line in enumerate(lines):
# In the case of reverse charge refund, the values for the unit price and total price should be negative
price_subtotal = line.price_subtotal if not rc_refund else -line.price_subtotal
price_subtotal = line.balance if convert_to_euros else line.price_subtotal
# The price_subtotal should be negative when:
# The line has downpayment lines, but is not a downpayment (i.e. the final invoice, from which downpayment lines are subtracted) or,
# the line is a reverse charge refund.
if (line._get_downpayment_lines() and not is_downpayment) or reverse_charge_refund:
price_subtotal = -abs(price_subtotal)
else:
price_subtotal = abs(price_subtotal)
# Unit price
unit_price = 0
price_unit = 0
if line.quantity and line.discount != 100.0:
unit_price = price_subtotal / ((1 - (line.discount or 0.0) / 100.0) * line.quantity)
price_unit = price_subtotal / ((1 - (line.discount or 0.0) / 100.0) * abs(line.quantity))
else:
unit_price = line.price_unit
price_unit = line.price_unit
description = line.name
if not is_downpayment:
if line.price_subtotal < 0:
moves = line._get_downpayment_lines().move_id
if moves:
description += ', '.join([move.name for move in moves])
line_dict = {
'line': line,
'line_number': num + 1,
'description': line.name or 'NO NAME',
'unit_price': unit_price,
'description': description or 'NO NAME',
'unit_price': price_unit,
'subtotal_price': price_subtotal,
}
invoice_lines.append(line_dict)
return invoice_lines
def _l10n_it_edi_prepare_fatturapa_tax_details(self, tax_details, rc_refund=False):
def _l10n_it_edi_prepare_fatturapa_tax_details(self, tax_details, reverse_charge_refund=False):
""" Returns a list of dictionaries passed to the template for the invoice lines (DatiRiepilogo)
"""
# Constraints within the edi make local rounding on price included taxes a problem.
# To solve this there is a <Arrotondamento> or 'rounding' field, such that:
# taxable base = sum(taxable base for each unit) + Arrotondamento
for _tax_name, tax_dict in tax_details['tax_details'].items():
base_amount_currency = tax_dict['base_amount_currency']
tax_amount_currency = tax_dict['tax_amount_currency']
tax_rate = tax_dict['tax'].amount
if tax_dict['tax'].price_include and tax_dict['tax'].amount_type == 'percent':
expected_base_amount_currency = tax_amount_currency * 100 / tax_rate if tax_rate else False
if expected_base_amount_currency and float_compare(base_amount_currency, expected_base_amount_currency, 2):
tax_dict['rounding'] = base_amount_currency - (tax_amount_currency * 100 / tax_rate)
tax_dict['base_amount_currency'] = base_amount_currency - tax_dict['rounding']
tax_lines = []
for _tax_name, tax_dict in tax_details['tax_details'].items():
# The assumption is that the company currency is EUR.
base_amount = tax_dict['base_amount']
tax_amount = tax_dict['tax_amount']
tax_rate = tax_dict['tax'].amount
expected_base_amount = tax_amount * 100 / tax_rate if tax_rate else False
tax = tax_dict['tax']
# Constraints within the edi make local rounding on price included taxes a problem.
# To solve this there is a <Arrotondamento> or 'rounding' field, such that:
# taxable base = sum(taxable base for each unit) + Arrotondamento
if tax.price_include and tax.amount_type == 'percent':
if expected_base_amount and float_compare(base_amount, expected_base_amount, 2):
tax_dict['rounding'] = base_amount - (tax_amount * 100 / tax_rate)
tax_dict['base_amount'] = base_amount - tax_dict['rounding']
if not reverse_charge_refund:
tax_dict['base_amount'] = abs(tax_dict['base_amount'])
tax_dict['tax_amount'] = abs(tax_dict['tax_amount'])
tax_line_dict = {
'tax': tax,
'rounding': tax_dict.get('rounding', False),
'base_amount': abs(tax_dict['base_amount_currency']) if not rc_refund else tax_dict['base_amount_currency'],
'tax_amount': abs(tax_dict['tax_amount_currency']) if not rc_refund else tax_dict['tax_amount_currency'],
'base_amount': tax_dict['base_amount'],
'tax_amount': tax_dict['tax_amount'],
}
tax_lines.append(tax_line_dict)
return tax_lines
@@ -187,12 +204,19 @@ class AccountMove(models.Model):
formato_trasmissione = "FPA12" if self._is_commercial_partner_pa() else "FPR12"
# Flags
in_eu = self.env['account.edi.format']._l10n_it_edi_partner_in_eu
is_self_invoice = self.env['account.edi.format']._l10n_it_edi_is_self_invoice(self)
document_type = self.env['account.edi.format']._l10n_it_get_document_type(self)
if self.env['account.edi.format']._l10n_it_is_simplified_document_type(document_type):
formato_trasmissione = "FSM10"
# Represent if the document is a reverse charge refund in a single variable
reverse_charge = document_type in ['TD17', 'TD18', 'TD19']
is_downpayment = document_type in ['TD02']
reverse_charge_refund = self.move_type == 'in_refund' and reverse_charge
convert_to_euros = self.currency_id.name != 'EUR'
# b64encode returns a bytestring, the template tries to turn it to string,
# but only gets the repr(pdf) --> "b'<base64_data>'"
pdf = self.env['ir.actions.report']._render_qweb_pdf("account.account_invoices", self.id)[0]
@@ -213,19 +237,22 @@ class AccountMove(models.Model):
or (partner.country_id.code == 'IT' and '0000000')
or 'XXXXXXX')
# Represent if the document is a reverse charge refund in a single variable
rc_refund = self.move_type == 'in_refund' and document_type in ['TD16', 'TD17', 'TD18']
# Self-invoices are technically -100%/+100% repartitioned
# but functionally need to be exported as 100%
document_total = self.amount_total
if is_self_invoice:
document_total += sum([abs(v['tax_amount_currency']) for k, v in tax_details['tax_details'].items()])
if rc_refund:
if reverse_charge_refund:
document_total = -abs(document_total)
invoice_lines = self._l10n_it_edi_prepare_fatturapa_line_details(rc_refund)
tax_lines = self._l10n_it_edi_prepare_fatturapa_tax_details(tax_details, rc_refund)
# Reference line for finding the conversion rate used in the document
conversion_line = self.invoice_line_ids.sorted(lambda l: abs(l.balance), reverse=True)[0] if self.invoice_line_ids else None
conversion_rate = float_repr(
abs(conversion_line.balance / conversion_line.amount_currency), precision_digits=5,
) if convert_to_euros and conversion_line else None
invoice_lines = self._l10n_it_edi_prepare_fatturapa_line_details(reverse_charge_refund, is_downpayment, convert_to_euros)
tax_lines = self._l10n_it_edi_prepare_fatturapa_tax_details(tax_details, reverse_charge_refund)
# Create file content.
template_values = {
@@ -239,7 +266,7 @@ class AccountMove(models.Model):
'buyer_is_company': is_self_invoice or partner.is_company,
'seller': seller,
'seller_partner': company.partner_id if not is_self_invoice else partner,
'currency': self.currency_id or self.company_currency_id,
'currency': self.currency_id or self.company_currency_id if not convert_to_euros else self.env.ref('base.EUR'),
'document_total': document_total,
'representative': company.l10n_it_tax_representative_partner_id,
'codice_destinatario': codice_destinatario,
@@ -263,9 +290,10 @@ class AccountMove(models.Model):
'get_vat_number': get_vat_number,
'get_vat_country': get_vat_country,
'in_eu': in_eu,
'rc_refund': rc_refund,
'rc_refund': reverse_charge_refund,
'invoice_lines': invoice_lines,
'tax_lines': tax_lines,
'conversion_rate': conversion_rate,
}
return template_values
@@ -518,7 +518,7 @@ class TestItEdi(TestItEdiCommon):
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_non_latin_and_latin_inovice(self):
def test_non_latin_and_latin_invoice(self):
invoice_etree = etree.fromstring(self.non_latin_and_latin_invoice._export_as_xml())
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),