ingadhoc/miscellaneous#379
Created by Juan José Scarafía
- label
- adhoc-dev:19.0-t-62336-jjs
- head
- 24c62863283e59897e01f7959bf3223fdc5f33e1
| ingadhoc/multi-company | ingadhoc/miscellaneous | |
|---|---|---|
| 19.0 | #264 | #379 |
[ADD] base_company_dependant: widget UX multicompañía para campos com…
…pany_dependent
En Odoo 18/19 los campos company_dependent dejaron de usar ir.property y ahora se almacenan directamente en la tabla del modelo como una columna JSONB (ej. {"1": 45, "2": false}). El ORM resuelve el valor para la compañía activa antes de enviarlo al cliente, pero esto produce tres pain points:
- El usuario no sabe si el valor que ve es "específico" (clave explícita en el JSON) o el "fallback global" (clave ausente, resuelve por ir.default).
- Modificar el valor puede afectar todas las compañías sin que el usuario lo sepa (estaba tocando el default).
- Para revisar el valor de cada compañía hay que hacer context-switching en el menú superior.
Nuevo módulo base_company_dependant que replica el paradigma del "Asistente de Traducciones" (icono fa-globe) pero para valores multicompañía, usando un icono fa-building-o.
models/ir_ui_view.py — Base(_inherit="base")
- Sobrescribe _get_view_field_attributes() para añadir "company_dependent" a la lista de atributos que el ORM serializa al cliente al cargar una vista.
- Sin este fix el frontend nunca recibe el metadato y el widget no se activa.
- Sigue el mismo patrón que html_editor usa para exponer sanitize.
models/base_company_dependant.py — models.AbstractModel base.company.dependant
- get_company_dependent_meta(res_model, res_id): · Una sola query SQL para TODOS los campos company_dependent del modelo (evita N+1 al cargar el formulario con múltiples campos). · Devuelve {field_name: is_specific} donde is_specific=True significa que el company_id activo tiene clave explícita en el JSON.
- get_company_dependent_values(res_model, res_id, field_name): · Lee la columna JSONB cruda via raw SQL (psycopg2.sql.Identifier). · Resuelve el fallback consultando ir.default. · Devuelve por cada compañía accesible (env.companies): {company_id, company_name, is_specific, value_id, display_value}. · Soporta tipos many2one, float e integer.
- set_company_dependent_values(res_model, res_id, field_name, values_dict): · Recibe {str(company_id): value | false | "RESET"}. · false → guarda la clave con valor false (vacío explícito, is_specific=True). · "RESET" → hace pop() de la clave (restaura al fallback global). · Escribe el JSON crudo y llama a invalidate_recordset para limpiar caché ORM. · Todos los identificadores SQL van por psycopg2.sql.Identifier (no interpolación).
static/src/company_dependent_service.js — Servicio company_dependent
- Caché por resModel:resId: la primera llamada dispara la RPC, las siguientes comparten la misma Promise en vuelo (batching automático).
- Una vez resuelta, cachea el objeto plano para lecturas síncronas (getMetaSync).
- invalidate(resModel, resId) para forzar re-fetch tras guardar en el diálogo.
static/src/many2one_patch.js — Patch de Many2OneField
- No modifica el registro en el registry; usa patch() sobre el prototipo.
- isCompanyDependent getter: props.record.fields[name].company_dependent === true.
- En setup(): llama a useService("company_dependent") y useState siempre (sin condicional, respetando el orden de hooks de OWL). Solo registra onWillStart si isCompanyDependent es verdadero.
- _loadCDMeta(): no invalida la caché (para aprovechar el batching cuando hay múltiples campos company_dependent en el mismo formulario). La invalidación la hace el diálogo antes de llamar al callback onSaved.
- Reemplaza Many2OneField.template por base_company_dependant.Many2OneField e inyecta CompanyDependentButton en Many2OneField.components.
static/src/company_dependent_button.js — CompanyDependentButton
- Renderiza el icono fa-building-o.
- Color dinámico: text-primary (específico), text-muted (fallback), text-secondary opacity-50 (cargando/null).
- Tooltip descriptivo según estado.
- Al hacer clic: guarda el registro (record.save()) antes de abrir el diálogo, igual que hace TranslationButton.
static/src/company_dependent_dialog.js — CompanyDependentDialog
- Carga datos via get_company_dependent_values en onWillStart.
- _getEffectiveRow(row): combina el estado original con los cambios pendientes (local state) para renderizado optimista sin necesidad de re-fetch.
- getAutoCompleteSources(row): genera sources para AutoComplete con onSelect dentro de cada opción (API correcta de AutoComplete; el callback NO va como prop del componente).
- onAutoCompleteChange(row, {inputValue, isOptionSelected}): detecta vaciado real (usuario borró texto y salió sin seleccionar) vs. selección normal.
- onResetRow(row): marca { is_reset: true } → el guardado enviará "RESET".
- onSave(): construye el dict para set_company_dependent_values, invalida la caché del servicio, y llama a onSaved (que recarga el record y re-fetchea el meta para actualizar el color del icono).
- Getters para todos los strings con caracteres especiales (_t()) para evitar errores del tokenizador OWL con acentos/eñes en expresiones de template.
static/src/templates.xml
- base_company_dependant.Many2OneField: wrapper flex con clase dinámica o_cd_fallback (activa CSS muted) + <CompanyDependentButton> condicional.
- base_company_dependant.CompanyDependentButton: botón con fa-building-o, color según isSpecific.
- base_company_dependant.CompanyDependentDialog: tabla responsive con columnas Compañía / Valor (AutoComplete) / Estado (badge) / Reset (botón).
- Todos los operadores lógicos en expresiones: && y || (JS), no and/or (Python). Strings en props de componentes con comillas simples internas. Arrow functions con bloques if extraídas a métodos del componente.
static/src/company_dependent.css
- .o_cd_fallback aplica color: var(--bs-secondary-color) + font-style: italic al input y a los links readonly del campo.
- .o_cd_btn alinea el icono inline sin romper el layout flex del formulario.
company_dependentno llegaba al cliente → fix en_get_view_field_attributes.- Tokenizer OWL: strings con acentos/espacios en props de componentes → getters JS.
- Operadores
and/oren templates OWL → reemplazados por&&/||. - Arrow function con bloque
ifen template → extraída a método del componente. class="o_input w-100"en prop de componente evaluado como JS →'o_input w-100'.- Prop
onSelecten<AutoComplete>no existe → movido aoption.onSelect(). - Batching roto al invalidar caché en
onWillStart→ invalidar solo desde diálogo.