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
4 changes: 2 additions & 2 deletions purchase_stock_ux/models/purchase_order_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,11 @@ def _compute_qty_received(self):
for move in exchange_move_ids
)

@api.depends("order_id.state", "move_ids.state")
@api.depends("order_id.state", "move_ids.state", "move_ids.to_refund")
def _compute_qty_returned(self):
for line in self:
qty = 0.0
for move in line.move_ids.filtered(
for move in line._get_po_line_moves().filtered(
lambda m: (
m.state == "done"
and m.location_id.usage != "supplier"
Expand Down
1 change: 1 addition & 0 deletions purchase_stock_ux/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from . import test_purchase_order
from . import test_stock_orderpoint
from . import test_cancel_remaining
from . import test_qty_returned
113 changes: 113 additions & 0 deletions purchase_stock_ux/tests/test_qty_returned.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from odoo import Command
from odoo.addons.purchase_stock.tests.common import PurchaseTestCommon


class TestQtyReturned(PurchaseTestCommon):
"""Cobertura de qty_returned (_compute_qty_returned) en compras.

Ticket 126775: se recibió el producto equivocado, se lo devolvió al proveedor marcando
"Para abonar" y se corrigió el producto en la misma línea de la OC. La devolución del
producto viejo se descontaba de lo pendiente a facturar del producto nuevo, dejando la
OC como "totalmente facturada" con cero facturado y sin forma de emitir la factura del
proveedor. El cómputo debe considerar solo los movimientos del producto de la línea,
igual que _compute_qty_received del core.
"""

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.env["res.partner"].create({"name": "Qty Returned Vendor"})
cls.warehouse = cls.env["stock.warehouse"].search([("company_id", "=", cls.env.company.id)], limit=1)
cls.warehouse.reception_steps = "one_step"

# ------------------------------------------------------------------ helpers
def _product(self, name):
return self.env["product.product"].create(
{
"name": name,
"type": "consu",
"is_storable": True,
"purchase_method": "purchase",
}
)

def _confirm_po(self, product, qty):
po = self.env["purchase.order"].create(
{
"partner_id": self.partner.id,
"picking_type_id": self.warehouse.in_type_id.id,
"order_line": [
Command.create(
{
"product_id": product.id,
"product_qty": qty,
"price_unit": 10.0,
"name": product.name,
}
)
],
}
)
po.button_confirm()
return po

def _validate(self, picking):
picking.action_assign()
for move in picking.move_ids.filtered(lambda m: m.state not in ("done", "cancel")):
move.quantity = move.product_uom_qty
move.picked = True
picking.button_validate()

def _receive_all(self, po):
for picking in po.picking_ids.filtered(lambda p: p.state not in ("done", "cancel")):
self._validate(picking)

def _return_all(self, po, qty, to_refund=True):
receipt = po.picking_ids.filtered(lambda p: p.picking_type_id.code == "incoming" and p.state == "done").sorted(
"id"
)[-1:]
wizard = (
self.env["stock.return.picking"]
.with_context(active_id=receipt.id, active_model="stock.picking", active_ids=receipt.ids)
.create({})
)
for wizard_line in wizard.product_return_moves:
wizard_line.quantity = qty
wizard_line.to_refund = to_refund
action = wizard.action_create_returns()
return_picking = self.env["stock.picking"].browse(action["res_id"])
self._validate(return_picking)
return return_picking

# -------------------------------------------------------------------- tests
def test_return_of_replaced_product_not_counted(self):
"""Ticket 126775: la devolución del producto reemplazado no debe contar como devuelta
en la línea -que ahora tiene otro producto- ni bloquear su facturación."""
old_product = self._product("QR producto viejo")
new_product = self._product("QR producto nuevo")
po = self._confirm_po(old_product, 10)
line = po.order_line
self._receive_all(po)
self._return_all(po, 10, to_refund=True)

line.product_id = new_product
line.invalidate_recordset()
# el cambio de producto no está en los depends de los campos almacenados
line._compute_qty_invoiced()
line._compute_invoice_status()

self.assertEqual(line.qty_returned, 0, "la devolución del producto viejo se contó en la línea nueva")
self.assertEqual(line.qty_to_invoice, 10, "la línea quedó sin cantidad pendiente a facturar")
self.assertEqual(line.invoice_status, "to invoice")

def test_return_of_same_product_counted(self):
"""No regresión: la devolución del mismo producto de la línea sí debe seguir contando."""
product = self._product("QR mismo producto")
po = self._confirm_po(product, 10)
line = po.order_line
self._receive_all(po)
self._return_all(po, 10, to_refund=True)

line.invalidate_recordset()
self.assertEqual(line.qty_returned, 10)
self.assertEqual(line.qty_to_invoice, 0)