[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:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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'])
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user