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