diff --git a/addons/account/models/account_analytic_account.py b/addons/account/models/account_analytic_account.py
index 9334690a8d3..a80a2f5e9ef 100644
--- a/addons/account/models/account_analytic_account.py
+++ b/addons/account/models/account_analytic_account.py
@@ -41,10 +41,16 @@ class AccountAnalyticAccount(models.Model):
def action_view_invoice(self):
self.ensure_one()
+ query = self.env['account.move.line']._search([('move_id.move_type', 'in', self.env['account.move'].get_sale_types())])
+ query.order = None
+ query.add_where('analytic_distribution ? %s', [str(self.id)])
+ query_string, query_param = query.select('DISTINCT move_id')
+ self._cr.execute(query_string, query_param)
+ move_ids = [line.get('move_id') for line in self._cr.dictfetchall()]
result = {
"type": "ir.actions.act_window",
"res_model": "account.move",
- "domain": [('line_ids.analytic_distribution_stored_char', '=ilike', f'%"{self.id}":%'), ('move_type', 'in', self.env['account.move'].get_sale_types())],
+ "domain": [('id', 'in', move_ids)],
"context": {"create": False},
"name": "Customer Invoices",
'view_mode': 'tree,form',
@@ -53,10 +59,16 @@ class AccountAnalyticAccount(models.Model):
def action_view_vendor_bill(self):
self.ensure_one()
+ query = self.env['account.move.line']._search([('move_id.move_type', 'in', self.env['account.move'].get_purchase_types())])
+ query.order = None
+ query.add_where('analytic_distribution ? %s', [str(self.id)])
+ query_string, query_param = query.select('DISTINCT move_id')
+ self._cr.execute(query_string, query_param)
+ move_ids = [line.get('move_id') for line in self._cr.dictfetchall()]
result = {
"type": "ir.actions.act_window",
"res_model": "account.move",
- "domain": [('line_ids.analytic_distribution_stored_char', '=ilike', f'%"{self.id}":%'), ('move_type', 'in', self.env['account.move'].get_purchase_types())],
+ "domain": [('id', 'in', move_ids)],
"context": {"create": False},
"name": "Vendor Bills",
'view_mode': 'tree,form',
diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py
index c1aca329d27..a82511bed55 100644
--- a/addons/account/models/account_move_line.py
+++ b/addons/account/models/account_move_line.py
@@ -15,7 +15,7 @@ INTEGRITY_HASH_LINE_FIELDS = ('debit', 'credit', 'account_id', 'partner_id')
class AccountMoveLine(models.Model):
_name = "account.move.line"
- _inherit = 'analytic.mixin'
+ _inherit = "analytic.mixin"
_description = "Journal Item"
_order = "date desc, move_name desc, sequence, id"
_check_company_auto = True
@@ -352,6 +352,9 @@ class AccountMoveLine(models.Model):
comodel_name='account.analytic.line', inverse_name='move_line_id',
string='Analytic lines',
)
+ analytic_distribution = fields.Json(
+ inverse="_inverse_analytic_distribution",
+ ) # add the inverse function used to trigger the creation/update of the analytic lines accordingly (field originally defined in the analytic mixin)
# === Early Pay fields === #
discount_date = fields.Date(
@@ -1071,10 +1074,10 @@ class AccountMoveLine(models.Model):
line.term_key = False
@api.depends('account_id', 'partner_id', 'product_id')
- def _compute_analytic_distribution_stored_char(self):
+ def _compute_analytic_distribution(self):
for line in self:
if line.display_type == 'product' or not line.move_id.is_invoice(include_receipts=True):
- distribution = self.env['account.analytic.distribution.model']._get_distributionjson({
+ distribution = self.env['account.analytic.distribution.model']._get_distribution({
"product_id": line.product_id.id,
"product_categ_id": line.product_id.categ_id.id,
"partner_id": line.partner_id.id,
@@ -1082,8 +1085,7 @@ class AccountMoveLine(models.Model):
"account_prefix": line.account_id.code,
"company_id": line.company_id.id,
})
- line.analytic_distribution_stored_char = distribution or line.analytic_distribution_stored_char
- line._compute_analytic_distribution()
+ line.analytic_distribution = distribution or line.analytic_distribution
# -------------------------------------------------------------------------
# INVERSE METHODS
@@ -1125,7 +1127,6 @@ class AccountMoveLine(models.Model):
@api.onchange('analytic_distribution')
def _inverse_analytic_distribution(self):
""" Unlink and recreate analytic_lines when modifying the distribution."""
- super()._inverse_analytic_distribution()
lines_to_modify = self.env['account.move.line'].browse([
line.id for line in self if line.parent_state == "posted"
])
@@ -2365,7 +2366,6 @@ class AccountMoveLine(models.Model):
def _prepare_analytic_lines(self):
self.ensure_one()
analytic_line_vals = []
- self._compute_analytic_distribution()
if self.analytic_distribution:
# distribution_on_each_plan corresponds to the proportion that is distributed to each plan to be able to
# give the real amount when we achieve a 100% distribution
diff --git a/addons/account/models/account_move_line_tax_details.py b/addons/account/models/account_move_line_tax_details.py
index 9b0254eeb01..11b4b8c20d2 100644
--- a/addons/account/models/account_move_line_tax_details.py
+++ b/addons/account/models/account_move_line_tax_details.py
@@ -182,8 +182,8 @@ class AccountMoveLine(models.Model):
)
AND (
NOT tax.analytic
- OR (base_line.analytic_distribution_stored_char IS NULL AND account_move_line.analytic_distribution_stored_char IS NULL)
- OR base_line.analytic_distribution_stored_char = account_move_line.analytic_distribution_stored_char
+ OR (base_line.analytic_distribution IS NULL AND account_move_line.analytic_distribution IS NULL)
+ OR base_line.analytic_distribution = account_move_line.analytic_distribution
)
LEFT JOIN affecting_base_tax_ids tax_line_tax_ids ON tax_line_tax_ids.id = account_move_line.id
JOIN affecting_base_tax_ids base_line_tax_ids ON base_line_tax_ids.id = base_line.id
diff --git a/addons/account/tests/test_account_analytic.py b/addons/account/tests/test_account_analytic.py
index 18eb98ce5cb..02f7d2a2054 100644
--- a/addons/account/tests/test_account_analytic.py
+++ b/addons/account/tests/test_account_analytic.py
@@ -21,10 +21,12 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
cls.analytic_account_a = cls.env['account.analytic.account'].create({
'name': 'analytic_account_a',
'plan_id': cls.default_plan.id,
+ 'company_id': False,
})
cls.analytic_account_b = cls.env['account.analytic.account'].create({
'name': 'analytic_account_b',
'plan_id': cls.default_plan.id,
+ 'company_id': False,
})
def create_invoice(self, partner, product):
@@ -123,7 +125,7 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
# Partner and product match, score 2
invoice = self.create_invoice(self.partner_a, self.product_a)
- self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
+ self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
# Match the partner but not the product, score 0
invoice = self.create_invoice(self.partner_a, self.product_b)
@@ -131,7 +133,7 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
# Product match, score 1
invoice = self.create_invoice(self.partner_b, self.product_a)
- self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_a.id: 100})
+ self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_a.id): 100})
# No rule match with the product, score 0
invoice = self.create_invoice(self.partner_b, self.product_b)
@@ -141,10 +143,12 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
"""Test that the distribution is recomputed if and only if it is needed when changing the partner."""
self.env['account.analytic.distribution.model'].create([{
'partner_id': self.partner_a.id,
- 'analytic_distribution': {self.analytic_account_a.id: 100}
+ 'analytic_distribution': {self.analytic_account_a.id: 100},
+ 'company_id': False,
}, {
'partner_id': self.partner_b.id,
- 'analytic_distribution': {self.analytic_account_b.id: 100}
+ 'analytic_distribution': {self.analytic_account_b.id: 100},
+ 'company_id': False,
}])
invoice = self.create_invoice(self.env['res.partner'], self.product_a)
@@ -153,27 +157,27 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
# A model is found, set the new values
invoice.partner_id = self.partner_a
- self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_a.id: 100})
+ self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_a.id): 100})
# A model is found, set the new values
invoice.partner_id = self.partner_b
- self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
+ self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
# No model is found, don't change previously set values
invoice.partner_id = invoice.company_id.partner_id
- self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
+ self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
# No model is found, don't change previously set values
invoice.partner_id = False
- self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
+ self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
# It manual value is not erased in form view when saving
with Form(invoice) as invoice_form:
invoice_form.partner_id = self.partner_a
with invoice_form.invoice_line_ids.edit(0) as line_form:
- self.assertEqual(line_form.analytic_distribution, {self.analytic_account_a.id: 100})
+ self.assertEqual(line_form.analytic_distribution, {str(self.analytic_account_a.id): 100})
line_form.analytic_distribution = {self.analytic_account_b.id: 100}
- self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
+ self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
def test_mandatory_plan_validation(self):
invoice = self.create_invoice(self.partner_b, self.product_a)
diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py
index f6f0e12823b..d5c742ed818 100644
--- a/addons/account/tests/test_account_move_out_invoice.py
+++ b/addons/account/tests/test_account_move_out_invoice.py
@@ -1173,7 +1173,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
'code': 'TEST'
})
- analytic_distribution = {analytic_account.id: 100}
+ analytic_distribution = {str(analytic_account.id): 100.00}
move_form = Form(self.invoice)
with move_form.invoice_line_ids.edit(0) as line_form:
@@ -1291,7 +1291,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
'code': 'TEST1'
})
- analytic_distribution = {analytic_account.id: 100}
+ analytic_distribution = {str(analytic_account.id): 100.00}
self.invoice.write({'invoice_line_ids': [(1, self.invoice.invoice_line_ids.ids[0], {
'analytic_distribution': analytic_distribution,
diff --git a/addons/account/tests/test_templates_consistency.py b/addons/account/tests/test_templates_consistency.py
index f37cc317891..c139044e585 100644
--- a/addons/account/tests/test_templates_consistency.py
+++ b/addons/account/tests/test_templates_consistency.py
@@ -78,7 +78,7 @@ class AccountingTestTemplConsistency(TransactionCase):
)
# lines
self.check_fields_consistency('account.reconcile.model.line.template', 'account.reconcile.model.line', exceptions=['chart_template_id'])
- self.check_fields_consistency('account.reconcile.model.line', 'account.reconcile.model.line.template', exceptions=['company_id', 'journal_id', 'analytic_distribution_stored_char', 'amount'])
+ self.check_fields_consistency('account.reconcile.model.line', 'account.reconcile.model.line.template', exceptions=['company_id', 'journal_id', 'analytic_distribution', 'amount'])
def test_account_group_fields(self):
'''Test fields consistency for ('account.group', 'account.group.template')
diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml
index 6235d49e0e1..3a5b5e386fe 100644
--- a/addons/account/views/account_move_views.xml
+++ b/addons/account/views/account_move_views.xml
@@ -70,7 +70,6 @@
-
@@ -187,7 +186,6 @@
-
-
-
@@ -1056,7 +1052,6 @@
domain="['|', ('parent_id', '=', False), ('is_company', '=', True)]"
attrs="{'column_invisible': [('parent.move_type', '!=', 'entry')]}"/>
-
-
diff --git a/addons/account/views/account_reconcile_model_views.xml b/addons/account/views/account_reconcile_model_views.xml
index d797674fb33..c238b3a2ff1 100644
--- a/addons/account/views/account_reconcile_model_views.xml
+++ b/addons/account/views/account_reconcile_model_views.xml
@@ -24,7 +24,6 @@
-
@@ -244,7 +243,6 @@
-
diff --git a/addons/analytic/__manifest__.py b/addons/analytic/__manifest__.py
index 4fbd254e376..13ea9a470fa 100644
--- a/addons/analytic/__manifest__.py
+++ b/addons/analytic/__manifest__.py
@@ -21,6 +21,7 @@ that have no counterpart in the general financial accounts.
'views/analytic_account_views.xml',
'views/analytic_plan_views.xml',
'views/analytic_distribution_model_views.xml',
+ 'data/analytic_data.xml'
],
'demo': [
'data/analytic_account_demo.xml'
diff --git a/addons/analytic/data/analytic_data.xml b/addons/analytic/data/analytic_data.xml
index a6e6db4941f..3dbaac923bf 100644
--- a/addons/analytic/data/analytic_data.xml
+++ b/addons/analytic/data/analytic_data.xml
@@ -3,7 +3,7 @@
Percentage Analytic
- 10
+
-
\ No newline at end of file
+
diff --git a/addons/analytic/models/analytic_distribution_model.py b/addons/analytic/models/analytic_distribution_model.py
index eba950fd56a..3fac11406b8 100644
--- a/addons/analytic/models/analytic_distribution_model.py
+++ b/addons/analytic/models/analytic_distribution_model.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models
-import json
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
class AccountAnalyticDistributionModel(models.Model):
_name = 'account.analytic.distribution.model'
@@ -26,10 +27,27 @@ class AccountAnalyticDistributionModel(models.Model):
company_id = fields.Many2one(
'res.company',
string='Company',
+ default=lambda self: self.env.company,
ondelete='cascade',
help="Select a company for which the analytic distribution will be used (e.g. create new customer invoice or Sales order if we select this company, it will automatically take this as an analytic account)",
)
+ @api.constrains('company_id')
+ def _check_company_accounts(self):
+ query = """
+ SELECT model.id
+ FROM account_analytic_distribution_model model
+ JOIN account_analytic_account account
+ ON model.analytic_distribution ? CAST(account.id AS VARCHAR)
+ WHERE account.company_id IS NOT NULL
+ AND (model.company_id IS NULL
+ OR model.company_id != account.company_id)
+ GROUP BY model.id
+ """
+ self.env.cr.execute(query)
+ if self.env.cr.dictfetchone():
+ raise UserError(_('You defined a distribution with analytic account(s) belonging to a specific company but a model shared between companies or with a different company'))
+
@api.model
def _get_distribution(self, vals):
""" Returns the distribution model that has the most fields that corresponds to the vals given
@@ -71,9 +89,3 @@ class AccountAnalyticDistributionModel(models.Model):
return [(fname, 'in', value)]
else:
return [(fname, 'in', [value, False])]
-
- @api.model
- def _get_distributionjson(self, vals):
- """ Returns the distribution model as a json for the compute_analytic_distribution_stored_char functions"""
- distribution = self._get_distribution(vals)
- return json.dumps(distribution) if distribution else None
diff --git a/addons/analytic/models/analytic_mixin.py b/addons/analytic/models/analytic_mixin.py
index 2770c80407a..a20f47f52d8 100644
--- a/addons/analytic/models/analytic_mixin.py
+++ b/addons/analytic/models/analytic_mixin.py
@@ -1,49 +1,51 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api
-import json
-from odoo.tools import float_repr
+from odoo.tools.float_utils import float_round
class AnalyticMixin(models.AbstractModel):
_name = 'analytic.mixin'
_description = 'Analytic Mixin'
- # We create 2 different fields, with a computed binary field, so we don't have to decode encode each time the json.
- # We also format the float values of the stored field, so we can use it as key (for tax detail for ex.)
- analytic_distribution_stored_char = fields.Char(
- compute="_compute_analytic_distribution_stored_char", store=True, copy=True)
- analytic_distribution = fields.Binary(
- string="Analytic",
- compute="_compute_analytic_distribution",
- inverse="_inverse_analytic_distribution",
- readonly=False,
+ analytic_distribution = fields.Json(
+ 'Analytic',
+ compute="_compute_analytic_distribution", store=True, copy=True, readonly=False,
+ precompute=True
)
- def _compute_analytic_distribution_stored_char(self):
+ def init(self):
+ # Add a gin index for json search on the keys, on the models that actually have a table
+ query = ''' SELECT table_name
+ FROM information_schema.tables
+ WHERE table_name=%s '''
+ self.env.cr.execute(query, [self._table])
+ if self.env.cr.dictfetchone():
+ query = f"""
+ CREATE INDEX IF NOT EXISTS {self._table}_analytic_distribution_index
+ ON {self._table} USING gin(analytic_distribution);
+ """
+ self.env.cr.execute(query)
+
+ def _compute_analytic_distribution(self):
pass
- @api.depends('analytic_distribution_stored_char')
- def _compute_analytic_distribution(self):
- for record in self:
- if record.analytic_distribution_stored_char:
- distribution_to_return = {}
- distribution_json = json.loads(record.analytic_distribution_stored_char)
- for account, distribution in distribution_json.items():
- distribution_to_return[int(account)] = float(distribution)
- # Check if the account exists, can be removed when we have a constraint between account and model
- account_ids = self.env['account.analytic.account'].browse(distribution_to_return.keys()).exists().ids
- record.analytic_distribution = {account_id: distribution_to_return[account_id] for account_id in account_ids}
-
- @api.onchange('analytic_distribution')
- def _inverse_analytic_distribution(self):
+ def write(self, vals):
+ """ Format the analytic_distribution float value, so equality on analytic_distribution can be done """
decimal_precision = self.env['decimal.precision'].precision_get('Percentage Analytic')
- self.env.remove_to_compute(self._fields['analytic_distribution_stored_char'], self)
- for record in self:
- if not record.analytic_distribution:
- record.analytic_distribution_stored_char = None
- else:
- distribution_to_return = {}
- for account, distribution in record.analytic_distribution.items():
- distribution_to_return[account] = float_repr(distribution, decimal_precision)
- record.analytic_distribution_stored_char = json.dumps(distribution_to_return)
+ vals = self._sanitize_values(vals, decimal_precision)
+ return super().write(vals)
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ """ Format the analytic_distribution float value, so equality on analytic_distribution can be done """
+ decimal_precision = self.env['decimal.precision'].precision_get('Percentage Analytic')
+ vals_list = [self._sanitize_values(vals, decimal_precision) for vals in vals_list]
+ return super().create(vals_list)
+
+ def _sanitize_values(self, vals, decimal_precision):
+ """ Normalize the float of the distribution """
+ if 'analytic_distribution' in vals:
+ vals['analytic_distribution'] = vals.get('analytic_distribution') and {
+ account_id: float_round(distribution, decimal_precision) for account_id, distribution in vals['analytic_distribution'].items()}
+ return vals
diff --git a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js
index bf744f4e4fc..ba5274f04ab 100644
--- a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js
+++ b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js
@@ -79,6 +79,12 @@ export class AnalyticDistribution extends Component {
if (this.editingRecord) {
await this.fetchAllPlans(this.props);
}
+ const args = {
+ domain: [["name", "like", "Percentage Analytic"]],
+ fields: ["digits"],
+ context: [],
+ }
+ this.decimal_precision = await this.orm.call("decimal.precision", "search_read", [], args);
await this.formatData(this.props);
}
@@ -559,7 +565,8 @@ export class AnalyticDistribution extends Component {
}
formatPercentage(value) {
- return formatPercentage(value / 100, { digits: [false, 2] });
+ return formatPercentage(value / 100, { digits: [false, this.decimal_precision[0].digits || 2] });
+
}
}
AnalyticDistribution.template = "analytic_distribution";
diff --git a/addons/analytic/static/tests/analytic_distribution_tests.js b/addons/analytic/static/tests/analytic_distribution_tests.js
index 631cada7203..da8614e6243 100644
--- a/addons/analytic/static/tests/analytic_distribution_tests.js
+++ b/addons/analytic/static/tests/analytic_distribution_tests.js
@@ -63,7 +63,7 @@ QUnit.module("Analytic", (hooks) => {
fields: {
label: { string: "Label", type: "char" },
amount: { string: "Amount", type: "float" },
- analytic_distribution: { string: "Analytic", type: "char" },
+ analytic_distribution: { string: "Analytic", type: "json" },
move_id: { string: "Account Move", type: "many2one", relation: "move" },
},
records: [
@@ -81,6 +81,15 @@ QUnit.module("Analytic", (hooks) => {
{ id: 1, display_name: "INV0001", line_ids: [1, 2]},
{ id: 2, display_name: "INV0002", line_ids: [3, 4]},
],
+ },
+ "decimal.precision": {
+ fields: {
+ name: { string: "Name", type: "char" },
+ digits: { string: "Digits", type: "int" },
+ },
+ records: [
+ { id: 1, name: "Percentage Analytic", digits: 2}
+ ]
}
},
};
diff --git a/addons/analytic/tests/test_analytic_account.py b/addons/analytic/tests/test_analytic_account.py
index be755e51853..507772451d1 100644
--- a/addons/analytic/tests/test_analytic_account.py
+++ b/addons/analytic/tests/test_analytic_account.py
@@ -114,8 +114,8 @@ class TestAnalyticAccount(TransactionCase):
distribution_json = self.env['account.analytic.distribution.model']._get_distribution({
"partner_id": self.partner_a.id,
})
- self.assertEqual(distribution_json, self.distribution_1.analytic_distribution, "Distribution 1 should be given")
+ self.assertEqual(distribution_json, {str(self.analytic_account_3.id): 100}, "Distribution 1 should be given")
distribution_json = self.env['account.analytic.distribution.model']._get_distribution({
"partner_id": self.partner_b.id,
})
- self.assertEqual(distribution_json, self.distribution_2.analytic_distribution, "Distribution 2 should be given")
+ self.assertEqual(distribution_json, {str(self.analytic_account_2.id): 100}, "Distribution 2 should be given")
diff --git a/addons/analytic/views/analytic_distribution_model_views.xml b/addons/analytic/views/analytic_distribution_model_views.xml
index bd96b272ed0..553dea7d93c 100644
--- a/addons/analytic/views/analytic_distribution_model_views.xml
+++ b/addons/analytic/views/analytic_distribution_model_views.xml
@@ -5,7 +5,6 @@
account.analytic.distribution.model
-
@@ -21,7 +20,6 @@