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 @@ -
-