diff --git a/stock_request/models/stock_move.py b/stock_request/models/stock_move.py index 077452f46..9c8e2a1d8 100644 --- a/stock_request/models/stock_move.py +++ b/stock_request/models/stock_move.py @@ -48,3 +48,31 @@ class StockMove(models.Model): "that of the location." ) ) + + def copy_data(self, default=None): + if not default: + default = {} + if "allocation_ids" not in default: + default["allocation_ids"] = [] + for alloc in self.allocation_ids: + default["allocation_ids"].append( + ( + 0, + 0, + { + "stock_request_id": alloc.stock_request_id.id, + "requested_product_uom_qty": alloc.requested_product_uom_qty, + }, + ) + ) + return super(StockMove, self).copy_data(default) + + def _action_cancel(self): + res = super()._action_cancel() + self.mapped("allocation_ids.stock_request_id").check_done() + return res + + def _action_done(self, cancel_backorder=False): + res = super()._action_done(cancel_backorder=cancel_backorder) + self.mapped("allocation_ids.stock_request_id").check_done() + return res diff --git a/stock_request/models/stock_move_line.py b/stock_request/models/stock_move_line.py index 74a463e4e..98a34d1a8 100644 --- a/stock_request/models/stock_move_line.py +++ b/stock_request/models/stock_move_line.py @@ -50,16 +50,21 @@ class StockMoveLine(models.Model): # We do sudo because potentially the user that completes the move # may not have permissions for stock.request. - to_allocate_qty = ml.qty_done - for allocation in ml.move_id.allocation_ids: + to_allocate_qty = qty_done + for allocation in ml.move_id.allocation_ids.sudo(): allocated_qty = 0.0 if allocation.open_product_qty: - allocated_qty = min(allocation.open_product_qty, qty_done) + allocated_qty = min(allocation.open_product_qty, to_allocate_qty) allocation.allocated_product_qty += allocated_qty to_allocate_qty -= allocated_qty - request = allocation.stock_request_id - message_data = self._prepare_message_data(ml, request, allocated_qty) - message = self._stock_request_confirm_done_message_content(message_data) - request.message_post(body=message, subtype="mail.mt_comment") - request.check_done() + if allocated_qty: + request = allocation.stock_request_id + message_data = self._prepare_message_data( + ml, request, allocated_qty + ) + message = self._stock_request_confirm_done_message_content( + message_data + ) + request.message_post(body=message, subtype="mail.mt_comment") + request.check_done() return res diff --git a/stock_request/models/stock_request.py b/stock_request/models/stock_request.py index f18cd3887..099a0b3c6 100644 --- a/stock_request/models/stock_request.py +++ b/stock_request/models/stock_request.py @@ -104,8 +104,16 @@ class StockRequest(models.Model): store=True, help="Quantity completed", ) + qty_cancelled = fields.Float( + "Qty Cancelled", + digits="Product Unit of Measure", + readonly=True, + compute="_compute_qty", + store=True, + help="Quantity cancelled", + ) picking_count = fields.Integer( - string="Delivery Orders", compute="_compute_picking_ids", readonly=True + string="Delivery Orders", compute="_compute_picking_ids", readonly=True, ) allocation_ids = fields.One2many( comodel_name="stock.request.allocation", @@ -136,12 +144,16 @@ class StockRequest(models.Model): ("name_uniq", "unique(name, company_id)", "Stock Request name must be unique") ] - @api.depends("allocation_ids") + @api.depends("allocation_ids", "allocation_ids.stock_move_id") def _compute_move_ids(self): for request in self: request.move_ids = request.allocation_ids.mapped("stock_move_id") - @api.depends("allocation_ids") + @api.depends( + "allocation_ids", + "allocation_ids.stock_move_id", + "allocation_ids.stock_move_id.picking_id", + ) def _compute_picking_ids(self): for request in self: request.picking_count = 0 @@ -159,14 +171,31 @@ class StockRequest(models.Model): ) def _compute_qty(self): for request in self: - done_qty = sum(request.allocation_ids.mapped("allocated_product_qty")) + incoming_qty = 0.0 + other_qty = 0.0 + for allocation in request.allocation_ids: + if allocation.stock_move_id.picking_code == "incoming": + incoming_qty += allocation.allocated_product_qty + else: + other_qty += allocation.allocated_product_qty + done_qty = abs(other_qty - incoming_qty) open_qty = sum(request.allocation_ids.mapped("open_product_qty")) - request.qty_done = request.product_id.uom_id._compute_quantity( - done_qty, request.product_uom_id - ) - request.qty_in_progress = request.product_id.uom_id._compute_quantity( + uom = request.product_id.uom_id + request.qty_done = uom._compute_quantity(done_qty, request.product_uom_id) + request.qty_in_progress = uom._compute_quantity( open_qty, request.product_uom_id ) + request.qty_cancelled = ( + max( + 0, + uom._compute_quantity( + request.product_qty - done_qty - open_qty, + request.product_uom_id, + ), + ) + if request.allocation_ids + else 0 + ) @api.constrains("order_id", "requested_by") def check_order_requested_by(self): @@ -208,7 +237,7 @@ class StockRequest(models.Model): def _action_confirm(self): self._action_launch_procurement_rule() - self.state = "open" + self.write({"state": "open"}) def action_confirm(self): self._action_confirm() @@ -219,14 +248,13 @@ class StockRequest(models.Model): return True def action_cancel(self): - self.mapped("move_ids")._action_cancel() - self.state = "cancel" + self.sudo().mapped("move_ids")._action_cancel() + self.write({"state": "cancel"}) return True def action_done(self): - self.state = "done" - if self.order_id: - self.order_id.check_done() + self.write({"state": "done"}) + self.mapped("order_id").check_done() return True def check_done(self): @@ -245,8 +273,20 @@ class StockRequest(models.Model): >= 0 ): request.action_done() + elif request._check_done_allocation(): + request.action_done() return True + def _check_done_allocation(self): + precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + self.ensure_one() + return ( + self.allocation_ids + and float_compare(self.qty_cancelled, 0, precision_digits=precision) > 0 + ) + def _prepare_procurement_values(self, group_id=False): """ Prepare specific key for moves or other components that diff --git a/stock_request/models/stock_request_allocation.py b/stock_request/models/stock_request_allocation.py index 4f2f962a9..8d12f75ea 100644 --- a/stock_request/models/stock_request_allocation.py +++ b/stock_request/models/stock_request_allocation.py @@ -52,11 +52,12 @@ class StockRequestAllocation(models.Model): ) allocated_product_qty = fields.Float( "Allocated Quantity", + copy=False, help="Quantity of the stock request allocated to the stock move, " "in the default UoM of the product", ) open_product_qty = fields.Float( - "Open Quantity", compute="_compute_open_product_qty" + "Open Quantity", compute="_compute_open_product_qty", ) @api.depends( @@ -78,7 +79,7 @@ class StockRequestAllocation(models.Model): ) def _compute_open_product_qty(self): for rec in self: - if rec.stock_move_id.state == "cancel": + if rec.stock_move_id.state in ["cancel", "done"]: rec.open_product_qty = 0.0 else: rec.open_product_qty = ( diff --git a/stock_request/models/stock_request_order.py b/stock_request/models/stock_request_order.py index dc5baf9b8..442e6e8a7 100644 --- a/stock_request/models/stock_request_order.py +++ b/stock_request/models/stock_request_order.py @@ -223,30 +223,28 @@ class StockRequestOrder(models.Model): line.procurement_group_id = self.procurement_group_id def action_confirm(self): - for line in self.stock_request_ids: - line.action_confirm() - self.state = "open" + self.mapped("stock_request_ids").action_confirm() + self.write({"state": "open"}) return True def action_draft(self): - for line in self.stock_request_ids: - line.action_draft() - self.state = "draft" + self.mapped("stock_request_ids").action_draft() + self.write({"state": "draft"}) return True def action_cancel(self): - for line in self.stock_request_ids: - line.action_cancel() - self.state = "cancel" + self.mapped("stock_request_ids").action_cancel() + self.write({"state": "cancel"}) return True def action_done(self): - self.state = "done" + self.write({"state": "done"}) return True def check_done(self): - if not self.stock_request_ids.filtered(lambda r: r.state != "done"): - self.action_done() + for rec in self: + if not rec.stock_request_ids.filtered(lambda r: r.state != "done"): + rec.action_done() return def action_view_transfer(self): diff --git a/stock_request/tests/test_stock_request.py b/stock_request/tests/test_stock_request.py index cbfb46477..dabb20bb3 100644 --- a/stock_request/tests/test_stock_request.py +++ b/stock_request/tests/test_stock_request.py @@ -688,11 +688,15 @@ class TestStockRequestBase(TestStockRequest): ) stock_request_2.product_uom_qty = 6.0 self.product.route_ids = [(6, 0, self.route.ids)] - stock_request_1.with_user(self.stock_request_manager).action_confirm() - stock_request_2.with_user(self.stock_request_manager).action_confirm() - self.assertEqual(len(stock_request_1.picking_ids), 1) - self.assertEqual(stock_request_1.picking_ids, stock_request_2.picking_ids) - self.assertEqual(stock_request_1.move_ids, stock_request_2.move_ids) + stock_request_1.sudo().action_confirm() + stock_request_2.sudo().action_confirm() + self.assertEqual(len(stock_request_1.sudo().picking_ids), 1) + self.assertEqual( + stock_request_1.sudo().picking_ids, stock_request_2.sudo().picking_ids + ) + self.assertEqual( + stock_request_1.sudo().move_ids, stock_request_2.sudo().move_ids + ) self.env["stock.quant"].create( { "product_id": self.product.id, @@ -700,12 +704,28 @@ class TestStockRequestBase(TestStockRequest): "quantity": 10.0, } ) - picking = stock_request_1.picking_ids[0] - picking.with_user(self.stock_request_manager).action_confirm() - picking.with_user(self.stock_request_manager).action_assign() + picking = stock_request_1.sudo().picking_ids[0] + picking.action_confirm() + picking.action_assign() + self.assertEqual(stock_request_1.qty_in_progress, 4) + self.assertEqual(stock_request_1.qty_done, 0) + self.assertEqual(stock_request_1.qty_cancelled, 0) + self.assertEqual(stock_request_2.qty_in_progress, 6) + self.assertEqual(stock_request_2.qty_done, 0) + self.assertEqual(stock_request_2.qty_cancelled, 0) packout1 = picking.move_line_ids[0] - packout1.qty_done = 10 - picking.with_user(self.stock_request_manager).action_done() + packout1.qty_done = 4 + self.env["stock.backorder.confirmation"].create( + {"pick_ids": [(4, picking.id)]} + ).process_cancel_backorder() + self.assertEqual(stock_request_1.qty_in_progress, 0) + self.assertEqual(stock_request_1.qty_done, 4) + self.assertEqual(stock_request_1.qty_cancelled, 0) + self.assertEqual(stock_request_1.state, "done") + self.assertEqual(stock_request_2.qty_in_progress, 0) + self.assertEqual(stock_request_2.qty_done, 0) + self.assertEqual(stock_request_2.qty_cancelled, 6) + self.assertEqual(stock_request_2.state, "done") def test_cancel_request(self): expected_date = fields.Datetime.now() @@ -993,3 +1013,107 @@ class TestStockRequestBase(TestStockRequest): order = self.request_order.with_user(self.stock_request_user).create(vals) order.stock_request_ids.onchange_warehouse_id() self.assertEqual(order.stock_request_ids[0].location_id, self.virtual_loc) + + def test_cancellation(self): + group = self.env["procurement.group"].create({"name": "Procurement group"}) + product2 = self._create_product("SH2", "Shoes2", False) + product3 = self._create_product("SH3", "Shoes3", False) + self.product.type = "consu" + product2.type = "consu" + product3.type = "consu" + vals = { + "company_id": self.main_company.id, + "warehouse_id": self.warehouse.id, + "location_id": self.virtual_loc.id, + "procurement_group_id": group.id, + "stock_request_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_id": self.product.uom_id.id, + "procurement_group_id": group.id, + "product_uom_qty": 5.0, + "company_id": self.main_company.id, + "warehouse_id": self.warehouse.id, + "location_id": self.virtual_loc.id, + }, + ), + ( + 0, + 0, + { + "product_id": product2.id, + "product_uom_id": self.product.uom_id.id, + "procurement_group_id": group.id, + "product_uom_qty": 5.0, + "company_id": self.main_company.id, + "warehouse_id": self.warehouse.id, + "location_id": self.virtual_loc.id, + }, + ), + ( + 0, + 0, + { + "product_id": product3.id, + "product_uom_id": self.product.uom_id.id, + "procurement_group_id": group.id, + "product_uom_qty": 5.0, + "company_id": self.main_company.id, + "warehouse_id": self.warehouse.id, + "location_id": self.virtual_loc.id, + }, + ), + ], + } + order = self.request_order.create(vals) + self.product.route_ids = [(6, 0, self.route.ids)] + product2.route_ids = [(6, 0, self.route.ids)] + product3.route_ids = [(6, 0, self.route.ids)] + order.action_confirm() + picking = order.picking_ids + self.assertEqual(1, len(picking)) + picking.action_assign() + self.assertEqual(3, len(picking.move_lines)) + line = picking.move_lines.filtered(lambda r: r.product_id == self.product) + line.quantity_done = 1 + sr1 = order.stock_request_ids.filtered(lambda r: r.product_id == self.product) + sr2 = order.stock_request_ids.filtered(lambda r: r.product_id == product2) + sr3 = order.stock_request_ids.filtered(lambda r: r.product_id == product3) + self.assertNotEqual(sr1.state, "done") + self.assertNotEqual(sr2.state, "done") + self.assertNotEqual(sr3.state, "done") + self.env["stock.backorder.confirmation"].create( + {"pick_ids": [(4, picking.id)]} + ).process() + sr1.refresh() + sr2.refresh() + sr3.refresh() + self.assertNotEqual(sr1.state, "done") + self.assertNotEqual(sr2.state, "done") + self.assertNotEqual(sr3.state, "done") + picking = order.picking_ids.filtered( + lambda r: r.state not in ["done", "cancel"] + ) + self.assertEqual(1, len(picking)) + picking.action_assign() + self.assertEqual(3, len(picking.move_lines)) + line = picking.move_lines.filtered(lambda r: r.product_id == self.product) + line.quantity_done = 4 + line = picking.move_lines.filtered(lambda r: r.product_id == product2) + line.quantity_done = 1 + + self.env["stock.backorder.confirmation"].create( + {"pick_ids": [(4, picking.id)]} + ).process_cancel_backorder() + sr1.refresh() + sr2.refresh() + sr3.refresh() + self.assertEqual(sr1.state, "done") + self.assertEqual(sr1.qty_cancelled, 0) + self.assertEqual(sr2.state, "done") + self.assertEqual(sr2.qty_cancelled, 4) + self.assertEqual(sr3.state, "done") + self.assertEqual(sr3.qty_cancelled, 5) diff --git a/stock_request/views/stock_request_order_views.xml b/stock_request/views/stock_request_order_views.xml index b88281161..fd3480065 100644 --- a/stock_request/views/stock_request_order_views.xml +++ b/stock_request/views/stock_request_order_views.xml @@ -166,12 +166,6 @@ - - + @@ -45,11 +46,6 @@ - - - + + +