Plan to transport
This commit is contained in:
@@ -188,10 +188,11 @@ def register():
|
|||||||
stock.ShipmentContainer,
|
stock.ShipmentContainer,
|
||||||
lot.Lot,
|
lot.Lot,
|
||||||
lot.LotQt,
|
lot.LotQt,
|
||||||
lot.LotReport,
|
lot.LotReport,
|
||||||
lot.LotContext,
|
lot.LotContext,
|
||||||
lot.LotShippingStart,
|
lot.LotShippingStart,
|
||||||
lot.LotMatchingStart,
|
lot.LotPlanTransportStart,
|
||||||
|
lot.LotMatchingStart,
|
||||||
lot.LotWeighingStart,
|
lot.LotWeighingStart,
|
||||||
lot.LotAddLot,
|
lot.LotAddLot,
|
||||||
lot.LotInvoicingLot,
|
lot.LotInvoicingLot,
|
||||||
@@ -299,8 +300,9 @@ def register():
|
|||||||
sale.PremiumComposition,
|
sale.PremiumComposition,
|
||||||
module='sale', type_='model')
|
module='sale', type_='model')
|
||||||
Pool.register(
|
Pool.register(
|
||||||
lot.LotShipping,
|
lot.LotShipping,
|
||||||
lot.LotMatching,
|
lot.LotPlanTransport,
|
||||||
|
lot.LotMatching,
|
||||||
lot.LotGoMatching,
|
lot.LotGoMatching,
|
||||||
#lot.LotMatchingUnit,
|
#lot.LotMatchingUnit,
|
||||||
lot.LotWeighing,
|
lot.LotWeighing,
|
||||||
|
|||||||
@@ -1160,10 +1160,22 @@ class LotQt(
|
|||||||
lot_s = fields.Many2One('lot.lot',"Sale lot")
|
lot_s = fields.Many2One('lot.lot',"Sale lot")
|
||||||
lot_quantity = fields.Numeric("Qt",digits=(1,5))
|
lot_quantity = fields.Numeric("Qt",digits=(1,5))
|
||||||
lot_unit = fields.Many2One('product.uom', "Unit")
|
lot_unit = fields.Many2One('product.uom', "Unit")
|
||||||
lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
||||||
lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
||||||
lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
|
lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
|
||||||
lot_move = fields.Many2One('stock.move',"Move")
|
planned_from_location = fields.Many2One(
|
||||||
|
'stock.location', "Planned From")
|
||||||
|
planned_to_location = fields.Many2One(
|
||||||
|
'stock.location', "Planned To")
|
||||||
|
planned_transport_type = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
|
('vessel', 'Vessel'),
|
||||||
|
('truck', 'Truck'),
|
||||||
|
], "Planned Transport")
|
||||||
|
planned_from_date = fields.Date("Planned From Date")
|
||||||
|
planned_to_date = fields.Date("Planned To Date")
|
||||||
|
planned_note = fields.Char("Planning Note")
|
||||||
|
lot_move = fields.Many2One('stock.move',"Move")
|
||||||
lot_av = fields.Selection([
|
lot_av = fields.Selection([
|
||||||
('available', 'Available'),
|
('available', 'Available'),
|
||||||
('reserved', 'Reserved'),
|
('reserved', 'Reserved'),
|
||||||
@@ -1213,13 +1225,116 @@ class LotQt(
|
|||||||
def default_lot_status(cls):
|
def default_lot_status(cls):
|
||||||
return 'forecast'
|
return 'forecast'
|
||||||
|
|
||||||
def get_rec_name(self, name):
|
def get_rec_name(self, name):
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
def isVirtualP(self):
|
def is_planned_transport(self):
|
||||||
if self.lot_p:
|
return bool(
|
||||||
Lot = Pool().get('lot.lot')
|
self.planned_from_location
|
||||||
l = Lot(self.lot_p)
|
or self.planned_to_location
|
||||||
|
or self.planned_transport_type
|
||||||
|
or self.planned_from_date
|
||||||
|
or self.planned_to_date
|
||||||
|
or self.planned_note)
|
||||||
|
|
||||||
|
def has_shipment(self):
|
||||||
|
return bool(
|
||||||
|
self.lot_shipment_in
|
||||||
|
or self.lot_shipment_out
|
||||||
|
or self.lot_shipment_internal)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _split_plan_values(cls, source, quantity, values):
|
||||||
|
return {
|
||||||
|
'lot_p': source.lot_p.id if source.lot_p else None,
|
||||||
|
'lot_s': source.lot_s.id if source.lot_s else None,
|
||||||
|
'lot_quantity': quantity,
|
||||||
|
'lot_unit': source.lot_unit.id if source.lot_unit else None,
|
||||||
|
'lot_av': source.lot_av,
|
||||||
|
'lot_status': source.lot_status,
|
||||||
|
'lot_visible': source.lot_visible,
|
||||||
|
'planned_from_location': values.get('planned_from_location'),
|
||||||
|
'planned_to_location': values.get('planned_to_location'),
|
||||||
|
'planned_transport_type': values.get('planned_transport_type'),
|
||||||
|
'planned_from_date': values.get('planned_from_date'),
|
||||||
|
'planned_to_date': values.get('planned_to_date'),
|
||||||
|
'planned_note': values.get('planned_note'),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def plan_to_transport(cls, source, quantity, values):
|
||||||
|
quantity = Decimal(str(quantity or 0)).quantize(Decimal("0.00001"))
|
||||||
|
if quantity <= 0:
|
||||||
|
raise UserError("Quantity to plan must be positive.")
|
||||||
|
if not any(values.values()):
|
||||||
|
raise UserError("Please enter at least one planning value.")
|
||||||
|
if source.has_shipment():
|
||||||
|
raise UserError("Already linked quantities cannot be planned.")
|
||||||
|
if source.is_planned_transport():
|
||||||
|
raise UserError("This quantity is already planned.")
|
||||||
|
|
||||||
|
available = Decimal(str(source.lot_quantity or 0)).quantize(
|
||||||
|
Decimal("0.00001"))
|
||||||
|
if quantity > abs(available):
|
||||||
|
raise UserError("Quantity to plan exceeds available quantity.")
|
||||||
|
|
||||||
|
signed_quantity = -quantity if available < 0 else quantity
|
||||||
|
source.lot_quantity = available - signed_quantity
|
||||||
|
planned_values = cls._split_plan_values(
|
||||||
|
source, signed_quantity, values)
|
||||||
|
cls.save([source])
|
||||||
|
cls.create([planned_values])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _cancel_plan_domain(cls, planned):
|
||||||
|
return [
|
||||||
|
('lot_p', '=', planned.lot_p.id if planned.lot_p else None),
|
||||||
|
('lot_s', '=', planned.lot_s.id if planned.lot_s else None),
|
||||||
|
('lot_shipment_in', '=', None),
|
||||||
|
('lot_shipment_out', '=', None),
|
||||||
|
('lot_shipment_internal', '=', None),
|
||||||
|
('planned_from_location', '=', None),
|
||||||
|
('planned_to_location', '=', None),
|
||||||
|
('planned_transport_type', '=', None),
|
||||||
|
('planned_from_date', '=', None),
|
||||||
|
('planned_to_date', '=', None),
|
||||||
|
('planned_note', '=', None),
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _cancel_plan_values(cls, planned):
|
||||||
|
return {
|
||||||
|
'lot_p': planned.lot_p.id if planned.lot_p else None,
|
||||||
|
'lot_s': planned.lot_s.id if planned.lot_s else None,
|
||||||
|
'lot_quantity': planned.lot_quantity,
|
||||||
|
'lot_unit': planned.lot_unit.id if planned.lot_unit else None,
|
||||||
|
'lot_av': planned.lot_av,
|
||||||
|
'lot_status': planned.lot_status,
|
||||||
|
'lot_visible': planned.lot_visible,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def cancel_transport_plan(cls, planned):
|
||||||
|
if planned.has_shipment():
|
||||||
|
raise UserError("Linked quantities cannot have their plan removed.")
|
||||||
|
if not planned.is_planned_transport():
|
||||||
|
raise UserError("This quantity is not planned.")
|
||||||
|
|
||||||
|
targets = cls.search(cls._cancel_plan_domain(planned), limit=1)
|
||||||
|
if targets:
|
||||||
|
target = targets[0]
|
||||||
|
target.lot_quantity = (
|
||||||
|
Decimal(str(target.lot_quantity or 0))
|
||||||
|
+ Decimal(str(planned.lot_quantity or 0)))
|
||||||
|
cls.save([target])
|
||||||
|
else:
|
||||||
|
cls.create([cls._cancel_plan_values(planned)])
|
||||||
|
cls.delete([planned])
|
||||||
|
|
||||||
|
def isVirtualP(self):
|
||||||
|
if self.lot_p:
|
||||||
|
Lot = Pool().get('lot.lot')
|
||||||
|
l = Lot(self.lot_p)
|
||||||
return (l.lot_type == 'virtual')
|
return (l.lot_type == 'virtual')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -1992,10 +2107,16 @@ class LotQt(
|
|||||||
(MaQt + AvQt).as_('r_tot'),
|
(MaQt + AvQt).as_('r_tot'),
|
||||||
pu.party.as_('r_supplier'),
|
pu.party.as_('r_supplier'),
|
||||||
sa.party.as_('r_client'),
|
sa.party.as_('r_client'),
|
||||||
Case((lqt.lot_shipment_in > 0, si.from_location),else_=Case((lqt.lot_shipment_internal > 0, sin.from_location),else_=pu.from_location)).as_('r_from_l'),
|
Case((lqt.lot_shipment_in > 0, si.from_location),else_=Case((lqt.lot_shipment_internal > 0, sin.from_location),else_=pu.from_location)).as_('r_from_l'),
|
||||||
Case((lqt.lot_shipment_in > 0, si.to_location),else_=Case((lqt.lot_shipment_internal > 0, sin.to_location),else_=pu.to_location)).as_('r_from_t'),
|
Case((lqt.lot_shipment_in > 0, si.to_location),else_=Case((lqt.lot_shipment_internal > 0, sin.to_location),else_=pu.to_location)).as_('r_from_t'),
|
||||||
Literal(None).as_('r_lot_pur_inv_state'),
|
lqt.planned_from_location.as_('r_planned_from_location'),
|
||||||
Literal(None).as_('r_lot_sale_inv_state'),
|
lqt.planned_to_location.as_('r_planned_to_location'),
|
||||||
|
lqt.planned_transport_type.as_('r_planned_transport_type'),
|
||||||
|
lqt.planned_from_date.as_('r_planned_from_date'),
|
||||||
|
lqt.planned_to_date.as_('r_planned_to_date'),
|
||||||
|
lqt.planned_note.as_('r_planned_note'),
|
||||||
|
Literal(None).as_('r_lot_pur_inv_state'),
|
||||||
|
Literal(None).as_('r_lot_sale_inv_state'),
|
||||||
Literal(None).as_('r_lot_pur_inv'),
|
Literal(None).as_('r_lot_pur_inv'),
|
||||||
Literal(None).as_('r_lot_sal_inv'),
|
Literal(None).as_('r_lot_sal_inv'),
|
||||||
Literal(None).as_('r_lot_state'),
|
Literal(None).as_('r_lot_state'),
|
||||||
@@ -2140,10 +2261,16 @@ class LotQt(
|
|||||||
(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
||||||
pu.party.as_("r_supplier"),
|
pu.party.as_("r_supplier"),
|
||||||
sa.party.as_("r_client"),
|
sa.party.as_("r_client"),
|
||||||
Case((lp.lot_shipment_in > 0, si.from_location), else_=Case((lp.lot_shipment_internal > 0, sin.from_location), else_=pu.from_location)).as_("r_from_l"),
|
Case((lp.lot_shipment_in > 0, si.from_location), else_=Case((lp.lot_shipment_internal > 0, sin.from_location), else_=pu.from_location)).as_("r_from_l"),
|
||||||
Case((lp.lot_shipment_in > 0, si.to_location), else_=Case((lp.lot_shipment_internal > 0, sin.to_location), else_=pu.to_location)).as_("r_from_t"),
|
Case((lp.lot_shipment_in > 0, si.to_location), else_=Case((lp.lot_shipment_internal > 0, sin.to_location), else_=pu.to_location)).as_("r_from_t"),
|
||||||
lp.lot_pur_inv_state.as_('r_lot_pur_inv_state'),
|
Literal(None).as_('r_planned_from_location'),
|
||||||
lp.lot_sale_inv_state.as_('r_lot_sale_inv_state'),
|
Literal(None).as_('r_planned_to_location'),
|
||||||
|
Literal(None).as_('r_planned_transport_type'),
|
||||||
|
Literal(None).as_('r_planned_from_date'),
|
||||||
|
Literal(None).as_('r_planned_to_date'),
|
||||||
|
Literal(None).as_('r_planned_note'),
|
||||||
|
lp.lot_pur_inv_state.as_('r_lot_pur_inv_state'),
|
||||||
|
lp.lot_sale_inv_state.as_('r_lot_sale_inv_state'),
|
||||||
Case((final.id > 0, final.id), else_=prov.id).as_('r_lot_pur_inv'),
|
Case((final.id > 0, final.id), else_=prov.id).as_('r_lot_pur_inv'),
|
||||||
Case((final_s.id > 0, final_s.id), else_=prov_s.id).as_('r_lot_sal_inv'),
|
Case((final_s.id > 0, final_s.id), else_=prov_s.id).as_('r_lot_sal_inv'),
|
||||||
lp.lot_state.as_('r_lot_state'),
|
lp.lot_state.as_('r_lot_state'),
|
||||||
@@ -2210,10 +2337,16 @@ class LotQt(
|
|||||||
Sum(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
Sum(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
||||||
Max(pu.party).as_("r_supplier"),
|
Max(pu.party).as_("r_supplier"),
|
||||||
Max(sa.party).as_("r_client"),
|
Max(sa.party).as_("r_client"),
|
||||||
Max(Case((lp.lot_shipment_in > 0, si.from_location), else_=Case((lp.lot_shipment_internal > 0, si.from_location), else_=pu.from_location))).as_("r_from_l"),
|
Max(Case((lp.lot_shipment_in > 0, si.from_location), else_=Case((lp.lot_shipment_internal > 0, si.from_location), else_=pu.from_location))).as_("r_from_l"),
|
||||||
Max(Case((lp.lot_shipment_in > 0, si.to_location), else_=Case((lp.lot_shipment_internal > 0, si.to_location), else_=pu.to_location))).as_("r_from_t"),
|
Max(Case((lp.lot_shipment_in > 0, si.to_location), else_=Case((lp.lot_shipment_internal > 0, si.to_location), else_=pu.to_location))).as_("r_from_t"),
|
||||||
Max(lp.lot_pur_inv_state).as_('r_lot_pur_inv_state'),
|
Literal(None).as_('r_planned_from_location'),
|
||||||
Max(lp.lot_sale_inv_state).as_('r_lot_sale_inv_state'),
|
Literal(None).as_('r_planned_to_location'),
|
||||||
|
Literal(None).as_('r_planned_transport_type'),
|
||||||
|
Literal(None).as_('r_planned_from_date'),
|
||||||
|
Literal(None).as_('r_planned_to_date'),
|
||||||
|
Literal(None).as_('r_planned_note'),
|
||||||
|
Max(lp.lot_pur_inv_state).as_('r_lot_pur_inv_state'),
|
||||||
|
Max(lp.lot_sale_inv_state).as_('r_lot_sale_inv_state'),
|
||||||
Max(Case((final.id > 0, final.id), else_=prov.id)).as_('r_lot_pur_inv'),
|
Max(Case((final.id > 0, final.id), else_=prov.id)).as_('r_lot_pur_inv'),
|
||||||
Max(Case((final_s.id > 0, final_s.id), else_=prov_s.id)).as_('r_lot_sal_inv'),
|
Max(Case((final_s.id > 0, final_s.id), else_=prov_s.id)).as_('r_lot_sal_inv'),
|
||||||
Max(lp.lot_state).as_('r_lot_state'),
|
Max(lp.lot_state).as_('r_lot_state'),
|
||||||
@@ -2267,10 +2400,16 @@ class LotQt(
|
|||||||
union.r_sale_line.as_("r_sale_line"),
|
union.r_sale_line.as_("r_sale_line"),
|
||||||
union.r_tot.as_("r_tot"),
|
union.r_tot.as_("r_tot"),
|
||||||
union.r_supplier.as_("r_supplier"),
|
union.r_supplier.as_("r_supplier"),
|
||||||
union.r_client.as_("r_client"),
|
union.r_client.as_("r_client"),
|
||||||
union.r_from_l.as_("r_from_l"),
|
union.r_from_l.as_("r_from_l"),
|
||||||
union.r_from_t.as_("r_from_t"),
|
union.r_from_t.as_("r_from_t"),
|
||||||
union.r_lot_pur_inv_state.as_('r_lot_pur_inv_state'),
|
union.r_planned_from_location.as_('r_planned_from_location'),
|
||||||
|
union.r_planned_to_location.as_('r_planned_to_location'),
|
||||||
|
union.r_planned_transport_type.as_('r_planned_transport_type'),
|
||||||
|
union.r_planned_from_date.as_('r_planned_from_date'),
|
||||||
|
union.r_planned_to_date.as_('r_planned_to_date'),
|
||||||
|
union.r_planned_note.as_('r_planned_note'),
|
||||||
|
union.r_lot_pur_inv_state.as_('r_lot_pur_inv_state'),
|
||||||
union.r_lot_sale_inv_state.as_('r_lot_sale_inv_state'),
|
union.r_lot_sale_inv_state.as_('r_lot_sale_inv_state'),
|
||||||
union.r_lot_pur_inv.as_('r_lot_pur_inv'),
|
union.r_lot_pur_inv.as_('r_lot_pur_inv'),
|
||||||
union.r_lot_sal_inv.as_('r_lot_sal_inv'),
|
union.r_lot_sal_inv.as_('r_lot_sal_inv'),
|
||||||
@@ -2351,6 +2490,18 @@ class LotReport(
|
|||||||
r_client = fields.Many2One('party.party',"Client")
|
r_client = fields.Many2One('party.party',"Client")
|
||||||
r_from_l = fields.Many2One('stock.location', 'From')
|
r_from_l = fields.Many2One('stock.location', 'From')
|
||||||
r_from_t = fields.Many2One('stock.location', 'To')
|
r_from_t = fields.Many2One('stock.location', 'To')
|
||||||
|
r_planned_from_location = fields.Many2One(
|
||||||
|
'stock.location', "Planned From")
|
||||||
|
r_planned_to_location = fields.Many2One(
|
||||||
|
'stock.location', "Planned To")
|
||||||
|
r_planned_transport_type = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
|
('vessel', 'Vessel'),
|
||||||
|
('truck', 'Truck'),
|
||||||
|
], "Planned Transport")
|
||||||
|
r_planned_from_date = fields.Date("Planned From Date")
|
||||||
|
r_planned_to_date = fields.Date("Planned To Date")
|
||||||
|
r_planned_note = fields.Char("Planning Note")
|
||||||
r_shipping_status = fields.Selection([
|
r_shipping_status = fields.Selection([
|
||||||
(None, ''),
|
(None, ''),
|
||||||
('unshipped', 'Unshipped'),
|
('unshipped', 'Unshipped'),
|
||||||
@@ -2855,12 +3006,37 @@ class LotShipping(Wizard):
|
|||||||
affected_lines.append(l.sale_line)
|
affected_lines.append(l.sale_line)
|
||||||
else:
|
else:
|
||||||
lqt = LotQt(r.id - 10000000)
|
lqt = LotQt(r.id - 10000000)
|
||||||
|
available_to_ship = abs(Decimal(str(
|
||||||
|
lqt.lot_quantity or 0)))
|
||||||
|
if shipped_quantity > available_to_ship:
|
||||||
|
raise UserError(
|
||||||
|
"Quantity to link exceeds available quantity.")
|
||||||
|
plan_lot = lqt.lot_p
|
||||||
|
plan_mode = 'both'
|
||||||
|
if not plan_lot:
|
||||||
|
plan_lot = lqt.lot_s
|
||||||
|
plan_mode = 'only sale'
|
||||||
|
signed_shipped_quantity = shipped_quantity
|
||||||
|
if Decimal(str(lqt.lot_quantity or 0)) < 0:
|
||||||
|
signed_shipped_quantity = -shipped_quantity
|
||||||
#Increase forecasted virtual part shipped
|
#Increase forecasted virtual part shipped
|
||||||
if not lqt.lot_p.updateVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s):
|
if not plan_lot.updateVirtualPart(
|
||||||
|
signed_shipped_quantity, shipment_origin,
|
||||||
|
lqt.lot_s, plan_mode):
|
||||||
logger.info("LotShipping2:%s",shipped_quantity)
|
logger.info("LotShipping2:%s",shipped_quantity)
|
||||||
lqt.lot_p.createVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s)
|
plan_lot.createVirtualPart(
|
||||||
|
signed_shipped_quantity, shipment_origin,
|
||||||
|
lqt.lot_s, plan_mode)
|
||||||
#Decrease forecasted virtual part non shipped
|
#Decrease forecasted virtual part non shipped
|
||||||
lqt.lot_p.updateVirtualPart(-shipped_quantity,None,lqt.lot_s)
|
if lqt.is_planned_transport():
|
||||||
|
lqt.lot_quantity -= signed_shipped_quantity
|
||||||
|
if abs(lqt.lot_quantity) < Decimal("0.00001"):
|
||||||
|
lqt.lot_quantity = 0
|
||||||
|
LotQt.save([lqt])
|
||||||
|
else:
|
||||||
|
plan_lot.updateVirtualPart(
|
||||||
|
-signed_shipped_quantity, None,
|
||||||
|
lqt.lot_s, plan_mode)
|
||||||
if lqt.lot_p and lqt.lot_p.line:
|
if lqt.lot_p and lqt.lot_p.line:
|
||||||
affected_lines.append(lqt.lot_p.line)
|
affected_lines.append(lqt.lot_p.line)
|
||||||
if lqt.lot_s and lqt.lot_s.sale_line:
|
if lqt.lot_s and lqt.lot_s.sale_line:
|
||||||
@@ -2872,9 +3048,9 @@ class LotShipping(Wizard):
|
|||||||
def end(self):
|
def end(self):
|
||||||
return 'reload'
|
return 'reload'
|
||||||
|
|
||||||
class LotShippingStart(ModelView):
|
class LotShippingStart(ModelView):
|
||||||
"Link lot to transport"
|
"Link lot to transport"
|
||||||
__name__ = "lot.shipping.start"
|
__name__ = "lot.shipping.start"
|
||||||
shipment = fields.Selection([
|
shipment = fields.Selection([
|
||||||
('in', 'Shipment In'),
|
('in', 'Shipment In'),
|
||||||
('out', 'Shipment Out'),
|
('out', 'Shipment Out'),
|
||||||
@@ -2891,9 +3067,128 @@ class LotShippingStart(ModelView):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_shipment(cls):
|
def default_shipment(cls):
|
||||||
return 'in'
|
return 'in'
|
||||||
|
|
||||||
|
|
||||||
|
class LotPlanTransport(Wizard):
|
||||||
|
"Plan lot to transport"
|
||||||
|
__name__ = "lot.plan_transport"
|
||||||
|
|
||||||
|
start = StateTransition()
|
||||||
|
|
||||||
|
plan = StateView(
|
||||||
|
'lot.plan_transport.start',
|
||||||
|
'purchase_trade.plan_transport_view_form', [
|
||||||
|
Button("Cancel", 'end', 'tryton-cancel'),
|
||||||
|
Button("Apply", 'planning', 'tryton-ok', default=True),
|
||||||
|
])
|
||||||
|
|
||||||
|
planning = StateTransition()
|
||||||
|
|
||||||
|
def transition_start(self):
|
||||||
|
if self.model.__name__ == 'lot.report':
|
||||||
|
return 'plan'
|
||||||
|
return 'end'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
if self.plan.cancel_plan:
|
||||||
|
LotQt.cancel_transport_plan(lotqt)
|
||||||
|
else:
|
||||||
|
LotQt.plan_to_transport(
|
||||||
|
lotqt, self.plan.quantity, plan_values)
|
||||||
|
if lotqt.lot_p and lotqt.lot_p.line:
|
||||||
|
affected_lines.append(lotqt.lot_p.line)
|
||||||
|
if lotqt.lot_s and lotqt.lot_s.sale_line:
|
||||||
|
affected_lines.append(lotqt.lot_s.sale_line)
|
||||||
|
Lot.assert_lines_quantity_consistency(affected_lines)
|
||||||
|
return 'end'
|
||||||
|
|
||||||
|
def end(self):
|
||||||
|
return 'reload'
|
||||||
|
|
||||||
|
|
||||||
|
class LotPlanTransportStart(ModelView):
|
||||||
|
"Plan lot to transport"
|
||||||
|
__name__ = "lot.plan_transport.start"
|
||||||
|
|
||||||
|
cancel_plan = fields.Boolean("Cancel existing plan")
|
||||||
|
planned_from_location = fields.Many2One(
|
||||||
|
'stock.location', "Planned From",
|
||||||
|
states={'readonly': Eval('cancel_plan')})
|
||||||
|
planned_to_location = fields.Many2One(
|
||||||
|
'stock.location', "Planned To",
|
||||||
|
states={'readonly': Eval('cancel_plan')})
|
||||||
|
planned_transport_type = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
|
('vessel', 'Vessel'),
|
||||||
|
('truck', 'Truck'),
|
||||||
|
], "Planned Transport", states={'readonly': Eval('cancel_plan')})
|
||||||
|
planned_from_date = fields.Date(
|
||||||
|
"Planned From Date", states={'readonly': Eval('cancel_plan')})
|
||||||
|
planned_to_date = fields.Date(
|
||||||
|
"Planned To Date", states={'readonly': Eval('cancel_plan')})
|
||||||
|
planned_note = fields.Char(
|
||||||
|
"Planning Note", states={'readonly': Eval('cancel_plan')})
|
||||||
|
quantity = fields.Numeric(
|
||||||
|
"Quantity", digits='unit', states={'readonly': Eval('cancel_plan')})
|
||||||
|
unit = fields.Many2One('product.uom', "Unit", readonly=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_cancel_plan(cls):
|
||||||
|
return False
|
||||||
|
|
||||||
class QtWarning(UserWarning):
|
class QtWarning(UserWarning):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -113,23 +113,38 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="name">lot_report_context_form</field>
|
<field name="name">lot_report_context_form</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record model="ir.ui.view" id="shipping_view_form">
|
<record model="ir.ui.view" id="shipping_view_form">
|
||||||
<field name="model">lot.shipping.start</field>
|
<field name="model">lot.shipping.start</field>
|
||||||
<field name="type">form</field>
|
<field name="type">form</field>
|
||||||
<field name="name">lot_shipping_start_form</field>
|
<field name="name">lot_shipping_start_form</field>
|
||||||
</record>
|
</record>
|
||||||
<record model="ir.action.wizard" id="act_shipping">
|
<record model="ir.ui.view" id="plan_transport_view_form">
|
||||||
<field name="name">🚢 Link lots to transport</field>
|
<field name="model">lot.plan_transport.start</field>
|
||||||
<field name="wiz_name">lot.shipping</field>
|
<field name="type">form</field>
|
||||||
<field name="model">lot.report</field>
|
<field name="name">lot_plan_transport_start_form</field>
|
||||||
</record>
|
</record>
|
||||||
|
<record model="ir.action.wizard" id="act_shipping">
|
||||||
|
<field name="name">🚢 Link lots to transport</field>
|
||||||
|
<field name="wiz_name">lot.shipping</field>
|
||||||
|
<field name="model">lot.report</field>
|
||||||
|
</record>
|
||||||
<record model="ir.action.keyword" id="act_shipping_keyword">
|
<record model="ir.action.keyword" id="act_shipping_keyword">
|
||||||
<field name="keyword">form_action</field>
|
<field name="keyword">form_action</field>
|
||||||
<field name="model">lot.report,-1</field>
|
<field name="model">lot.report,-1</field>
|
||||||
<field name="action" ref="act_shipping"/>
|
<field name="action" ref="act_shipping"/>
|
||||||
</record>
|
</record>
|
||||||
|
<record model="ir.action.wizard" id="act_plan_transport">
|
||||||
<record model="ir.ui.view" id="add_lot_view_form">
|
<field name="name">Plan to transport</field>
|
||||||
|
<field name="wiz_name">lot.plan_transport</field>
|
||||||
|
<field name="model">lot.report</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.action.keyword" id="act_plan_transport_keyword">
|
||||||
|
<field name="keyword">form_action</field>
|
||||||
|
<field name="model">lot.report,-1</field>
|
||||||
|
<field name="action" ref="act_plan_transport"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.ui.view" id="add_lot_view_form">
|
||||||
<field name="model">lot.add.lot</field>
|
<field name="model">lot.add.lot</field>
|
||||||
<field name="type">form</field>
|
<field name="type">form</field>
|
||||||
<field name="name">lot_add_start_form</field>
|
<field name="name">lot_add_start_form</field>
|
||||||
|
|||||||
@@ -89,6 +89,108 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Valuation.get_totals(), (Decimal('42.50'), Decimal(0)))
|
Valuation.get_totals(), (Decimal('42.50'), Decimal(0)))
|
||||||
|
|
||||||
|
def test_lotqt_plan_to_transport_splits_source_quantity(self):
|
||||||
|
'planning transport creates an isolated lot.qt split'
|
||||||
|
LotQt = lot_module.LotQt
|
||||||
|
source = Mock(
|
||||||
|
lot_p=Mock(id=1),
|
||||||
|
lot_s=Mock(id=2),
|
||||||
|
lot_quantity=Decimal('500'),
|
||||||
|
lot_unit=Mock(id=3),
|
||||||
|
lot_av='available',
|
||||||
|
lot_status='forecast',
|
||||||
|
lot_visible=True,
|
||||||
|
)
|
||||||
|
source.has_shipment.return_value = False
|
||||||
|
source.is_planned_transport.return_value = False
|
||||||
|
values = {
|
||||||
|
'planned_from_location': 10,
|
||||||
|
'planned_to_location': 20,
|
||||||
|
'planned_transport_type': 'truck',
|
||||||
|
'planned_from_date': datetime.date(2026, 7, 1),
|
||||||
|
'planned_to_date': datetime.date(2026, 7, 5),
|
||||||
|
'planned_note': 'Early planning',
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(LotQt, 'save') as save, patch.object(
|
||||||
|
LotQt, 'create') as create:
|
||||||
|
LotQt.plan_to_transport(source, Decimal('125'), values)
|
||||||
|
|
||||||
|
self.assertEqual(source.lot_quantity, Decimal('375.00000'))
|
||||||
|
save.assert_called_once_with([source])
|
||||||
|
create.assert_called_once_with([{
|
||||||
|
'lot_p': 1,
|
||||||
|
'lot_s': 2,
|
||||||
|
'lot_quantity': Decimal('125.00000'),
|
||||||
|
'lot_unit': 3,
|
||||||
|
'lot_av': 'available',
|
||||||
|
'lot_status': 'forecast',
|
||||||
|
'lot_visible': True,
|
||||||
|
'planned_from_location': 10,
|
||||||
|
'planned_to_location': 20,
|
||||||
|
'planned_transport_type': 'truck',
|
||||||
|
'planned_from_date': datetime.date(2026, 7, 1),
|
||||||
|
'planned_to_date': datetime.date(2026, 7, 5),
|
||||||
|
'planned_note': 'Early planning',
|
||||||
|
}])
|
||||||
|
|
||||||
|
def test_lotqt_plan_to_transport_preserves_sale_only_sign(self):
|
||||||
|
'planning a sale-only lot.qt keeps the negative quantity direction'
|
||||||
|
LotQt = lot_module.LotQt
|
||||||
|
source = Mock(
|
||||||
|
lot_p=None,
|
||||||
|
lot_s=Mock(id=2),
|
||||||
|
lot_quantity=Decimal('-500'),
|
||||||
|
lot_unit=Mock(id=3),
|
||||||
|
lot_av='available',
|
||||||
|
lot_status='forecast',
|
||||||
|
lot_visible=True,
|
||||||
|
)
|
||||||
|
source.has_shipment.return_value = False
|
||||||
|
source.is_planned_transport.return_value = False
|
||||||
|
|
||||||
|
with patch.object(LotQt, 'save'), patch.object(
|
||||||
|
LotQt, 'create') as create:
|
||||||
|
LotQt.plan_to_transport(
|
||||||
|
source, Decimal('125'),
|
||||||
|
{'planned_transport_type': 'vessel'})
|
||||||
|
|
||||||
|
self.assertEqual(source.lot_quantity, Decimal('-375.00000'))
|
||||||
|
self.assertEqual(
|
||||||
|
create.call_args[0][0][0]['lot_quantity'],
|
||||||
|
Decimal('-125.00000'))
|
||||||
|
self.assertEqual(create.call_args[0][0][0]['lot_p'], None)
|
||||||
|
self.assertEqual(create.call_args[0][0][0]['lot_s'], 2)
|
||||||
|
|
||||||
|
def test_lotqt_cancel_transport_plan_merges_matching_open_line(self):
|
||||||
|
'cancelling a transport plan returns quantity to the open lot.qt'
|
||||||
|
LotQt = lot_module.LotQt
|
||||||
|
planned = Mock(
|
||||||
|
lot_p=Mock(id=1),
|
||||||
|
lot_s=None,
|
||||||
|
lot_quantity=Decimal('125'),
|
||||||
|
lot_unit=Mock(id=3),
|
||||||
|
lot_av='available',
|
||||||
|
lot_status='forecast',
|
||||||
|
lot_visible=True,
|
||||||
|
)
|
||||||
|
planned.has_shipment.return_value = False
|
||||||
|
planned.is_planned_transport.return_value = True
|
||||||
|
target = Mock(lot_quantity=Decimal('375'))
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
LotQt, 'search', return_value=[target]) as search, patch.object(
|
||||||
|
LotQt, 'save') as save, patch.object(
|
||||||
|
LotQt, 'create') as create, patch.object(
|
||||||
|
LotQt, 'delete') as delete:
|
||||||
|
LotQt.cancel_transport_plan(planned)
|
||||||
|
|
||||||
|
search.assert_called_once()
|
||||||
|
self.assertEqual(target.lot_quantity, Decimal('500'))
|
||||||
|
save.assert_called_once_with([target])
|
||||||
|
create.assert_not_called()
|
||||||
|
delete.assert_called_once_with([planned])
|
||||||
|
|
||||||
@with_transaction()
|
@with_transaction()
|
||||||
def test_get_mtm_applies_component_ratio_as_percentage(self):
|
def test_get_mtm_applies_component_ratio_as_percentage(self):
|
||||||
'get_mtm treats component ratio as a percentage'
|
'get_mtm treats component ratio as a percentage'
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<form col="2">
|
||||||
|
<label name="cancel_plan"/>
|
||||||
|
<field name="cancel_plan"/>
|
||||||
|
<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>
|
||||||
@@ -13,6 +13,12 @@
|
|||||||
<field name="r_lot_product" width="120"/>
|
<field name="r_lot_product" width="120"/>
|
||||||
<field name="r_from_l" width="80"/>
|
<field name="r_from_l" width="80"/>
|
||||||
<field name="r_from_t" width="80"/>
|
<field name="r_from_t" width="80"/>
|
||||||
|
<field name="r_planned_from_location" width="100" optional="1"/>
|
||||||
|
<field name="r_planned_to_location" width="100" optional="1"/>
|
||||||
|
<field name="r_planned_transport_type" width="110" optional="1"/>
|
||||||
|
<field name="r_planned_from_date" width="110" optional="1"/>
|
||||||
|
<field name="r_planned_to_date" width="110" optional="1"/>
|
||||||
|
<field name="r_planned_note" width="140" optional="1"/>
|
||||||
<field name="r_shipment_type" width="90"/>
|
<field name="r_shipment_type" width="90"/>
|
||||||
<field name="r_shipment_origin" width="160">
|
<field name="r_shipment_origin" width="160">
|
||||||
<prefix name="sh_icon"/>
|
<prefix name="sh_icon"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user