Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions multi_company_field_visible/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

===========================
Multi Company Field Visible
===========================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:4ae26a43f6e5ebff4a96b7fd445ec024401e47a1db1cec64e299e118d1b69899
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmulti--company-lightgray.png?logo=github
:target: https://github.com/OCA/multi-company/tree/19.0/multi_company_field_visible
:alt: OCA/multi-company
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/multi-company-19-0/multi-company-19-0-multi_company_field_visible
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/multi-company&target_branch=19.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

The OCA multi-company stack (``base_multi_company`` and its satellites)
adds a ``Companies`` field that scopes a record to one or more
companies, but hides it behind the ``base.group_multi_company`` group.
Users without that group -- a single-company merchant, for example --
cannot see or set the company of their own records.

This module adds an ``Own Company`` proxy field on every model based on
``multi.company.abstract``. It lets those users see and manage **only
their own company**:

- The real ``company_ids`` value is never exposed, so a record shared
with (or global to) other companies never reveals them.
- Edits are merged: changing the own company never wipes the access
granted to companies the user cannot see.
- The field can never be left blank for these users; it falls back to
their current company.

This is the base module: it carries the logic but exposes nothing on its
own. Install one of the bridge modules
(``partner_multi_company_field_visible``,
``product_multi_company_field_visible``) to expose the field on a given
model.

**Table of contents**

.. contents::
:local:

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/multi-company/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/multi-company/issues/new?body=module:%20multi_company_field_visible%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Canarias Conectada

Contributors
------------

- Canarias Conectada

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/multi-company <https://github.com/OCA/multi-company/tree/19.0/multi_company_field_visible>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions multi_company_field_visible/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
14 changes: 14 additions & 0 deletions multi_company_field_visible/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2026 Canarias Conectada
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html

{
"name": "Multi Company Field Visible",
"summary": "Let non multi-company users manage their own company on records",
"version": "19.0.1.0.0",
"author": "Canarias Conectada, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/multi-company",
"category": "Tools",
"license": "AGPL-3",
"depends": ["base_multi_company"],
"installable": True,
}
46 changes: 46 additions & 0 deletions multi_company_field_visible/i18n/es_ES.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * multi_company_field_visible
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 19.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. module: multi_company_field_visible
#: model:ir.model.fields,field_description:multi_company_field_visible.field_multi_company_abstract__display_name
msgid "Display Name"
msgstr "Nombre mostrado"

#. module: multi_company_field_visible
#: model:ir.model.fields,field_description:multi_company_field_visible.field_multi_company_abstract__id
msgid "ID"
msgstr "ID"

#. module: multi_company_field_visible
#: model:ir.model,name:multi_company_field_visible.model_multi_company_abstract
msgid "Multi-Company Abstract"
msgstr "Abstracto multicompañía"

#. module: multi_company_field_visible
#: model:ir.model.fields,field_description:multi_company_field_visible.field_multi_company_abstract__own_company_ids
msgid "Own Company"
msgstr "Compañía propia"

#. module: multi_company_field_visible
#: model:ir.model.fields,field_description:multi_company_field_visible.field_multi_company_abstract__show_own_company_field
msgid "Show Own Company Field"
msgstr "Mostrar campo de compañía propia"

#. module: multi_company_field_visible
#. odoo-python
#: code:addons/multi_company_field_visible/models/multi_company_abstract.py:0
msgid "You must assign at least your own company to “%(name)s”."
msgstr "Debe asignar al menos su propia compañía a «%(name)s»."
45 changes: 45 additions & 0 deletions multi_company_field_visible/i18n/multi_company_field_visible.pot
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * multi_company_field_visible
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 19.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: multi_company_field_visible
#: model:ir.model.fields,field_description:multi_company_field_visible.field_multi_company_abstract__display_name
msgid "Display Name"
msgstr ""

#. module: multi_company_field_visible
#: model:ir.model.fields,field_description:multi_company_field_visible.field_multi_company_abstract__id
msgid "ID"
msgstr ""

#. module: multi_company_field_visible
#: model:ir.model,name:multi_company_field_visible.model_multi_company_abstract
msgid "Multi-Company Abstract"
msgstr ""

#. module: multi_company_field_visible
#: model:ir.model.fields,field_description:multi_company_field_visible.field_multi_company_abstract__own_company_ids
msgid "Own Company"
msgstr ""

#. module: multi_company_field_visible
#: model:ir.model.fields,field_description:multi_company_field_visible.field_multi_company_abstract__show_own_company_field
msgid "Show Own Company Field"
msgstr ""

#. module: multi_company_field_visible
#. odoo-python
#: code:addons/multi_company_field_visible/models/multi_company_abstract.py:0
msgid "You must assign at least your own company to “%(name)s”."
msgstr ""
1 change: 1 addition & 0 deletions multi_company_field_visible/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import multi_company_abstract
170 changes: 170 additions & 0 deletions multi_company_field_visible/models/multi_company_abstract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Copyright 2026 Canarias Conectada
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html

from odoo import Command, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import config


class MultiCompanyAbstract(models.AbstractModel):
_inherit = "multi.company.abstract"

