[IMP] account: automatically adapt account group items

Task 2092104
Accounts group Hierarchy should be automatically well done without to
have to specify the parent (based on the account group CODE)

Before, we had to set all the accounts in the correct groups manually
and it was cumbersome.
Now:
* No need to fulfill account group parent
* No need to fulfill account group id on account

We can still change the groups if there are exceptions; the groups are
only set at create and write time.
This commit is contained in:
wan
2019-11-27 14:10:13 +00:00
parent c8d09da004
commit a124050511
6 changed files with 190 additions and 34 deletions
+133 -24
View File
@@ -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'
+33 -4
View File
@@ -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.
"""
@@ -124,6 +124,13 @@
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="account_group_comp_rule" model="ir.rule">
<field name="name">Account Group multi-company</field>
<field name="model_id" ref="model_account_group"/>
<field name="global" eval="True"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record id="account_root_comp_rule" model="ir.rule">
<field name="name">Account Root multi-company</field>
<field name="model_id" ref="model_account_root"/>
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
13 access_account_account_partner_manager account.account partner manager model_account_account base.group_partner_manager 1 0 0 0
14 access_account_group_manager account.group model_account_group account.group_account_manager 1 1 1 1
15 access_account_group account.group model_account_group account.group_account_user 1 0 0 0
16 access_account_group_template account.group.template model_account_group_template account.group_account_manager 1 1 1 1
17 access_account_root_manager account.root model_account_root account.group_account_manager 1 0 0 0
18 access_account_root account.root model_account_root account.group_account_user 1 0 0 0
19 access_account_tax account.tax model_account_tax account.group_account_invoice 1 0 0 0
@@ -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'])
+9 -5
View File
@@ -18,7 +18,6 @@
<field name="user_type_id" widget="account_hierarchy_selection"/>
<field name="tax_ids" widget="many2many_tags" domain="[('company_id','=',company_id)]" attrs="{'invisible': [('internal_group', '=', 'off_balance')]}"/>
<field name="tag_ids" widget="many2many_tags" domain="[('applicability', '!=', 'taxes')]" context="{'default_applicability': 'accounts'}" options="{'no_create_edit': True}"/>
<field name="group_id"/>
<field name="allowed_journal_ids" widget="many2many_tags" domain="[('company_id','=',company_id)]" options="{'no_create_edit': True}"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
<field name="currency_id" options="{'no_create': True}" groups="base.group_multi_currency"/>
@@ -1790,8 +1789,11 @@ action = model.setting_init_bank_account_action()
<sheet>
<group>
<field name="name"/>
<field name="code_prefix"/>
<field name="parent_id"/>
<label for="code_prefix_start" string="Code Prefix"/>
<div>
From <field name="code_prefix_start" class="oe_inline"/> to <field name="code_prefix_end" class="oe_inline"/>
</div>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group>
</sheet>
</form>
@@ -1804,7 +1806,7 @@ action = model.setting_init_bank_account_action()
<field name="arch" type="xml">
<search string="Account groups">
<field name="name"
filter_domain="['|', ('code_prefix', '=like', str(self) + '%'), ('name', 'ilike', self)]"
filter_domain="['|', ('code_prefix_start', '=like', str(self) + '%'), ('name', 'ilike', self)]"
string="Account group"/>
</search>
</field>
@@ -1815,8 +1817,10 @@ action = model.setting_init_bank_account_action()
<field name="model">account.group</field>
<field name="arch" type="xml">
<tree string="Account Group">
<field name="code_prefix"/>
<field name="code_prefix_start"/>
<field name="code_prefix_end"/>
<field name="name"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>