Skip to content

Commit dd8f91b

Browse files
committed
Merge PR #904 into 18.0
Signed-off-by pedrobaeza
2 parents 541a517 + ddd8cc8 commit dd8f91b

File tree

4 files changed

+248
-4
lines changed

4 files changed

+248
-4
lines changed

purchase_sale_stock_inter_company/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
from . import res_config
44
from . import stock_picking
55
from . import stock_move_line
6+
from . import sale_order_line
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2025 Tecnativa - Carlos Lopez
2+
from odoo import models
3+
4+
5+
class SaleOrderLine(models.Model):
6+
_inherit = "sale.order.line"
7+
8+
def _get_location_final(self):
9+
partner = self.order_id.partner_id
10+
commercial_partner = partner.commercial_partner_id
11+
Company = self.env["res.company"].sudo()
12+
dest_company = Company.search(
13+
[("partner_id", "parent_of", commercial_partner.id)], limit=1
14+
)
15+
# Check if the partner belongs to another company and use its customer location
16+
# instead of the partner's shipping_id location.
17+
if (
18+
dest_company
19+
and dest_company != self.company_id
20+
and partner != self.order_id.partner_shipping_id
21+
):
22+
return partner.with_company(self.company_id).property_stock_customer
23+
return super()._get_location_final()

