diff --git a/addons/l10n_it_edi/data/invoice_it_template.xml b/addons/l10n_it_edi/data/invoice_it_template.xml
index 8462e5ea738..b70094dd3b6 100644
--- a/addons/l10n_it_edi/data/invoice_it_template.xml
+++ b/addons/l10n_it_edi/data/invoice_it_template.xml
@@ -25,6 +25,16 @@
+
+ Currency
+
+
+
+
+ Exch.Rate
+
+
+
diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py
index b8f4f458688..68e6567e04c 100644
--- a/addons/l10n_it_edi/models/account_invoice.py
+++ b/addons/l10n_it_edi/models/account_invoice.py
@@ -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 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 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''"
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
diff --git a/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py b/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py
index 32bd25f6cfc..2b3737f94c6 100644
--- a/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py
+++ b/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py
@@ -518,7 +518,7 @@ class TestItEdi(TestItEdiCommon):
invoice_etree = self.with_applied_xpath(invoice_etree, "")
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),