ingadhoc/miscellaneous#379

Created by Juan José Scarafía
Closed
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:

  1. 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).
  2. Modificar el valor puede afectar todas las compañías sin que el usuario lo sepa (estaba tocando el default).
  3. 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.pyBase(_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.pymodels.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.jsCompanyDependentButton
- 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.jsCompanyDependentDialog
- 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.


  1. company_dependent no llegaba al cliente → fix en _get_view_field_attributes.
  2. Tokenizer OWL: strings con acentos/espacios en props de componentes → getters JS.
  3. Operadores and/or en templates OWL → reemplazados por &&/||.
  4. Arrow function con bloque if en template → extraída a método del componente.
  5. class="o_input w-100" en prop de componente evaluado como JS → 'o_input w-100'.
  6. Prop onSelect en <AutoComplete> no existe → movido a option.onSelect().
  7. Batching roto al invalidar caché en onWillStart → invalidar solo desde diálogo.