diff --git a/addons/account/i18n/account.pot b/addons/account/i18n/account.pot
index 9f6ed9c24b1..3c8b047f7b2 100644
--- a/addons/account/i18n/account.pot
+++ b/addons/account/i18n/account.pot
@@ -9249,6 +9249,13 @@ msgstr ""
msgid "Never"
msgstr ""
+#. module: account
+#. odoo-python
+#: code:addons/account/models/account_tax.py:0
+#, python-format
+msgid "New"
+msgstr ""
+
#. module: account
#: model:ir.model.fields,field_description:account.field_account_setup_bank_manual_config__new_journal_name
msgid "New Journal Name"
@@ -11379,6 +11386,14 @@ msgstr ""
msgid "Remake the sequence of Journal Entries."
msgstr ""
+#. module: account
+#. odoo-python
+#: code:addons/account/models/account_tax.py:0
+#: code:addons/account/models/account_tax.py:0
+#, python-format
+msgid "Removed"
+msgstr ""
+
#. module: account
#: model:account.account,name:account.1_expense_rent
msgid "Rent"
diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py
index 77b397c1efb..8416c0acc63 100644
--- a/addons/account/models/account_tax.py
+++ b/addons/account/models/account_tax.py
@@ -95,10 +95,10 @@ class AccountTax(models.Model):
help="This dummy field lets us use another search method on the field 'name'."
"This allows more freedom on how to search the 'name' compared to 'filter_domain'."
"See '_search_name' and '_parse_name_search' for why this is not possible with 'filter_domain'.")
- type_tax_use = fields.Selection(TYPE_TAX_USE, string='Tax Type', required=True, default="sale",
+ type_tax_use = fields.Selection(TYPE_TAX_USE, string='Tax Type', required=True, default="sale", tracking=True,
help="Determines where the tax is selectable. Note: 'None' means a tax can't be used by itself, however it can still be used in a group. 'adjustment' is used to perform tax adjustment.")
tax_scope = fields.Selection([('service', 'Services'), ('consu', 'Goods')], string="Tax Scope", help="Restrict the use of taxes to a type of product.")
- amount_type = fields.Selection(default='percent', string="Tax Computation", required=True,
+ amount_type = fields.Selection(default='percent', string="Tax Computation", required=True, tracking=True,
selection=[('group', 'Group of Taxes'), ('fixed', 'Fixed'), ('percent', 'Percentage of Price'), ('division', 'Percentage of Price Tax Included')],
help="""
- Group of Taxes: The tax is a set of sub taxes.
@@ -121,7 +121,7 @@ class AccountTax(models.Model):
amount = fields.Float(required=True, digits=(16, 4), default=0.0, tracking=True)
description = fields.Char(string='Description', translate=True)
invoice_label = fields.Char(string='Label on Invoices', translate=True)
- price_include = fields.Boolean(string='Included in Price', default=False,
+ price_include = fields.Boolean(string='Included in Price', default=False, tracking=True,
help="Check this if the price you use on the product and invoices includes this tax.")
include_base_amount = fields.Boolean(string='Affect Base of Subsequent Taxes', default=False, tracking=True,
help="If set, taxes with a higher sequence than this one will be affected by it, provided they accept it.")
@@ -213,12 +213,6 @@ class AccountTax(models.Model):
if record.tax_group_id.country_id and record.tax_group_id.country_id != record.country_id:
raise ValidationError(_("The tax group must have the same country_id as the tax using it."))
- @api.constrains('amount_type', 'type_tax_use', 'price_include', 'include_base_amount')
- def _constrains_fields_after_tax_is_used(self):
- for tax in self:
- if tax.is_used:
- raise ValidationError(_("This tax has been used in transactions. For that reason, it is forbidden to modify this field."))
-
@api.depends('company_id')
def _compute_country_id(self):
for tax in self:
@@ -292,42 +286,55 @@ class AccountTax(models.Model):
for tax in self:
tax.is_used = tax.id in used_taxes
- @api.depends('repartition_line_ids.account_id', 'repartition_line_ids.factor_percent', 'repartition_line_ids.use_in_tax_closing', 'repartition_line_ids.tag_ids')
+ @api.depends('repartition_line_ids.account_id', 'repartition_line_ids.sequence', 'repartition_line_ids.factor_percent', 'repartition_line_ids.use_in_tax_closing', 'repartition_line_ids.tag_ids')
def _compute_repartition_lines_str(self):
for tax in self:
repartition_lines_str = tax.repartition_lines_str or ""
if tax.is_used:
- for repartition_line in tax.repartition_line_ids:
- repartition_line_info = {
- _('id'): repartition_line.id,
+ repartition_line_info = {}
+ invoice_sequence = 0
+ refund_sequence = 0
+ for repartition_line in tax.repartition_line_ids.sorted(key=lambda r: (r.document_type, r.sequence)):
+ # Clean sequence numbers to avoid unnecessary logging when complex
+ # operations are executed such as:
+ # 1. Create a invoice repartition line with a factor of 50%
+ # 2. Delete the invoice line above
+ # 3. Update the last refund repartition line factor to 50%
+ sequence = (invoice_sequence := invoice_sequence + 1) if repartition_line.document_type == 'invoice' else (refund_sequence := refund_sequence + 1)
+ repartition_line_info[(repartition_line.document_type, sequence)] = {
_('Factor Percent'): repartition_line.factor_percent,
- _('Account'): repartition_line.account_id.name or _('None'),
+ _('Account'): repartition_line.account_id.display_name or _('None'),
_('Tax Grids'): repartition_line.tag_ids.mapped('name') or _('None'),
_('Use in tax closing'): _('True') if repartition_line.use_in_tax_closing else _('False'),
}
- repartition_lines_str += str(repartition_line_info) + '//'
- repartition_lines_str = repartition_lines_str.strip('//')
+ repartition_lines_str = str(repartition_line_info)
tax.repartition_lines_str = repartition_lines_str
- def _message_log_repartition_lines(self, old_value_str, new_value_str):
+ def _message_log_repartition_lines(self, old_values_str, new_values_str):
self.ensure_one()
if not self.is_used:
return
- old_values = old_value_str.split('//')
- new_values = new_value_str.split('//')
+ old_line_values_dict = ast.literal_eval(old_values_str)
+ new_line_values_dict = ast.literal_eval(new_values_str)
- kwargs = {}
- for old_value, new_value in zip(old_values, new_values):
- if old_value != new_value:
- old_value = ast.literal_eval(old_value)
- new_value = ast.literal_eval(new_value)
- diff_keys = [key for key in old_value if old_value[key] != new_value[key]]
- repartition_line = self.env['account.tax.repartition.line'].search([('id', '=', new_value['id'])])
+ # Categorize the lines that were added/removed/modified
+ modified_lines = [
+ (line, old_line_values_dict[line], new_line_values_dict[line])
+ for line in old_line_values_dict.keys() & new_line_values_dict.keys()
+ ]
+ added_and_deleted_lines = [
+ (line, _('Removed'), old_line_values_dict[line]) if line in old_line_values_dict else (line, _('New'), new_line_values_dict[line])
+ for line in old_line_values_dict.keys() ^ new_line_values_dict.keys()
+ ]
+
+ for (document_type, sequence), old_value, new_value in modified_lines:
+ diff_keys = [key for key in old_value if old_value[key] != new_value[key]]
+ if diff_keys:
body = Markup("{type} {rep} {seq}: