diff --git a/.copier-answers.yml b/.copier-answers.yml index 4c666a1bb..74b700afe 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: d46567f +_commit: a740779 _src_path: https://github.com/ingadhoc/addons-repo-template.git description: '' is_private: false diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..a30ad3cff --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,325 @@ +# Instrucciones para Copilot – Revisión de código Odoo (v18.0) + +## Contexto + +* El repositorio contiene **módulos Odoo preparados para Odoo 18** (rama `18.0`). +* El objetivo es **revisar cambios de código** y **sugerir mejoras seguras y relevantes**, sin caer en micro-comentarios. + +--- + +## Reglas generales (aplican a todo el código) + +1. **Responder siempre en español.** +2. Detectar y corregir **errores de tipeo u ortografía evidentes** en nombres de variables, métodos o comentarios (cuando sean claros). +3. No sugerir traducciones de docstrings o comentarios entre idiomas (no proponer pasar del inglés al español o viceversa). +4. No proponer agregar docstrings si el método no tiene uno. + + * Si ya existe un docstring, puede sugerirse un estilo básico acorde a PEP8, pero **no será un error** si faltan `return`, tipos o parámetros documentados. +5. No proponer cambios puramente estéticos (espacios, comillas simples vs dobles, orden de imports, etc.). +6. Mantener el feedback **muy conciso** en los PRs: priorizar pocos puntos claros, evitar párrafos largos y no repetir el contexto que ya está explicado en la descripción del PR. +7. Sobre traducciones: usar `_()` o `self.env._()` es indistinto; solo marcar si hay mensajes de error o textos no traducidos que deban serlo. + +--- + +## Revisión de modelos (`models/*.py`) – cuestiones generales + +* Verificar que: + + * Los campos (`fields.*`) tengan nombres claros, consistentes y no entren en conflicto con otros módulos. + * Las relaciones (`Many2one`, `One2many`, `Many2many`) estén bien definidas y referencien modelos válidos, con `ondelete` apropiado. + * Las constraints declaradas con `_sql_constraints` o `@api.constrains` mantengan la integridad esperada y mensajes claros. +* Sugerir uso de `@api.depends` si un campo compute carece de dependencias explícitas. +* Si se redefine un método de Odoo, asegurar que se llama correctamente `super()`, manteniendo el contrato original. +* Si hay lógica nueva, evitar loops costosos con búsquedas dentro de iteraciones; sugerir `mapped`, `filtered`, dominios vectorizados u otras formas más eficientes. + +--- + +## 🧾 Revisión del manifest (`__manifest__.py`) – reglas generales + +* Confirmar que todos los archivos usados (vistas, seguridad, datos, reportes, wizards) estén referenciados en el manifest. +* Verificar dependencias declaradas: que no falten módulos requeridos ni se declaren innecesarios. +* **Regla de versión (obligatoria):** + Solo sugerir bump de versión si el `__manifest__.py` no incrementa `version` y se modificó la estructura de un modelo, una vista, o algún record .xml (ej. cambios en definición de campos, vistas XML, datos XML, seguridad). +* Solo hacerlo una vez por revisión, aunque haya múltiples archivos afectados. + +--- + +## Revisión de vistas XML (`views/*.xml`) – reglas generales + +* Confirmar que se usen herencias (`inherit_id`, `xpath`) en lugar de redefinir vistas completas sin necesidad. +* Validar que los campos referenciados existan en los modelos correspondientes. +* Evitar duplicar gran parte del `arch`; prioriza `xpath` específicos y claros. + +### Notas específicas Odoo 18 (vistas / UI) + +* Las vistas de lista usan el nuevo elemento `` en lugar de ``; si se ve código nuevo en 18 que sigue usando `` para listas estándar, sugiere adaptarlo cuando sea coherente con el resto del módulo. +* Muchas condiciones en vistas pueden escribirse con atributos declarativos (`invisible`, `readonly`, `required`) más simples que combinaciones complejas de `attrs`; sugiere simplificar cuando el diff haga la vista más compleja sin necesidad. + +--- + +## Seguridad y acceso – reglas generales + +* Verificar los archivos `ir.model.access.csv` para nuevos modelos: deben tener permisos mínimos necesarios. +* No proponer abrir acceso global sin justificación. +* Si se agregan nuevos modelos o campos de control de acceso, **recordar el bump de versión** (ver sección de manifest). +* Si se cambian `record rules`, revisar especialmente combinaciones multi-compañía y multi-website. + +### Seguridad y rendimiento del ORM + +* Reforzar las advertencias sobre **SQL crudo**: si el diff muestra `self.env.cr.execute("...%s..." % var)` u otras interpolaciones inseguras, recomendar reemplazarlo por dominios ORM (`search`, `browse`) o, si es inevitable, parametrizar la query para heredar sanitización y reglas de acceso. + * Ejemplo inseguro que debe marcarse: `self.env.cr.execute("SELECT * FROM res_partner WHERE email = '%s'" % email)`. + * Variante segura aceptable: `self.env.cr.execute("SELECT * FROM res_partner WHERE email = %s", (email,))` o, mejor aún, `self.env['res.partner'].search([('email', '=', email)])`. +* Señalar cualquier uso de `eval` o construcción manual de domains a partir de input de usuario (`eval(domain_string)`), proponiendo dominios expresados como listas de tuplas o mediante objetos `Domain`. + * Ejemplo inseguro: `records = self.env['res.partner'].search(eval("[('name','ilike','%s')]" % user_input))`. + * Forma segura: `records = self.env['res.partner'].search([('name', 'ilike', user_input)])`. +* Vigilar patrones ineficientes comunes: bucles que ejecutan `search`/`write` por registro, filtrados manuales tras `search([])` o cómputos que podrían resolverse con `search_count`, `mapped`, `filtered` o `browse` masivo. + * Ejemplo a señalar: `for partner_id in partner_ids: partner = self.env['res.partner'].search([('id', '=', partner_id)])`. + * Proponer `partners = self.env['res.partner'].browse(partner_ids)` y operar sobre el recordset completo. +* Para lecturas planas o exportaciones, preferir `search_fetch(fields=...)` para limitar columnas y reducir memoria. + * Caso ilustrativo: reemplazar listas armadas a mano con `result = self.env['res.partner'].search_fetch(domain=[('is_company', '=', True)], fields=['name', 'email', 'vat'])`. +* Recordar que los writes vectorizados (`recordset.write`) y las operaciones en lotes evitan locks prolongados y mejoran la trazabilidad de auditoría del ORM. + * Ejemplo recomendado: `partners.write({'comment': 'Actualizado masivamente'})` en lugar de iterar y escribir registro por registro. +* Tener en cuenta la **navegación de campos relacionales** en Odoo: acceder a campos encadenados como `m.fiscal_position_id.l10n_ar_tax_ids` es seguro incluso cuando `fiscal_position_id` está vacío (devuelve un recordset vacío). Por eso, expresiones como `not m.fiscal_position_id.l10n_ar_tax_ids` ya cubren el caso en que no haya posición fiscal y **no hace falta** añadir un chequeo previo separado sobre `fiscal_position_id`. +* Revisar accesos directos por índice en listas o recordsets, por ejemplo `lines[0].id`: si el conjunto está vacío puede lanzar `IndexError`. Copilot debe sugerir patrones más seguros (por ejemplo `if lines: first = lines[0]`) o, cuando sea posible, reescribir la lógica para trabajar sobre el recordset completo en lugar de un único elemento. + +--- + +## Cambios estructurales y scripts de migración – **cuestiones generales** + +Cuando el diff sugiera **cambios de estructura de datos**, **siempre evaluar** si corresponde proponer un **script de migración** en `migrations/` (pre/post/end) **y recordar el bump de versión**. + +### Reglas generales de estructura de `migrations/` + +* La carpeta dentro de `migrations/` debe corresponder con la versión declarada en el manifest (p. ej. `migrations/18.0.4.0/`). +* Los scripts deben ser idempotentes, trabajar en lotes y registrar logs claros. + +### Ejemplos de cambios estructurales (actualizado con tus criterios) + +En estos casos **normalmente corresponde** proponer migración (salvo notas en contra): + +1. **Renombrar campos o modelos** + + * **Campos:** proponer migración **solo si el campo es almacenado** en base de datos: + * campos normales (`Char`, `Many2one`, `Boolean`, etc.), + * campos `compute` con `store=True`. + * Campos `compute` **sin** `store=True` no requieren script por el renombre en sí (son virtuales). + * **Modelos:** renombrar modelos **siempre** implica revisar migración (`ir.model`, `ir.model.data`, tablas relacionales, vistas, acciones…). + +2. **Cambiar tipos de campo** + + * Se considera cambio estructural cuando **cambia la representación en la base de datos** (p.ej. `Char → Many2one`, `Selection → Many2one`, `Integer → Monetary`, `Many2one → Many2many`, etc.). + * Cambios “compatibles” a nivel de PostgreSQL **no suelen requerir script**, por ejemplo: + * `Char → Text` o ajustes de tamaño de `Char`; + * cambios de precisión en `Float` sin cambio de semántica. + * Aun así, si el cambio implica lógica nueva (p.ej. pasar de `Boolean` a `Selection` con múltiples estados) puede requerir mapeo de datos. + +3. **Quitar campos para reestructurar información** + + * Por ejemplo, dividir un campo en varios (split) o fusionar varios en uno (merge). + * Siempre revisar si hay datos que deban preservarse antes de eliminar el campo original. + +4. **Agregar campos `compute` almacenados (`store=True`) con backfill** + + * Si el campo nuevo es `compute` y `store=True`, y se espera que tenga valor para **registros históricos**, conviene: + * Proponer **script `post`** que haga el backfill **en lotes**. + * Añadir una **advertencia explícita** cuando el modelo tiene muchos registros (p.ej. millones) para que el cálculo no se haga en una sola transacción que bloquee la tabla. + +5. **Cambiar dominios o valores de campos `selection`** + + * **Añadir nuevos valores de `selection`**: + En general **no requiere migración** si solo se agregan opciones nuevas y no se tocan las existentes. + * **Eliminar o renombrar keys existentes de `selection`**: + * Puede dejar valores históricos huérfanos o inválidos → proponer script que mapee `old_value → new_value` o que normalice registros antiguos. + * Mencionar que hay que tener en cuenta el comportamiento de campos relacionados (p.ej. un `Many2one` con `ondelete` específico) si el `selection` influye en lógica que crea o elimina registros. + * **Cambios de dominio** en campos relacionales (`Many2one`, `Many2many`): + * Si el nuevo dominio excluye valores usados históricamente, puede ser necesario limpiar o remapear datos para que no queden registros en estados imposibles. + * Recordar que el `ondelete` del campo define qué ocurre al eliminar registros apuntados; hay que respetarlo al limpiar datos. + +6. **Cambiar o añadir `_sql_constraints` (unique / index)** + + * Cambios en constraints `UNIQUE` o adición de nuevas constraints/índices pueden **fallar con datos existentes** (duplicados, valores nulos, etc.). + * Al menos, Copilot debe: + * emitir una **advertencia** sobre el riesgo de fallo en el upgrade, + * sugerir revisar datos previos (y, cuando se vea necesario, un **pre-script** que limpie duplicados o normalice datos antes de aplicar la constraint). + +7. **Cambios en `ir.model.data` / XML IDs** + + * Renombres de XML IDs (`module.name → module2.name2`) o cambios en `module` / `name` suelen requerir: + * script para actualizar referencias dependientes (acciones, vistas, menús, records en otros módulos), + * o uso de utilidades de upgrade. + * Caso especial: registros con `no_update="1"`: + * Si cambia solo texto/etiquetas menores, puede no hacer falta migración. + * **Si cambia el contenido lógico** (ej. campo `domain`, configuración, secuencias) y el registro tiene `no_update="1"`, debes **sugerir forzar el cambio**: + * vía script que actualice explícitamente los registros por su `xml_id`, + * o mediante un proceso de “force update” apropiado. + +8. **Cambios de reglas de acceso / propiedad** + + * Cambios profundos en `record rules` o en campos que determinan propiedad (company, website, owner…) pueden necesitar scripts para: + * recomputar propiedad, + * asignar company/website por defecto, + * o migrar datos entre reglas. + +> **Nota:** No se incluye en esta lista el caso “Añadir `required=True` a campos existentes sin default” como condición automática de migración; Copilot no debe sugerir script de migración **solo** por ese motivo, salvo que en el diff se vea claro que hay datos históricos incompatibles. + +--- + +## Scripts de migración en `migrations/`: pre / post / end (reglas generales) + +> **Objetivo:** preservar datos y mantener instalabilidad/actualizabilidad segura. + +- **pre**: Se ejecutan antes de actualizar el módulo. Útiles para preparar datos o estructuras que eviten fallos durante el upgrade. +- **post**: Se ejecutan justo después de actualizar el módulo. Ideales para recalcular datos, limpiar residuos o ajustar referencias tras el cambio. +- **end**: Se ejecutan al final de la actualización de todos los módulos. Indicados para tareas globales que dependen de múltiples módulos o para ajustes finales. + +### Mapeo de cambio → acción recomendada (actualizado) + +* **Rename de campo almacenado (mismo modelo)** + + * **Pre-script**: crear columna/alias temporal o copiar datos del campo viejo al nuevo antes de que Odoo toque el esquema, si el cambio puede romper constraints. + * **Post-script**: limpieza de residuos, recomputes de campos derivados si aplica. + +* **Renombrar modelo** + + * **Pre-script**: preparar mapeos en `ir.model` y `ir.model.data`, y ajustar referencias técnicas si es necesario. + * **Post-script**: re-enlazar vistas, acciones, menús, reglas y volver a chequear accesos. + +* **Eliminar campo y mover datos a otros campos (split/merge)** + + * **Pre-script**: copiar datos a los nuevos campos (cuando sea posible) antes de que el schema elimine la columna original. + * **Post-script**: normalizar referencias, recalcular computes, limpiar helpers. + +* **Agregar campo `compute` con `store=True`** + + * **Pre-script (opcional y solo en modelos muy grandes)**: crear columna en DB o preparar estructura para evitar locks largos. + * **Post-script (recomendado)**: backfill **en lotes** para poblar el valor almacenado; es importante para modelos con muchos registros. + +* **Cambiar tipo de campo con cambio real de representación** + + * **Pre-script**: crear columna temporal con el nuevo tipo y migrar datos (con conversión). + * **Post-script**: intercambiar/renombrar columnas, borrar la vieja, disparar recomputes si hace falta. + +* **Cambios en `selection` (eliminar/renombrar keys existentes)** + + * **Pre-script**: mapear valores antiguos → nuevos (tabla de mapeo) usando helpers como `change_field_selection_values()` cuando aplique. + * **Post-script**: validar que no quedan valores huérfanos y que las reglas de negocio siguen cumpliéndose. + * **Añadir keys nuevas**: **no proponer script** salvo que el diff muestre una migración masiva explícita de valores. + +* **Nuevas constraints `_sql_constraints` (unique) / índices** + + * **Pre-script (recomendado cuando haya riesgo)**: detectar y resolver duplicados o datos inconsistentes antes de crear la constraint. + * **Post-script**: crear el índice/constraint y, si procede, validar que no haya fallos. + +* **Cambios en registros XML con `no_update="1"`** + + * **Post-script**: actualizar esos registros por API (respetando `xml_id`) cuando el contenido lógico haya cambiado y no vaya a ser reaplicado por el upgrade normal. + +* **Cambios de reglas de acceso / multi-company / multi-website** + + * **Pre- o post-script** según el caso, para rellenar campos obligatorios (company, website, owner) y evitar que registros queden inaccesibles. + +> **Regla general:** si el cambio puede **romper durante el upgrade**, usa **pre-script**; si requiere **recalcular o reaplicar** después del código nuevo, usa **post-script**. Si se necesita una acción global al final, usa **end-script**. + +--- + +## Cobertura de tests automatizados – reglas generales + +* Cuando el diff introduzca **funcionalidad nueva no trivial** (nuevos métodos con lógica compleja, nuevos flujos de negocio, refactors grandes, nuevas APIs, etc.), revisar si existe cobertura de tests razonable para esos cambios. +* Si no se ve una cobertura clara, sugerir de forma **concreta y breve** qué tipo de test añadir (unitarios de modelo, tests de wizards, tours, pruebas sobre reportes, etc.), sin exigir una suite completa para cada cambio. +* Para cambios pequeños o puramente cosméticos (ajustes en textos, vistas simples, pequeñas correcciones) **no hace falta** proponer la creación de tests nuevos. + +--- + +## Convenciones de scripts en `migrations/` (generales) + +* Ubicación: `migrations//`. +* Nombres sugeridos: + + * `pre_.py` + * `post_.py` +* Requisitos: + + * Idempotentes (seguros si se ejecutan más de una vez). + * En lotes (`batch_size` razonable) para datasets grandes. + * Logs claros (uso de `_logger.info`). + * Manejo de transacciones cuando aplique (evitar locks largos). + * Documentar al inicio **qué suponen** y **qué garantizan**. + +**Esqueleto mínimo (ejemplo):** + +```python +# migrations//pre_rename_partner_ref.py +from odoo import api, SUPERUSER_ID + +def migrate(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + partners = env['res.partner'].with_context(active_test=False).search([('old_ref', '!=', False)]) + for batch in range(0, len(partners), 500): + sub = partners[batch:batch+500] + for p in sub: + if not p.new_ref: + p.new_ref = p.old_ref +``` + +```python +# migrations//post_backfill_stored_amount_total.py +from odoo import api, SUPERUSER_ID + +def migrate(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + Orders = env['sale.order'].with_context(active_test=False) + ids = Orders.search([]).ids + for i in range(0, len(ids), 200): + batch = Orders.browse(ids[i:i+200]) + # Forzar recompute del stored + batch._compute_amount_total() +``` + +--- + +## Checklist rápida para el review (general) + +| Categoría | Qué comprobar Copilot | +| ------------------ | -------------------------------------------------------------------------------------------------------- | +| Modelos | Relaciones válidas; constraints; uso adecuado de `@api.depends`; `super()` correcto | +| Vistas XML | Herencias correctas; campos válidos; adaptación a cambios de versión (p.ej. `` vs ``) | +| Manifest | **Bump de versión obligatorio** si hay cambios estructurales en modelos/vistas/records .xml; archivos referenciados | +| Seguridad | Accesos mínimos necesarios; reglas revisadas | +| Migraciones | **Si hay cambios estructurales, sugerir script en `migrations/` (pre/post/end)** y describir qué hace | +| Rendimiento / ORM | Evitar loops costosos; no SQL innecesario; aprovechar las optimizaciones del ORM de la versión | +| Ortografía & typos | Errores evidentes corregibles sin modificar idioma ni estilo | + +--- + +## Heurística práctica para el bump de versión (general) + +* **SI** el diff modifica la estructura de un modelo, una vista, o algún record .xml (ej. cambios en definición de campos, vistas XML, datos XML, seguridad) + **Y** `__manifest__.py` no cambia `version` → **Sugerir bump**. +* **SI** hay scripts `migrations/pre_*.py` o `migrations/post_*.py` nuevos → **Sugerir al menos minor bump**. +* **SI** hay cambios que rompen compatibilidad (renombres, cambios de tipo con impacto, limpieza masiva de datos) → **Sugerir minor/major** según impacto. + +--- + +## Estilo del feedback (general) + +* Ser breve, claro y útil. Ejemplos: + + * “El campo `partner_id` no se encuentra referenciado en la vista.” + * “Este método redefine `write()` sin usar `super()`.” + * “Tip: hay un error ortográfico en el nombre del parámetro.” + * **Bump + migración:** “Se renombra `old_ref` → `new_ref`: falta **bump de versión** y **pre-script** en `migrations/` para copiar valores antes del upgrade; añadir **post-script** para recompute del stored.” + +* Evitar explicaciones largas o reescrituras completas salvo que el cambio sea claro y necesario. +* Priorizar comentarios en forma de **lista corta de puntos** (3–7 ítems) y frases breves en lugar de bloques de texto extensos. + +--- + +## Resumen operativo para Copilot + +1. **Detecta cambios estructurales en modelos, vistas o records .xml → exige bump de `version` en `__manifest__.py` si no está incrementada.** +2. **Si hay cambio estructural (según la lista actualizada) → propone y describe script(s) de migración en `migrations/` (pre/post/end)**, con enfoque idempotente y en lotes. +3. Distingue entre: + + * **cuestiones generales** (válidas para cualquier versión), + * y **matices específicos de Odoo 18** (por ejemplo, uso de ``, passkeys, tours y comportamiento del framework). +4. Mantén el feedback **concreto, breve y accionable**. \ No newline at end of file diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 349c52d82..baa05dbf9 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -6,8 +6,13 @@ name: pre-commit on: push: - branches: "[0-9][0-9].0" + branches: + - "1[8-9].0" + - "[2-9][0-9].0" pull_request_target: + branches: + - "1[8-9].0*" + - "[2-9][0-9].0*" jobs: pre-commit: diff --git a/.gitignore b/.gitignore index a6d076d8a..59c990894 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,36 @@ coverage.xml # Sphinx documentation docs/_build/ + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc269814a..c4be55ffa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,8 @@ repos: - id: check-docstring-first - id: check-executables-have-shebangs - id: check-merge-conflict + args: ['--assume-in-merge'] + exclude: '\.rst$' - id: check-symlinks - id: check-xml - id: check-yaml diff --git a/price_security_sale_margin/models/sale_order.py b/price_security_sale_margin/models/sale_order.py index e7c6bf3dd..fc232771d 100644 --- a/price_security_sale_margin/models/sale_order.py +++ b/price_security_sale_margin/models/sale_order.py @@ -22,15 +22,15 @@ def _get_view(self, view_id=None, view_type="form", **options): modifiers["readonly"] = True node.set("modifiers", json.dumps(modifiers)) if self.env.user.has_group("price_security.group_only_view_sale_price"): - invisible_fields = ( - arch.xpath("//field[@name='purchase_price']") - + arch.xpath("//field[@name='margin']") - + arch.xpath("//field[@name='margin_percent']") - + arch.xpath("//field[@name='margin_percent']/..") + invisible_fields = arch.xpath( + "//field[@name='purchase_price']" + "|//field[@name='order_line']//field[@name='margin']" + "|//field[@name='order_line']//field[@name='margin_percent']" + "|//div[@class='d-flex float-end']" ) for node in invisible_fields: - node.set("column_invisible", "1") + node.set("invisible", "1") modifiers = json.loads(node.get("modifiers") or "{}") - modifiers["column_invisible"] = True + modifiers["invisible"] = True node.set("modifiers", json.dumps(modifiers)) return arch, view diff --git a/product_catalog_tree/__manifest__.py b/product_catalog_tree/__manifest__.py index acd7b5611..4b8dcf930 100644 --- a/product_catalog_tree/__manifest__.py +++ b/product_catalog_tree/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Product Catalog Tree", - "version": "18.0.1.2.0", + "version": "18.0.1.3.0", "category": "Products", "sequence": 14, "summary": "", diff --git a/product_catalog_tree/models/product_catalog_mixin.py b/product_catalog_tree/models/product_catalog_mixin.py index ee908952f..2277539c5 100644 --- a/product_catalog_tree/models/product_catalog_mixin.py +++ b/product_catalog_tree/models/product_catalog_mixin.py @@ -8,5 +8,12 @@ class ProductCatalogMixin(models.AbstractModel): def action_add_from_catalog(self): action = super().action_add_from_catalog() tree_view_id = self.env.ref("product_catalog_tree.product_view_tree_catalog").id + search_view_id = self.env.ref("product.product_search_form_view").id action["views"] = [(tree_view_id, "list")] + action["views"] + action["search_view_id"] = (search_view_id, "search") + # add warehouse location filter if order has warehouse + context = action.get("context", {}) + if "order_id" in context and self._name == "sale.order": + if self._fields.get("warehouse_id") and self.warehouse_id: + action["context"]["search_default_location_id"] = self.warehouse_id.lot_stock_id.id return action diff --git a/product_catalog_tree/models/product_product.py b/product_catalog_tree/models/product_product.py index e0db1c95b..fc2a47ef6 100644 --- a/product_catalog_tree/models/product_product.py +++ b/product_catalog_tree/models/product_product.py @@ -35,6 +35,12 @@ def write(self, vals): def _compute_catalog_values(self): res_model = self._context.get("product_catalog_order_model") order_id = self._context.get("order_id") + + if not res_model or not order_id: + self.product_catalog_qty = 0 + self.product_catalog_price = 0 + return + order = self.env[res_model].browse(order_id) order_line_info = order.with_company(order.company_id)._get_product_catalog_order_line_info( product_ids=self.ids @@ -46,9 +52,15 @@ def _compute_catalog_values(self): def _inverse_catalog_values(self, product_catalog_qty): res_model = self._context.get("product_catalog_order_model") order_id = self._context.get("order_id") + + if not res_model or not order_id: + return + order = self.env[res_model].browse(order_id) for rec in self: - order.with_company(order.company_id)._update_order_line_info(rec.id, product_catalog_qty) + # Actualizar la información de la línea de orden + # Call the order method with a cleared context to avoid errors on creating move line + order.with_company(order.company_id).with_context(**{})._update_order_line_info(rec.id, product_catalog_qty) def increase_quantity(self): for rec in self: diff --git a/product_catalog_tree/views/product_product_views.xml b/product_catalog_tree/views/product_product_views.xml index 6ed8b1036..bdb777ab5 100644 --- a/product_catalog_tree/views/product_product_views.xml +++ b/product_catalog_tree/views/product_product_views.xml @@ -24,4 +24,16 @@ + + product.product.search.form.supplier + product.product + + + + + + + + diff --git a/product_catalog_tree_custom/__init__.py b/product_catalog_tree_custom/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/product_catalog_tree_custom/__manifest__.py b/product_catalog_tree_custom/__manifest__.py new file mode 100644 index 000000000..272e8b390 --- /dev/null +++ b/product_catalog_tree_custom/__manifest__.py @@ -0,0 +1,19 @@ +{ + "name": "Product Catalog Tree Custom", + "version": "18.0.1.0.0", + "category": "Product", + "summary": "Customizations for product catalog tree view", + "author": "ADHOC SA", + "license": "LGPL-3", + "depends": [ + "product_catalog_tree", + "website_sale", + "product_price_taxes_included", + "product_brand", + ], + "data": [ + "views/product_catalog_list_views.xml", + ], + "installable": True, + "application": False, +} diff --git a/product_catalog_tree_custom/views/product_catalog_list_views.xml b/product_catalog_tree_custom/views/product_catalog_list_views.xml new file mode 100644 index 000000000..96069c06c --- /dev/null +++ b/product_catalog_tree_custom/views/product_catalog_list_views.xml @@ -0,0 +1,46 @@ + + + + + gg.product.view.list.catalog.ext + product.product + + extension + 50 + + + + + hide + + + + + hide + + + + + show + + + + + show + + + + show + + + + show + + + + show + + + + + diff --git a/product_replenishment_cost/i18n/es.po b/product_replenishment_cost/i18n/es.po index c50ba6194..3c570ae08 100644 --- a/product_replenishment_cost/i18n/es.po +++ b/product_replenishment_cost/i18n/es.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 18.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-07-03 05:15+0000\n" +"POT-Creation-Date: 2025-11-21 10:29+0000\n" "PO-Revision-Date: 2024-11-08 12:54+0000\n" "Last-Translator: ced_adhoc, 2025\n" "Language-Team: Spanish (https://app.transifex.com/adhoc/teams/46451/es/)\n" @@ -287,11 +287,6 @@ msgstr "Productos sin regla de reposición" msgid "Purchase Order Line" msgstr "Línea de pedido de compra" -#. module: product_replenishment_cost -#: model:ir.model.fields,field_description:product_replenishment_cost.field_product_replenishment_cost_rule__rating_ids -msgid "Ratings" -msgstr "" - #. module: product_replenishment_cost #: model:ir.model.fields,help:product_replenishment_cost.field_product_product__replenishment_base_cost #: model:ir.model.fields,help:product_replenishment_cost.field_product_template__replenishment_base_cost @@ -434,16 +429,6 @@ msgstr "Lista de precios de proveedor" msgid "Supplierinfo" msgstr "Información del Proveedor" -#. module: product_replenishment_cost -#. odoo-python -#: code:addons/product_replenishment_cost/models/product_template.py:0 -msgid "" -"The argument 'company_ids' and the key 'force_company' on the context can't " -"be used together" -msgstr "" -"El argumento 'company_ids' y la clave 'force_company'  en el contexto no " -"pueden usarse juntos" - #. module: product_replenishment_cost #: model_terms:ir.ui.view,arch_db:product_replenishment_cost.view_update_from_replenishment_cost_wizard_form msgid "" diff --git a/product_replenishment_cost/models/product_template.py b/product_replenishment_cost/models/product_template.py index 6b130b34e..7a2c7c440 100644 --- a/product_replenishment_cost/models/product_template.py +++ b/product_replenishment_cost/models/product_template.py @@ -5,7 +5,6 @@ import logging from odoo import _, api, fields, models -from odoo.exceptions import ValidationError from odoo.tools import float_compare _logger = logging.getLogger(__name__) @@ -44,7 +43,7 @@ class ProductTemplate(models.Model): replenishment_base_cost = fields.Float( digits="Product Price", tracking=True, - help="Replanishment Cost expressed in 'Replenishment Base Cost " "Currency'.", + help="Replanishment Cost expressed in 'Replenishment Base Cost Currency'.", ) replenishment_base_cost_currency_id = fields.Many2one( "res.currency", @@ -111,12 +110,6 @@ def cron_update_cost_from_replenishment_cost(self, limit=None, company_ids=None, tampoco podemos usar get_param porque justamente no se va a refrescar el valor """ - # allow force_company for backward compatibility - force_company = self._context.get("force_company", False) - if force_company and company_ids: - raise ValidationError( - _("The argument 'company_ids' and the key 'force_company' on the context can't be used together") - ) # buscamos cual fue el ultimo registro actualziado parameter_name = "product_replenishment_cost.last_updated_record_id" last_updated_param = self.env["ir.config_parameter"].sudo().search([("key", "=", parameter_name)], limit=1) @@ -126,10 +119,8 @@ def cron_update_cost_from_replenishment_cost(self, limit=None, company_ids=None, domain = [("id", ">", int(last_updated_param.value))] records = self.with_context(prefetch_fields=False).search(domain, order="id asc") - # use company_ids or force_company or search for all companies - if force_company: - company_ids = [force_company] - elif not company_ids: + # use company_ids or search for all companies + if not company_ids: company_ids = self.env["res.company"].search([]).ids for company_id in company_ids: diff --git a/product_replenishment_cost_mrp/README.rst b/product_replenishment_cost_mrp/README.rst index 2e4d00c09..8137cd2d9 100644 --- a/product_replenishment_cost_mrp/README.rst +++ b/product_replenishment_cost_mrp/README.rst @@ -14,8 +14,9 @@ Integration between Replenishment Cost and Manufacture ====================================================== -#. Agregado de método para cálculo de costo de reposición utilizando el costo de reposición de la LdM. -#. Modifica el reporte de "Estructura de Lista de Materiales" para que utilice el costo de reposición. +#. Add a method to compute the replenishment cost using the BoM's replenishment cost. +#. Modify the "Bill of Materials Structure" report to use the replenishment cost +#. Support for replenishment cost calculation in subcontracted BOMs, adding the supplier/subcontractor cost to the components' cost Installation ============ diff --git a/product_replenishment_cost_mrp/__manifest__.py b/product_replenishment_cost_mrp/__manifest__.py index 83b8ee4a1..701f10d88 100644 --- a/product_replenishment_cost_mrp/__manifest__.py +++ b/product_replenishment_cost_mrp/__manifest__.py @@ -8,6 +8,10 @@ "product_replenishment_cost", "mrp", ], - "data": ["views/mrp_bom_views.xml", "views/product_template_views.xml", "report/mrp_report_bom_structure.xml"], + "data": [ + "views/mrp_bom_views.xml", + "views/product_template_views.xml", + "report/mrp_report_bom_structure.xml", + ], "installable": True, } diff --git a/product_replenishment_cost_mrp/models/product_template.py b/product_replenishment_cost_mrp/models/product_template.py index 324c46f85..5ffb29154 100644 --- a/product_replenishment_cost_mrp/models/product_template.py +++ b/product_replenishment_cost_mrp/models/product_template.py @@ -46,7 +46,11 @@ def _compute_replenishment_cost(self): rec.update({"replenishment_base_cost_on_currency": 0.0, "replenishment_cost": 0.0}) continue # el explode es para product.product, tomamos la primer variante - result, result2 = bom.explode(rec.with_context(active_test=rec.active).product_variant_ids[0], 1) + product_variant = rec.with_context(active_test=rec.active).product_variant_ids[:1] + if not product_variant: + rec.update({"replenishment_base_cost_on_currency": 0.0, "replenishment_cost": 0.0}) + continue + result, result2 = bom.explode(product_variant, 1) for sbom, sbom_data in result2: sbom_rep_cost = ( sbom.product_id.uom_id._compute_price( @@ -57,6 +61,36 @@ def _compute_replenishment_cost(self): price += sbom.product_id.product_tmpl_id.currency_id._convert( sbom_rep_cost, product_currency, company, date, round=False ) + + # Add subcontracting cost if bom type is 'subcontract' + if bom.type == "subcontract": + # Look for the seller/subcontractor set in the BOM + product = rec.product_variant_ids[:1] + if product: + seller = product._select_seller( + quantity=1, uom_id=bom.product_uom_id, params={"subcontractor_ids": bom.subcontractor_ids} + ) + else: + # If no product variant, look for sellers in the template + seller = rec.seller_ids.filtered(lambda s: s.partner_id in bom.subcontractor_ids)[:1] + + if seller: + if bom.product_uom_id.ratio == 0: + raise ValueError( + _( + "El ratio de la unidad de medida del producto '%s' en el BOM es cero. " + "Esto provocaría una división por cero. Verifique la configuración de la UoM." + ) + % bom.display_name + ) + # Calculate the subcontracting cost + ratio_uom_seller = seller.product_uom.ratio / bom.product_uom_id.ratio + subcontract_price = seller.currency_id._convert( + seller.price, product_currency, company, date, round=False + ) + # Add the subcontract price to the total price + price += subcontract_price / ratio_uom_seller + # NO implementamos total va a ser borrado. Ver si implementamos mas adelante (tener en cuenta convertir # moneda) # if bom.routing_id: diff --git a/product_replenishment_cost_mrp/report/mrp_report_bom_structure.py b/product_replenishment_cost_mrp/report/mrp_report_bom_structure.py index eb51168e5..d28532135 100644 --- a/product_replenishment_cost_mrp/report/mrp_report_bom_structure.py +++ b/product_replenishment_cost_mrp/report/mrp_report_bom_structure.py @@ -4,6 +4,31 @@ class ReportReplenishmentBomStructure(models.AbstractModel): _inherit = "report.mrp.report_bom_structure" + @api.model + def _get_subcontracting_line(self, bom, seller, level, bom_quantity): + """Override to convert seller price to the proper currency""" + res = super()._get_subcontracting_line(bom, seller, level, bom_quantity) + currency = self.env.context.get("force_currency") or self.env.company.currency_id + + # Convert the seller price to the proper currency + if bom.product_uom_id.ratio == 0: + raise ValueError( + "El ratio de la unidad de medida del producto en el BOM es cero. " + "Esto provocaría una división por cero. Verifique la configuración de la UoM." + ) + ratio_uom_seller = seller.product_uom.ratio / bom.product_uom_id.ratio + price = seller.currency_id._convert(seller.price, currency, self.env.company, fields.Date.today(), round=True) + res.update( + { + "prod_cost": price / ratio_uom_seller * bom_quantity, + "bom_cost": price / ratio_uom_seller * bom_quantity, + "currency": currency, + "currency_id": currency.id, + } + ) + + return res + @api.model def _get_bom_data( self, @@ -22,7 +47,7 @@ def _get_bom_data( ): """Here we use the replenishment cost for the uom unit""" if not self.env.context.get("force_currency"): - self = self.with_context(force_currency=product.currency_id) + self = self.with_context(force_currency=product.currency_id if product else bom.product_tmpl_id.currency_id) res = super(ReportReplenishmentBomStructure, self)._get_bom_data( bom, warehouse, @@ -43,6 +68,9 @@ def _get_bom_data( current_quantity = line_qty if bom_line: current_quantity = bom_line.product_uom_id._compute_quantity(line_qty, bom.product_uom_id) or 0 + + # Only update prod_cost (costo del producto final), not bom_cost + # bom_cost is automatically calculated as: components + operations + subcontracting if not is_minimized: if product: price = product.uom_id._compute_price(product.replenishment_cost, bom.product_uom_id) * current_quantity @@ -79,10 +107,13 @@ def _get_component_data( parent_bom, parent_product, warehouse, bom_line, line_quantity, level, index, product_info, ignore_stock ) currency = self.env.context.get("force_currency") or self.env.company.currency_id - price = ( - bom_line.product_id.uom_id._compute_price(bom_line.product_id.replenishment_cost, bom_line.product_uom_id) - * line_quantity - ) + + # Use replenishment_cost, if not defined use standard_price as fallback + component_cost = bom_line.product_id.replenishment_cost + if not component_cost: + component_cost = bom_line.product_id.standard_price + + price = bom_line.product_id.uom_id._compute_price(component_cost, bom_line.product_uom_id) * line_quantity price = bom_line.product_id.currency_id._convert( price, currency, self.env.company, fields.Date.today(), round=True ) diff --git a/product_replenishment_cost_sale_margin/models/sale_order_line.py b/product_replenishment_cost_sale_margin/models/sale_order_line.py index f49f6c146..642dc2425 100644 --- a/product_replenishment_cost_sale_margin/models/sale_order_line.py +++ b/product_replenishment_cost_sale_margin/models/sale_order_line.py @@ -1,10 +1,9 @@ -from odoo import api, fields, models +from odoo import fields, models class SaleOrderLine(models.Model): _inherit = "sale.order.line" - @api.depends("product_id.replenishment_cost") def _compute_purchase_price(self): super()._compute_purchase_price() for line in self.filtered("product_id"): diff --git a/product_ux/__manifest__.py b/product_ux/__manifest__.py index b9622cdcd..84bd8ba59 100644 --- a/product_ux/__manifest__.py +++ b/product_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Product UX", - "version": "18.0.1.1.0", + "version": "18.0.1.2.0", "category": "Products", "sequence": 14, "summary": "", diff --git a/product_ux/models/product_product.py b/product_ux/models/product_product.py index ca78b7c5b..f26b32fcd 100644 --- a/product_ux/models/product_product.py +++ b/product_ux/models/product_product.py @@ -3,13 +3,26 @@ # directory ############################################################################## from odoo import api, fields, models +from odoo.tools import create_index class ProductProduct(models.Model): _inherit = "product.product" + _order = "default_code, name, id" + + def init(self): + super().init() + create_index( + self.env.cr, + indexname="is_favorite_idx", + tablename="product_product", + expressions=["is_favorite"], + where="is_favorite IS TRUE", + ) active = fields.Boolean(tracking=True) pricelist_price = fields.Float(compute="_compute_product_pricelist_price", digits="Product Price") + is_favorite = fields.Boolean(related="product_tmpl_id.is_favorite", readonly=True, store=True) @api.depends_context("pricelist", "quantity", "uom", "date", "no_variant_attributes_price_extra") def _compute_product_pricelist_price(self): diff --git a/pyproject.toml b/pyproject.toml index 9f837a8cb..9b15bb049 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,9 +27,9 @@ ignore = [ [tool.ruff.lint.pycodestyle] # line-length is set in [tool.ruff], and it's used by the formatter # in case the formatted can't autofix the line length, it will be reported as an error -# only if it exceeds the max-line-length set here. We use 999 to effectively disable +# only if it exceeds the max-line-length set here. We use 320 (max available value) to disable # this check. -max-line-length = 999 +max-line-length = 320 [tool.ruff.lint.isort] combine-as-imports = true @@ -46,6 +46,7 @@ known-third-party = [ "urllib2", "yaml", ] +section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] [tool.ruff.lint.mccabe] max-complexity = 20