Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand All @@ -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.

---
Expand Down Expand Up @@ -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. `<list>` vs `<tree>`) |
| 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 |
Expand All @@ -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.
Expand All @@ -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 `<list>`, 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.
4. Mantén el feedback **concreto, breve y accionable**.
18 changes: 17 additions & 1 deletion purchase_stock_ux/models/purchase_order_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions purchase_stock_ux/models/stock_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
51 changes: 51 additions & 0 deletions purchase_stock_ux/wizards/stock_return_picking.py
Original file line number Diff line number Diff line change
@@ -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