[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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user