Skip to content
Closed
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
54 changes: 54 additions & 0 deletions purchase_stock_ux/models/purchase_order_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,67 @@ def _onchange_product_qty(self):
return {"warning": warning_mess}
return {}

<<<<<<< 43b4f59c454753480ff90f3037d1b07e42d82ed7
@api.depends("order_id.state", "move_ids.state")
||||||| e67119e1f5b065fafbd0d5310bb17129cb453079
@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")
=======
@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", "move_ids.to_refund")
>>>>>>> 955b1890650ae278b691ed999cdbebc9ee835608
def _compute_qty_returned(self):
for line in self:
qty = 0.0
<<<<<<< 43b4f59c454753480ff90f3037d1b07e42d82ed7
# Count only real vendor returns (excludes the subcontract receipt move).
for move in line.move_ids.filtered(lambda m: m.state == "done" and m.to_refund and m._is_purchase_return()):
qty += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom_id)
||||||| e67119e1f5b065fafbd0d5310bb17129cb453079
for move in line.move_ids.filtered(
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)
=======
for move in line._get_po_line_moves().filtered(
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)
>>>>>>> 955b1890650ae278b691ed999cdbebc9ee835608
line.qty_returned = qty

# Overwrite the origin method to introduce the qty_on_voucher
Expand Down
7 changes: 7 additions & 0 deletions purchase_stock_ux/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@

from . import test_purchase_order
from . import test_stock_orderpoint
<<<<<<< 43b4f59c454753480ff90f3037d1b07e42d82ed7
||||||| e67119e1f5b065fafbd0d5310bb17129cb453079
from . import test_cancel_remaining
=======
from . import test_cancel_remaining
from . import test_qty_returned
>>>>>>> 955b1890650ae278b691ed999cdbebc9ee835608
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)
Loading