From dc4dcead6ed5e16eebdf7a9f79afea31b24ec33f Mon Sep 17 00:00:00 2001 From: william-andre Date: Fri, 23 Jun 2023 09:52:33 +0000 Subject: [PATCH] [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 --- addons/snailmail/models/res_company.py | 2 +- odoo/addons/base/models/res_company.py | 118 +++++++++++++++++-- odoo/addons/base/views/res_company_views.xml | 9 +- odoo/addons/base/views/res_users_views.xml | 2 +- 4 files changed, 115 insertions(+), 16 deletions(-) diff --git a/addons/snailmail/models/res_company.py b/addons/snailmail/models/res_company.py index 2aa46f3ca43..fad22acbcd3 100644 --- a/addons/snailmail/models/res_company.py +++ b/addons/snailmail/models/res_company.py @@ -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) diff --git a/odoo/addons/base/models/res_company.py b/odoo/addons/base/models/res_company.py index a6ad022089c..b94f5959c41 100644 --- a/odoo/addons/base/models/res_company.py +++ b/odoo/addons/base/models/res_company.py @@ -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)]) + ) diff --git a/odoo/addons/base/views/res_company_views.xml b/odoo/addons/base/views/res_company_views.xml index d075ae85700..4e365816f53 100644 --- a/odoo/addons/base/views/res_company_views.xml +++ b/odoo/addons/base/views/res_company_views.xml @@ -37,12 +37,17 @@ - + + + + + + @@ -56,6 +61,7 @@ + @@ -108,6 +114,7 @@ Companies res.company tree,kanban,form + [('parent_id', '=', False)]

Create a new company diff --git a/odoo/addons/base/views/res_users_views.xml b/odoo/addons/base/views/res_users_views.xml index 6dc7c0d9a78..746bc305119 100644 --- a/odoo/addons/base/views/res_users_views.xml +++ b/odoo/addons/base/views/res_users_views.xml @@ -223,7 +223,7 @@ - +