purchase_sale_stock_inter_company/models/stock_picking.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,14 @@ def _action_done_intercompany_actions(self, purchase):
102102
)
103103
)
104104
po_move_lines = po_move_pending.move_line_ids
105-
if not po_move_lines:
105+
# Don’t raise an error
106+
# if there are no move_line_ids and the location is transit.
107+
# In vendor locations, reservations are bypassed,
108+
# but in transit locations,
109+
# we need to create the move lines to assign lots/serials.
110+
if not po_move_pending or (
111+
po_move_lines and move.location_dest_id.usage != "transit"
112+
):
106113
raise UserError(
107114
_(
108115
"There's no corresponding line in PO %(po)s for assigning "
@@ -206,7 +213,7 @@ def _is_intercompany_reception(self):
206213
:return: bool
207214
"""
208215
return (
209-
self.location_id.usage == "supplier"
216+
self.location_id.usage in ["supplier", "transit"]
210217
and self.purchase_id.sudo().intercompany_sale_order_id
211218
)
212219

@@ -216,6 +223,6 @@ def _is_intercompany_delivery(self):
216223
:return: bool
217224
"""
218225
return (
219-
self.location_dest_id.usage == "customer"
226+
self.location_dest_id.usage in ["customer", "transit"]
220227
and self.sale_id.sudo().auto_purchase_order_id
221228
)

purchase_sale_stock_inter_company/tests/test_inter_company_purchase_sale_stock.py

Lines changed: 214 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,99 @@ def test_sync_picking_lot(self):
409409
po_picking_id.mapped("move_ids.move_line_ids.lot_id.name"),
410410
)
411411

412+
def test_sync_picking_lot_with_transit_location(self):
413+
"""
414+
Test that the lot is synchronized on the moves
415+
when using inter-company transit locations
416+
company B: Sale picking from Stock to Transit Location
417+
company A: Purchase picking from Transit Location to Stock
418+
"""
419+
self.company_a.sync_picking = True
420+
self.company_b.sync_picking = True
421+
# Set inter-company locations on partners
422+
interco_location = self.env.ref("stock.stock_location_inter_company")
423+
self.partner_company_b.with_company(self.company_a).write(
424+
{
425+
"property_stock_customer": interco_location.id,
426+
"property_stock_supplier": interco_location.id,
427+
}
428+
)
429+
self.partner_company_a.with_company(self.company_b).write(
430+
{
431+
"property_stock_customer": interco_location.id,
432+
"property_stock_supplier": interco_location.id,
433+
}
434+
)
435+
436+
purchase = self._create_purchase_order(
437+
self.partner_company_b, self.stockable_product_serial
438+
)
439+
sale = self._approve_po(purchase)
440+
441+
# validate the SO picking
442+
po_picking_id = purchase.picking_ids
443+
so_picking_id = sale.picking_ids
444+
445+
so_move = so_picking_id.move_ids
446+
so_move.move_line_ids = [
447+
Command.clear(),
448+
Command.create(
449+
{
450+
"location_id": so_move.location_id.id,
451+
"location_dest_id": so_move.location_dest_id.id,
452+
"product_id": self.stockable_product_serial.id,
453+
"product_uom_id": self.stockable_product_serial.uom_id.id,
454+
"quantity": 1,
455+
"lot_id": self.serial_1.id,
456+
"picking_id": so_picking_id.id,
457+
},
458+
),
459+
Command.create(
460+
{
461+
"location_id": so_move.location_id.id,
462+
"location_dest_id": so_move.location_dest_id.id,
463+
"product_id": self.stockable_product_serial.id,
464+
"product_uom_id": self.stockable_product_serial.uom_id.id,
465+
"quantity": 1,
466+
"lot_id": self.serial_2.id,
467+
"picking_id": so_picking_id.id,
468+
},
469+
),
470+
Command.create(
471+
{
472+
"location_id": so_move.location_id.id,
473+
"location_dest_id": so_move.location_dest_id.id,
474+
"product_id": self.stockable_product_serial.id,
475+
"product_uom_id": self.stockable_product_serial.uom_id.id,
476+
"quantity": 1,
477+
"lot_id": self.serial_3.id,
478+
"picking_id": so_picking_id.id,
479+
},
480+
),
481+
]
482+
so_picking_id.button_validate()
483+
self.assertEqual(so_picking_id.location_id.usage, "internal")
484+
self.assertEqual(so_picking_id.location_dest_id.usage, "transit")
485+
self.assertEqual(po_picking_id.location_id.usage, "transit")
486+
self.assertEqual(po_picking_id.location_dest_id.usage, "internal")
487+
488+
so_lots = so_move.mapped("move_line_ids.lot_id")
489+
po_lots = po_picking_id.mapped("move_ids.move_line_ids.lot_id")
490+
self.assertEqual(
491+
len(so_lots),
492+
len(po_lots),
493+
msg="There aren't the same number of lots on both moves",
494+
)
495+
self.assertEqual(
496+
so_lots, po_lots, msg="The lots of the moves should be the same"
497+
)
498+
self.assertEqual(
499+
so_lots.mapped("name"),
500+
po_lots.mapped("name"),
501+
msg="The lots should have the same name in both moves",
502+
)
503+
self.assertFalse(so_lots.company_id, msg="Lots should not have a company.")
504+
412505
def test_sync_picking_same_product_multiple_lines(self):
413506
"""
414507
Picking synchronization should work even when there
@@ -533,7 +626,7 @@ def test_raise_picking_problem(self):
533626
# Set quantities done on the picking and validate
534627
for move in so_picking_id.move_ids:
535628
move.quantity = move.product_uom_qty
536-
with self.assertRaises(UserError):
629+
with self.assertRaisesRegex(UserError, "There's no corresponding line in PO"):
537630
so_picking_id.button_validate()
538631

539632
def test_sync_picking_multi_step(self):
@@ -609,3 +702,123 @@ def test_sync_picking_multi_step(self):
609702
new_receipt_picking = done_purchase_picking._get_next_transfers()
610703
self.assertEqual(len(new_receipt_picking), 1)
611704
self.assertEqual(new_receipt_picking.state, "assigned")
705+
706+
def test_sync_picking_multi_step_with_transit(self):
707+
"""
708+
Test that the lot is synchronized on the moves
709+
when using inter-company transit locations
710+
and warehouses are configured with multi-step routes.
711+
company B: Sale picking
712+
Picking 1: from Stock to Packing
713+
Picking 2: from Packing to Transit Location
714+
company A: Purchase picking
715+
Picking 1: from Transit Location to Input
716+
Picking 2: from Input to Stock
717+
"""
718+
self.company_a.sync_picking = True
719+
self.warehouse_a.reception_steps = "two_steps"
720+
self.company_b.sync_picking = True
721+
self.warehouse_c.delivery_steps = "pick_ship"
722+
# Set inter-company locations on partners
723+
interco_location = self.env.ref("stock.stock_location_inter_company")
724+
self.partner_company_b.with_company(self.company_a).write(
725+
{
726+
"property_stock_customer": interco_location.id,
727+
"property_stock_supplier": interco_location.id,
728+
}
729+
)
730+
self.partner_company_a.with_company(self.company_b).write(
731+
{
732+
"property_stock_customer": interco_location.id,
733+
"property_stock_supplier": interco_location.id,
734+
}
735+
)
736+
purchase = self._create_purchase_order(
737+
self.partner_company_b, self.stockable_product_serial
738+
)
739+
sale = self._approve_po(purchase)
740+
self.assertEqual(len(purchase.picking_ids), 1)
741+
# Only a single picking is created for the sale.
742+
# When this picking is validated, two pickings should be created:
743+
# one for the backorder and one for the delivery.
744+
self.assertEqual(len(sale.picking_ids), 1)
745+
# Check the locations
746+
self.assertEqual(purchase.picking_ids.location_id.usage, "transit")
747+
self.assertEqual(purchase.picking_ids.location_dest_id.usage, "internal")
748+
self.assertEqual(sale.picking_ids.location_id.usage, "internal")
749+
self.assertEqual(sale.picking_ids.location_dest_id.usage, "internal")
750+
self.assertEqual(sale.picking_ids.move_ids.location_final_id.usage, "transit")
751+
# validate the SO internal picking
752+
so_internal_pick = sale.picking_ids
753+
so_move = so_internal_pick.move_ids
754+
so_move.move_line_ids = [
755+
Command.clear(),
756+
Command.create(
757+
{
758+
"location_id": so_move.location_id.id,
759+
"location_dest_id": so_move.location_dest_id.id,
760+
"product_id": self.stockable_product_serial.id,
761+
"product_uom_id": self.stockable_product_serial.uom_id.id,
762+
"quantity": 1,
763+
"lot_id": self.serial_1.id,
764+
"picking_id": so_internal_pick.id,
765+
},
766+
),
767+
Command.create(
768+
{
769+
"location_id": so_move.location_id.id,
770+
"location_dest_id": so_move.location_dest_id.id,
771+
"product_id": self.stockable_product_serial.id,
772+
"product_uom_id": self.stockable_product_serial.uom_id.id,
773+
"quantity": 1,
774+
"lot_id": self.serial_2.id,
775+
"picking_id": so_internal_pick.id,
776+
},
777+
),
778+
Command.create(
779+
{
780+
"location_id": so_move.location_id.id,
781+
"location_dest_id": so_move.location_dest_id.id,
782+
"product_id": self.stockable_product_serial.id,
783+
"product_uom_id": self.stockable_product_serial.uom_id.id,
784+
"quantity": 1,
785+
"lot_id": self.serial_3.id,
786+
"picking_id": so_internal_pick.id,
787+
},
788+
),
789+
]
790+
so_internal_pick.with_user(self.user_company_b).button_validate()
791+
self.assertEqual(so_internal_pick.state, "done")
792+
po_picking = purchase.picking_ids
793+
# check po_picking state
794+
self.assertEqual(po_picking.state, "waiting")
795+
# validate the SO picking
796+
so_picking = sale.picking_ids.filtered(
797+
lambda x: x.location_dest_id.usage == "customer"
798+
)
799+
so_picking.with_user(self.user_company_b).button_validate()
800+
self.assertEqual(so_picking.state, "done")
801+
# The location at the picking level is set to Customer by the operation type,
802+
# but at the move level, it is Transit.
803+
self.assertEqual(so_picking.location_dest_id.usage, "customer")
804+
self.assertEqual(so_picking.move_ids.location_dest_id.usage, "transit")
805+
# the move in the receipt should have a "next move" due to "two_steps"
806+
self.assertTrue(purchase.picking_ids.move_ids.move_dest_ids)
807+
self.assertEqual(len(sale.picking_ids), 2) # Pick + Delivery
808+
# Quantities should have been synced
809+
self.assertEqual(
810+
po_picking.move_ids.quantity,
811+
so_picking.move_ids.quantity,
812+
)
813+
# Check picking state
814+
self.assertEqual(po_picking.state, "done")
815+
new_receipt_picking = po_picking._get_next_transfers()
816+
self.assertEqual(len(new_receipt_picking), 1)
817+
self.assertEqual(new_receipt_picking.state, "assigned")
818+
# check the lots
819+
so_lots = so_move.mapped("move_line_ids.lot_id")
820+
po_lots = po_picking.mapped("move_ids.move_line_ids.lot_id")
821+
self.assertEqual(
822+
so_lots, po_lots, msg="The lots of the moves should be the same"
823+
)
824+
self.assertFalse(so_lots.company_id, msg="Lots should not have a company.")

0 commit comments

Comments
 (0)