From 8276a9b51b7c5fc0ae6dd557ae88a9c31f1d1962 Mon Sep 17 00:00:00 2001
From: "Adrien Widart (awt)" <awt@odoo.com>
Date: Mon, 7 Nov 2022 14:57:42 +0000
Subject: [PATCH] [FIX] mrp_subcontracting_dropshipping,purchase_stock: return
 to supplier

When returning a dropshipped and subcontracted product to the supplier
location, the received quantity of the PO line will be incorrect

To reproduce the issue:
1. Create two storable products P_compo, P_Finished
2. Create a BoM:
    - Product: P_finished
    - Type: Subcontracting
    - Subcontractors: a subcontractor S
    - Components: 1 x P_compo
3. Create and confirm a PO:
    - Vendor: S
    - Deliver To: Dropship
    - Drop Ship Address: a partner P
    - Products: 1 x P_finished
4. Validate the receipt
5. Create a return with 1 x P_finished:
    - Update SO/PO quantities: True
    - Return Location: Partner Locations/Vendors
6. Validate the return
7. Go back to the PO

Error: The qty received is 2, it should be 0

There is currently no code to handle the return of a dropshipped product

OPW-3030895

closes odoo/odoo#105185

Signed-off-by: William Henrotin (whe) <whe@odoo.com>
---
 .../models/stock_move.py                      |  4 ++
 .../tests/test_purchase_subcontracting.py     | 38 ++++++++++++++-----
 addons/purchase_stock/models/purchase.py      | 13 +------
 addons/purchase_stock/models/stock.py         |  9 +++++
 4 files changed, 43 insertions(+), 21 deletions(-)

diff --git a/addons/mrp_subcontracting_dropshipping/models/stock_move.py b/addons/mrp_subcontracting_dropshipping/models/stock_move.py
index fd507f22b192..309367f78f19 100644
--- a/addons/mrp_subcontracting_dropshipping/models/stock_move.py
+++ b/addons/mrp_subcontracting_dropshipping/models/stock_move.py
@@ -7,6 +7,10 @@ from odoo import models
 class StockMove(models.Model):
     _inherit = "stock.move"
 
