diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 4346c3448ed..d32e2e3a6df 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -238,7 +238,7 @@ class AccountAccount(models.Model):
company_id = fields.Many2one('res.company', string='Company', required=True,
default=lambda self: self.env.company)
tag_ids = fields.Many2many('account.account.tag', 'account_account_account_tag', string='Tags', help="Optional tags you may want to assign for custom reporting")
- group_id = fields.Many2one('account.group')
+ group_id = fields.Many2one('account.group', compute='_compute_account_group', store=True)
root_id = fields.Many2one('account.root', compute='_compute_account_root', store=True)
allowed_journal_ids = fields.Many2many('account.journal', string="Allowed Journals", help="Define in which journals this account can be used. If empty, can be used in all journals.")
@@ -281,6 +281,13 @@ class AccountAccount(models.Model):
for record in self:
record.root_id = record.code and (ord(record.code[0]) * 1000 + ord(record.code[1])) or False
+ @api.depends('code')
+ def _compute_account_group(self):
+ if self.ids:
+ self.env['account.group']._adapt_accounts_for_account_groups(self)
+ else:
+ self.group_id = False
+
def _search_used(self, operator, value):
if operator not in ['=', '!='] or not isinstance(value, bool):
raise UserError(_('Operation not supported'))
@@ -404,22 +411,6 @@ class AccountAccount(models.Model):
elif self.internal_group == 'expense' and not self.tax_ids:
self.tax_ids = self.company_id.account_purchase_tax_id
- @api.onchange('code')
- def onchange_code(self):
- AccountGroup = self.env['account.group']
-
- group = False
- code_prefix = self.code
-
- # find group with longest matching prefix
- while code_prefix:
- matching_group = AccountGroup.search([('code_prefix', '=', code_prefix)], limit=1)
- if matching_group:
- group = matching_group
- break
- code_prefix = code_prefix[:-1]
- self.group_id = group
-
def name_get(self):
result = []
for account in self:
@@ -550,19 +541,40 @@ class AccountGroup(models.Model):
_name = "account.group"
_description = 'Account Group'
_parent_store = True
- _order = 'code_prefix'
+ _order = 'code_prefix_start'
- parent_id = fields.Many2one('account.group', index=True, ondelete='cascade')
+ parent_id = fields.Many2one('account.group', index=True, ondelete='cascade', readonly=True)
parent_path = fields.Char(index=True)
name = fields.Char(required=True)
- code_prefix = fields.Char()
+ code_prefix_start = fields.Char()
+ code_prefix_end = fields.Char()
+ company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company)
+
+ _sql_constraints = [
+ (
+ 'check_length_prefix',
+ 'CHECK(char_length(COALESCE(code_prefix_start, \'\')) = char_length(COALESCE(code_prefix_end, \'\')))',
+ 'The length of the starting and the ending code prefix must be the same'
+ ),
+ ]
+
+ @api.onchange('code_prefix_start')
+ def _onchange_code_prefix_start(self):
+ if not self.code_prefix_end or self.code_prefix_end < self.code_prefix_start:
+ self.code_prefix_end = self.code_prefix_start
+
+ @api.onchange('code_prefix_end')
+ def _onchange_code_prefix_end(self):
+ if not self.code_prefix_start or self.code_prefix_start > self.code_prefix_end:
+ self.code_prefix_start = self.code_prefix_end
def name_get(self):
result = []
for group in self:
- name = group.name
- if group.code_prefix:
- name = group.code_prefix + ' ' + name
+ prefix = group.code_prefix_start and str(group.code_prefix_start)
+ if prefix and group.code_prefix_end != group.code_prefix_start:
+ prefix += '-' + str(group.code_prefix_end)
+ name = (prefix and (prefix + ' ') or '') + group.name
result.append((group.id, name))
return result
@@ -573,10 +585,107 @@ class AccountGroup(models.Model):
domain = []
else:
criteria_operator = ['|'] if operator not in expression.NEGATIVE_TERM_OPERATORS else ['&', '!']
- domain = criteria_operator + [('code_prefix', '=ilike', name + '%'), ('name', operator, name)]
+ domain = criteria_operator + [('code_prefix_start', '=ilike', name + '%'), ('name', operator, name)]
group_ids = self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid)
return models.lazy_name_get(self.browse(group_ids).with_user(name_get_uid))
+ @api.constrains('code_prefix_start', 'code_prefix_end')
+ def _constraint_prefix_overlap(self):
+ self.env['account.group'].flush()
+ query = """
+ SELECT other.id FROM account_group this
+ JOIN account_group other
+ ON char_length(other.code_prefix_start) = char_length(this.code_prefix_start)
+ AND other.id != this.id
+ AND other.company_id = this.company_id
+ AND (
+ other.code_prefix_start <= this.code_prefix_start AND this.code_prefix_start <= other.code_prefix_end
+ OR
+ other.code_prefix_start >= this.code_prefix_start AND this.code_prefix_end >= other.code_prefix_start
+ )
+ WHERE this.id IN %(ids)s
+ """
+ self.env.cr.execute(query, {'ids': tuple(self.ids)})
+ res = self.env.cr.fetchall()
+ if res:
+ raise ValidationError(_('Account Groups with the same granularity can\'t overlap'))
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if 'code_prefix_start' in vals and not vals.get('code_prefix_end'):
+ vals['code_prefix_end'] = vals['code_prefix_start']
+ res_ids = super(AccountGroup, self).create(vals_list)
+ res_ids._adapt_accounts_for_account_groups()
+ res_ids._adapt_parent_account_group()
+ return res_ids
+
+ def write(self, vals):
+ res = super(AccountGroup, self).write(vals)
+ if 'code_prefix_start' in vals or 'code_prefix_end' in vals:
+ self._adapt_accounts_for_account_groups()
+ self._adapt_parent_account_group()
+ return res
+
+ def unlink(self):
+ for record in self:
+ account_ids = self.env['account.account'].search([('group_id', '=', record.id)])
+ account_ids.write({'group_id': record.parent_id.id})
+
+ children_ids = self.env['account.group'].search([('parent_id', '=', record.id)])
+ children_ids.write({'parent_id': record.parent_id.id})
+ super(AccountGroup, self).unlink()
+
+ def _adapt_accounts_for_account_groups(self, account_ids=None):
+ """Ensure consistency between accounts and account groups.
+
+ Find and set the most specific group matching the code of the account.
+ The most specific is the one with the longest prefixes and with the starting
+ prefix being smaller than the account code and the ending prefix being greater.
+ """
+ if not self and not account_ids:
+ return
+ self.env['account.group'].flush()
+ self.env['account.account'].flush()
+ query = """
+ UPDATE account_account account SET group_id = (
+ SELECT agroup.id FROM account_group agroup
+ WHERE agroup.code_prefix_start <= LEFT(account.code, char_length(agroup.code_prefix_start))
+ AND agroup.code_prefix_end >= LEFT(account.code, char_length(agroup.code_prefix_end))
+ AND agroup.company_id = account.company_id
+ ORDER BY char_length(agroup.code_prefix_start) DESC LIMIT 1
+ ) WHERE account.company_id in %(company_ids)s {where_account};
+ """.format(
+ where_account=account_ids and 'AND account.id IN %(account_ids)s' or ''
+ )
+ self.env.cr.execute(query, {'company_ids': tuple((self.company_id or account_ids.company_id).ids), 'account_ids': account_ids and tuple(account_ids.ids)})
+ self.env['account.account'].invalidate_cache(fnames=['group_id'])
+
+ def _adapt_parent_account_group(self):
+ """Ensure consistency of the hierarchy of account groups.
+
+ Find and set the most specific parent for each group.
+ The most specific is the one with the longest prefixes and with the starting
+ prefix being smaller than the child prefixes and the ending prefix being greater.
+ """
+ if not self:
+ return
+ self.env['account.group'].flush()
+ query = """
+ UPDATE account_group agroup SET parent_id = (
+ SELECT parent.id FROM account_group parent
+ WHERE char_length(parent.code_prefix_start) < char_length(agroup.code_prefix_start)
+ AND parent.code_prefix_start <= LEFT(agroup.code_prefix_start, char_length(parent.code_prefix_start))
+ AND parent.code_prefix_end >= LEFT(agroup.code_prefix_end, char_length(parent.code_prefix_end))
+ AND parent.id != agroup.id
+ AND parent.company_id = %(company_id)s
+ ORDER BY char_length(parent.code_prefix_start) DESC LIMIT 1
+ ) WHERE agroup.company_id = %(company_id)s;
+ """
+ self.env.cr.execute(query, {'company_id': self.company_id.id})
+ self.env['account.group'].invalidate_cache(fnames=['parent_id'])
+ self.env['account.group'].search([('company_id', '=', self.company_id.id)])._parent_store_update()
+
class AccountRoot(models.Model):
_name = 'account.root'
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
index 5d11e7195a1..6a5220f50ed 100644
--- a/addons/account/models/chart_template.py
+++ b/addons/account/models/chart_template.py
@@ -35,6 +35,18 @@ def preserve_existing_tags_on_taxes(cr, registry, module):
# ---------------------------------------------------------------
+class AccountGroupTemplate(models.Model):
+ _name = "account.group.template"
+ _description = 'Template for Account Groups'
+ _order = 'code_prefix_start'
+
+ parent_id = fields.Many2one('account.group.template', index=True, ondelete='cascade')
+ name = fields.Char(required=True)
+ code_prefix_start = fields.Char()
+ code_prefix_end = fields.Char()
+ chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
+
+
class AccountAccountTemplate(models.Model):
_name = "account.account.template"
_description = 'Templates for Accounts'
@@ -56,8 +68,6 @@ class AccountAccountTemplate(models.Model):
help="This optional field allow you to link an account template to a specific chart template that may differ from the one its root parent belongs to. This allow you "
"to define chart templates that extend another and complete it with few new accounts (You don't need to define the whole structure that is common to both several times).")
tag_ids = fields.Many2many('account.account.tag', 'account_account_template_account_tag', string='Account tag', help="Optional tags you may want to assign for custom reporting")
- group_id = fields.Many2one('account.group')
- root_id = fields.Many2one('account.root')
@api.depends('name', 'code')
def name_get(self):
@@ -193,7 +203,7 @@ class AccountChartTemplate(models.Model):
accounting_props.sudo().unlink()
# delete account, journal, tax, fiscal position and reconciliation model
- models_to_delete = ['account.reconcile.model', 'account.fiscal.position', 'account.tax', 'account.move', 'account.journal']
+ models_to_delete = ['account.reconcile.model', 'account.fiscal.position', 'account.tax', 'account.move', 'account.journal', 'account.group']
for model in models_to_delete:
res = self.env[model].sudo().search([('company_id', '=', company.id)])
if len(res):
@@ -544,6 +554,9 @@ class AccountChartTemplate(models.Model):
account_template_ref = self.generate_account(taxes_ref, account_ref, code_digits, company)
account_ref.update(account_template_ref)
+ # Generate account groups, from template
+ self.generate_account_groups(company)
+
# writing account values after creation of accounts
for key, value in generated_tax_res['account_dict']['account.tax'].items():
if value['cash_basis_transition_account_id'] or value['cash_basis_base_account_id']:
@@ -635,7 +648,6 @@ class AccountChartTemplate(models.Model):
'tax_ids': [(6, 0, tax_ids)],
'company_id': company.id,
'tag_ids': [(6, 0, [t.id for t in account_template.tag_ids])],
- 'group_id': account_template.group_id.id,
}
return val
@@ -665,6 +677,23 @@ class AccountChartTemplate(models.Model):
acc_template_ref[template.id] = account.id
return acc_template_ref
+ def generate_account_groups(self, company):
+ """ This method generates account groups from account groups templates.
+ :param company: company to generate the account groups for
+ """
+ self.ensure_one()
+ group_templates = self.env['account.group.template'].search([('chart_template_id', '=', self.id)])
+ template_vals = []
+ for group_template in group_templates:
+ vals = {
+ 'name': group_template.name,
+ 'code_prefix_start': group_template.code_prefix_start,
+ 'code_prefix_end': group_template.code_prefix_end,
+ 'company_id': company.id,
+ }
+ template_vals.append((group_template, vals))
+ groups = self._create_records_with_xmlid('account.group', template_vals, company)
+
def _prepare_reconcile_model_vals(self, company, account_reconcile_model, acc_template_ref, tax_template_ref):
""" This method generates a dictionary of all the values for the account.reconcile.model that will be created.
"""
diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml
index e36c40d8b22..c665789497f 100644
--- a/addons/account/security/account_security.xml
+++ b/addons/account/security/account_security.xml
@@ -124,6 +124,13 @@
['|',('company_id','=',False),('company_id', 'in', company_ids)]
+
+ Account Group multi-company
+
+
+ [('company_id', 'in', company_ids)]
+
+
Account Root multi-company
diff --git a/addons/account/security/ir.model.access.csv b/addons/account/security/ir.model.access.csv
index 78dda1abdb3..3ee0856bb24 100644
--- a/addons/account/security/ir.model.access.csv
+++ b/addons/account/security/ir.model.access.csv
@@ -13,6 +13,7 @@ access_account_account_user,account.account user,model_account_account,base.grou
access_account_account_partner_manager,account.account partner manager,model_account_account,base.group_partner_manager,1,0,0,0
access_account_group_manager,account.group,model_account_group,account.group_account_manager,1,1,1,1
access_account_group,account.group,model_account_group,account.group_account_user,1,0,0,0
+access_account_group_template,account.group.template,model_account_group_template,account.group_account_manager,1,1,1,1
access_account_root_manager,account.root,model_account_root,account.group_account_manager,1,0,0,0
access_account_root,account.root,model_account_root,account.group_account_user,1,0,0,0
access_account_tax,account.tax,model_account_tax,account.group_account_invoice,1,0,0,0
diff --git a/addons/account/tests/test_templates_consistency.py b/addons/account/tests/test_templates_consistency.py
index aaa2217a515..f91599cd552 100644
--- a/addons/account/tests/test_templates_consistency.py
+++ b/addons/account/tests/test_templates_consistency.py
@@ -34,7 +34,7 @@ class AccountingTestTemplConsistency(TransactionCase):
self.check_fields_consistency(
'account.account.template', 'account.account', exceptions=['chart_template_id', 'nocreate'])
self.check_fields_consistency(
- 'account.account', 'account.account.template', exceptions=['company_id', 'deprecated', 'opening_debit', 'opening_credit', 'allowed_journal_ids'])
+ 'account.account', 'account.account.template', exceptions=['company_id', 'deprecated', 'opening_debit', 'opening_credit', 'allowed_journal_ids', 'group_id', 'root_id'])
def test_account_tax_fields(self):
'''Test fields consistency for ('account.tax', 'account.tax.template')
@@ -62,3 +62,9 @@ class AccountingTestTemplConsistency(TransactionCase):
'''
self.check_fields_consistency('account.reconcile.model.template', 'account.reconcile.model', exceptions=['chart_template_id'])
self.check_fields_consistency('account.reconcile.model', 'account.reconcile.model.template', exceptions=['company_id', 'journal_id', 'analytic_account_id', 'second_journal_id', 'second_analytic_account_id', 'analytic_tag_ids', 'second_analytic_tag_ids'])
+
+ def test_account_group_fields(self):
+ '''Test fields consistency for ('account.group', 'account.group.template')
+ '''
+ self.check_fields_consistency('account.group', 'account.group.template', exceptions=['company_id', 'parent_path'])
+ self.check_fields_consistency('account.group.template', 'account.group', exceptions=['chart_template_id'])
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index b3e11cf47c7..0a819cb2f78 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -18,7 +18,6 @@
-
@@ -1790,8 +1789,11 @@ action = model.setting_init_bank_account_action()
-
-
+
+
+ From to
+
+
@@ -1804,7 +1806,7 @@ action = model.setting_init_bank_account_action()
@@ -1815,8 +1817,10 @@ action = model.setting_init_bank_account_action()
account.group
-
+
+
+