[IMP] account: Add generic methods to help the taxes computation

Unify taxes computation between 'account', 'sale' and 'purchase' including:
- computation of price_subtotal/price_total.
- computation of business models total (using the json field)

closes odoo/odoo#80231

Task: 2654784
Related: odoo/enterprise#25620
Signed-off-by: Olivier Colson <oco@odoo.com>
This commit is contained in:
Laurent Smet
2022-03-29 20:20:15 +02:00
parent 60fc564b3e
commit cd6c33575b
13 changed files with 725 additions and 543 deletions
+155 -421
View File
@@ -648,47 +648,6 @@ class AccountMove(models.Model):
if fiscal_position:
self.fiscal_position_id = fiscal_position
@api.model
def _get_tax_grouping_key_from_tax_line(self, tax_line):
''' Create the dictionary based on a tax line that will be used as key to group taxes together.
/!\ Must be consistent with '_get_tax_grouping_key_from_base_line'.
:param tax_line: An account.move.line being a tax line (with 'tax_repartition_line_id' set then).
:return: A dictionary containing all fields on which the tax will be grouped.
'''
return {
'tax_repartition_line_id': tax_line.tax_repartition_line_id.id,
'group_tax_id': tax_line.group_tax_id.id,
'account_id': tax_line.account_id.id,
'currency_id': tax_line.currency_id.id,
'analytic_tag_ids': [(6, 0, tax_line.tax_line_id.analytic and tax_line.analytic_tag_ids.ids or [])],
'analytic_account_id': tax_line.tax_line_id.analytic and tax_line.analytic_account_id.id,
'tax_ids': [(6, 0, tax_line.tax_ids.ids)],
'tax_tag_ids': [(6, 0, tax_line.tax_tag_ids.ids)],
'partner_id': tax_line.partner_id.id,
}
@api.model
def _get_tax_grouping_key_from_base_line(self, base_line, tax_vals):
''' Create the dictionary based on a base line that will be used as key to group taxes together.
/!\ Must be consistent with '_get_tax_grouping_key_from_tax_line'.
:param base_line: An account.move.line being a base line (that could contains something in 'tax_ids').
:param tax_vals: An element of compute_all(...)['taxes'].
:return: A dictionary containing all fields on which the tax will be grouped.
'''
tax_repartition_line = self.env['account.tax.repartition.line'].browse(tax_vals['tax_repartition_line_id'])
account = base_line._get_default_tax_account(tax_repartition_line) or base_line.account_id
return {
'tax_repartition_line_id': tax_vals['tax_repartition_line_id'],
'group_tax_id': tax_vals['group'].id if tax_vals['group'] else False,
'account_id': account.id,
'currency_id': base_line.currency_id.id,
'analytic_tag_ids': [(6, 0, tax_vals['analytic'] and base_line.analytic_tag_ids.ids or [])],
'analytic_account_id': tax_vals['analytic'] and base_line.analytic_account_id.id,
'tax_ids': [(6, 0, tax_vals['tax_ids'])],
'tax_tag_ids': [(6, 0, tax_vals['tag_ids'])],
'partner_id': base_line.partner_id.id,
}
def _get_tax_force_sign(self):
""" The sign must be forced to a negative sign in case the balance is on credit
to avoid negatif taxes amount.
@@ -700,169 +659,111 @@ class AccountMove(models.Model):
self.ensure_one()
return -1 if self.move_type in ('out_invoice', 'in_refund', 'out_receipt') else 1
def _preprocess_taxes_map(self, taxes_map):
""" Useful in case we want to pre-process taxes_map """
return taxes_map
def _recompute_tax_lines(self, recompute_tax_base_amount=False):
""" Compute the dynamic tax lines of the journal entry.
:param recompute_tax_base_amount: Flag forcing only the recomputation of the `tax_base_amount` field.
:param recompute_tax_base_amount: recompute only the tax_base_amount field.
"""
self.ensure_one()
in_draft_mode = self != self._origin
sign = -1 if self.is_inbound(include_receipts=True) else 1
def _serialize_tax_grouping_key(grouping_dict):
''' Serialize the dictionary values to be used in the taxes_map.
:param grouping_dict: The values returned by '_get_tax_grouping_key_from_tax_line' or '_get_tax_grouping_key_from_base_line'.
:return: A string representing the values.
'''
return '-'.join(str(v) for v in grouping_dict.values())
def fill_accounting_tax_vals(update_vals):
currency = self.env['res.currency'].browse(update_vals['currency_id'])
tax_amount = update_vals.pop('tax_amount')
def _compute_base_line_taxes(base_line):
''' Compute taxes amounts both in company currency / foreign currency as the ratio between
amount_currency & balance could not be the same as the expected currency rate.
The 'amount_currency' value will be set on compute_all(...)['taxes'] in multi-currency.
:param base_line: The account.move.line owning the taxes.
:return: The result of the compute_all method.
'''
move = base_line.move_id
if move.is_invoice(include_receipts=True):
handle_price_include = True
sign = -1 if move.is_inbound() else 1
quantity = base_line.quantity
is_refund = move.move_type in ('out_refund', 'in_refund')
price_unit_wo_discount = sign * base_line.price_unit * (1 - (base_line.discount / 100.0))
tax_rep = self.env['account.tax.repartition.line'].browse(update_vals['tax_repartition_line_id'])
tax_id = update_vals.pop('tax_id')
if tax_id == tax_rep.tax_id.id:
group_tax = None
else:
handle_price_include = False
quantity = 1.0
tax_type = base_line.tax_ids[0].type_tax_use if base_line.tax_ids else None
is_refund = (tax_type == 'sale' and base_line.debit) or (tax_type == 'purchase' and base_line.credit)
price_unit_wo_discount = base_line.amount_currency
group_tax = self.env['account.tax'].browse(tax_id)
return base_line.tax_ids._origin.with_context(force_sign=move._get_tax_force_sign()).compute_all(
price_unit_wo_discount,
currency=base_line.currency_id,
quantity=quantity,
product=base_line.product_id,
partner=base_line.partner_id,
is_refund=is_refund,
handle_price_include=handle_price_include,
include_caba_tags=move.always_tax_exigible,
)
taxes_map = {}
# ==== Add tax lines ====
to_remove = self.env['account.move.line']
for line in self.line_ids.filtered('tax_repartition_line_id'):
grouping_dict = self._get_tax_grouping_key_from_tax_line(line)
grouping_key = _serialize_tax_grouping_key(grouping_dict)
if grouping_key in taxes_map:
# A line with the same key does already exist, we only need one
# to modify it; we have to drop this one.
to_remove += line
else:
taxes_map[grouping_key] = {
'tax_line': line,
'amount': 0.0,
'tax_base_amount': 0.0,
'grouping_dict': False,
}
if not recompute_tax_base_amount:
self.line_ids -= to_remove
# ==== Mount base lines ====
for line in self.line_ids.filtered(lambda line: not line.tax_repartition_line_id):
# Don't call compute_all if there is no tax.
if not line.tax_ids:
if not recompute_tax_base_amount:
line.tax_tag_ids = [(5, 0, 0)]
continue
compute_all_vals = _compute_base_line_taxes(line)
# Assign tags on base line
if not recompute_tax_base_amount:
line.tax_tag_ids = compute_all_vals['base_tags'] or [(5, 0, 0)]
for tax_vals in compute_all_vals['taxes']:
grouping_dict = self._get_tax_grouping_key_from_base_line(line, tax_vals)
grouping_key = _serialize_tax_grouping_key(grouping_dict)
tax_repartition_line = self.env['account.tax.repartition.line'].browse(tax_vals['tax_repartition_line_id'])
tax = tax_repartition_line.invoice_tax_id or tax_repartition_line.refund_tax_id
taxes_map_entry = taxes_map.setdefault(grouping_key, {
'tax_line': None,
'amount': 0.0,
'tax_base_amount': 0.0,
'grouping_dict': False,
})
taxes_map_entry['amount'] += tax_vals['amount']
taxes_map_entry['tax_base_amount'] += self._get_base_amount_to_display(tax_vals['base'], tax_repartition_line, tax_vals['group'])
taxes_map_entry['grouping_dict'] = grouping_dict
# ==== Pre-process taxes_map ====
taxes_map = self._preprocess_taxes_map(taxes_map)
# ==== Process taxes_map ====
for taxes_map_entry in taxes_map.values():
# The tax line is no longer used in any base lines, drop it.
if taxes_map_entry['tax_line'] and not taxes_map_entry['grouping_dict']:
if not recompute_tax_base_amount:
self.line_ids -= taxes_map_entry['tax_line']
continue
currency = self.env['res.currency'].browse(taxes_map_entry['grouping_dict']['currency_id'])
# tax_base_amount field is expressed using the company currency.
tax_base_amount = currency._convert(taxes_map_entry['tax_base_amount'], self.company_currency_id, self.company_id, self.date or fields.Date.context_today(self))
# Recompute only the tax_base_amount.
if recompute_tax_base_amount:
if taxes_map_entry['tax_line']:
taxes_map_entry['tax_line'].tax_base_amount = tax_base_amount
continue
balance = currency._convert(
taxes_map_entry['amount'],
tax_base_amount = sign * currency._convert(
update_vals['tax_base_amount'],
self.company_currency_id,
self.company_id,
self.date or fields.Date.context_today(self),
)
currency_id = taxes_map_entry['grouping_dict']['currency_id']
to_write_on_line = {
'amount_currency': taxes_map_entry['amount'],
'currency_id': currency_id,
'debit': self._get_debit_from_balance(balance, currency_id),
'credit': self._get_credit_from_balance(balance, currency_id),
tax_base_amount = self._get_base_amount_to_display(tax_base_amount, tax_rep, group_tax)
amount_currency = sign * tax_amount
balance = currency._convert(
amount_currency,
self.company_currency_id,
self.company_id,
self.date or fields.Date.context_today(self),
)
update_vals.update({
'name': tax_rep.tax_id.name,
'tax_base_amount': tax_base_amount,
}
'amount_currency': amount_currency,
'debit': self._get_debit_from_balance(balance, currency.id),
'credit': self._get_credit_from_balance(balance, currency.id),
'exclude_from_invoice_tab': True,
'currency_id': currency.id,
'group_tax_id': group_tax.id if group_tax else None,
})
if taxes_map_entry['tax_line']:
# Update an existing tax line.
taxes_map_entry['tax_line'].update(to_write_on_line)
line_ids_commands = []
is_invoice = self.is_invoice(include_receipts=True)
if is_invoice:
base_lines = self.line_ids.filtered(lambda x: not x.display_type and not x.exclude_from_invoice_tab)
force_sign = 1
else:
base_lines = self.line_ids.filtered(lambda x: not x.display_type and not x.tax_repartition_line_id)
force_sign = self._get_tax_force_sign()
tax_lines = self.line_ids.filtered(lambda x: not x.display_type and x.tax_repartition_line_id)
tax_results = self.env['account.tax'].with_context(force_sign=force_sign)._compute_taxes(
[x._convert_to_tax_base_line_dict() for x in base_lines],
tax_lines=[x._convert_to_tax_line_dict() for x in tax_lines],
handle_price_include=is_invoice,
include_caba_tags=self.always_tax_exigible,
)
# ==== BASE LINES =====
if not recompute_tax_base_amount:
# Recompute 'tax_tag_ids'.
for base_line_vals, to_update in tax_results['base_lines_to_update']:
base_line = base_line_vals['record']
line_ids_commands.append(Command.update(base_line.id, {
'tax_tag_ids': to_update['tax_tag_ids'],
}))
# ===== TAX LINES =====
# Tax lines that are no longer neeeded.
if not recompute_tax_base_amount:
for tax_line_vals in tax_results['tax_lines_to_delete']:
tax_line = tax_line_vals['record']
line_ids_commands.append(Command.unlink(tax_line.id))
# Newly created tax lines.
if not recompute_tax_base_amount:
for tax_line_vals in tax_results['tax_lines_to_add']:
fill_accounting_tax_vals(tax_line_vals)
line_ids_commands.append(Command.create(tax_line_vals))
# Update of existing tax lines.
for tax_line_vals, to_update in tax_results['tax_lines_to_update']:
tax_line = tax_line_vals['record']
fill_accounting_tax_vals(to_update)
if recompute_tax_base_amount:
fields_to_update = ('tax_base_amount',)
else:
# Create a new tax line.
create_method = in_draft_mode and self.env['account.move.line'].new or self.env['account.move.line'].create
tax_repartition_line_id = taxes_map_entry['grouping_dict']['tax_repartition_line_id']
tax_repartition_line = self.env['account.tax.repartition.line'].browse(tax_repartition_line_id)
tax = tax_repartition_line.invoice_tax_id or tax_repartition_line.refund_tax_id
taxes_map_entry['tax_line'] = create_method({
**to_write_on_line,
'name': tax.name,
'move_id': self.id,
'company_id': line.company_id.id,
'company_currency_id': line.company_currency_id.id,
'tax_base_amount': tax_base_amount,
'exclude_from_invoice_tab': True,
**taxes_map_entry['grouping_dict'],
})
fields_to_update = ('tax_base_amount', 'amount_currency', 'debit', 'credit')
line_ids_commands.append(Command.update(tax_line.id, {k: v for k, v in to_update.items() if k in fields_to_update}))
if in_draft_mode:
taxes_map_entry['tax_line'].update(taxes_map_entry['tax_line']._get_fields_onchange_balance(force_computation=True))
(self.update if in_draft_mode else self.write)({'line_ids': line_ids_commands})
if in_draft_mode:
for tax_line in tax_lines:
tax_line.update(tax_line._get_fields_onchange_balance(force_computation=True))
@api.model
def _get_base_amount_to_display(self, base_amount, tax_rep_ln, parent_tax_group=None):
@@ -1702,243 +1603,21 @@ class AccountMove(models.Model):
Only set on invoices.
"""
for move in self:
if not move.is_invoice(include_receipts=True):
if move.is_invoice(include_receipts=True):
base_lines = move.line_ids.filtered(lambda x: not x.display_type and not x.exclude_from_invoice_tab)
tax_lines = move.line_ids.filtered(lambda x: not x.display_type and x.tax_repartition_line_id)
tax_totals = self.env['account.tax']._prepare_tax_totals_json(
[x._convert_to_tax_base_line_dict() for x in base_lines],
move.currency_id,
tax_lines=[x._convert_to_tax_line_dict() for x in tax_lines],
)
tax_totals['allow_tax_edition'] = move.is_purchase_document(include_receipts=True)
move.tax_totals_json = json.dumps(tax_totals)
else:
# Non-invoice moves don't support that field (because of multicurrency: all lines of the invoice share the same currency)
move.tax_totals_json = None
continue
tax_lines_data = move._prepare_tax_lines_data_for_totals_from_invoice()
move.tax_totals_json = json.dumps({
**self._get_tax_totals(move.partner_id, tax_lines_data, move.amount_total, move.amount_untaxed, move.currency_id),
'allow_tax_edition': move.is_purchase_document(include_receipts=False) and move.state == 'draft',
})
def _prepare_tax_lines_data_for_totals_from_invoice(self, tax_line_id_filter=None, tax_ids_filter=None):
""" Prepares data to be passed as tax_lines_data parameter of _get_tax_totals() from an invoice.
NOTE: tax_line_id_filter and tax_ids_filter are used in l10n_latam to restrict the taxes with consider
in the totals.
:param tax_line_id_filter: a function(aml, tax) returning true if tax should be considered on tax move line aml.
:param tax_ids_filter: a function(aml, taxes) returning true if taxes should be considered on base move line aml.
:return: A list of dict in the format described in _get_tax_totals's tax_lines_data's docstring.
"""
self.ensure_one()
tax_line_id_filter = tax_line_id_filter or (lambda aml, tax: True)
tax_ids_filter = tax_ids_filter or (lambda aml, tax: True)
balance_multiplicator = -1 if self.is_inbound() else 1
tax_lines_data = []
for line in self.line_ids:
if line.tax_line_id and tax_line_id_filter(line, line.tax_line_id):
tax_lines_data.append({
'line_key': 'tax_line_%s' % line.id,
'tax_amount': line.amount_currency * balance_multiplicator,
'tax': line.tax_line_id,
})
if line.tax_ids:
for base_tax in line.tax_ids.flatten_taxes_hierarchy():
if tax_ids_filter(line, base_tax):
tax_lines_data.append({
'line_key': 'base_line_%s' % line.id,
'base_amount': line.amount_currency * balance_multiplicator,
'tax': base_tax,
'tax_affecting_base': line.tax_line_id,
})
return tax_lines_data
@api.model
def _prepare_tax_lines_data_for_totals_from_object(self, object_lines, tax_results_function):
""" Prepares data to be passed as tax_lines_data parameter of _get_tax_totals() from any
object using taxes. This helper is intended for purchase.order and sale.order, as a common
function centralizing their behavior.
:param object_lines: A list of records corresponding to the sub-objects generating the tax totals
(sale.order.line or purchase.order.line, for example)
:param tax_results_function: A function to be called to get the results of the tax computation for a
line in object_lines. It takes the object line as its only parameter
and returns a dict in the same format as account.tax's compute_all
(most probably after calling it with the right parameters).
:return: A list of dict in the format described in _get_tax_totals's tax_lines_data's docstring.
"""
tax_lines_data = []
for line in object_lines:
tax_results = tax_results_function(line)
for tax_result in tax_results['taxes']:
current_tax = self.env['account.tax'].browse(tax_result['id'])
# Tax line
tax_lines_data.append({
'line_key': f"tax_line_{line.id}_{tax_result['id']}",
'tax_amount': tax_result['amount'],
'tax': current_tax,
})
# Base for this tax line
tax_lines_data.append({
'line_key': 'base_line_%s' % line.id,
'base_amount': tax_results['total_excluded'],
'tax': current_tax,
})
# Base for the taxes whose base is affected by this tax line
if tax_result['tax_ids']:
affected_taxes = self.env['account.tax'].browse(tax_result['tax_ids'])
for affected_tax in affected_taxes:
tax_lines_data.append({
'line_key': 'affecting_base_line_%s_%s' % (line.id, tax_result['id']),
'base_amount': tax_result['amount'],
'tax': affected_tax,
'tax_affecting_base': current_tax,
})
return tax_lines_data
@api.model
def _get_tax_totals(self, partner, tax_lines_data, amount_total, amount_untaxed, currency):
""" Compute the tax totals for the provided data.
:param partner: The partner to compute totals for
:param tax_lines_data: All the data about the base and tax lines as a list of dictionaries.
Each dictionary represents an amount that needs to be added to either a tax base or amount.
A tax amount looks like:
{
'line_key': unique identifier,
'tax_amount': the amount computed for this tax
'tax': the account.tax object this tax line was made from
}
For base amounts:
{
'line_key': unique identifier,
'base_amount': the amount to add to the base of the tax
'tax': the tax basing itself on this amount
'tax_affecting_base': (optional key) the tax whose tax line is having the impact
denoted by 'base_amount' on the base of the tax, in case of taxes
affecting the base of subsequent ones.
}
:param amount_total: Total amount, with taxes.
:param amount_untaxed: Total amount without taxes.
:param currency: The currency in which the amounts are computed.
:return: A dictionary in the following form:
{
'amount_total': The total amount to be displayed on the document, including every total types.
'amount_untaxed': The untaxed amount to be displayed on the document.
'formatted_amount_total': Same as amount_total, but as a string formatted accordingly with partner's locale.
'formatted_amount_untaxed': Same as amount_untaxed, but as a string formatted accordingly with partner's locale.
'allow_tax_edition': True if the user should have the ability to manually edit the tax amounts by group
to fix rounding errors.
'groups_by_subtotals': A dictionary formed liked {'subtotal': groups_data}
Where total_type is a subtotal name defined on a tax group, or the default one: 'Untaxed Amount'.
And groups_data is a list of dict in the following form:
{
'tax_group_name': The name of the tax groups this total is made for.
'tax_group_amount': The total tax amount in this tax group.
'tax_group_base_amount': The base amount for this tax group.
'formatted_tax_group_amount': Same as tax_group_amount, but as a string
formatted accordingly with partner's locale.
'formatted_tax_group_base_amount': Same as tax_group_base_amount, but as a string
formatted accordingly with partner's locale.
'tax_group_id': The id of the tax group corresponding to this dict.
'group_key': A unique key identifying this total dict,
}
'subtotals': A list of dictionaries in the following form, one for each subtotal in groups_by_subtotals' keys
{
'name': The name of the subtotal
'amount': The total amount for this subtotal, summing all
the tax groups belonging to preceding subtotals and the base amount
'formatted_amount': Same as amount, but as a string
formatted accordingly with partner's locale.
}
}
"""
account_tax = self.env['account.tax']
grouped_taxes = defaultdict(lambda: defaultdict(lambda: {'base_amount': 0.0, 'tax_amount': 0.0, 'base_line_keys': set()}))
subtotal_priorities = {}
for line_data in tax_lines_data:
tax_group = line_data['tax'].tax_group_id
# Update subtotals priorities
if tax_group.preceding_subtotal:
subtotal_title = tax_group.preceding_subtotal
new_priority = tax_group.sequence
else:
# When needed, the default subtotal is always the most prioritary
subtotal_title = _("Untaxed Amount")
new_priority = 0
if subtotal_title not in subtotal_priorities or new_priority < subtotal_priorities[subtotal_title]:
subtotal_priorities[subtotal_title] = new_priority
# Update tax data
tax_group_vals = grouped_taxes[subtotal_title][tax_group]
if 'base_amount' in line_data:
# Base line
if tax_group == line_data.get('tax_affecting_base', account_tax).tax_group_id:
# In case the base has a tax_line_id belonging to the same group as the base tax,
# the base for the group will be computed by the base tax's original line (the one with tax_ids and no tax_line_id)
continue
if line_data['line_key'] not in tax_group_vals['base_line_keys']:
# If the base line hasn't been taken into account yet, at its amount to the base total.
tax_group_vals['base_line_keys'].add(line_data['line_key'])
tax_group_vals['base_amount'] += line_data['base_amount']
else:
# Tax line
tax_group_vals['tax_amount'] += line_data['tax_amount']
# Compute groups_by_subtotal
groups_by_subtotal = {}
for subtotal_title, groups in grouped_taxes.items():
groups_vals = [{
'tax_group_name': group.name,
'tax_group_amount': amounts['tax_amount'],
'tax_group_base_amount': amounts['base_amount'],
'formatted_tax_group_amount': formatLang(self.env, amounts['tax_amount'], currency_obj=currency),
'formatted_tax_group_base_amount': formatLang(self.env, amounts['base_amount'], currency_obj=currency),
'tax_group_id': group.id,
'group_key': '%s-%s' %(subtotal_title, group.id),
} for group, amounts in sorted(groups.items(), key=lambda l: l[0].sequence)]
groups_by_subtotal[subtotal_title] = groups_vals
# Compute subtotals
subtotals_list = [] # List, so that we preserve their order
previous_subtotals_tax_amount = 0
for subtotal_title in sorted((sub for sub in subtotal_priorities), key=lambda x: subtotal_priorities[x]):
subtotal_value = amount_untaxed + previous_subtotals_tax_amount
subtotals_list.append({
'name': subtotal_title,
'amount': subtotal_value,
'formatted_amount': formatLang(self.env, subtotal_value, currency_obj=currency),
})
subtotal_tax_amount = sum(group_val['tax_group_amount'] for group_val in groups_by_subtotal[subtotal_title])
previous_subtotals_tax_amount += subtotal_tax_amount
# Assign json-formatted result to the field
return {
'amount_total': amount_total,
'amount_untaxed': amount_untaxed,
'formatted_amount_total': formatLang(self.env, amount_total, currency_obj=currency),
'formatted_amount_untaxed': formatLang(self.env, amount_untaxed, currency_obj=currency),
'groups_by_subtotal': groups_by_subtotal,
'subtotals': subtotals_list,
'allow_tax_edition': False,
}
@api.depends('date', 'line_ids.debit', 'line_ids.credit', 'line_ids.tax_line_id', 'line_ids.tax_ids', 'line_ids.tax_tag_ids')
def _compute_tax_lock_date_message(self):
@@ -5430,6 +5109,61 @@ class AccountMoveLine(models.Model):
# MISC
# -------------------------------------------------------------------------
def _convert_to_tax_base_line_dict(self):
""" Convert the current record to a dictionary in order to use the generic taxes computation method
defined on account.tax.
:return: A python dictionary.
"""
self.ensure_one()
is_invoice = self.move_id.is_invoice(include_receipts=True)
sign = -1 if self.move_id.is_inbound(include_receipts=True) else 1
if is_invoice:
is_refund = self.move_id.move_type in ('out_refund', 'in_refund')
else:
tax_type = self.tax_ids[0].type_tax_use if self.tax_ids else None
is_refund = (tax_type == 'sale' and self.balance > 0.0) or (tax_type == 'purchase' and self.balance < 0.0)
return self.env['account.tax']._convert_to_tax_base_line_dict(
self,
partner=self.partner_id,
currency=self.currency_id,
product=self.product_id,
taxes=self.tax_ids,
price_unit=self.price_unit if is_invoice else self.amount_currency,
quantity=self.quantity if is_invoice else 1.0,
discount=self.discount if is_invoice else 0.0,
account=self.account_id,
analytic_account=self.analytic_account_id,
analytic_tags=self.analytic_tag_ids,
price_subtotal=sign * self.amount_currency,
is_refund=is_refund,
)
def _convert_to_tax_line_dict(self):
""" Convert the current record to a dictionary in order to use the generic taxes computation method
defined on account.tax.
:return: A python dictionary.
"""
self.ensure_one()
sign = -1 if self.move_id.is_inbound(include_receipts=True) else 1
return self.env['account.tax']._convert_to_tax_line_dict(
self,
partner=self.partner_id,
currency=self.currency_id,
taxes=self.tax_ids,
tax_tags=self.tax_tag_ids,
tax_repartition_line=self.tax_repartition_line_id,
group_tax=self.group_tax_id,
account=self.account_id,
analytic_account=self.analytic_account_id,
analytic_tags=self.analytic_tag_ids,
tax_amount=sign * self.amount_currency,
)
def _get_analytic_tag_ids(self):
self.ensure_one()
return self.analytic_tag_ids.filtered(lambda r: not r.active_analytic_distribution).ids
+449 -3
View File
@@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo import api, fields, models, _, Command
from odoo.osv import expression
from odoo.tools.float_utils import float_round as round
from odoo.exceptions import UserError, ValidationError
from odoo.tools.misc import formatLang
from odoo.tools import frozendict
from collections import defaultdict
import math
import logging
TYPE_TAX_USE = [
@@ -643,7 +645,7 @@ class AccountTax(models.Model):
'amount': sign * line_amount,
'base': round(sign * tax_base_amount, precision_rounding=prec),
'sequence': tax.sequence,
'account_id': tax.cash_basis_transition_account_id.id if tax.tax_exigibility == 'on_payment' else repartition_line.account_id.id,
'account_id': repartition_line._get_aml_target_tax_account().id,
'analytic': tax.analytic,
'price_include': price_include,
'tax_exigibility': tax.tax_exigibility,
@@ -679,6 +681,439 @@ class AccountTax(models.Model):
'total_void': sign * currency.round(total_void),
}
@api.model
def _convert_to_tax_base_line_dict(
self, base_line,
partner=None, currency=None, product=None, taxes=None, price_unit=None, quantity=None,
discount=None, account=None, analytic_account=None, analytic_tags=None, price_subtotal=None,
is_refund=False,
):
return {
'record': base_line,
'partner': partner or self.env['res.partner'],
'currency': currency or self.env['res.currency'],
'product': product or self.env['product.product'],
'taxes': taxes or self.env['account.tax'],
'price_unit': price_unit or 0.0,
'quantity': quantity or 0.0,
'discount': discount or 0.0,
'account': account or self.env['account.account'],
'analytic_account': analytic_account or self.env['account.analytic.account'],
'analytic_tags': analytic_tags or self.env['account.analytic.tag'],
'price_subtotal': price_subtotal or 0.0,
'is_refund': is_refund,
}
@api.model
def _convert_to_tax_line_dict(
self, tax_line,
partner=None, currency=None, taxes=None, tax_tags=None, tax_repartition_line=None,
group_tax=None, account=None, analytic_account=None, analytic_tags=None, tax_amount=None,
):
return {
'record': tax_line,
'partner': partner or self.env['res.partner'],
'currency': currency or self.env['res.currency'],
'taxes': taxes or self.env['account.tax'],
'tax_tags': tax_tags or self.env['account.account.tag'],
'tax_repartition_line': tax_repartition_line or self.env['account.tax.repartition.line'],
'group_tax': group_tax or self.env['account.tax'],
'account': account or self.env['account.account'],
'analytic_account': analytic_account or self.env['account.analytic.account'],
'analytic_tags': analytic_tags or self.env['account.analytic.tag'],
'tax_amount': tax_amount or 0.0,
}
@api.model
def _get_generation_dict_from_base_line(self, line_vals, tax_vals):
""" Take a tax results returned by the taxes computation method and return a dictionary representing the way
the tax amounts will be grouped together. To do so, the dictionary will be converted into a string key.
Then, the existing tax lines sharing the same key will be updated and the missing ones will be created.
:param line_vals: A python dict returned by '_convert_to_tax_base_line_dict'.
:param tax_vals: A python dict returned by 'compute_all' under the 'taxes' key.
:return: A python dict.
"""
tax_repartition_line = self.env['account.tax.repartition.line'].browse(tax_vals['tax_repartition_line_id'])
tax_account = tax_repartition_line._get_aml_target_tax_account() or line_vals['account']
return {
'account_id': tax_account.id,
'currency_id': line_vals['currency'].id,
'partner_id': line_vals['partner'].id,
'tax_repartition_line_id': tax_repartition_line.id,
'tax_ids': [Command.set(tax_vals['tax_ids'])],
'tax_tag_ids': [Command.set(tax_vals['tag_ids'])],
'tax_id': tax_vals['group'].id if tax_vals['group'] else tax_vals['id'],
'analytic_tag_ids': [Command.set(line_vals['analytic_tags'].ids if tax_vals['analytic'] else [])],
'analytic_account_id': line_vals['analytic_account'].id if tax_vals['analytic'] else False,
}
@api.model
def _get_generation_dict_from_tax_line(self, line_vals):
""" Turn the values corresponding to a tax line and convert it into a dictionary. The dictionary will be
converted into a string key. This allows updating the existing tax lines instead of creating new ones
everytime.
:param line_vals: A python dict returned by '_convert_to_tax_line_dict'.
:return: A python dict representing the grouping key used to update an existing tax line.
"""
tax = line_vals['tax_repartition_line'].tax_id
return {
'account_id': line_vals['account'].id,
'currency_id': line_vals['currency'].id,
'partner_id': line_vals['partner'].id,
'tax_repartition_line_id': line_vals['tax_repartition_line'].id,
'tax_ids': [Command.set(line_vals['taxes'].ids)],
'tax_tag_ids': [Command.set(line_vals['tax_tags'].ids)],
'tax_id': (line_vals['group_tax'] or tax).id,
'analytic_tag_ids': [Command.set(line_vals['analytic_tags'].ids if tax.analytic else [])],
'analytic_account_id': line_vals['analytic_account'].id if tax.analytic else False,
}
def _compute_taxes(self, base_lines, tax_lines=None,
handle_price_include=True, include_caba_tags=False):
""" Generic method to compute the taxes for different business models.
:param base_lines: A list of python dictionaries created using the '_convert_to_tax_base_line_dict' method.
:param tax_lines: A list of python dictionaries created using the '_convert_to_tax_line_dict' method.
:param handle_price_include: Manage the price-included taxes.
:param include_caba_tags: Manage tags for taxes being exigible on_payment.
:return: A python dictionary containing:
The complete diff on tax lines if 'tax_lines' is passed as parameter:
* tax_lines_to_add: To create new tax lines.
* tax_lines_to_delete: To track the tax lines that are no longer used.
* tax_lines_to_update: The values to update the existing tax lines.
* base_lines_to_update: The values to update the existing base lines:
* tax_tag_ids: The tags related to taxes.
* price_subtotal: The amount without tax.
* price_total: The amount with taxes.
* totals: A mapping for each involved currency to:
* amount_untaxed: The base amount without tax.
* amount_tax: The total tax amount.
"""
res = {
'tax_lines_to_add': [],
'tax_lines_to_delete': [],
'tax_lines_to_update': [],
'base_lines_to_update': [],
'totals': defaultdict(lambda: {
'amount_untaxed': 0.0,
'amount_tax': 0.0,
}),
}
# =========================================================================================
# BASE LINES
# For each base line, populate 'base_lines_to_update'.
# Compute 'tax_base_amount'/'tax_amount' for each pair <base line, tax repartition line>
# using the grouping key generated by the '_get_generation_dict_from_base_line' method.
# =========================================================================================
base_line_map = {}
for line_vals in base_lines:
price_unit_after_discount = line_vals['price_unit'] * (1 - (line_vals['discount'] / 100.0))
taxes = line_vals['taxes']._origin
currency = line_vals['currency'] or self.env.company.currency_id
if taxes:
taxes_res = taxes.compute_all(
price_unit_after_discount,
currency=currency,
quantity=line_vals['quantity'],
product=line_vals['product'],
partner=line_vals['partner'],
is_refund=line_vals['is_refund'],
handle_price_include=handle_price_include,
include_caba_tags=include_caba_tags,
)
to_update_vals = {
'tax_tag_ids': [Command.set(taxes_res['base_tags'])],
'price_subtotal': taxes_res['total_excluded'],
'price_total': taxes_res['total_included'],
}
for tax_res in taxes_res['taxes']:
grouping_dict = self._get_generation_dict_from_base_line(line_vals, tax_res)
tax_res['grouping_dict'] = grouping_dict
grouping_key = frozendict(grouping_dict)
base_line_map.setdefault(grouping_key, {
**tax_res['grouping_dict'],
'tax_base_amount': 0.0,
'tax_amount': 0.0,
})
base_line_map[grouping_key]['tax_base_amount'] += tax_res['base']
base_line_map[grouping_key]['tax_amount'] += tax_res['amount']
else:
price_subtotal = currency.round(price_unit_after_discount * line_vals['quantity'])
to_update_vals = {
'tax_tag_ids': [],
'price_subtotal': price_subtotal,
'price_total': price_subtotal,
}
res['base_lines_to_update'].append((line_vals, to_update_vals))
res['totals'][currency]['amount_untaxed'] += to_update_vals['price_subtotal']
# =========================================================================================
# TAX LINES
# Map each existing tax lines using the grouping key generated by the
# '_get_generation_dict_from_tax_line' method.
# Since everything is indexed using the grouping key, we are now able to decide if
# (1) we can reuse an existing tax line and update its amounts
# (2) some tax lines are no longer used and can be dropped
# (3) we need to create new tax lines
# =========================================================================================
# Track the existing tax lines using the grouping key.
existing_tax_line_map = {}
for line_vals in tax_lines or []:
grouping_key = frozendict(self._get_generation_dict_from_tax_line(line_vals))
# After a modification (e.g. changing the analytic account of the tax line), if two tax lines are sharing
# the same key, keep only one.
if grouping_key in existing_tax_line_map:
res['tax_lines_to_delete'].append(line_vals)
else:
existing_tax_line_map[grouping_key] = line_vals
# Update/create the tax lines.
for grouping_key, tax_values in base_line_map.items():
if tax_values['currency_id']:
currency = self.env['res.currency'].browse(tax_values['currency_id'])
res['totals'][currency]['amount_tax'] += currency.round(tax_values['tax_amount'])
if grouping_key in existing_tax_line_map:
# Update an existing tax line.
line_vals = existing_tax_line_map.pop(grouping_key)
res['tax_lines_to_update'].append((line_vals, tax_values))
else:
# Create a new tax line.
res['tax_lines_to_add'].append(tax_values)
for line_vals in existing_tax_line_map.values():
res['tax_lines_to_delete'].append(line_vals)
return res
@api.model
def _aggregate_taxes_by_tax_group(self, vals_list):
""" Aggregate the values passed as parameter representing the tax amounts for each tax.
:param vals_list: A list of dictionary containing:
* record: The originator record.
* amount: The tax amount or the base amount depending if 'tax' is specified or not.
* taxes: The applied taxes for base lines, optional for tax lines (to be used if they affect the base of
another tax).
* tax: The applied tax if 'record' is a tax line.
:return: A list of dictionaries containing:
* tax_group: An account.tax.group.
* tax_amount: The total tax amount for this group.
* base_amount: The total base amount for this group.
"""
tax_group_mapping = defaultdict(lambda: {
'base_lines': set(),
'base_amount': 0.0,
'tax_amount': 0.0,
})
for vals in vals_list:
record = vals.get('record')
amount = vals['amount']
taxes = vals.get('taxes')
originator_tax = vals.get('tax')
# Compute tax amounts.
if originator_tax:
tax_group_vals = tax_group_mapping[originator_tax.tax_group_id]
tax_group_vals['tax_amount'] += amount
# Compute base amounts.
if not taxes:
continue
for tax in taxes.flatten_taxes_hierarchy():
if originator_tax and originator_tax.tax_group_id == tax.tax_group_id:
continue
tax_group_vals = tax_group_mapping[tax.tax_group_id]
if not record or record not in tax_group_vals['base_lines']:
tax_group_vals['base_amount'] += amount
tax_group_vals['base_lines'].add(record)
tax_groups = sorted(tax_group_mapping.keys(), key=lambda x: x.sequence)
tax_group_vals_list = []
for tax_group in tax_groups:
tax_group_vals = tax_group_mapping[tax_group]
tax_group_vals_list.append({
'tax_group': tax_group,
'tax_amount': tax_group_vals['tax_amount'],
'base_amount': tax_group_vals['base_amount'],
})
return tax_group_vals_list
def _prepare_tax_totals_json(self, base_lines, currency, tax_lines=None):
""" Compute the tax totals details for the business documents.
:param base_lines: A list of python dictionaries created using the '_convert_to_tax_base_line_dict' method.
:param currency: The currency set on the business document.
:param tax_lines: Optional list of python dictionaries created using the '_convert_to_tax_line_dict' method.
If specified, the taxes will be recomputed using them instead of recomputing the taxes on
the provided base lines.
:return: A dictionary in the following form:
{
'amount_total': The total amount to be displayed on the document, including every total
types.
'amount_untaxed': The untaxed amount to be displayed on the document.
'formatted_amount_total': Same as amount_total, but as a string formatted accordingly with
partner's locale.
'formatted_amount_untaxed': Same as amount_untaxed, but as a string formatted accordingly with
partner's locale.
'allow_tax_edition': True if the user should have the ability to manually edit the tax amounts
by group to fix rounding errors.
'groups_by_subtotals': A dictionary formed liked {'subtotal': groups_data}
Where total_type is a subtotal name defined on a tax group, or the
default one: 'Untaxed Amount'.
And groups_data is a list of dict in the following form:
{
'tax_group_name': The name of the tax groups this total is made for.
'tax_group_amount': The total tax amount in this tax group.
'tax_group_base_amount': The base amount for this tax group.
'formatted_tax_group_amount': Same as tax_group_amount, but as a string formatted accordingly
with partner's locale.
'formatted_tax_group_base_amount': Same as tax_group_base_amount, but as a string formatted
accordingly with partner's locale.
'tax_group_id': The id of the tax group corresponding to this dict.
}
'subtotals': A list of dictionaries in the following form, one for each subtotal in
'groups_by_subtotals' keys.
{
'name': The name of the subtotal
'amount': The total amount for this subtotal, summing all the tax groups
belonging to preceding subtotals and the base amount
'formatted_amount': Same as amount, but as a string formatted accordingly with
partner's locale.
}
}
"""
vals_list = []
recompute_taxes = tax_lines is None
amount_untaxed = 0.0
amount_tax = 0.0
# ==== Recompute taxes on-the-fly for business models that are not creating any tax lines ====
if recompute_taxes:
tax_results = self._compute_taxes(
base_lines,
tax_lines=tax_lines,
)
for base_line_vals, to_update in tax_results['base_lines_to_update']:
base_line_vals['price_subtotal'] = to_update['price_subtotal']
for tax_line_vals in tax_results['tax_lines_to_add']:
tax_ids = tax_line_vals['tax_ids'][0][2] if tax_line_vals['tax_ids'] else []
tax_repartition_line = self.env['account.tax.repartition.line'].browse(tax_line_vals['tax_repartition_line_id'])
vals_list.append({
'amount': tax_line_vals['tax_amount'],
'tax': tax_repartition_line.tax_id,
'taxes': self.env['account.tax'].browse(tax_ids),
})
# ==== Aggregate the tax values by tax group ====
for line_vals in base_lines:
amount_untaxed += line_vals['price_subtotal']
taxes = line_vals['taxes']
if not taxes:
continue
vals_list.append({
'record': line_vals['record'],
'amount': line_vals['price_subtotal'],
'taxes': taxes,
})
if not recompute_taxes:
for line_vals in tax_lines:
tax_repartition_line = line_vals['tax_repartition_line']
taxes = line_vals['taxes']
vals_list.append({
'record': line_vals['record'],
'amount': line_vals['tax_amount'],
'taxes': taxes,
'tax': tax_repartition_line.tax_id,
})
tax_group_vals_list = self._aggregate_taxes_by_tax_group(vals_list)
# ==== Partition the tax group values by subtotals ====
subtotal_order = {}
groups_by_subtotal = {}
for tax_group_vals in tax_group_vals_list:
tax_group = tax_group_vals['tax_group']
if tax_group.preceding_subtotal:
subtotal_title = tax_group.preceding_subtotal
sequence = tax_group.sequence + 1 # Avoid sequence = 0 here.
else:
subtotal_title = _("Untaxed Amount")
sequence = 0
if subtotal_title not in subtotal_order:
subtotal_order[subtotal_title] = sequence
groups_by_subtotal[subtotal_title] = []
groups_by_subtotal[subtotal_title].append({
'group_key': tax_group.id,
'tax_group_id': tax_group.id,
'tax_group_name': tax_group.name,
'tax_group_amount': tax_group_vals['tax_amount'],
'tax_group_base_amount': tax_group_vals['base_amount'],
'formatted_tax_group_amount': formatLang(self.env, tax_group_vals['tax_amount'], currency_obj=currency),
'formatted_tax_group_base_amount': formatLang(self.env, tax_group_vals['base_amount'], currency_obj=currency),
})
# ==== Build the final result ====
subtotals = []
for subtotal_title in sorted(subtotal_order.keys(), key=lambda k: subtotal_order[k]):
amount_total = amount_untaxed + amount_tax
subtotals.append({
'name': subtotal_title,
'amount': amount_total,
'formatted_amount': formatLang(self.env, amount_total, currency_obj=currency),
})
amount_tax += sum(x['tax_group_amount'] for x in groups_by_subtotal[subtotal_title])
amount_total = amount_untaxed + amount_tax
return {
'amount_untaxed': amount_untaxed,
'amount_total': amount_total,
'formatted_amount_total': formatLang(self.env, amount_total, currency_obj=currency),
'formatted_amount_untaxed': formatLang(self.env, amount_untaxed, currency_obj=currency),
'groups_by_subtotal': groups_by_subtotal,
'subtotals': subtotals,
'allow_tax_edition': False,
}
@api.model
def _fix_tax_included_price(self, price, prod_taxes, line_taxes):
"""Subtract tax amount from price when corresponding "price included" taxes do not apply"""
@@ -766,3 +1201,14 @@ class AccountTaxRepartitionLine(models.Model):
def _onchange_repartition_type(self):
if self.repartition_type == 'base':
self.account_id = None
def _get_aml_target_tax_account(self):
""" Get the default tax account to set on a business line.
:return: An account.account record or an empty recordset.
"""
self.ensure_one()
if self.tax_id.tax_exigibility == 'on_payment':
return self.tax_id.cash_basis_transition_account_id
else:
return self.account_id
@@ -486,7 +486,7 @@ class TestAccountMove(AccountTestInvoicingCommon):
move = move_form.save()
self.assertRecordValues(move.line_ids, [
self.assertRecordValues(move.line_ids.sorted(lambda x: -x.balance), [
{'name': 'debit_line_1', 'debit': 1000.0, 'credit': 0.0, 'tax_ids': [self.included_percent_tax.id], 'tax_line_id': False},
{'name': 'included_tax_line', 'debit': 200.0, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.included_percent_tax.id},
{'name': 'credit_line_1', 'debit': 0.0, 'credit': 1200.0, 'tax_ids': [], 'tax_line_id': False},
@@ -527,7 +527,7 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon):
], self.move_vals)
move_form = Form(self.invoice)
with move_form.line_ids.edit(2) as line_form:
with move_form.invoice_line_ids.edit(0) as line_form:
# Reset field except the discount that becomes 100%.
# /!\ The modification is made on the accounting tab.
line_form.quantity = 1
@@ -211,7 +211,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
], self.move_vals)
move_form = Form(self.invoice)
with move_form.line_ids.edit(2) as line_form:
with move_form.invoice_line_ids.edit(0) as line_form:
# Reset field except the discount that becomes 100%.
# /!\ The modification is made on the accounting tab.
line_form.quantity = 1
@@ -517,7 +517,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
], self.move_vals)
move_form = Form(self.invoice)
with move_form.line_ids.edit(2) as line_form:
with move_form.invoice_line_ids.edit(0) as line_form:
# Reset field except the discount that becomes 100%.
# /!\ The modification is made on the accounting tab.
line_form.quantity = 1
@@ -210,7 +210,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
], self.move_vals)
move_form = Form(self.invoice)
with move_form.line_ids.edit(2) as line_form:
with move_form.invoice_line_ids.edit(0) as line_form:
# Reset field except the discount that becomes 100%.
# /!\ The modification is made on the accounting tab.
line_form.quantity = 1
+21 -11
View File
@@ -324,22 +324,32 @@ class AccountMove(models.Model):
def _l10n_ar_get_invoice_totals_for_report(self):
self.ensure_one()
tax_ids_filter = tax_line_id_filter = None
include_vat = self._l10n_ar_include_vat()
base_lines = self.line_ids.filtered(lambda x: not x.display_type and not x.exclude_from_invoice_tab)
tax_lines = self.line_ids.filtered(lambda x: not x.display_type and x.tax_repartition_line_id)
# Base lines.
base_line_vals_list = [x._convert_to_tax_base_line_dict() for x in base_lines]
if include_vat:
tax_ids_filter = (lambda aml, tax: not bool(tax.tax_group_id.l10n_ar_vat_afip_code))
tax_line_id_filter = (lambda aml, tax: not bool(tax.tax_group_id.l10n_ar_vat_afip_code))
tax_lines_data = self._prepare_tax_lines_data_for_totals_from_invoice(
tax_ids_filter=tax_ids_filter, tax_line_id_filter=tax_line_id_filter)
for vals in base_line_vals_list:
vals['taxes'] = vals['taxes']\
.flatten_taxes_hierarchy()\
.filtered(lambda tax: not tax.tax_group_id.l10n_ar_vat_afip_code)
# Tax lines.
tax_line_vals_list = [x._convert_to_tax_line_dict() for x in tax_lines]
if include_vat:
amount_untaxed = self.currency_id.round(
self.amount_total - sum([x['tax_amount'] for x in tax_lines_data if 'tax_amount' in x]))
else:
amount_untaxed = self.amount_untaxed
return self._get_tax_totals(self.partner_id, tax_lines_data, self.amount_total, amount_untaxed, self.currency_id)
tax_line_vals_list = [
x
for x in tax_line_vals_list
if not x['tax_repartition_line'].tax_id.tax_group_id.l10n_ar_vat_afip_code
]
return self.env['account.tax']._prepare_tax_totals_json(
base_line_vals_list,
self.currency_id,
tax_lines=tax_line_vals_list,
)
def _l10n_ar_include_vat(self):
self.ensure_one()
+25 -7
View File
@@ -50,10 +50,28 @@ class AccountTax(models.Model):
l10n_in_reverse_charge = fields.Boolean("Reverse charge", help="Tick this if this tax is reverse charge. Only for Indian accounting")
def get_grouping_key(self, invoice_tax_val):
""" Returns a string that will be used to group account.invoice.tax sharing the same properties"""
key = super(AccountTax, self).get_grouping_key(invoice_tax_val)
if self.company_id.account_fiscal_country_id.code == 'IN':
key += "-%s-%s"% (invoice_tax_val.get('l10n_in_product_id', False),
invoice_tax_val.get('l10n_in_uom_id', False))
return key
@api.model
def _get_generation_dict_from_base_line(self, line_vals, tax_vals):
# EXTENDS account
# Group taxes also by product.
res = super()._get_generation_dict_from_base_line(line_vals, tax_vals)
record = line_vals['record']
if isinstance(record, models.Model)\
and record._name == 'account.move.line'\
and record.company_id.account_fiscal_country_id.code == 'IN':
res['product_id'] = record.product_id.id
res['product_uom_id'] = record.product_uom_id.id
return res
@api.model
def _get_generation_dict_from_tax_line(self, line_vals):
# EXTENDS account
# Group taxes also by product.
res = super()._get_generation_dict_from_tax_line(line_vals)
record = line_vals['record']
if isinstance(record, models.Model)\
and record._name == 'account.move.line'\
and record.company_id.account_fiscal_country_id.code == 'IN':
res['product_id'] = record.product_id.id
res['product_uom_id'] = record.product_uom_id.id
return res
-30
View File
@@ -48,36 +48,6 @@ class AccountMove(models.Model):
return self.env.ref('l10n_in.state_in_ot')
return partner.state_id
@api.model
def _get_tax_grouping_key_from_tax_line(self, tax_line):
# OVERRIDE to group taxes also by product.
res = super()._get_tax_grouping_key_from_tax_line(tax_line)
if tax_line.move_id.journal_id.company_id.account_fiscal_country_id.code == 'IN':
res['product_id'] = tax_line.product_id.id
res['product_uom_id'] = tax_line.product_uom_id.id
return res
@api.model
def _get_tax_grouping_key_from_base_line(self, base_line, tax_vals):
# OVERRIDE to group taxes also by product.
res = super()._get_tax_grouping_key_from_base_line(base_line, tax_vals)
if base_line.move_id.journal_id.company_id.account_fiscal_country_id.code == 'IN':
res['product_id'] = base_line.product_id.id
res['product_uom_id'] = base_line.product_uom_id.id
return res
@api.model
def _get_tax_key_for_group_add_base(self, line):
# DEPRECATED: TO BE REMOVED IN MASTER
tax_key = super(AccountMove, self)._get_tax_key_for_group_add_base(line)
tax_key += [
line.product_id.id,
line.product_uom_id.id,
]
return tax_key
def _l10n_in_get_shipping_partner(self):
"""Overwrite in sale"""
self.ensure_one()
+33 -35
View File
@@ -24,17 +24,10 @@ class PurchaseOrder(models.Model):
@api.depends('order_line.price_total')
def _amount_all(self):
for order in self:
amount_untaxed = amount_tax = 0.0
for line in order.order_line:
line._compute_amount()
amount_untaxed += line.price_subtotal
amount_tax += line.price_tax
currency = order.currency_id or order.partner_id.property_purchase_currency_id or self.env.company.currency_id
order.update({
'amount_untaxed': currency.round(amount_untaxed),
'amount_tax': currency.round(amount_tax),
'amount_total': amount_untaxed + amount_tax,
})
order_lines = order.order_line.filtered(lambda x: not x.display_type)
order.amount_untaxed = sum(order_lines.mapped('price_subtotal'))
order.amount_total = sum(order_lines.mapped('price_total'))
order.amount_tax = sum(order_lines.mapped('price_tax'))
@api.depends('state', 'order_line.qty_to_invoice')
def _get_invoiced(self):
@@ -205,14 +198,12 @@ class PurchaseOrder(models.Model):
@api.depends('order_line.taxes_id', 'order_line.price_subtotal', 'amount_total', 'amount_untaxed')
def _compute_tax_totals_json(self):
def compute_taxes(order_line):
return order_line.taxes_id._origin.compute_all(**order_line._prepare_compute_all_values())
account_move = self.env['account.move']
for order in self:
tax_lines_data = account_move._prepare_tax_lines_data_for_totals_from_object(order.order_line, compute_taxes)
tax_totals = account_move._get_tax_totals(order.partner_id, tax_lines_data, order.amount_total, order.amount_untaxed, order.currency_id)
order.tax_totals_json = json.dumps(tax_totals)
order_lines = order.order_line.filtered(lambda x: not x.display_type)
order.tax_totals_json = json.dumps(self.env['account.tax']._prepare_tax_totals_json(
[x._convert_to_tax_base_line_dict() for x in order_lines],
order.currency_id,
))
@api.depends('company_id.account_fiscal_country_id', 'fiscal_position_id.country_id', 'fiscal_position_id.foreign_vat')
def _compute_tax_country_id(self):
@@ -978,27 +969,34 @@ class PurchaseOrderLine(models.Model):
@api.depends('product_qty', 'price_unit', 'taxes_id')
def _compute_amount(self):
for line in self:
taxes = line.taxes_id.compute_all(**line._prepare_compute_all_values())
tax_results = self.env['account.tax']._compute_taxes([line._convert_to_tax_base_line_dict()])
totals = list(tax_results['totals'].values())[0]
amount_untaxed = totals['amount_untaxed']
amount_tax = totals['amount_tax']
line.update({
'price_tax': taxes['total_included'] - taxes['total_excluded'],
'price_total': taxes['total_included'],
'price_subtotal': taxes['total_excluded'],
'price_subtotal': amount_untaxed,
'price_tax': amount_tax,
'price_total': amount_untaxed + amount_tax,
})
def _prepare_compute_all_values(self):
# Hook method to returns the different argument values for the
# compute_all method, due to the fact that discounts mechanism
# is not implemented yet on the purchase orders.
# This method should disappear as soon as this feature is
# also introduced like in the sales module.
def _convert_to_tax_base_line_dict(self):
""" Convert the current record to a dictionary in order to use the generic taxes computation method
defined on account.tax.
:return: A python dictionary.
"""
self.ensure_one()
return {
'price_unit': self.price_unit,
'currency': self.order_id.currency_id,
'quantity': self.product_qty,
'product': self.product_id,
'partner': self.order_id.partner_id,
}
return self.env['account.tax']._convert_to_tax_base_line_dict(
self,
partner=self.order_id.partner_id,
currency=self.order_id.currency_id,
product=self.product_id,
taxes=self.taxes_id,
price_unit=self.price_unit,
quantity=self.product_qty,
price_subtotal=self.price_subtotal,
)
def _compute_tax_id(self):
for line in self:
+10 -19
View File
@@ -404,21 +404,16 @@ class SaleOrder(models.Model):
user_id=user_id, domain=[('company_id', 'in', [company_id, False])])
order.team_id = cached_teams[key]
@api.depends('order_line.price_total')
@api.depends('order_line.price_subtotal', 'order_line.price_tax', 'order_line.price_total')
def _amount_all(self):
"""
Compute the total amounts of the SO.
"""
for order in self:
amount_untaxed = amount_tax = 0.0
for line in order.order_line:
amount_untaxed += line.price_subtotal
amount_tax += line.price_tax
order.update({
'amount_untaxed': amount_untaxed,
'amount_tax': amount_tax,
'amount_total': amount_untaxed + amount_tax,
})
order_lines = order.order_line.filtered(lambda x: not x.display_type)
order.amount_untaxed = sum(order_lines.mapped('price_subtotal'))
order.amount_total = sum(order_lines.mapped('price_total'))
order.amount_tax = sum(order_lines.mapped('price_tax'))
@api.depends('order_line.invoice_lines')
def _get_invoiced(self):
@@ -546,16 +541,12 @@ class SaleOrder(models.Model):
@api.depends('order_line.tax_id', 'order_line.price_unit', 'amount_total', 'amount_untaxed')
def _compute_tax_totals_json(self):
def compute_taxes(order_line):
price = order_line.price_unit * (1 - (order_line.discount or 0.0) / 100.0)
order = order_line.order_id
return order_line.tax_id._origin.compute_all(price, order.currency_id, order_line.product_uom_qty, product=order_line.product_id, partner=order.partner_shipping_id)
account_move = self.env['account.move']
for order in self:
tax_lines_data = account_move._prepare_tax_lines_data_for_totals_from_object(order.order_line, compute_taxes)
tax_totals = account_move._get_tax_totals(order.partner_id, tax_lines_data, order.amount_total, order.amount_untaxed, order.currency_id)
order.tax_totals_json = json.dumps(tax_totals)
order_lines = order.order_line.filtered(lambda x: not x.display_type)
order.tax_totals_json = json.dumps(self.env['account.tax']._prepare_tax_totals_json(
[x._convert_to_tax_base_line_dict() for x in order_lines],
order.currency_id,
))
@api.depends('state')
def _compute_type_name(self):
+27 -12
View File
@@ -544,25 +544,40 @@ class SaleOrderLine(models.Model):
for line in self:
line.price_reduce = line.price_unit * (1.0 - line.discount / 100.0)
def _convert_to_tax_base_line_dict(self):
""" Convert the current record to a dictionary in order to use the generic taxes computation method
defined on account.tax.
:return: A python dictionary.
"""
self.ensure_one()
return self.env['account.tax']._convert_to_tax_base_line_dict(
self,
partner=self.order_id.partner_id,
currency=self.order_id.currency_id,
product=self.product_id,
taxes=self.tax_id,
price_unit=self.price_unit,
quantity=self.product_uom_qty,
discount=self.discount,
price_subtotal=self.price_subtotal,
)
@api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id')
def _compute_amount(self):
"""
Compute the amounts of the SO line.
"""
for line in self:
if line.display_type:
line.update({
'price_tax': 0.0,
'price_total': 0.0,
'price_subtotal': 0.0,
})
continue
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.tax_id.compute_all(price, line.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_shipping_id)
tax_results = self.env['account.tax']._compute_taxes([line._convert_to_tax_base_line_dict()])
totals = list(tax_results['totals'].values())[0]
amount_untaxed = totals['amount_untaxed']
amount_tax = totals['amount_tax']
line.update({
'price_tax': taxes['total_included'] - taxes['total_excluded'],
'price_total': taxes['total_included'],
'price_subtotal': taxes['total_excluded'],
'price_subtotal': amount_untaxed,
'price_tax': amount_tax,
'price_total': amount_untaxed + amount_tax,
})
if self.env.context.get('import_file', False) and not self.env.user.user_has_groups('account.group_account_manager'):
line.tax_id.invalidate_cache(['invoice_repartition_line_ids'], [line.tax_id.id])