# Display proxy over ``company_ids`` meant for a *non* multi-company user (a
# merchant). It only ever exposes the companies that user owns, so the real
# ``company_ids`` -- which may also point to other companies (shared or
# global records) -- never reaches them. Editable, with a safe merge.
own_company_ids = fields.Many2many(
comodel_name="res.company",
# Unique technical label to avoid clashing with ``company_id``; the
# user-facing label ("Company") is set on the field in the views.
string="Own Company",
compute="_compute_own_company_ids",
inverse="_inverse_own_company_ids",
search="_search_own_company_ids",
# The value depends on *who* reads it (each user owns a different set of
# companies), so it must not be shared across users in the cache.
depends_context=("uid",),
)
# Drives the view visibility (``invisible="not show_own_company_field"``):
# only ON for a non multi-company user when the model is enabled in settings.
show_own_company_field = fields.Boolean(
compute="_compute_show_own_company_field",
# Depends on *who* reads it (their group and the per-model setting), so
# the result must not be shared across users in the cache.
compute_sudo=False,
depends_context=("uid",),
)

@api.depends("company_ids")
def _compute_own_company_ids(self):
for record in self:
if not record._show_own_company_field_enabled():
# Nobody reads this proxy when the real field is shown
# instead (multi-company users) or the model's toggle is
# off, so skip it entirely. Setting a Many2many field --
# even to fill a compute's cache -- goes through the same
# write path as a real edit, which checks 'read' access on
# any newly-referenced res.company; since this field's
# value is scoped to `self.env.user.company_ids` (every
# company the user belongs to) rather than
# `self.env.companies` (only the ones active in the
# company switcher right now), a multi-company user who
# narrowed their active selection would otherwise trip
# that check on a company they own but didn't select.
record.own_company_ids = False
continue
# ``sudo`` reads the raw m2m without tripping over res.company rules;
# the result is already narrowed to the user's own companies, so
# nothing foreign is ever revealed.
own = self.env.user.company_ids
record.own_company_ids = record.sudo().company_ids & own

def _inverse_own_company_ids(self):
own = self.env.user.company_ids
for record in self:
stored = record.sudo().company_ids
# Companies the user cannot see stay untouched (merge, no leak/wipe).
hidden = stored - own
# Only accept companies the user actually owns; a merchant may pick
# one, several or all of them, but never a foreign one.
chosen = record.own_company_ids & own
# Never blank: fall back to the user's current company.
if not chosen:
chosen = self.env.company
record.sudo().company_ids = [Command.set((hidden | chosen).ids)]

def _search_own_company_ids(self, operator, value):
# Mirror a search on the proxy onto the real field, scoped to the
# user's own companies so it can never surface foreign records.
own = self.env.user.company_ids
return [
"&",
("company_ids", operator, value),
("company_ids", "in", own.ids),
]

def _compute_show_own_company_field(self):
show = self._show_own_company_field_enabled()
for record in self:
record.show_own_company_field = show

@api.model
def _show_own_company_field_enabled(self):
# Multi-company users already have the full ``company_ids`` field gated
# by ``base.group_multi_company``; the proxy is only for the others.
if self.env.user.has_group("base.group_multi_company"):
return False
return self._own_company_field_param_enabled()

@api.model
def _own_company_field_param_enabled(self):
"""Whether the per-model settings toggle is on. Enabled by default."""
key = self._own_company_field_param_key()
if not key:
# The abstract model has no concrete key; each bridge overrides
# ``_own_company_field_param_key`` for its model.
return False
param = self.env["ir.config_parameter"].sudo().get_param(key, "True")
return param not in ("False", "false", "0", "")

@api.model
def _own_company_field_param_key(self):
"""``ir.config_parameter`` key gating this model. Bridges override it."""
return None

def _check_own_company_not_blank(self):
# Safety net behind the inverse: a non multi-company user must not end
# up with a blank (global) company set on an exposed model.
#
# This is deliberately NOT an ``@api.constrains``: while ``write()``
# holds ``company_ids`` protected (it feeds the computed
# ``company_id``), ``record.sudo().company_ids`` still reads the
# *pre-write* value, so a constrain would silently pass on the very
# write that empties it. Called instead from ``create``/``write``
# below, once the ORM call has fully returned and the field is
# readable again.
if config["test_enable"] and not self.env.context.get(
"test_multi_company_field_visible"
):
return
if self.env.context.get("default_parent_id") is False:
# Core's own ``res.company.create()`` creates a brand new
# company's own contact through exactly this context marker
# (``odoo/addons/base/models/res_company.py``, the "create
# missing partners" block). At that point in the ORM's create
# flow, ``base_multi_company``'s ``_inverse_company_id`` fires
# as a side effect and briefly clears ``company_ids`` (its
# source, ``company_id``, has nothing to compute from yet:
# the company doesn't have ``partner_id`` set back to this
# very partner until a moment later). This transient, internal
# empty state is not a real user-facing edit, so it must not
# be rejected here; whatever consuming module scopes a
# company's own contact to itself (e.g.
# ``partner_multi_company_restrict``) fixes it up right after.
return
if self.env.user.has_group("base.group_multi_company"):
# Admins may leave it empty on purpose ("All companies" / global).
return
for record in self:
if not record._own_company_field_param_enabled():
continue
record.invalidate_recordset(["company_ids"])
if not record.sudo().company_ids:
raise ValidationError(
self.env._(
"You must assign at least your own company to “%(name)s”.",
name=record.display_name,
)
)

def write(self, vals):
result = super().write(vals)
if "company_ids" in vals:
self._check_own_company_not_blank()
return result

@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
records._check_own_company_not_blank()
return records
3 changes: 3 additions & 0 deletions multi_company_field_visible/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
1 change: 1 addition & 0 deletions multi_company_field_visible/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Canarias Conectada
Loading
Loading