diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c985deec..a30ad3cf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -17,6 +17,7 @@ * 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. --- @@ -38,13 +39,7 @@ * 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):** - Siempre que el diff incluya **modificaciones en**: - - * definición de campos o modelos (`models/*.py`, `wizards/*.py`), - * vistas o datos XML (`views/*.xml`, `data/*.xml`, `report/*.xml`, `wizards/*.xml`), - * seguridad (`security/*.csv`, `security/*.xml`), - - **y el `__manifest__.py` no incrementa `version`, sugerir el bump de versión** (por ejemplo, `1.0.0 → 1.0.1`). + 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. --- @@ -288,7 +283,7 @@ def migrate(cr, registry): | ------------------ | -------------------------------------------------------------------------------------------------------- | | 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 en modelos/vistas/seguridad/datos; archivos referenciados | +| 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 | @@ -298,7 +293,7 @@ def migrate(cr, registry): ## Heurística práctica para el bump de versión (general) -* **SI** el diff toca cualquiera de: `models/`, `views/`, `data/`, `report/`, `security/`, `wizards/` +* **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. @@ -321,12 +316,10 @@ def migrate(cr, registry): ## Resumen operativo para Copilot -1. **Detecta cambios en modelos/vistas/seguridad/datos → exige bump de `version` en `__manifest__.py`.** +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**. - -[^odoo18]: Resumen basado en la documentación oficial de Odoo 18 Release Notes y artículos técnicos que analizan sus mejoras de rendimiento y UX. \ No newline at end of file +4. Mantén el feedback **concreto, breve y accionable**. \ No newline at end of file diff --git a/purchase_stock_ux/models/purchase_order_line.py b/purchase_stock_ux/models/purchase_order_line.py index 3631e5c2..735dbde5 100644 --- a/purchase_stock_ux/models/purchase_order_line.py +++ b/purchase_stock_ux/models/purchase_order_line.py @@ -156,12 +156,28 @@ def _onchange_product_qty(self): return {"warning": warning_mess} return {} + @api.depends("qty_received_method", "qty_received_manual") + def _compute_qty_received(self): + super()._compute_qty_received() + for line in self.filtered(lambda l: l.qty_received_method in ["manual", "stock_moves"]): + exchange_move_ids = line.move_ids.filtered( + lambda m: m.state == "done" and m.location_id.usage != "supplier" and m._is_exchange_move_helper() + ) + if exchange_move_ids: + line.qty_received -= sum( + line.product_uom._compute_quantity(move.product_uom_qty, line.product_uom) + for move in exchange_move_ids + ) + @api.depends("order_id.state", "move_ids.state") def _compute_qty_returned(self): for line in self: qty = 0.0 for move in line.move_ids.filtered( - lambda m: m.state == "done" and m.location_id.usage != "supplier" and m.to_refund + lambda m: m.state == "done" + and m.location_id.usage != "supplier" + and m.to_refund + and not m._is_exchange_move_helper() ): qty += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom) line.qty_returned = qty diff --git a/purchase_stock_ux/models/stock_move.py b/purchase_stock_ux/models/stock_move.py index bdc6d9f9..f9ac75a1 100644 --- a/purchase_stock_ux/models/stock_move.py +++ b/purchase_stock_ux/models/stock_move.py @@ -26,3 +26,13 @@ def _prepare_merge_moves_distinct_fields(self): if self.env.context.get("cancel_from_order") and "price_unit" in distinct_fields: distinct_fields.remove("price_unit") return distinct_fields + + def _is_exchange_move_helper(self): + # Como is_exchange_move se crea en sale_stock_ux, chequeamos si el campo existe antes de usarlo + # sino existe el valor deberia ser False + # en 19 vamos mover el campo a stock ux asi no tenemos que hacer este + # feo hack + self.ensure_one() + if self.fields_get().get("is_exchange_move"): + return self.is_exchange_move + return False diff --git a/purchase_stock_ux/wizards/stock_return_picking.py b/purchase_stock_ux/wizards/stock_return_picking.py new file mode 100644 index 00000000..05513d14 --- /dev/null +++ b/purchase_stock_ux/wizards/stock_return_picking.py @@ -0,0 +1,51 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import _, api, models +from odoo.exceptions import UserError + + +class StockReturnPicking(models.TransientModel): + _inherit = "stock.return.picking" + + @api.model + def default_get(self, fields): + """Set to_refund=True by default for purchase returns.""" + result = super().default_get(fields) + try: + for line in result["product_return_moves"]: + assert line[0] == 0 + # Marcar to_refund por defecto en devoluciones a proveedor + line[2]["to_refund"] = True + except KeyError: + pass + return result + + def action_create_exchanges(self): + """Create exchanges for purchase returns. + + Exchange = devuelves producto defectuoso al proveedor y el proveedor + te envía un reemplazo. No es un reembolso monetario. + """ + if any(self.product_return_moves.mapped("to_refund")): + raise UserError(_("You cannot create exchanges for return lines marked to refund.")) + return super(StockReturnPicking, self.with_context(is_exchange_move=True)).action_create_exchanges() + + +class StockReturnPickingLine(models.TransientModel): + _inherit = "stock.return.picking.line" + + def _prepare_move_default_values(self, new_picking): + """Mark moves created from exchanges with is_exchange_move flag.""" + vals = super()._prepare_move_default_values(new_picking) + if self.env.context.get("is_exchange_move"): + vals["is_exchange_move"] = True + return vals + + def _prepare_picking_default_values_based_on(self, picking): + """Propagate is_exchange_move flag to the new picking.""" + vals = super()._prepare_picking_default_values_based_on(picking) + if self.env.context.get("is_exchange_move"): + vals["is_exchange_move"] = True + return vals