[REF] account_edi*: Improve the tax details computation for EDI

Add a generic method to compute taxes and group results together.

closes odoo/odoo#71743

Related: odoo/enterprise#18758
Signed-off-by: oco-odoo <oco-odoo@users.noreply.github.com>
This commit is contained in:
Laurent Smet
2021-06-30 07:37:19 +00:00
parent 5ab1d5f239
commit b98f6bd12a
7 changed files with 225 additions and 146 deletions
+183 -89
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
@@ -135,6 +136,187 @@ class AccountMove(models.Model):
# Export Electronic Document
####################################################
@api.model
def _add_edi_tax_values(self, results, grouping_key, serialized_grouping_key, tax_values):
# Add to global results.
results['tax_amount'] += tax_values['tax_amount']
results['tax_amount_currency'] += tax_values['tax_amount_currency']
# Add to tax details.
if serialized_grouping_key not in results['tax_details']:
tax_details = results['tax_details'][serialized_grouping_key]
tax_details.update(grouping_key)
tax_details.update({
'base_amount': tax_values['base_amount'],
'base_amount_currency': tax_values['base_amount_currency'],
})
else:
tax_details = results['tax_details'][serialized_grouping_key]
tax_details['tax_amount'] += tax_values['tax_amount']
tax_details['tax_amount_currency'] += tax_values['tax_amount_currency']
tax_details['group_tax_details'].append(tax_values)
def _prepare_edi_tax_details(self, filter_to_apply=None, grouping_key_generator=None):
''' Compute amounts related to taxes for the current invoice.
:param filter_to_apply: Optional filter to exclude some tax values from the final results.
The filter is defined as a method getting a dictionary as parameter
representing the tax values for a single repartition line.
This dictionary contains:
'base_line_id': An account.move.line record.
'tax_id': An account.tax record.
'tax_repartition_line_id': An account.tax.repartition.line record.
'base_amount': The tax base amount expressed in company currency.
'tax_amount': The tax amount expressed in company currency.
'base_amount_currency': The tax base amount expressed in foreign currency.
'tax_amount_currency': The tax amount expressed in foreign currency.
If the filter is returning False, it means the current tax values will be
ignored when computing the final results.
:param grouping_key_generator: Optional method used to group tax values together. By default, the tax values
are grouped by tax. This parameter is a method getting a dictionary as parameter
(same signature as 'filter_to_apply').
This method must returns a dictionary where values will be used to create the
grouping_key to aggregate tax values together. The returned dictionary is added
to each tax details in order to retrieve the full grouping_key later.
:return: The full tax details for the current invoice and for each invoice line
separately. The returned dictionary is the following:
'base_amount': The total tax base amount in company currency for the whole invoice.
'tax_amount': The total tax amount in company currency for the whole invoice.
'base_amount_currency': The total tax base amount in foreign currency for the whole invoice.
'tax_amount_currency': The total tax amount in foreign currency for the whole invoice.
'tax_details': A mapping of each grouping key (see 'grouping_key_generator') to a dictionary
containing:
'base_amount': The tax base amount in company currency for the current group.
'tax_amount': The tax amount in company currency for the current group.
'base_amount_currency': The tax base amount in foreign currency for the current group.
'tax_amount_currency': The tax amount in foreign currency for the current group.
'group_tax_details': The list of all tax values aggregated into this group.
'invoice_line_tax_details': A mapping of each invoice line to a dictionary containing:
'base_amount': The total tax base amount in company currency for the whole invoice line.
'tax_amount': The total tax amount in company currency for the whole invoice line.
'base_amount_currency': The total tax base amount in foreign currency for the whole invoice line.
'tax_amount_currency': The total tax amount in foreign currency for the whole invoice line.
'tax_details': A mapping of each grouping key (see 'grouping_key_generator') to a dictionary
containing:
'base_amount': The tax base amount in company currency for the current group.
'tax_amount': The tax amount in company currency for the current group.
'base_amount_currency': The tax base amount in foreign currency for the current group.
'tax_amount_currency': The tax amount in foreign currency for the current group.
'group_tax_details': The list of all tax values aggregated into this group.
'''
self.ensure_one()
def _serialize_python_dictionary(vals):
return '-'.join(str(vals[k]) for k in sorted(vals.keys()))
def default_grouping_key_generator(tax_values):
return {'tax': tax_values['tax_id']}
# Compute the taxes values for each invoice line.
invoice_lines = self.invoice_line_ids.filtered(lambda line: not line.display_type)
invoice_lines_tax_values_dict = {}
sign = -1 if self.is_inbound() else 1
for invoice_line in invoice_lines:
taxes_res = invoice_line.tax_ids.compute_all(
invoice_line.price_unit * (1 - (invoice_line.discount / 100.0)),
currency=invoice_line.currency_id,
quantity=invoice_line.quantity,
product=invoice_line.product_id,
partner=invoice_line.partner_id,
is_refund=invoice_line.move_id.move_type in ('in_refund', 'out_refund'),
)
tax_values_list = invoice_lines_tax_values_dict[invoice_line] = []
rate = abs(invoice_line.balance) / abs(invoice_line.amount_currency) if invoice_line.amount_currency else 0.0
for tax_res in taxes_res['taxes']:
tax_values_list.append({
'base_line_id': invoice_line,
'tax_id': self.env['account.tax'].browse(tax_res['id']),
'tax_repartition_line_id': self.env['account.tax.repartition.line'].browse(tax_res['tax_repartition_line_id']),
'base_amount': sign * invoice_line.company_currency_id.round(tax_res['base'] / rate if rate else 0.0),
'tax_amount': sign * invoice_line.company_currency_id.round(tax_res['amount'] / rate if rate else 0.0),
'base_amount_currency': sign * tax_res['base'],
'tax_amount_currency': sign * tax_res['amount'],
})
grouping_key_generator = grouping_key_generator or default_grouping_key_generator
# Apply 'filter_to_apply'.
if filter_to_apply:
invoice_lines_tax_values_dict = {
invoice_line: [x for x in tax_values_list if filter_to_apply(x)]
for invoice_line, tax_values_list in invoice_lines_tax_values_dict.items()
}
# Initialize the results dict.
invoice_global_tax_details = {
'base_amount': 0.0,
'tax_amount': 0.0,
'base_amount_currency': 0.0,
'tax_amount_currency': 0.0,
'tax_details': defaultdict(lambda: {
'base_amount': 0.0,
'tax_amount': 0.0,
'base_amount_currency': 0.0,
'tax_amount_currency': 0.0,
'group_tax_details': [],
}),
'invoice_line_tax_details': defaultdict(lambda: {
'base_amount': 0.0,
'tax_amount': 0.0,
'base_amount_currency': 0.0,
'tax_amount_currency': 0.0,
'tax_details': defaultdict(lambda: {
'base_amount': 0.0,
'tax_amount': 0.0,
'base_amount_currency': 0.0,
'tax_amount_currency': 0.0,
'group_tax_details': [],
}),
}),
}
# Apply 'grouping_key_generator' to 'invoice_lines_tax_values_list' and add all values to the final results.
for invoice_line in invoice_lines:
tax_values_list = invoice_lines_tax_values_dict[invoice_line]
# Add to invoice global tax amounts.
invoice_global_tax_details['base_amount'] += invoice_line.balance
invoice_global_tax_details['base_amount_currency'] += invoice_line.amount_currency
for tax_values in tax_values_list:
grouping_key = grouping_key_generator(tax_values)
serialized_grouping_key = _serialize_python_dictionary(grouping_key)
# Add to invoice line global tax amounts.
if serialized_grouping_key not in invoice_global_tax_details['invoice_line_tax_details'][invoice_line]:
invoice_line_global_tax_details = invoice_global_tax_details['invoice_line_tax_details'][invoice_line]
invoice_line_global_tax_details.update({
'base_amount': invoice_line.balance,
'base_amount_currency': invoice_line.amount_currency,
})
else:
invoice_line_global_tax_details = invoice_global_tax_details['invoice_line_tax_details'][invoice_line]
self._add_edi_tax_values(invoice_global_tax_details, grouping_key, serialized_grouping_key, tax_values)
self._add_edi_tax_values(invoice_line_global_tax_details, grouping_key, serialized_grouping_key, tax_values)
return invoice_global_tax_details
def _prepare_edi_vals_to_export(self):
''' The purpose of this helper is to prepare values in order to export an invoice through the EDI system.
This includes the computation of the tax details for each invoice line that could be very difficult to
@@ -144,59 +326,18 @@ class AccountMove(models.Model):
'''
self.ensure_one()
def convert(amount):
return self.currency_id._convert(amount, self.company_currency_id, self.company_id, self.date)
res = {
'record': self,
'balance_multiplicator': -1 if self.is_inbound() else 1,
'invoice_line_vals_list': [],
}
# Invoice lines details.
tax_detail_per_tax = {}
added_base_amount_keys = set()
for index, line in enumerate(self.invoice_line_ids.filtered(lambda line: not line.display_type), start=1):
line_vals = line._prepare_edi_vals_to_export()
line_vals['index'] = index
res['invoice_line_vals_list'].append(line_vals)
# Tax details.
for tax_vals in line_vals['tax_detail_vals_list']:
tax_detail_per_tax.setdefault(tax_vals['tax'], {
'tax': tax_vals['tax'],
'orig_tax': tax_vals['orig_tax'],
'tax_base_amount_currency': 0.0,
'tax_amount': 0.0,
'tax_amount_currency': 0.0,
'tax_amount_closing': 0.0,
'tax_amount_currency_closing': 0.0,
'tag_ids': set(),
})
vals = tax_detail_per_tax[tax_vals['tax']]
# Avoid adding multiple times the same base (e.g. with multiple taxes on the same line or multiple
# repartition lines).
base_amount_key = (line.id, tax_vals['tax']['id'])
if base_amount_key not in added_base_amount_keys:
vals['tax_base_amount_currency'] += tax_vals['tax_base_amount_currency']
added_base_amount_keys.add(base_amount_key)
vals['tax_amount_currency'] += tax_vals['tax_amount_currency']
vals['tax_amount_currency_closing'] += tax_vals['tax_amount_currency_closing']
for tag in tax_vals['tags']:
vals['tag_ids'].add(tag.id)
# Format the aggregated tax details as a list.
res['tax_detail_vals_list'] = []
for tax_detail_vals in tax_detail_per_tax.values():
res['tax_detail_vals_list'].append({
**tax_detail_vals,
'tags': self.env['account.account.tag'].browse(tax_detail_vals['tag_ids']),
'tax_base_amount': convert(tax_detail_vals['tax_base_amount_currency']),
'tax_amount': convert(tax_detail_vals['tax_amount_currency']),
'tax_amount_closing': convert(tax_detail_vals['tax_amount_currency_closing']),
})
# Totals.
res.update({
'total_price_subtotal_before_discount': sum(x['price_subtotal_before_discount'] for x in res['invoice_line_vals_list']),
@@ -390,9 +531,6 @@ class AccountMoveLine(models.Model):
'''
self.ensure_one()
def convert(amount):
return self.currency_id._convert(amount, self.company_currency_id, self.company_id, self.date)
res = {
'line': self,
'price_unit_after_discount': self.price_unit * (1 - (self.discount / 100.0)),
@@ -403,50 +541,6 @@ class AccountMoveLine(models.Model):
res['price_discount'] = res['price_subtotal_before_discount'] - self.price_subtotal
# Tax details.
tax_detail_per_tax = {}
taxes_res = self.tax_ids.compute_all(
res['price_unit_after_discount'],
currency=self.currency_id,
quantity=self.quantity,
product=self.product_id,
partner=self.partner_id,
is_refund=self.move_id.move_type in ('in_refund', 'out_refund'),
)
taxes_added_to_base = set()
for tax_vals in taxes_res['taxes']:
tax_rep = self.env['account.tax.repartition.line'].browse(tax_vals['tax_repartition_line_id'])
tax = tax_rep.tax_id
tax_detail_per_tax.setdefault(tax, {
'tax': tax,
'orig_tax': tax_vals['group'].id if tax_vals['group'] else tax.id,
'tax_base_amount_currency': 0.0,
'tax_amount_currency': 0.0,
'tax_amount_currency_closing': 0.0,
'tag_ids': set(),
})
vals = tax_detail_per_tax[tax]
# Avoid adding multiple times the same base (e.g. with multiple repartition lines).
if tax.id not in taxes_added_to_base:
vals['tax_base_amount_currency'] = tax_vals['base']
taxes_added_to_base.add(tax.id)
vals['tax_amount_currency'] += tax_vals['amount']
vals['tax_amount_currency_closing'] += tax_vals['amount'] if tax_rep.use_in_tax_closing else 0
for tag_id in tax_rep.tag_ids:
vals['tag_ids'].add(tag_id)
res['tax_detail_vals_list'] = []
for tax_detail_vals in tax_detail_per_tax.values():
res['tax_detail_vals_list'].append({
**tax_detail_vals,
'tags': self.env['account.account.tag'].browse(tax_detail_vals['tag_ids']),
'tax_base_amount': convert(tax_detail_vals['tax_base_amount_currency']),
'tax_amount': convert(tax_detail_vals['tax_amount_currency']),
'tax_amount_closing': convert(tax_detail_vals['tax_amount_currency_closing']),
})
return res
def reconcile(self):
@@ -48,7 +48,8 @@
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<t t-foreach="line_vals['tax_detail_vals_list']" t-as="tax_detail_vals">
<t t-foreach="tax_details['invoice_line_tax_details'][line]['tax_details'].values()"
t-as="tax_detail_vals">
<t t-set="tax" t-value="tax_detail_vals['tax']"/>
<ram:ApplicableTradeTax t-if="tax.amount_type == 'percent'">
<ram:RateApplicablePercent t-esc="tax.amount"/>
@@ -182,15 +183,15 @@
</ram:SpecifiedTradeSettlementPaymentMeans>
<!-- Tax Summary. -->
<t t-foreach="tax_detail_vals_list" t-as="tax_detail_vals">
<t t-foreach="tax_details['tax_details'].values()" t-as="tax_detail_vals">
<t t-set="tax" t-value="tax_detail_vals['tax']"/>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(tax_detail_vals['tax_amount_currency'], record.currency_id)"/>
t-esc="format_monetary(balance_multiplicator * tax_detail_vals['tax_amount_currency'], record.currency_id)"/>
<ram:BasisAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(tax_detail_vals['tax_base_amount_currency'], record.currency_id)"/>
t-esc="format_monetary(balance_multiplicator * tax_detail_vals['base_amount_currency'], record.currency_id)"/>
<ram:RateApplicablePercent
t-if="tax.amount_type == 'percent'"
t-esc="tax.amount"/>
@@ -59,6 +59,7 @@ class AccountEdiFormat(models.Model):
# Create file content.
template_values = {
**invoice._prepare_edi_vals_to_export(),
'tax_details': invoice._prepare_edi_tax_details(),
'format_date': format_date,
'format_monetary': format_monetary,
'is_html_empty': is_html_empty,
@@ -82,11 +82,11 @@
t-att-currencyID="line.currency_id.name"
t-esc="format_monetary(line.price_subtotal)"/>
<cac:TaxTotal
t-foreach="line_vals['tax_detail_vals_list']"
t-foreach="tax_details['invoice_line_tax_details'][line]['tax_details'].values()"
t-as="tax_detail_vals">
<cbc:TaxAmount
t-att-currencyID="line.currency_id.name"
t-esc="format_monetary(tax_detail_vals['tax_amount_currency'])"/>
t-esc="format_monetary(-tax_detail_vals['tax_amount_currency'])"/>
</cac:TaxTotal>
<cac:Item>
<cbc:Description t-if="line.name" t-esc="line.name.replace('\n', ', ')"/>
@@ -190,6 +190,7 @@ class AccountEdiFormat(models.Model):
return {
**invoice._prepare_edi_vals_to_export(),
'tax_details': invoice._prepare_edi_tax_details(),
'ubl_version': 2.1,
'type_code': 380 if invoice.move_type == 'out_invoice' else 381,
'payment_means_code': 42 if invoice.journal_id.bank_account_id else 31,
@@ -82,9 +82,11 @@
<cac:SellersItemIdentification t-if="line.product_id.default_code">
<cbc:ID t-esc="line.product_id.default_code"/>
</cac:SellersItemIdentification>
<cac:ClassifiedTaxCategory>
<cbc:ID t-esc="line_vals['tax_detail_vals']['tax_category']"/>
<cbc:Percent t-esc="line_vals['tax_detail_vals']['tax_percent']"/>
<cac:ClassifiedTaxCategory
t-foreach="tax_details['invoice_line_tax_details'][line]['tax_details'].values()"
t-as="tax_detail_vals">
<cbc:ID t-esc="tax_detail_vals['tax_category']"/>
<cbc:Percent t-esc="tax_detail_vals['tax_percent']"/>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
@@ -139,14 +141,14 @@
<cac:TaxTotal>
<cbc:TaxAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(total_tax_amount_currency)"/>
<cac:TaxSubtotal t-foreach="tax_detail_vals_list" t-as="tax_detail_vals">
t-esc="format_monetary(balance_multiplicator * tax_details['tax_amount_currency'])"/>
<cac:TaxSubtotal t-foreach="tax_details['tax_details'].values()" t-as="tax_detail_vals">
<cbc:TaxableAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(tax_detail_vals['base_currency'])"/>
t-esc="format_monetary(balance_multiplicator * tax_detail_vals['base_amount_currency'])"/>
<cbc:TaxAmount
t-att-currencyID="record.currency_id.name"
t-esc="format_monetary(tax_detail_vals['amount_currency'])"/>
t-esc="format_monetary(balance_multiplicator * tax_detail_vals['tax_amount_currency'])"/>
<cac:TaxCategory>
<cbc:ID t-esc="tax_detail_vals['tax_category']"/>
<cbc:Percent t-esc="tax_detail_vals['tax_percent']"/>
@@ -159,7 +161,7 @@
<cac:TaxTotal t-if="record.currency_id != record.company_currency_id">
<cbc:TaxAmount
t-att-currencyID="record.company_currency_id.name"
t-esc="format_monetary(total_tax_amount)"/>
t-esc="format_monetary(balance_multiplicator * tax_details['tax_amount'])"/>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount
@@ -65,54 +65,34 @@ class AccountEdiFormat(models.Model):
'profile_id': 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0',
})
all_tax_detail_per_tax = {}
# Tax details.
def grouping_key_generator(tax_values):
tax = tax_values['tax_id']
return {
'tax_percent': tax.amount,
'tax_category': 'S' if tax.amount else 'Z',
}
values['tax_details'] = invoice._prepare_edi_tax_details(
filter_to_apply=lambda x: x['tax_repartition_line_id'].use_in_tax_closing,
grouping_key_generator=grouping_key_generator,
)
for line_vals in values['invoice_line_vals_list']:
line_vals['price_subtotal_with_no_tax_closing'] = line_vals['line'].price_subtotal
tax_detail_per_tax = {}
for tax_detail_vals in line_vals['tax_detail_vals_list']:
tax = tax_detail_vals['tax']
tax_percent = tax.amount
tax_category = 'S' if tax_percent else 'Z'
key = (tax_category, tax_percent)
tax_detail_per_tax.setdefault(key, {
'base': tax_detail_vals['tax_base_amount'],
'base_currency': tax_detail_vals['tax_base_amount_currency'],
'amount': 0.0,
'amount_currency': 0.0,
'tax_percent': tax_percent,
'tax_category': tax_category,
})
vals = tax_detail_per_tax[key]
vals['amount'] += tax_detail_vals['tax_amount_closing']
vals['amount_currency'] += tax_detail_vals['tax_amount_currency_closing']
delta_tax = tax_detail_vals['tax_amount_currency'] - tax_detail_vals['tax_amount_currency_closing']
line_vals['price_subtotal_with_no_tax_closing'] += delta_tax
if len(tax_detail_per_tax) > 1:
if len(values['tax_details']['invoice_line_tax_details'][line_vals['line']]['tax_details']) > 1:
raise UserError("Multiple vat percentage not supported on the same invoice line")
line_vals['tax_detail_vals'] = list(tax_detail_per_tax.values())[0]
tax_details_no_tax_closing = invoice._prepare_edi_tax_details(
filter_to_apply=lambda x: not x['tax_repartition_line_id'].use_in_tax_closing,
)
for line_vals in values['invoice_line_vals_list']:
line_vals['price_subtotal_with_no_tax_closing'] = line_vals['line'].price_subtotal
for tax_detail in tax_details_no_tax_closing['invoice_line_tax_details'][line_vals['line']]['tax_details'].values():
line_vals['price_subtotal_with_no_tax_closing'] += tax_detail['tax_amount_currency']
for key, tax_vals in tax_detail_per_tax.items():
all_tax_detail_per_tax.setdefault(key, {
**tax_vals,
'base': 0.0,
'base_currency': 0.0,
'amount': 0.0,
'amount_currency': 0.0,
})
vals = all_tax_detail_per_tax[key]
vals['base'] += tax_vals['base']
vals['base_currency'] += tax_vals['base_currency']
vals['amount'] += tax_vals['amount']
vals['amount_currency'] += tax_vals['amount_currency']
values['tax_detail_vals_list'] = list(all_tax_detail_per_tax.values())
values['total_untaxed_amount'] = sum(x['price_subtotal_with_no_tax_closing'] for x in values['invoice_line_vals_list'])
values['total_tax_amount'] = sum(x['amount'] for x in values['tax_detail_vals_list'])
values['total_tax_amount_currency'] = sum(x['amount_currency'] for x in values['tax_detail_vals_list'])
# Misc.
for partner_vals in (values['customer_vals'], values['supplier_vals']):
partner = partner_vals['partner']