+    def _is_purchase_return(self):
+        res = super()._is_purchase_return()
+        return res or self._is_dropshipped_returned()
+
     def _is_dropshipped(self):
         res = super()._is_dropshipped()
         return res or (
diff --git a/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py b/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py
index 5d7e0d485e60..8395aea162e8 100644
--- a/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py
+++ b/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py
@@ -139,7 +139,8 @@ class TestSubcontractingDropshippingFlows(TestMrpSubcontractingCommon):
         """
         Create and confirm a PO with a subcontracted move. The picking type of
         the PO is 'Dropship' and the delivery address a customer. Then, process
-        a return with the stock location as destination
+        a return with the stock location as destination and another return with
+        the supplier as destination
         """
         subcontractor, client = self.env['res.partner'].create([
             {'name': 'SuperSubcontractor'},
@@ -178,7 +179,7 @@ class TestSubcontractingDropshippingFlows(TestMrpSubcontractingCommon):
             "order_line": [(0, 0, {
                 'product_id': p_finished.id,
                 'name': p_finished.name,
-                'product_qty': 1.0,
+                'product_qty': 2.0,
             })],
         })
         po.button_confirm()
@@ -187,27 +188,46 @@ class TestSubcontractingDropshippingFlows(TestMrpSubcontractingCommon):
         self.assertEqual(mo.picking_type_id, self.warehouse.subcontracting_type_id)
 
         delivery = po.picking_ids
-        delivery.move_line_ids.qty_done = 1.0
+        delivery.move_line_ids.qty_done = 2.0
         delivery.button_validate()
 
         self.assertEqual(delivery.state, 'done')
         self.assertEqual(mo.state, 'done')
-        self.assertEqual(po.order_line.qty_received, 1)
+        self.assertEqual(po.order_line.qty_received, 2)
 
+        # return 1 x P_finished to the stock location
         stock_location = self.warehouse.lot_stock_id
         stock_location.return_location = True
         return_form = Form(self.env['stock.return.picking'].with_context(active_ids=delivery.ids, active_id=delivery.id, active_model='stock.picking'))
+        with return_form.product_return_moves.edit(0) as line:
+            line.quantity = 1.0
         return_form.location_id = stock_location
         return_wizard = return_form.save()
         return_picking_id, _pick_type_id = return_wizard._create_returns()
 
-        delivery_return = self.env['stock.picking'].browse(return_picking_id)
-        delivery_return.move_line_ids.qty_done = 1.0
-        delivery_return.button_validate()
+        delivery_return01 = self.env['stock.picking'].browse(return_picking_id)
+        delivery_return01.move_line_ids.qty_done = 1.0
+        delivery_return01.button_validate()
 
-        self.assertEqual(delivery_return.state, 'done')
+        self.assertEqual(delivery_return01.state, 'done')
         self.assertEqual(p_finished.qty_available, 1, 'One product has been returned to the stock location, so it should be available')
-        self.assertEqual(po.order_line.qty_received, 1, 'One product has been returned to the stock location, so we should still consider it as received')
+        self.assertEqual(po.order_line.qty_received, 2, 'One product has been returned to the stock location, so we should still consider it as received')
+
+        # return 1 x P_finished to the supplier location
+        supplier_location = dropship_picking_type.default_location_src_id
+        return_form = Form(self.env['stock.return.picking'].with_context(active_ids=delivery.ids, active_id=delivery.id, active_model='stock.picking'))
+        with return_form.product_return_moves.edit(0) as line:
+            line.quantity = 1.0
+        return_form.location_id = supplier_location
+        return_wizard = return_form.save()
+        return_picking_id, _pick_type_id = return_wizard._create_returns()
+
+        delivery_return02 = self.env['stock.picking'].browse(return_picking_id)
+        delivery_return02.move_line_ids.qty_done = 1.0
+        delivery_return02.button_validate()
+
+        self.assertEqual(delivery_return02.state, 'done')
+        self.assertEqual(po.order_line.qty_received, 1)
 
     def test_po_to_subcontractor(self):
         """
diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py
index c7b919e644dd..036edef539e2 100644
--- a/addons/purchase_stock/models/purchase.py
+++ b/addons/purchase_stock/models/purchase.py
@@ -294,7 +294,7 @@ class PurchaseOrderLine(models.Model):
                 # the PO. Therefore, we can skip them since they will be handled later on.
                 for move in line.move_ids.filtered(lambda m: m.product_id == line.product_id):
                     if move.state == 'done':
-                        if move.location_dest_id.usage == "supplier":
+                        if move._is_purchase_return():
                             if move.to_refund:
                                 total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP')
                         elif move.origin_returned_move_id and move.origin_returned_move_id._is_dropshipped() and not move._is_dropshipped_returned():
@@ -303,17 +303,6 @@ class PurchaseOrderLine(models.Model):
                             # receive the product physically in our stock. To avoid counting the
                             # quantity twice, we do nothing.
                             pass
-                        elif (
-                            move.location_dest_id.usage == "internal"
-                            and move.location_id.usage != "supplier"
-                            and move.warehouse_id
-                            and move.location_dest_id
-                            not in self.env["stock.location"].search(
-                                [("id", "child_of", move.warehouse_id.view_location_id.id)]
-                            )
-                        ):
-                            if move.to_refund:
-                                total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP')
                         else:
                             total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP')
                 line._track_qty_received(total)
diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py
index 7f131ba3da56..5281b06e215e 100644
--- a/addons/purchase_stock/models/stock.py
+++ b/addons/purchase_stock/models/stock.py
@@ -135,6 +135,15 @@ class StockMove(models.Model):
                 _('Odoo is not able to generate the anglo saxon entries. The total valuation of %s is zero.') % related_aml.product_id.display_name)
         return valuation_price_unit_total, valuation_total_qty
 
+    def _is_purchase_return(self):
+        self.ensure_one()
+        return self.location_dest_id.usage == "supplier" or (
+                self.location_dest_id.usage == "internal"
+                and self.location_id.usage != "supplier"
+                and self.warehouse_id
+                and self.location_dest_id not in self.env["stock.location"].search([("id", "child_of", self.warehouse_id.view_location_id.id)])
+        )
+
 
 class StockWarehouse(models.Model):
     _inherit = 'stock.warehouse'
-- 
GitLab