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),