Steps to reproduce issue: 1. Have Sales installed 2. Enable Analytic Accounting and create two Projects 3. Go into both Project Settings and add distinct Analytic Accounts 4. Create a Vendor Bill 5. Add a Product Line and put both accounts in the Analytic Distribution 6. Add an account in another column to one of the existing Analytic Distribution Lines 7. Go to Projects and open the Project Updates of the Project whose account was alone in the Analytic Distribution Line 8. You get the error: > invalid literal for int() with base 10: '[int],[int]' Explanation: With commit odoo@dc696c8ed4850a83b6af721002a90f2944891923, the way IDs are stored changed. They are now computed in a csv string. A part of the Analytic Accounting pipeline didn't get updated after that change, making a lot of the features not working properly. Suggested fix: - We must take into account the multiple analytic plans in any order possible in the ID search. - Because the amount of `account.move.line` can increase rapidly, the new ID search must be indexable. - We reworked the counting query to avoid `account.move` being counted multiple times. opw-3626619 closes odoo/odoo#147664 Related: odoo/enterprise#54311 Signed-off-by: William André (wan) <wan@odoo.com>
122 lines
4.8 KiB
Python
122 lines
4.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.tools import SQL
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class NonMatchingDistribution(Exception):
|
|
pass
|
|
|
|
|
|
class AccountAnalyticDistributionModel(models.Model):
|
|
_name = 'account.analytic.distribution.model'
|
|
_inherit = 'analytic.mixin'
|
|
_description = 'Analytic Distribution Model'
|
|
_rec_name = 'create_date'
|
|
_order = 'id desc'
|
|
|
|
partner_id = fields.Many2one(
|
|
'res.partner',
|
|
string='Partner',
|
|
ondelete='cascade',
|
|
help="Select a partner for which the analytic distribution will be used (e.g. create new customer invoice or Sales order if we select this partner, it will automatically take this as an analytic account)",
|
|
)
|
|
partner_category_id = fields.Many2one(
|
|
'res.partner.category',
|
|
string='Partner Category',
|
|
ondelete='cascade',
|
|
help="Select a partner category for which the analytic distribution will be used (e.g. create new customer invoice or Sales order if we select this partner, it will automatically take this as an analytic account)",
|
|
)
|
|
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):
|
|
"""Ensure accounts specific to a company isn't used in any distribution model that wouldn't be specific to the company"""
|
|
query = SQL(
|
|
"""
|
|
SELECT model.id
|
|
FROM account_analytic_distribution_model model
|
|
JOIN account_analytic_account account
|
|
ON ARRAY[account.id::text] && %s
|
|
WHERE account.company_id IS NOT NULL AND model.id = ANY(%s)
|
|
AND (model.company_id IS NULL
|
|
OR model.company_id != account.company_id)
|
|
""",
|
|
self._query_analytic_accounts('model'),
|
|
self.ids,
|
|
)
|
|
self.flush_model(['company_id', 'analytic_distribution'])
|
|
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
|
|
This method should be called to prefill analytic distribution field on several models """
|
|
domain = []
|
|
for fname, value in vals.items():
|
|
domain += self._create_domain(fname, value) or []
|
|
best_score = 0
|
|
res = {}
|
|
fnames = set(self._get_fields_to_check())
|
|
for rec in self.search(domain):
|
|
try:
|
|
score = sum(rec._check_score(key, vals.get(key)) for key in fnames)
|
|
if score > best_score:
|
|
res = rec.analytic_distribution
|
|
best_score = score
|
|
except NonMatchingDistribution:
|
|
continue
|
|
return res
|
|
|
|
def _get_fields_to_check(self):
|
|
return (
|
|
set(self.env['account.analytic.distribution.model']._fields)
|
|
- set(self.env['analytic.mixin']._fields)
|
|
- set(models.MAGIC_COLUMNS) - {'display_name', '__last_update'}
|
|
)
|
|
|
|
def _check_score(self, key, value):
|
|
self.ensure_one()
|
|
if key == 'company_id':
|
|
if not self.company_id or value == self.company_id.id:
|
|
return 1 if self.company_id else 0.5
|
|
raise NonMatchingDistribution
|
|
if not self[key]:
|
|
return 0
|
|
if value and ((self[key].id in value) if isinstance(value, (list, tuple))
|
|
else (value.startswith(self[key])) if key.endswith('_prefix')
|
|
else (value == self[key].id)
|
|
):
|
|
return 1
|
|
raise NonMatchingDistribution
|
|
|
|
def _create_domain(self, fname, value):
|
|
if not value:
|
|
return False
|
|
if fname == 'partner_category_id':
|
|
value += [False]
|
|
return [(fname, 'in', value)]
|
|
else:
|
|
return [(fname, 'in', [value, False])]
|
|
|
|
def action_read_distribution_model(self):
|
|
self.ensure_one()
|
|
return {
|
|
'name': self.display_name,
|
|
'type': 'ir.actions.act_window',
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'account.analytic.distribution.model',
|
|
'res_id': self.id,
|
|
}
|