[IMP] base_company_dependent: extend widget to settings, ORM-mode fields and dark mode#409
Open
JrAdhoc wants to merge 1 commit into
Open
[IMP] base_company_dependent: extend widget to settings, ORM-mode fields and dark mode#409JrAdhoc wants to merge 1 commit into
JrAdhoc wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR extends the company-dependent UX to support “ORM mode” for fields whose per-company behavior is not backed by a native JSONB company_dependent column (e.g., res.config.settings related-to-company fields and product.template.standard_price).
Changes:
- Added backend support for dual persistence strategies (
jsonvsorm) with newmodeRPC kwargs and ORM-mode getters/setters. - Patched OWL components/templates to pass a
modeinto the company-dependent dialog and to replace the Settings building icon with an interactive button. - Added a product view opt-in for
standard_priceand expanded tests/docs; updated styling for Bootstrap dark-mode variables.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| base_company_dependent/views/product_template_views.xml | Opts standard_price into ORM mode via field options. |
| base_company_dependent/tests/test_base_company_dependent.py | Adds ORM-mode backend tests around _resolve_orm_target / setters / “specific” heuristic. |
| base_company_dependent/static/src/templates.xml | Passes mode from patched fields into CompanyDependentButton. |
| base_company_dependent/static/src/settings_patch.xml | Replaces settings static icon with CompanyDependentButton in SearchableSetting. |
| base_company_dependent/static/src/settings_patch.js | Patches Setting to show the interactive button and discover field name when missing. |
| base_company_dependent/static/src/fields_patch.js | Adds explicit opt-in behavior and exposes cdMode to templates. |
| base_company_dependent/static/src/company_dependent_dialog.js | Sends mode to backend RPC calls (get/set values). |
| base_company_dependent/static/src/company_dependent_button.js | Accepts mode, warns when no fieldName, forwards mode to dialog. |
| base_company_dependent/static/src/company_dependent.css | Switches table/tint colors to Bootstrap dark-aware variables. |
| base_company_dependent/models/base_company_dependent.py | Implements strategy detection, ORM-mode value load/save, and meta updates. |
| base_company_dependent/manifest.py | Adds product dependency and installs the new view; bumps version. |
| base_company_dependent/README.rst | Documents ORM mode, settings integration, and expanded supported field types. |
|
|
||
| @api.model | ||
| def set_company_dependent_values(self, res_model, res_id, field_name, values_dict): | ||
| def set_company_dependent_values(self, res_model, res_id, field_name, values_dict, mode=None): |
Comment on lines
817
to
825
| if mode is None: | ||
| try: | ||
| mode = self._detect_field_strategy(res_model, field_name) | ||
| except ValueError: | ||
| mode = "json" | ||
|
|
||
| if mode == "orm": | ||
| return self._set_values_orm(res_model, res_id, field_name, values_dict) | ||
| self.env["ir.model.access"].check(res_model, "write") |
Comment on lines
+406
to
+408
| for company_id_str, value in values_dict.items(): | ||
| company_id = int(company_id_str) | ||
| company = self.env["res.company"].browse(company_id) |
Comment on lines
+200
to
+206
| # Related genérico: seguir la cadena | ||
| record = self.env[res_model].browse(res_id) | ||
| current = record | ||
| for part in related_parts[:-1]: | ||
| current = current[part] | ||
| if current: | ||
| return current._name, related_parts[-1], current.id |
Comment on lines
+208
to
+215
| # Computed/inverse: buscar si el campo existe como CD en un modelo | ||
| # relacionado. Caso emblemático: product.template → product.product. | ||
| if hasattr(field, "related_field") and field.related_field: | ||
| return self._resolve_orm_target( | ||
| field.related_field.model_name, | ||
| res_id, | ||
| field.related_field.name, | ||
| ) |
| rec = self.env["res.company"].browse(company_id) | ||
| else: | ||
| rec = record.with_company(company) | ||
| write_value = False if value == "RESET" else value |
| self.assertEqual(target_field, company_field) | ||
| self.assertEqual(target_id, self.env.company.id) | ||
|
|
||
| def test_set_values_orm_res_company_branch(self): |
| original_c1 = self.company_1[bool_field] | ||
| original_c2 = self.company_2[bool_field] | ||
|
|
||
| result = self.helper._set_values_orm( |
| * by `invisible=...` or by `groups=...` the user doesn't belong to). | ||
| */ | ||
| _discoverFieldFromDOM() { | ||
| const el = this.settingRef?.el || this.__owl__?.bdom?.el; |
| /* ── Diálogo general ─────────────────────────────────────────────────────── */ | ||
| .o_cd_dialog .table th { | ||
| background-color: var(--bs-light, #f8f9fa); | ||
| background-color: var(--bs-tertiary-bg); |
6f8e215 to
0acbce5
Compare
…lds and dark mode
Backend (models/base_company_dependent.py):
* Add ORM-mode strategy (companion to JSONB-native) for fields whose values do
not live in a JSONB column: computed fields with depends_context('company')
and related fields. Auto-detected by _detect_field_strategy; can be forced
from views via options="{'company_dependent_mode': 'orm'}".
* _resolve_orm_target maps res.config.settings related to company_id.* to
res.company directly, and product.template.standard_price to product.product
when there is a single variant. Generic related traversal restricted to
many2one (one2many/many2many would silently pick an arbitrary record);
related_field recursion guards on browse(...).exists() so res_id mismatches
across models do not silently resolve to the wrong target.
* set_company_dependent_values: write access check moved before both branches
(was missing on the ORM path); _set_values_orm additionally checks write on
the resolved target_model and skips company_ids outside env.companies, so
crafted RPC payloads cannot bypass record rules or cross-company guards.
* Verbose [CD ORM SET/GET] traces lowered from info to debug — they fire per
dialog open and per company, which spams logs on multi-company setups.
* Company hierarchy up to 3 levels (parent -> child -> grandchild) exposed to
the dialog for the copy-to-children action.
Frontend:
* fields_patch.js extends the building-icon button to CharField, IntegerField,
FloatField, MonetaryField, BooleanField, SelectionField and DateTimeField
(was Many2One only). templates.xml provides the extended OWL templates.
* settings_patch.js / settings_patch.xml patch Setting and SearchableSetting
so the static fa-building-o on <setting company_dependent="1"> becomes the
interactive CompanyDependentButton. When upstream's compiler does not pass
fieldName (settings whose first child is a wrapper <div>, e.g. default_taxes,
default_stock_valuation_accounts, main_currency), the patch discovers the
first visible .o_field_widget[name] from the rendered DOM via the public
settingRef and falls back to that field's string as label. Strict-equality
check on the companyDependent prop avoids running the lookup on every
setting (upstream passes the literal string "false" for non-CD settings,
which is truthy in JS).
CSS (company_dependent.css):
* Replace hardcoded light backgrounds (var(--bs-white), var(--bs-light),
var(--bs-gray-100/200), rgba(0, 0, 0, *)) with Bootstrap 5.3 dark-aware
variables so the modal hierarchy tinting and hover render correctly in
both light and dark themes. Header keeps a literal fallback in case the
theme does not define --bs-tertiary-bg.
Views: product_template_views.xml adds the orm-mode opt-in on
product.template.standard_price.
Tests: cover ORM-mode set/get on res.company, target resolution for
res.config.settings related fields, the non-CD is_specific fallback, and
the public RPC path enforcing write access + filtering inaccessible
companies (39 tests, all green).
Bump to 19.0.1.2.0 and sync README. Reset semantics on ORM-mode targets
documented: when the resolved target is a plain res.company column (no JSONB),
reset == write the type's falsy value because there is no per-company key
to remove.
Task-id: 62336
0acbce5 to
2ea10f7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Closes the multicompany-UX widget work for v19 (task #62336). On top of the Many2One-only MVP that landed earlier in 19.0:
Backend (
models/base_company_dependent.py)depends_context('company')and related fields. Auto-detected by_detect_field_strategy; can be forced from views viaoptions=\"{'company_dependent_mode': 'orm'}\"._resolve_orm_targetmapsres.config.settingsrelated tocompany_id.*tores.companydirectly, andproduct.template.standard_pricetoproduct.productwhen there is a single variant (fallback for multi-variant keeps the existing inverse-on-template behaviour).Frontend
fields_patch.jsextends the building-icon button toCharField,IntegerField,FloatField,MonetaryField,BooleanField,SelectionFieldandDateTimeField(was Many2One only).templates.xmlprovides the extended OWL templates with matching field shapes and theo_cd_fallbackmuted-display class.settings_patch.js/settings_patch.xmlpatchSetting/SearchableSettingso the staticfa-building-oon<setting company_dependent=\"1\">is replaced by the interactiveCompanyDependentButton. When upstream's compiler does not passfieldName(settings whose first child is a wrapper<div>, e.g.default_taxes,default_stock_valuation_accounts,main_currency), the patch discovers the first visible.o_field_widget[name]from the rendered DOM and falls back to that field'sstringas label. Strict-equality check on thecompanyDependentprop avoids running the lookup on every setting (upstream passes the literal string\"false\"for non-CD settings, which is truthy in JS).CSS (
company_dependent.css)var(--bs-white),var(--bs-light),var(--bs-gray-100/200),rgba(0, 0, 0, *)) with Bootstrap 5.3 dark-aware variables (var(--bs-tertiary-bg),rgba(var(--bs-emphasis-color-rgb), *)) so the modal hierarchy tinting and hover render correctly in dark mode too.Views
product_template_views.xmladds the orm-mode opt-in onproduct.template.standard_price.Bump to
19.0.1.2.0and syncREADME.rst.Test plan
odoo-bin -d <db> -u base_company_dependent --test-enable --stop-after-init— 37 tests green (new ones cover ORM set onres.company, target resolution forres.config.settingsrelated fields, and the non-CDis_specificfallback).<setting company_dependent=\"1\">whose first child is a<div>(e.g.default_taxes,main_currency,default_stock_valuation_accounts) — the modal opens for the first visible field and persists changes per company.product.template.standard_price(single-variant) — open the modal, edit per company, the value is delegated toproduct.productJSONB.