[IMP] base: add more support for sub companies

Add fields and tooling to support sub companies.
The new fields are
* `parent_ids`, which consists of all the ancestors of the current
  company
* `root_id`, the upmost parent, which is the company controlling the
  subsidiaries
* `parent_path` is added to avoid a hit on the performances when using
  the multibranch feature

Also add a mechanism to ensure some fields are shared with a root
company and all it's descendents.
All fields listed by `_get_company_root_delegated_field_names` will be
readonly and copied from the `root_id`.

task-3371677

Part-of: odoo/odoo#125642
This commit is contained in:
william-andre
2023-07-20 11:49:04 +02:00
committed by qdp-odoo
parent b783718ad1
commit dc4dcead6e
4 changed files with 115 additions and 16 deletions
+1 -1
View File
@@ -6,6 +6,6 @@ from odoo import fields, models
class Company(models.Model):
_inherit = "res.company"
snailmail_color = fields.Boolean(string='Color', default=True)
snailmail_color = fields.Boolean(default=True)
snailmail_cover = fields.Boolean(string='Add a Cover Page', default=False)
snailmail_duplex = fields.Boolean(string='Both sides', default=False)
+105 -13
View File
@@ -2,18 +2,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import io
import logging
import os
import warnings
from odoo import api, fields, models, tools, _, Command
from odoo.exceptions import ValidationError, UserError
from odoo.modules.module import get_resource_path
from odoo.tools import html2plaintext
from odoo.tools import file_open
from random import randrange
from PIL import Image
from odoo.tools import html2plaintext, file_open, ormcache
_logger = logging.getLogger(__name__)
@@ -22,6 +16,7 @@ class Company(models.Model):
_name = "res.company"
_description = 'Companies'
_order = 'sequence, name'
_parent_store = True
def copy(self, default=None):
raise UserError(_('Duplicating a company is not allowed. Please create a new company instead.'))
@@ -37,7 +32,10 @@ class Company(models.Model):
active = fields.Boolean(default=True)
sequence = fields.Integer(help='Used to order Companies in the company switcher', default=10)
parent_id = fields.Many2one('res.company', string='Parent Company', index=True)
child_ids = fields.One2many('res.company', 'parent_id', string='Child Companies')
child_ids = fields.One2many('res.company', 'parent_id', string='Branches')
parent_path = fields.Char(index=True, unaccent=False)
parent_ids = fields.Many2many('res.company', compute='_compute_parent_ids', compute_sudo=True)
root_id = fields.Many2one('res.company', compute='_compute_parent_ids', compute_sudo=True)
partner_id = fields.Many2one('res.partner', string='Partner', required=True)
report_header = fields.Html(string='Company Tagline', translate=True, help="Appears by default on the top right corner of your printed documents (report header).")
report_footer = fields.Html(string='Report Footer', translate=True, help="Footer text displayed at the bottom of all reports.")
@@ -71,6 +69,7 @@ class Company(models.Model):
font = fields.Selection([("Lato", "Lato"), ("Roboto", "Roboto"), ("Open_Sans", "Open Sans"), ("Montserrat", "Montserrat"), ("Oswald", "Oswald"), ("Raleway", "Raleway"), ('Tajawal', 'Tajawal')], default="Lato")
primary_color = fields.Char()
secondary_color = fields.Char()
color = fields.Integer(compute='_compute_color', inverse='_inverse_color')
layout_background = fields.Selection([('Blank', 'Blank'), ('Geometric', 'Geometric'), ('Custom', 'Custom')], default="Blank", required=True)
layout_background_image = fields.Binary("Background Image")
_sql_constraints = [
@@ -86,6 +85,16 @@ class Company(models.Model):
if hasattr(sup, 'init'):
sup.init()
def _get_company_root_delegated_field_names(self):
"""Get the set of fields delegated to the root company.
Some fields need to be identical on all branches of the company. All
fields listed by this function will be copied from the root company and
appear as readonly in the form view.
:rtype: set
"""
return ['currency_id']
def _get_company_address_field_names(self):
""" Return a list of fields coming from the address partner to match
on company address fields. Fields are labeled same on both models. """
@@ -94,7 +103,13 @@ class Company(models.Model):
def _get_company_address_update(self, partner):
return dict((fname, partner[fname])
for fname in self._get_company_address_field_names())
@api.depends('parent_path')
def _compute_parent_ids(self):
for company in self:
company.parent_ids = self.browse(int(id) for id in company.parent_path.split('/') if id) if company.parent_path else company
company.root_id = company.parent_ids[0]
# TODO @api.depends(): currently now way to formulate the dependency on the
# partner's contact address
def _compute_address(self):
@@ -140,6 +155,15 @@ class Company(models.Model):
for company in self:
company.uses_default_logo = not company.logo or company.logo == default_logo
@api.depends('root_id')
def _compute_color(self):
for company in self:
company.color = company.root_id.partner_id.color or (company.root_id.id % 12)
def _inverse_color(self):
for company in self:
company.root_id.partner_id.color = company.color
@api.onchange('state_id')
def _onchange_state(self):
if self.state_id.country_id:
@@ -150,6 +174,28 @@ class Company(models.Model):
if self.country_id:
self.currency_id = self.country_id.currency_id
@api.onchange('parent_id')
def _onchange_parent_id(self):
if self.parent_id:
for fname in self._get_company_root_delegated_field_names():
if self[fname] != self.parent_id[fname]:
self[fname] = self.parent_id[fname]
@api.model
def _get_view(self, view_id=None, view_type='form', **options):
def make_delegated_fields_readonly(node):
for child in node.iterchildren():
if child.tag == 'field' and child.get('name') in delegated_fnames:
child.set('attrs', "{'readonly': [('parent_id', '!=', False)]}")
else:
make_delegated_fields_readonly(child)
return node
delegated_fnames = set(self._get_company_root_delegated_field_names())
arch, view = super()._get_view(view_id, view_type, **options)
arch = make_delegated_fields_readonly(arch)
return arch, view
@api.model
def _name_search(self, name, domain=None, operator='ilike', limit=None, order=None):
context = dict(self.env.context)
@@ -209,6 +255,12 @@ class Company(models.Model):
for vals, partner in zip(no_partner_vals_list, partners):
vals['partner_id'] = partner.id
for vals in vals_list:
# Copy delegated fields from root to branches
if parent := self.browse(vals.get('parent_id')):
for fname in self._get_company_root_delegated_field_names():
vals.setdefault(fname, self._fields[fname].convert_to_write(parent[fname], parent))
self.env.registry.clear_cache()
companies = super().create(vals_list)
@@ -241,6 +293,9 @@ class Company(models.Model):
# and thus needs to invalidate the assets cache when this is changed
self.env.registry.clear_cache('assets') # not 100% it is useful a test is missing if it is the case
if 'parent_id' in values:
raise UserError(_("The company hierarchy cannot be changed."))
if values.get('currency_id'):
currency = self.env['res.currency'].browse(values['currency_id'])
if not currency.active:
@@ -248,6 +303,16 @@ class Company(models.Model):
res = super(Company, self).write(values)
for company in self:
# Copy modified delegated fields from root to branches
if (changed := set(values) & set(self._get_company_root_delegated_field_names())) and not company.parent_id:
branches = self.sudo().search([
('id', 'child_of', company.id),
('id', '!=', company.id),
])
for fname in sorted(changed):
branches[fname] = company[fname]
# invalidate company cache to recompute address based on updated partner
company_address_fields = self._get_company_address_field_names()
company_address_fields_upd = set(company_address_fields) & set(values.keys())
@@ -272,10 +337,14 @@ class Company(models.Model):
active_users=company_active_users,
))
@api.constrains('parent_id')
def _check_parent_id(self):
if not self._check_recursion():
raise ValidationError(_('You cannot create recursive companies.'))
@api.constrains(lambda self: self._get_company_root_delegated_field_names() +['parent_id'])
def _check_root_delegated_fields(self):
for company in self:
if company.parent_id:
for fname in company._get_company_root_delegated_field_names():
if company[fname] != company.parent_id[fname]:
description = self.env['ir.model.fields']._get("res.company", fname).field_description
raise ValidationError(_("The %s of a subsidiary must be the same as it's root company.", description))
def open_company_edit_report(self):
warnings.warn("Since 17.0.", DeprecationWarning, 2)
@@ -301,3 +370,26 @@ class Company(models.Model):
main_company = self.env['res.company'].sudo().search([], limit=1, order="id")
return main_company
@ormcache('tuple(self.env.companies.ids)', 'self.id')
def __accessible_branches(self):
# Get branches of this company that the current user can use
self.ensure_one()
children = self.search([('id', 'child_of', self.id)])
accessible = self.env.companies
return (children & accessible).ids
def _accessible_branches(self):
return self.browse(self.__accessible_branches())
def _all_branches_selected(self):
"""Return whether or all the branches of the main company are selected.
Is ``True`` if all the branches, and only those, are selected.
Can be used when some actions only make sense for a whole company regardless of the
branches.
"""
return (
len(self.env.companies.root_id) == 1
and self.env.companies == self.env['res.company'].sudo().search([('id', 'child_of', self.env.company.root_id.ids)])
)
+8 -1
View File
@@ -37,12 +37,17 @@
<field name="mobile" class="o_force_ltr"/>
<field name="email"/>
<field name="website" string="Website" widget="url" placeholder="e.g. https://www.odoo.com"/>
<field name="parent_id" groups="base.group_multi_company"/>
<field name="parent_id" groups="base.group_no_one"/>
<field name="parent_id" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="color" widget="color_picker"/>
</group>
<group name="social_media"/>
</group>
</page>
<page string="Branches" name="branches">
<field name="child_ids" nolabel="1" context="{'default_parent_id': active_id}"/>
</page>
</notebook>
</sheet>
</form>
@@ -56,6 +61,7 @@
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="partner_id"/>
<field name="child_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</tree>
</field>
</record>
@@ -108,6 +114,7 @@
<field name="name">Companies</field>
<field name="res_model">res.company</field>
<field name="view_mode">tree,kanban,form</field>
<field name="domain">[('parent_id', '=', False)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new company
+1 -1
View File
@@ -223,7 +223,7 @@
<notebook colspan="4">
<page name="access_rights" string="Access Rights">
<group string="Multi Companies" attrs="{'invisible': [('companies_count', '&lt;=', 1)]}">
<field string="Allowed Companies" name="company_ids" widget="many2many_tags" options="{'no_create': True}"/>
<field string="Allowed Companies" name="company_ids" widget="many2many_tags" options="{'no_create': True, 'color_field': 'color'}"/>
<field string="Default Company" name="company_id" context="{'user_preference': 0}"/>
<field string="Companies count" name="companies_count" invisible="1"/>
</group>