diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py
index fb362a4..483dc84 100755
--- a/modules/purchase_trade/__init__.py
+++ b/modules/purchase_trade/__init__.py
@@ -192,6 +192,7 @@ def register():
lot.LotContext,
lot.LotShippingStart,
lot.LotPlanTransportStart,
+ lot.LotPlanTransportLine,
lot.LotMatchingStart,
lot.LotWeighingStart,
lot.LotAddLot,
diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py
index e67921d..78c3cbf 100755
--- a/modules/purchase_trade/lot.py
+++ b/modules/purchase_trade/lot.py
@@ -3235,61 +3235,65 @@ class LotPlanTransport(Wizard):
def default_plan(self, fields):
if not self.records:
return {}
- record = self.records[0]
- quantity = record.r_lot_quantity or record.r_lot_matched or 0
- values = {
- 'quantity': quantity,
- 'unit': record.r_lot_unit_line.id if record.r_lot_unit_line else None,
- 'cancel_plan': bool(
- record.r_planned_from_location
- or record.r_planned_to_location
- or record.r_planned_transport_type
- or record.r_planned_from_date
- or record.r_planned_to_date
- or record.r_planned_note),
- 'planned_from_location': (
- record.r_planned_from_location.id
- if record.r_planned_from_location else None),
- 'planned_to_location': (
- record.r_planned_to_location.id
- if record.r_planned_to_location else None),
- 'planned_transport_type': record.r_planned_transport_type,
- 'planned_from_date': record.r_planned_from_date,
- 'planned_to_date': record.r_planned_to_date,
- 'planned_note': record.r_planned_note,
- }
- return values
+ lines = []
+ for record in self.records:
+ quantity = record.r_lot_quantity or record.r_lot_matched or 0
+ if record.id < 10000000:
+ raise UserError(
+ "Only open quantity lines can be planned.")
+ lines.append({
+ 'lotqt': record.id - 10000000,
+ 'quantity': quantity,
+ 'unit': (
+ record.r_lot_unit_line.id
+ if record.r_lot_unit_line else None),
+ 'cancel_plan': bool(
+ record.r_planned_from_location
+ or record.r_planned_to_location
+ or record.r_planned_transport_type
+ or record.r_planned_from_date
+ or record.r_planned_to_date
+ or record.r_planned_note),
+ 'planned_from_location': (
+ record.r_planned_from_location.id
+ if record.r_planned_from_location else None),
+ 'planned_to_location': (
+ record.r_planned_to_location.id
+ if record.r_planned_to_location else None),
+ 'planned_transport_type': record.r_planned_transport_type,
+ 'planned_from_date': record.r_planned_from_date,
+ 'planned_to_date': record.r_planned_to_date,
+ 'planned_note': record.r_planned_note,
+ })
+ return {'lines': lines}
def transition_planning(self):
Lot = Pool().get('lot.lot')
LotQt = Pool().get('lot.qt')
affected_lines = []
- if len(self.records) != 1:
- raise UserError("Please select exactly one quantity line to plan.")
- plan_values = {
- 'planned_from_location': (
- self.plan.planned_from_location.id
- if self.plan.planned_from_location else None),
- 'planned_to_location': (
- self.plan.planned_to_location.id
- if self.plan.planned_to_location else None),
- 'planned_transport_type': self.plan.planned_transport_type,
- 'planned_from_date': self.plan.planned_from_date,
- 'planned_to_date': self.plan.planned_to_date,
- 'planned_note': self.plan.planned_note,
- }
+ if not self.plan.lines:
+ raise UserError("Please select at least one quantity line to plan.")
with Lot.skip_quantity_consistency():
- for record in self.records:
- if record.id < 10000000:
- raise UserError(
- "Only open quantity lines can be planned.")
- lotqt = LotQt(record.id - 10000000)
+ for line in self.plan.lines:
+ lotqt = line.lotqt
+ plan_values = {
+ 'planned_from_location': (
+ line.planned_from_location.id
+ if line.planned_from_location else None),
+ 'planned_to_location': (
+ line.planned_to_location.id
+ if line.planned_to_location else None),
+ 'planned_transport_type': line.planned_transport_type,
+ 'planned_from_date': line.planned_from_date,
+ 'planned_to_date': line.planned_to_date,
+ 'planned_note': line.planned_note,
+ }
self._append_lotqt_affected_lines(lotqt, affected_lines)
- if self.plan.cancel_plan:
+ if line.cancel_plan:
LotQt.cancel_transport_plan(lotqt)
else:
LotQt.plan_to_transport(
- lotqt, self.plan.quantity, plan_values)
+ lotqt, line.quantity, plan_values)
Lot.assert_lines_quantity_consistency(affected_lines)
return 'end'
@@ -3301,6 +3305,16 @@ class LotPlanTransportStart(ModelView):
"Plan lot to transport"
__name__ = "lot.plan_transport.start"
+ lines = fields.One2Many(
+ 'lot.plan_transport.line', 'plan', "Planning lines")
+
+
+class LotPlanTransportLine(ModelView):
+ "Plan lot to transport line"
+ __name__ = "lot.plan_transport.line"
+
+ plan = fields.Many2One('lot.plan_transport.start', "Plan")
+ lotqt = fields.Many2One('lot.qt', "Quantity line", readonly=True)
cancel_plan = fields.Boolean("Cancel existing plan")
planned_from_location = fields.Many2One(
'stock.location', "Planned From",
diff --git a/modules/purchase_trade/lot.xml b/modules/purchase_trade/lot.xml
index ac378ae..b8e0e29 100755
--- a/modules/purchase_trade/lot.xml
+++ b/modules/purchase_trade/lot.xml
@@ -123,6 +123,11 @@ this repository contains the full copyright notices and license terms. -->
form
lot_plan_transport_start_form
+
+ lot.plan_transport.line
+ tree
+ lot_plan_transport_line_tree
+
🚢 Link lots to transport
lot.shipping
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index a714ad6..587840e 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -206,6 +206,106 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(affected_lines, [purchase_line, sale_line])
+ def test_plan_transport_defaults_multiple_lines(self):
+ 'plan transport wizard opens one planning line per selected quantity'
+ wizard = lot_module.LotPlanTransport()
+ wizard.records = [
+ Mock(
+ id=10000001,
+ r_lot_quantity=Decimal('100'),
+ r_lot_matched=Decimal('0'),
+ r_lot_unit_line=Mock(id=3),
+ r_planned_from_location=Mock(id=10),
+ r_planned_to_location=Mock(id=20),
+ r_planned_transport_type='vessel',
+ r_planned_from_date=datetime.date(2026, 7, 1),
+ r_planned_to_date=datetime.date(2026, 7, 5),
+ r_planned_note='First leg'),
+ Mock(
+ id=10000002,
+ r_lot_quantity=Decimal('200'),
+ r_lot_matched=Decimal('0'),
+ r_lot_unit_line=Mock(id=4),
+ r_planned_from_location=None,
+ r_planned_to_location=None,
+ r_planned_transport_type=None,
+ r_planned_from_date=None,
+ r_planned_to_date=None,
+ r_planned_note=None),
+ ]
+
+ values = wizard.default_plan(None)
+
+ self.assertEqual(len(values['lines']), 2)
+ self.assertEqual(values['lines'][0]['lotqt'], 1)
+ self.assertEqual(values['lines'][0]['quantity'], Decimal('100'))
+ self.assertEqual(values['lines'][0]['planned_from_location'], 10)
+ self.assertTrue(values['lines'][0]['cancel_plan'])
+ self.assertEqual(values['lines'][1]['lotqt'], 2)
+ self.assertEqual(values['lines'][1]['quantity'], Decimal('200'))
+ self.assertFalse(values['lines'][1]['cancel_plan'])
+
+ def test_plan_transport_applies_each_planning_line(self):
+ 'plan transport wizard applies each line independently'
+ wizard = lot_module.LotPlanTransport()
+ lotqt_a = Mock(
+ lot_p=Mock(line=Mock()),
+ lot_s=None)
+ lotqt_b = Mock(
+ lot_p=None,
+ lot_s=Mock(sale_line=Mock()))
+ wizard.plan = Mock(lines=[
+ Mock(
+ lotqt=lotqt_a,
+ cancel_plan=False,
+ quantity=Decimal('100'),
+ planned_from_location=Mock(id=10),
+ planned_to_location=Mock(id=20),
+ planned_transport_type='vessel',
+ planned_from_date=datetime.date(2026, 7, 1),
+ planned_to_date=datetime.date(2026, 7, 5),
+ planned_note='A'),
+ Mock(
+ lotqt=lotqt_b,
+ cancel_plan=False,
+ quantity=Decimal('200'),
+ planned_from_location=Mock(id=11),
+ planned_to_location=Mock(id=21),
+ planned_transport_type='truck',
+ planned_from_date=datetime.date(2026, 7, 6),
+ planned_to_date=datetime.date(2026, 7, 8),
+ planned_note='B'),
+ ])
+ Lot = Mock()
+ Lot.skip_quantity_consistency.return_value.__enter__ = Mock()
+ Lot.skip_quantity_consistency.return_value.__exit__ = Mock()
+ LotQt = Mock()
+ pool = Mock()
+ pool.get.side_effect = [Lot, LotQt]
+
+ with patch('trytond.modules.purchase_trade.lot.Pool',
+ return_value=pool):
+ state = wizard.transition_planning()
+
+ self.assertEqual(state, 'end')
+ LotQt.plan_to_transport.assert_any_call(lotqt_a, Decimal('100'), {
+ 'planned_from_location': 10,
+ 'planned_to_location': 20,
+ 'planned_transport_type': 'vessel',
+ 'planned_from_date': datetime.date(2026, 7, 1),
+ 'planned_to_date': datetime.date(2026, 7, 5),
+ 'planned_note': 'A',
+ })
+ LotQt.plan_to_transport.assert_any_call(lotqt_b, Decimal('200'), {
+ 'planned_from_location': 11,
+ 'planned_to_location': 21,
+ 'planned_transport_type': 'truck',
+ 'planned_from_date': datetime.date(2026, 7, 6),
+ 'planned_to_date': datetime.date(2026, 7, 8),
+ 'planned_note': 'B',
+ })
+ Lot.assert_lines_quantity_consistency.assert_called_once()
+
def test_lot_shipping_defaults_planned_locations(self):
'link to transport carries planned locations into shipment create'
wizard = lot_module.LotShipping()
diff --git a/modules/purchase_trade/view/lot_plan_transport_line_tree.xml b/modules/purchase_trade/view/lot_plan_transport_line_tree.xml
new file mode 100644
index 0000000..74bce35
--- /dev/null
+++ b/modules/purchase_trade/view/lot_plan_transport_line_tree.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/purchase_trade/view/lot_plan_transport_start_form.xml b/modules/purchase_trade/view/lot_plan_transport_start_form.xml
index c95d176..fc5b850 100644
--- a/modules/purchase_trade/view/lot_plan_transport_start_form.xml
+++ b/modules/purchase_trade/view/lot_plan_transport_start_form.xml
@@ -1,20 +1,4 @@
-