From b62e6d45d4fc71e5067c25b406f7d429d977692c Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Tue, 20 Sep 2022 07:40:21 +0000 Subject: [PATCH] [FIX] purchase_stock, mrp_subcontracting_dropshipping: dropship to subcontract A user can not buy a subcontracted product and directly deliver it (dropship) to another subcontractor. Moreover, in such situation, the received quantity is not correctly computed. To reproduce the issue: (Enable debug mode) 1. In Settings, enable "Storage Locations" 2. Create three products: - P1: - Storable - With a vendor V1 - P2: - Storable - With a vendor V2 - P3: - Consumable 3. Edit V1: - Customer Location: Physical Locations/Subcontracting Location 4. Create two BoMs: - Product: P1 - Type: Subcontracting - Subcontractors: V1 - Components: 1 x P2 - Product: P2 - Type: Subcontracting - Subcontractors: V2 - Components: 1 x P3 5. Create a PO: - Vendor: V2 - Deliver To: Dropship - Drop Ship Address: V1 - Products: 1 x P2 6. Confirm the PO Error: a Validation Error is raised at `mrp.production` creation because of a missing field (`picking_type_id`). In some cases, when getting the values to create the MO, the basic `_prepare_subcontract_mo_vals` does not return any `picking_type_id`. That's the reason why an override has been added in `/mrp_subcontracting_dropshipping` (see [1] for more details). Thanks to this override, if the usage of the destination location is `customer`, we know that we are in a "dropship" situation and we manually define the `picking_type_id`: https://github.com/odoo/odoo/blob/d73e70f22e47e81e59aff0c9f578aff260447256/addons/mrp_subcontracting_dropshipping/models/stock_picking.py#L15-L17 However, in the above case, the subcontracted stock move starts from a subcontracted location and also goes to a subcontracted location (because of step 3). As a result, the if-condition is not respected and the `picking_type_id` is not defined. Once this issue is solved, there is a second one: suppose the PO confirmed. The user validates the transfer. New error: the received quantity on the PO is not updated. This is because of an incorrect condition in `/purchase_stock._compute_qty_received`: https://github.com/odoo/odoo/blob/d0537e32e5aa4b0fe2ad674ab3ec7c42ae1a12f9/addons/purchase_stock/models/purchase.py#L306-L315 Added by [2] and modified by [3], this condition checks that: - the destination location usage is `internal` (correct, this is a subcontracting location) - the source location usage is not `supplier` (correct, this is a subcontracting location, so it is `internal`) - the destination location is not part of the warehouse children (here is the issue: the SM does not have any warehouse, so it gives a false positive) So, because of the incorrect third condition, the condition is respected. And because `to_refund` is `False` (which is correct), we don't do anything. That's the reason why the received quantity is not updated. [1] d73e70f22e47e81e59aff0c9f578aff260447256 [2] e6a1e240f18bbc95475cd82154bb91a455704b15 [3] fd22fe221026e353aac7414348b029ae7e290b2f OPW-2922546 closes odoo/odoo#102224 X-original-commit: a51b4158b964a11fa371a9eb8c877c9ec395f412 Signed-off-by: Arnold Moyaux (arm) Signed-off-by: Adrien Widart --- .../models/stock_picking.py | 5 +- .../tests/test_purchase_subcontracting.py | 72 +++++++++++++++++++ addons/purchase_stock/models/purchase.py | 1 + 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/addons/mrp_subcontracting_dropshipping/models/stock_picking.py b/addons/mrp_subcontracting_dropshipping/models/stock_picking.py index b81c9cd2819..4ae776d4e5d 100644 --- a/addons/mrp_subcontracting_dropshipping/models/stock_picking.py +++ b/addons/mrp_subcontracting_dropshipping/models/stock_picking.py @@ -24,7 +24,10 @@ class StockPicking(models.Model): def _prepare_subcontract_mo_vals(self, subcontract_move, bom): res = super()._prepare_subcontract_mo_vals(subcontract_move, bom) - if not res.get('picking_type_id') and subcontract_move.location_dest_id.usage == 'customer': + if not res.get('picking_type_id') and ( + subcontract_move.location_dest_id.usage == 'customer' + or subcontract_move.location_dest_id.is_subcontracting_location + ): # If the if-condition is respected, it means that `subcontract_move` is not # related to a specific warehouse. This can happen if, for instance, the user # confirms a PO with a subcontracted product that should be delivered to a diff --git a/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py b/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py index 769b9f33070..63534e5e4e9 100644 --- a/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py +++ b/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py @@ -248,3 +248,75 @@ class TestSubcontractingDropshippingFlows(TestMrpSubcontractingCommon): self.assertEqual(delivery.state, 'done') self.assertEqual(mo.state, 'done') self.assertEqual(po.order_line.qty_received, 1) + + def test_po_to_subcontractor(self): + """ + Create and confirm a PO with a subcontracted move. The bought product is + also a component of another subcontracted product. The picking type of + the PO is 'Dropship' and the delivery address is the other subcontractor + """ + subcontractor, super_subcontractor = self.env['res.partner'].create([ + {'name': 'Subcontractor'}, + {'name': 'SuperSubcontractor'}, + ]) + + super_product, product, component = self.env['product.product'].create([{ + 'name': 'Super Product', + 'type': 'product', + 'seller_ids': [(0, 0, {'partner_id': super_subcontractor.id})], + }, { + 'name': 'Product', + 'type': 'product', + 'seller_ids': [(0, 0, {'partner_id': subcontractor.id})], + }, { + 'name': 'Component', + 'type': 'consu', + }]) + + _, bom_product = self.env['mrp.bom'].create([{ + 'product_tmpl_id': super_product.product_tmpl_id.id, + 'product_qty': 1, + 'type': 'subcontract', + 'subcontractor_ids': [(6, 0, super_subcontractor.ids)], + 'bom_line_ids': [ + (0, 0, {'product_id': product.id, 'product_qty': 1}), + ], + }, { + 'product_tmpl_id': product.product_tmpl_id.id, + 'product_qty': 1, + 'type': 'subcontract', + 'subcontractor_ids': [(6, 0, subcontractor.ids)], + 'bom_line_ids': [ + (0, 0, {'product_id': component.id, 'product_qty': 1}), + ], + }]) + + dropship_picking_type = self.env['stock.picking.type'].search([ + ('company_id', '=', self.env.company.id), + ('default_location_src_id.usage', '=', 'supplier'), + ('default_location_dest_id.usage', '=', 'customer'), + ], limit=1, order='sequence') + + po = self.env['purchase.order'].create({ + "partner_id": subcontractor.id, + "picking_type_id": dropship_picking_type.id, + "dest_address_id": super_subcontractor.id, + "order_line": [(0, 0, { + 'product_id': product.id, + 'name': product.name, + 'product_qty': 1.0, + })], + }) + po.button_confirm() + + mo = self.env['mrp.production'].search([('bom_id', '=', bom_product.id)]) + self.assertEqual(mo.picking_type_id, self.warehouse.subcontracting_type_id) + + delivery = po.picking_ids + self.assertEqual(delivery.location_dest_id, super_subcontractor.property_stock_subcontractor) + self.assertTrue(delivery.is_dropship) + + delivery.move_line_ids.qty_done = 1.0 + delivery.button_validate() + + self.assertEqual(po.order_line.qty_received, 1.0) diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py index ec47a056c67..a3a1dce3b40 100644 --- a/addons/purchase_stock/models/purchase.py +++ b/addons/purchase_stock/models/purchase.py @@ -331,6 +331,7 @@ class PurchaseOrderLine(models.Model): 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)]