Planned
This commit is contained in:
@@ -192,6 +192,7 @@ def register():
|
|||||||
lot.LotContext,
|
lot.LotContext,
|
||||||
lot.LotShippingStart,
|
lot.LotShippingStart,
|
||||||
lot.LotPlanTransportStart,
|
lot.LotPlanTransportStart,
|
||||||
|
lot.LotPlanTransportLine,
|
||||||
lot.LotMatchingStart,
|
lot.LotMatchingStart,
|
||||||
lot.LotWeighingStart,
|
lot.LotWeighingStart,
|
||||||
lot.LotAddLot,
|
lot.LotAddLot,
|
||||||
|
|||||||
@@ -3235,61 +3235,65 @@ class LotPlanTransport(Wizard):
|
|||||||
def default_plan(self, fields):
|
def default_plan(self, fields):
|
||||||
if not self.records:
|
if not self.records:
|
||||||
return {}
|
return {}
|
||||||
record = self.records[0]
|
lines = []
|
||||||
quantity = record.r_lot_quantity or record.r_lot_matched or 0
|
for record in self.records:
|
||||||
values = {
|
quantity = record.r_lot_quantity or record.r_lot_matched or 0
|
||||||
'quantity': quantity,
|
if record.id < 10000000:
|
||||||
'unit': record.r_lot_unit_line.id if record.r_lot_unit_line else None,
|
raise UserError(
|
||||||
'cancel_plan': bool(
|
"Only open quantity lines can be planned.")
|
||||||
record.r_planned_from_location
|
lines.append({
|
||||||
or record.r_planned_to_location
|
'lotqt': record.id - 10000000,
|
||||||
or record.r_planned_transport_type
|
'quantity': quantity,
|
||||||
or record.r_planned_from_date
|
'unit': (
|
||||||
or record.r_planned_to_date
|
record.r_lot_unit_line.id
|
||||||
or record.r_planned_note),
|
if record.r_lot_unit_line else None),
|
||||||
'planned_from_location': (
|
'cancel_plan': bool(
|
||||||
record.r_planned_from_location.id
|
record.r_planned_from_location
|
||||||
if record.r_planned_from_location else None),
|
or record.r_planned_to_location
|
||||||
'planned_to_location': (
|
or record.r_planned_transport_type
|
||||||
record.r_planned_to_location.id
|
or record.r_planned_from_date
|
||||||
if record.r_planned_to_location else None),
|
or record.r_planned_to_date
|
||||||
'planned_transport_type': record.r_planned_transport_type,
|
or record.r_planned_note),
|
||||||
'planned_from_date': record.r_planned_from_date,
|
'planned_from_location': (
|
||||||
'planned_to_date': record.r_planned_to_date,
|
record.r_planned_from_location.id
|
||||||
'planned_note': record.r_planned_note,
|
if record.r_planned_from_location else None),
|
||||||
}
|
'planned_to_location': (
|
||||||
return values
|
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):
|
def transition_planning(self):
|
||||||
Lot = Pool().get('lot.lot')
|
Lot = Pool().get('lot.lot')
|
||||||
LotQt = Pool().get('lot.qt')
|
LotQt = Pool().get('lot.qt')
|
||||||
affected_lines = []
|
affected_lines = []
|
||||||
if len(self.records) != 1:
|
if not self.plan.lines:
|
||||||
raise UserError("Please select exactly one quantity line to plan.")
|
raise UserError("Please select at least 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,
|
|
||||||
}
|
|
||||||
with Lot.skip_quantity_consistency():
|
with Lot.skip_quantity_consistency():
|
||||||
for record in self.records:
|
for line in self.plan.lines:
|
||||||
if record.id < 10000000:
|
lotqt = line.lotqt
|
||||||
raise UserError(
|
plan_values = {
|
||||||
"Only open quantity lines can be planned.")
|
'planned_from_location': (
|
||||||
lotqt = LotQt(record.id - 10000000)
|
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)
|
self._append_lotqt_affected_lines(lotqt, affected_lines)
|
||||||
if self.plan.cancel_plan:
|
if line.cancel_plan:
|
||||||
LotQt.cancel_transport_plan(lotqt)
|
LotQt.cancel_transport_plan(lotqt)
|
||||||
else:
|
else:
|
||||||
LotQt.plan_to_transport(
|
LotQt.plan_to_transport(
|
||||||
lotqt, self.plan.quantity, plan_values)
|
lotqt, line.quantity, plan_values)
|
||||||
Lot.assert_lines_quantity_consistency(affected_lines)
|
Lot.assert_lines_quantity_consistency(affected_lines)
|
||||||
return 'end'
|
return 'end'
|
||||||
|
|
||||||
@@ -3301,6 +3305,16 @@ class LotPlanTransportStart(ModelView):
|
|||||||
"Plan lot to transport"
|
"Plan lot to transport"
|
||||||
__name__ = "lot.plan_transport.start"
|
__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")
|
cancel_plan = fields.Boolean("Cancel existing plan")
|
||||||
planned_from_location = fields.Many2One(
|
planned_from_location = fields.Many2One(
|
||||||
'stock.location', "Planned From",
|
'stock.location', "Planned From",
|
||||||
|
|||||||
@@ -123,6 +123,11 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="type">form</field>
|
<field name="type">form</field>
|
||||||
<field name="name">lot_plan_transport_start_form</field>
|
<field name="name">lot_plan_transport_start_form</field>
|
||||||
</record>
|
</record>
|
||||||
|
<record model="ir.ui.view" id="plan_transport_line_view_tree">
|
||||||
|
<field name="model">lot.plan_transport.line</field>
|
||||||
|
<field name="type">tree</field>
|
||||||
|
<field name="name">lot_plan_transport_line_tree</field>
|
||||||
|
</record>
|
||||||
<record model="ir.action.wizard" id="act_shipping">
|
<record model="ir.action.wizard" id="act_shipping">
|
||||||
<field name="name">🚢 Link lots to transport</field>
|
<field name="name">🚢 Link lots to transport</field>
|
||||||
<field name="wiz_name">lot.shipping</field>
|
<field name="wiz_name">lot.shipping</field>
|
||||||
|
|||||||
@@ -206,6 +206,106 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
|
|
||||||
self.assertEqual(affected_lines, [purchase_line, sale_line])
|
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):
|
def test_lot_shipping_defaults_planned_locations(self):
|
||||||
'link to transport carries planned locations into shipment create'
|
'link to transport carries planned locations into shipment create'
|
||||||
wizard = lot_module.LotShipping()
|
wizard = lot_module.LotShipping()
|
||||||
|
|||||||
12
modules/purchase_trade/view/lot_plan_transport_line_tree.xml
Normal file
12
modules/purchase_trade/view/lot_plan_transport_line_tree.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<tree editable="1">
|
||||||
|
<field name="lotqt" width="90"/>
|
||||||
|
<field name="cancel_plan" width="90"/>
|
||||||
|
<field name="quantity" width="100"/>
|
||||||
|
<field name="unit" width="80"/>
|
||||||
|
<field name="planned_from_location" width="120"/>
|
||||||
|
<field name="planned_to_location" width="120"/>
|
||||||
|
<field name="planned_transport_type" width="110"/>
|
||||||
|
<field name="planned_from_date" width="120"/>
|
||||||
|
<field name="planned_to_date" width="120"/>
|
||||||
|
<field name="planned_note" width="180"/>
|
||||||
|
</tree>
|
||||||
@@ -1,20 +1,4 @@
|
|||||||
<form col="2">
|
<form col="4">
|
||||||
<label name="cancel_plan"/>
|
<field name="lines" colspan="4" mode="tree"
|
||||||
<field name="cancel_plan"/>
|
view_ids="purchase_trade.plan_transport_line_view_tree"/>
|
||||||
<label name="planned_from_location"/>
|
|
||||||
<field name="planned_from_location"/>
|
|
||||||
<label name="planned_to_location"/>
|
|
||||||
<field name="planned_to_location"/>
|
|
||||||
<label name="planned_transport_type"/>
|
|
||||||
<field name="planned_transport_type"/>
|
|
||||||
<label name="planned_from_date"/>
|
|
||||||
<field name="planned_from_date"/>
|
|
||||||
<label name="planned_to_date"/>
|
|
||||||
<field name="planned_to_date"/>
|
|
||||||
<label name="quantity"/>
|
|
||||||
<field name="quantity"/>
|
|
||||||
<label name="unit"/>
|
|
||||||
<field name="unit"/>
|
|
||||||
<label name="planned_note"/>
|
|
||||||
<field name="planned_note"/>
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user