Add physical + Link
This commit is contained in:
@@ -1210,8 +1210,8 @@ class Lot(metaclass=PoolMeta):
|
|||||||
# super().write(*args)
|
# super().write(*args)
|
||||||
# lots = sum(args[::2], [])
|
# lots = sum(args[::2], [])
|
||||||
|
|
||||||
class LotQt(
|
class LotQt(
|
||||||
ModelSQL, ModelView):
|
ModelSQL, ModelView):
|
||||||
"Lots Qt"
|
"Lots Qt"
|
||||||
__name__ = 'lot.qt'
|
__name__ = 'lot.qt'
|
||||||
|
|
||||||
@@ -1356,6 +1356,72 @@ class LotQt(
|
|||||||
or self.lot_shipment_out
|
or self.lot_shipment_out
|
||||||
or self.lot_shipment_internal)
|
or self.lot_shipment_internal)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def link_to_transport(cls, source, quantity, shipment_origin):
|
||||||
|
quantity = abs(Decimal(str(quantity or 0))).quantize(
|
||||||
|
Decimal("0.00001"))
|
||||||
|
available = abs(Decimal(str(source.lot_quantity or 0))).quantize(
|
||||||
|
Decimal("0.00001"))
|
||||||
|
if not quantity or quantity > available:
|
||||||
|
raise UserError("Quantity to link exceeds available quantity.")
|
||||||
|
|
||||||
|
shipment_model, shipment_id = str(shipment_origin).split(',', 1)
|
||||||
|
shipment_field = {
|
||||||
|
'stock.shipment.in': 'lot_shipment_in',
|
||||||
|
'stock.shipment.out': 'lot_shipment_out',
|
||||||
|
'stock.shipment.internal': 'lot_shipment_internal',
|
||||||
|
}.get(shipment_model)
|
||||||
|
if not shipment_field:
|
||||||
|
raise UserError("Shipment type is not supported.")
|
||||||
|
shipment_id = int(shipment_id)
|
||||||
|
signed_quantity = quantity
|
||||||
|
if Decimal(str(source.lot_quantity or 0)) < 0:
|
||||||
|
signed_quantity = -quantity
|
||||||
|
|
||||||
|
if source.is_planned_transport():
|
||||||
|
values = cls._split_plan_values(
|
||||||
|
source, signed_quantity, {
|
||||||
|
'planned_from_location': cls._record_id(
|
||||||
|
source.planned_from_location),
|
||||||
|
'planned_to_location': cls._record_id(
|
||||||
|
source.planned_to_location),
|
||||||
|
'planned_transport_type': source.planned_transport_type,
|
||||||
|
'planned_from_date': source.planned_from_date,
|
||||||
|
'planned_to_date': source.planned_to_date,
|
||||||
|
'planned_note': source.planned_note,
|
||||||
|
})
|
||||||
|
values[shipment_field] = shipment_id
|
||||||
|
linked, = cls.create([values])
|
||||||
|
source.lot_quantity -= signed_quantity
|
||||||
|
if abs(source.lot_quantity) < Decimal("0.00001"):
|
||||||
|
source.lot_quantity = 0
|
||||||
|
cls.save([source])
|
||||||
|
return linked
|
||||||
|
|
||||||
|
plan_lot = source.lot_p or source.lot_s
|
||||||
|
plan_mode = 'both' if source.lot_p else 'only sale'
|
||||||
|
if not plan_lot.updateVirtualPart(
|
||||||
|
signed_quantity, shipment_origin,
|
||||||
|
source.lot_s, plan_mode):
|
||||||
|
plan_lot.createVirtualPart(
|
||||||
|
signed_quantity, shipment_origin,
|
||||||
|
source.lot_s, plan_mode)
|
||||||
|
plan_lot.updateVirtualPart(
|
||||||
|
-signed_quantity, None, source.lot_s, plan_mode)
|
||||||
|
domain = [
|
||||||
|
('lot_p', '=', source.lot_p),
|
||||||
|
('lot_s', '=', source.lot_s),
|
||||||
|
(shipment_field, '=', shipment_id),
|
||||||
|
]
|
||||||
|
linked = cls.search(domain)
|
||||||
|
if not linked:
|
||||||
|
raise UserError("The linked transport quantity was not created.")
|
||||||
|
return linked[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _record_id(record):
|
||||||
|
return record.id if record else None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _split_plan_values(cls, source, quantity, values):
|
def _split_plan_values(cls, source, quantity, values):
|
||||||
return {
|
return {
|
||||||
@@ -3302,7 +3368,7 @@ class LotContext(ModelView):
|
|||||||
return super().get_context(fields_names=fields_names)
|
return super().get_context(fields_names=fields_names)
|
||||||
|
|
||||||
|
|
||||||
class LotShipping(Wizard):
|
class LotShipping(Wizard):
|
||||||
"Link lot to transport"
|
"Link lot to transport"
|
||||||
__name__ = "lot.shipping"
|
__name__ = "lot.shipping"
|
||||||
|
|
||||||
@@ -3325,14 +3391,20 @@ class LotShipping(Wizard):
|
|||||||
def default_ship(self, fields):
|
def default_ship(self, fields):
|
||||||
values = {}
|
values = {}
|
||||||
planned_from_locations = {
|
planned_from_locations = {
|
||||||
record.r_planned_from_location.id
|
location.id
|
||||||
for record in self.records
|
for record in self.records
|
||||||
if record.r_planned_from_location
|
for location in [
|
||||||
|
record.r_planned_from_location
|
||||||
|
or getattr(record.r_purchase, 'from_location', None)]
|
||||||
|
if location
|
||||||
}
|
}
|
||||||
planned_to_locations = {
|
planned_to_locations = {
|
||||||
record.r_planned_to_location.id
|
location.id
|
||||||
for record in self.records
|
for record in self.records
|
||||||
if record.r_planned_to_location
|
for location in [
|
||||||
|
record.r_planned_to_location
|
||||||
|
or getattr(record.r_purchase, 'to_location', None)]
|
||||||
|
if location
|
||||||
}
|
}
|
||||||
if len(planned_from_locations) == 1:
|
if len(planned_from_locations) == 1:
|
||||||
values['planned_from_location'] = next(iter(
|
values['planned_from_location'] = next(iter(
|
||||||
@@ -3368,6 +3440,12 @@ class LotShipping(Wizard):
|
|||||||
values['from_location'] = self.ship.planned_from_location.id
|
values['from_location'] = self.ship.planned_from_location.id
|
||||||
if self.ship.planned_to_location:
|
if self.ship.planned_to_location:
|
||||||
values['to_location'] = self.ship.planned_to_location.id
|
values['to_location'] = self.ship.planned_to_location.id
|
||||||
|
bl_number = getattr(self.ship, 'shipment_bl_number', None)
|
||||||
|
bl_date = getattr(self.ship, 'shipment_bl_date', None)
|
||||||
|
if bl_number:
|
||||||
|
values['bl_number'] = bl_number
|
||||||
|
if bl_date:
|
||||||
|
values['bl_date'] = bl_date
|
||||||
return values
|
return values
|
||||||
|
|
||||||
def _create_shipment_in(self):
|
def _create_shipment_in(self):
|
||||||
@@ -3375,34 +3453,6 @@ class LotShipping(Wizard):
|
|||||||
shipment, = ShipmentIn.create([self._shipment_in_create_values()])
|
shipment, = ShipmentIn.create([self._shipment_in_create_values()])
|
||||||
self.ship.shipment_in = shipment
|
self.ship.shipment_in = shipment
|
||||||
|
|
||||||
def _scheduled_lotqt_values(self, lqt, quantity):
|
|
||||||
values = {
|
|
||||||
'lot_p': self._record_id(lqt.lot_p),
|
|
||||||
'lot_s': self._record_id(lqt.lot_s),
|
|
||||||
'lot_quantity': quantity,
|
|
||||||
'lot_unit': self._record_id(lqt.lot_unit),
|
|
||||||
'lot_av': lqt.lot_av,
|
|
||||||
'lot_status': lqt.lot_status,
|
|
||||||
'lot_visible': lqt.lot_visible,
|
|
||||||
'planned_from_location': self._record_id(
|
|
||||||
lqt.planned_from_location),
|
|
||||||
'planned_to_location': self._record_id(lqt.planned_to_location),
|
|
||||||
'planned_transport_type': lqt.planned_transport_type,
|
|
||||||
'planned_from_date': lqt.planned_from_date,
|
|
||||||
'planned_to_date': lqt.planned_to_date,
|
|
||||||
'planned_note': lqt.planned_note,
|
|
||||||
}
|
|
||||||
if self.ship.shipment == 'in':
|
|
||||||
values['lot_shipment_in'] = self.ship.shipment_in.id
|
|
||||||
elif self.ship.shipment == 'out':
|
|
||||||
values['lot_shipment_out'] = self.ship.shipment_out.id
|
|
||||||
elif self.ship.shipment == 'int':
|
|
||||||
values['lot_shipment_internal'] = self.ship.shipment_internal.id
|
|
||||||
return values
|
|
||||||
|
|
||||||
def _create_scheduled_lotqt(self, LotQt, lqt, quantity):
|
|
||||||
LotQt.create([self._scheduled_lotqt_values(lqt, quantity)])
|
|
||||||
|
|
||||||
def transition_shipping(self):
|
def transition_shipping(self):
|
||||||
Lot = Pool().get('lot.lot')
|
Lot = Pool().get('lot.lot')
|
||||||
LotQt = Pool().get('lot.qt')
|
LotQt = Pool().get('lot.qt')
|
||||||
@@ -3478,41 +3528,8 @@ 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(
|
LotQt.link_to_transport(
|
||||||
lqt.lot_quantity or 0)))
|
lqt, shipped_quantity, shipment_origin)
|
||||||
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
|
|
||||||
if lqt.is_planned_transport():
|
|
||||||
self._create_scheduled_lotqt(
|
|
||||||
LotQt, lqt, signed_shipped_quantity)
|
|
||||||
else:
|
|
||||||
if not plan_lot.updateVirtualPart(
|
|
||||||
signed_shipped_quantity, shipment_origin,
|
|
||||||
lqt.lot_s, plan_mode):
|
|
||||||
logger.info("LotShipping2:%s", shipped_quantity)
|
|
||||||
plan_lot.createVirtualPart(
|
|
||||||
signed_shipped_quantity, shipment_origin,
|
|
||||||
lqt.lot_s, plan_mode)
|
|
||||||
#Decrease forecasted virtual part non shipped
|
|
||||||
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:
|
||||||
@@ -4261,7 +4278,7 @@ class LotUnship(Wizard):
|
|||||||
def end(self):
|
def end(self):
|
||||||
return 'reload'
|
return 'reload'
|
||||||
|
|
||||||
class LotAdding(Wizard):
|
class LotAdding(Wizard):
|
||||||
"Adding physical lots"
|
"Adding physical lots"
|
||||||
__name__ = "lot.add"
|
__name__ = "lot.add"
|
||||||
|
|
||||||
@@ -4295,14 +4312,11 @@ class LotAdding(Wizard):
|
|||||||
lqt = LotQt(i - 10000000)
|
lqt = LotQt(i - 10000000)
|
||||||
if not lqt.lot_p:
|
if not lqt.lot_p:
|
||||||
raise UserError("To add physical lot to sale, use action 'Apply matching'!")
|
raise UserError("To add physical lot to sale, use action 'Apply matching'!")
|
||||||
if not lqt.has_shipment():
|
|
||||||
raise UserError(
|
|
||||||
"Please link the quantity to transport before adding "
|
|
||||||
"physical lots.")
|
|
||||||
lot = lqt.lot_p
|
lot = lqt.lot_p
|
||||||
|
purchase = lot.line.purchase
|
||||||
unit = lot.lot_unit_line.id
|
unit = lot.lot_unit_line.id
|
||||||
quantity = lqt.lot_quantity
|
quantity = lqt.lot_quantity
|
||||||
sh_in = lqt.lot_shipment_in.id if lqt.lot_shipment_in else None
|
sh_in = lqt.lot_shipment_in.id if lqt.lot_shipment_in else None
|
||||||
sh_int = lqt.lot_shipment_internal.id if lqt.lot_shipment_internal else None
|
sh_int = lqt.lot_shipment_internal.id if lqt.lot_shipment_internal else None
|
||||||
sh_out = lqt.lot_shipment_out.id if lqt.lot_shipment_out else None
|
sh_out = lqt.lot_shipment_out.id if lqt.lot_shipment_out else None
|
||||||
else:
|
else:
|
||||||
@@ -4314,12 +4328,58 @@ class LotAdding(Wizard):
|
|||||||
'shipment_in':sh_in,
|
'shipment_in':sh_in,
|
||||||
'shipment_internal':sh_int,
|
'shipment_internal':sh_int,
|
||||||
'shipment_out':sh_out,
|
'shipment_out':sh_out,
|
||||||
|
'transport_missing': not any((sh_in, sh_int, sh_out)),
|
||||||
|
'shipment_supplier': (
|
||||||
|
purchase.party.id
|
||||||
|
if not any((sh_in, sh_int, sh_out)) and purchase.party
|
||||||
|
else None),
|
||||||
|
'transport_from_location': (
|
||||||
|
(lqt.planned_from_location or purchase.from_location).id
|
||||||
|
if not any((sh_in, sh_int, sh_out))
|
||||||
|
and (lqt.planned_from_location or purchase.from_location)
|
||||||
|
else None),
|
||||||
|
'transport_to_location': (
|
||||||
|
(lqt.planned_to_location or purchase.to_location).id
|
||||||
|
if not any((sh_in, sh_int, sh_out))
|
||||||
|
and (lqt.planned_to_location or purchase.to_location)
|
||||||
|
else None),
|
||||||
}
|
}
|
||||||
|
|
||||||
def transition_adding(self):
|
def transition_adding(self):
|
||||||
if self.records:
|
if self.records:
|
||||||
LotQt = Pool().get('lot.qt')
|
LotQt = Pool().get('lot.qt')
|
||||||
lqt = LotQt(self.records[0].id - 10000000)
|
lqt = LotQt(self.records[0].id - 10000000)
|
||||||
|
if not lqt.has_shipment():
|
||||||
|
shipment = self.add.shipment_in
|
||||||
|
if self.add.create_new_shipment and shipment:
|
||||||
|
raise UserError(
|
||||||
|
"Choose either an existing shipment or create a new "
|
||||||
|
"shipment.")
|
||||||
|
if self.add.create_new_shipment:
|
||||||
|
if not self.add.shipment_supplier:
|
||||||
|
raise UserError("Shipment supplier is required.")
|
||||||
|
ShipmentIn = Pool().get('stock.shipment.in')
|
||||||
|
values = {
|
||||||
|
'supplier': self.add.shipment_supplier.id,
|
||||||
|
}
|
||||||
|
if self.add.transport_from_location:
|
||||||
|
values['from_location'] = (
|
||||||
|
self.add.transport_from_location.id)
|
||||||
|
if self.add.transport_to_location:
|
||||||
|
values['to_location'] = self.add.transport_to_location.id
|
||||||
|
if self.add.shipment_bl_number:
|
||||||
|
values['bl_number'] = self.add.shipment_bl_number
|
||||||
|
if self.add.shipment_bl_date:
|
||||||
|
values['bl_date'] = self.add.shipment_bl_date
|
||||||
|
shipment, = ShipmentIn.create([values])
|
||||||
|
if not shipment:
|
||||||
|
raise UserError(
|
||||||
|
"Select a Shipment In or create a new shipment.")
|
||||||
|
Lot = Pool().get('lot.lot')
|
||||||
|
with Lot.skip_quantity_consistency():
|
||||||
|
lqt = LotQt.link_to_transport(
|
||||||
|
lqt, abs(Decimal(str(lqt.lot_quantity or 0))),
|
||||||
|
'stock.shipment.in,%s' % shipment.id)
|
||||||
LotQt.add_physical_lots(
|
LotQt.add_physical_lots(
|
||||||
lqt, self.add.lots, self.add.unlink_remainder)
|
lqt, self.add.lots, self.add.unlink_remainder)
|
||||||
return 'end'
|
return 'end'
|
||||||
@@ -4327,13 +4387,44 @@ class LotAdding(Wizard):
|
|||||||
def end(self):
|
def end(self):
|
||||||
return 'reload'
|
return 'reload'
|
||||||
|
|
||||||
class LotAddLot(ModelView):
|
class LotAddLot(ModelView):
|
||||||
"Add physical lots"
|
"Add physical lots"
|
||||||
__name__ = "lot.add.lot"
|
__name__ = "lot.add.lot"
|
||||||
unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
||||||
quantity = fields.Numeric("Quantity available",digits='unit',readonly=True)
|
quantity = fields.Numeric("Quantity available",digits='unit',readonly=True)
|
||||||
unlink_remainder = fields.Boolean("Unlink remaining quantity")
|
unlink_remainder = fields.Boolean("Unlink remaining quantity")
|
||||||
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In",states={'invisible': ~Eval('shipment_in')})
|
transport_missing = fields.Boolean("Transport missing", readonly=True)
|
||||||
|
create_new_shipment = fields.Boolean(
|
||||||
|
"Create a new shipment",
|
||||||
|
states={'invisible': ~Eval('transport_missing')})
|
||||||
|
shipment_supplier = fields.Many2One(
|
||||||
|
'party.party', "Shipment supplier",
|
||||||
|
states={
|
||||||
|
'invisible': ~(
|
||||||
|
Eval('transport_missing') & Eval('create_new_shipment')),
|
||||||
|
'required': (
|
||||||
|
Eval('transport_missing') & Eval('create_new_shipment')),
|
||||||
|
})
|
||||||
|
transport_from_location = fields.Many2One(
|
||||||
|
'stock.location', "From location", readonly=True,
|
||||||
|
states={'invisible': ~Eval('transport_missing')})
|
||||||
|
transport_to_location = fields.Many2One(
|
||||||
|
'stock.location', "To location", readonly=True,
|
||||||
|
states={'invisible': ~Eval('transport_missing')})
|
||||||
|
shipment_bl_number = fields.Char(
|
||||||
|
"BL number",
|
||||||
|
states={'invisible': ~(
|
||||||
|
Eval('transport_missing') & Eval('create_new_shipment'))})
|
||||||
|
shipment_bl_date = fields.Date(
|
||||||
|
"BL date",
|
||||||
|
states={'invisible': ~(
|
||||||
|
Eval('transport_missing') & Eval('create_new_shipment'))})
|
||||||
|
shipment_in = fields.Many2One(
|
||||||
|
'stock.shipment.in', "Shipment In",
|
||||||
|
states={
|
||||||
|
'invisible': ~Eval('transport_missing') & ~Eval('shipment_in'),
|
||||||
|
'readonly': ~Eval('transport_missing'),
|
||||||
|
})
|
||||||
shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal",states={'invisible': ~Eval('shipment_internal')})
|
shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal",states={'invisible': ~Eval('shipment_internal')})
|
||||||
shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out",states={'invisible': ~Eval('shipment_out')})
|
shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out",states={'invisible': ~Eval('shipment_out')})
|
||||||
lots = fields.One2Many('lot.add.line','lal',"Lots")
|
lots = fields.One2Many('lot.add.line','lal',"Lots")
|
||||||
@@ -4341,6 +4432,10 @@ class LotAddLot(ModelView):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def default_unlink_remainder(cls):
|
def default_unlink_remainder(cls):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_create_new_shipment(cls):
|
||||||
|
return False
|
||||||
|
|
||||||
class LotAddLine(ModelView):
|
class LotAddLine(ModelView):
|
||||||
"Lots"
|
"Lots"
|
||||||
|
|||||||
@@ -713,8 +713,12 @@ class CharterParty(ModelSQL, ModelView):
|
|||||||
class ShipmentIn(metaclass=PoolMeta):
|
class ShipmentIn(metaclass=PoolMeta):
|
||||||
__name__ = 'stock.shipment.in'
|
__name__ = 'stock.shipment.in'
|
||||||
|
|
||||||
from_location = fields.Many2One('stock.location', 'From location')
|
from_location = fields.Many2One(
|
||||||
to_location = fields.Many2One('stock.location', 'To location')
|
'stock.location', 'From location',
|
||||||
|
domain=[('type', '!=', 'customer')])
|
||||||
|
to_location = fields.Many2One(
|
||||||
|
'stock.location', 'To location',
|
||||||
|
domain=[('type', '!=', 'supplier')])
|
||||||
shipment_type = fields.Function(fields.Selection([
|
shipment_type = fields.Function(fields.Selection([
|
||||||
(None, ''),
|
(None, ''),
|
||||||
('dropship', 'Dropship'),
|
('dropship', 'Dropship'),
|
||||||
|
|||||||
@@ -372,6 +372,22 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
'planned_to_location': 20,
|
'planned_to_location': 20,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def test_lot_shipping_defaults_purchase_locations_without_plan(self):
|
||||||
|
'link to transport falls back to purchase locations without a plan'
|
||||||
|
wizard = lot_module.LotShipping()
|
||||||
|
wizard.records = [Mock(
|
||||||
|
r_planned_from_location=None,
|
||||||
|
r_planned_to_location=None,
|
||||||
|
r_purchase=Mock(
|
||||||
|
from_location=Mock(id=10),
|
||||||
|
to_location=Mock(id=20)),
|
||||||
|
)]
|
||||||
|
|
||||||
|
self.assertEqual(wizard.default_ship(None), {
|
||||||
|
'planned_from_location': 10,
|
||||||
|
'planned_to_location': 20,
|
||||||
|
})
|
||||||
|
|
||||||
def test_lot_shipping_defaults_uniform_planned_locations(self):
|
def test_lot_shipping_defaults_uniform_planned_locations(self):
|
||||||
'link to transport carries uniform planned locations from selection'
|
'link to transport carries uniform planned locations from selection'
|
||||||
wizard = lot_module.LotShipping()
|
wizard = lot_module.LotShipping()
|
||||||
@@ -509,6 +525,47 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
Lot.assert_lines_quantity_consistency.assert_called_once_with([
|
Lot.assert_lines_quantity_consistency.assert_called_once_with([
|
||||||
lot.line])
|
lot.line])
|
||||||
|
|
||||||
|
def test_lot_shipping_open_quantity_uses_shared_transport_link(self):
|
||||||
|
'standalone link action keeps linking open lot.qt quantities'
|
||||||
|
wizard = lot_module.LotShipping()
|
||||||
|
shipment = Mock(id=40)
|
||||||
|
wizard.ship = Mock(
|
||||||
|
shipment='in',
|
||||||
|
shipment_in=shipment,
|
||||||
|
shipment_out=None,
|
||||||
|
shipment_internal=None,
|
||||||
|
create_new_shipment=False,
|
||||||
|
quantity=None,
|
||||||
|
)
|
||||||
|
wizard.records = [Mock(
|
||||||
|
id=10000007,
|
||||||
|
r_lot_shipment_in=None,
|
||||||
|
r_lot_quantity=Decimal('125'),
|
||||||
|
r_lot_matched=Decimal('0'),
|
||||||
|
)]
|
||||||
|
purchase_line = Mock()
|
||||||
|
source = Mock(
|
||||||
|
lot_p=Mock(line=purchase_line),
|
||||||
|
lot_s=None,
|
||||||
|
)
|
||||||
|
Lot = Mock()
|
||||||
|
Lot.skip_quantity_consistency.return_value.__enter__ = Mock()
|
||||||
|
Lot.skip_quantity_consistency.return_value.__exit__ = Mock()
|
||||||
|
LotQt = Mock(return_value=source)
|
||||||
|
Move = Mock()
|
||||||
|
pool = Mock()
|
||||||
|
pool.get.side_effect = [Lot, LotQt, Move]
|
||||||
|
|
||||||
|
with patch('trytond.modules.purchase_trade.lot.Pool',
|
||||||
|
return_value=pool):
|
||||||
|
state = wizard.transition_shipping()
|
||||||
|
|
||||||
|
self.assertEqual(state, 'end')
|
||||||
|
LotQt.link_to_transport.assert_called_once_with(
|
||||||
|
source, Decimal('125.00000'), 'stock.shipment.in,40')
|
||||||
|
Lot.assert_lines_quantity_consistency.assert_called_once_with([
|
||||||
|
purchase_line])
|
||||||
|
|
||||||
def test_lot_unship_restores_planned_open_quantity(self):
|
def test_lot_unship_restores_planned_open_quantity(self):
|
||||||
'unlinking a planned scheduled lot.qt returns it to planned'
|
'unlinking a planned scheduled lot.qt returns it to planned'
|
||||||
wizard = lot_module.LotUnship()
|
wizard = lot_module.LotUnship()
|
||||||
@@ -648,14 +705,10 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
|
|
||||||
def test_lot_shipping_scheduled_lotqt_keeps_planned_locations(self):
|
def test_lot_shipping_scheduled_lotqt_keeps_planned_locations(self):
|
||||||
'scheduled lot.qt keeps the planned from and to locations'
|
'scheduled lot.qt keeps the planned from and to locations'
|
||||||
wizard = lot_module.LotShipping()
|
LotQt = Pool().get('lot.qt')
|
||||||
wizard.ship = Mock(
|
linked = Mock()
|
||||||
shipment='in',
|
|
||||||
shipment_in=Mock(id=40),
|
|
||||||
shipment_out=None,
|
|
||||||
shipment_internal=None,
|
|
||||||
)
|
|
||||||
lqt = Mock(
|
lqt = Mock(
|
||||||
|
lot_quantity=Decimal('125'),
|
||||||
lot_p=Mock(id=1),
|
lot_p=Mock(id=1),
|
||||||
lot_s=Mock(id=2),
|
lot_s=Mock(id=2),
|
||||||
lot_unit=Mock(id=3),
|
lot_unit=Mock(id=3),
|
||||||
@@ -669,11 +722,15 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
planned_to_date=datetime.date(2026, 7, 5),
|
planned_to_date=datetime.date(2026, 7, 5),
|
||||||
planned_note='POL to POD',
|
planned_note='POL to POD',
|
||||||
)
|
)
|
||||||
LotQt = Mock()
|
lqt.is_planned_transport.return_value = True
|
||||||
|
|
||||||
wizard._create_scheduled_lotqt(LotQt, lqt, Decimal('125'))
|
with patch.object(
|
||||||
|
LotQt, 'create', return_value=[linked]) as create, \
|
||||||
|
patch.object(LotQt, 'save') as save:
|
||||||
|
result = LotQt.link_to_transport(
|
||||||
|
lqt, Decimal('125'), 'stock.shipment.in,40')
|
||||||
|
|
||||||
LotQt.create.assert_called_once_with([{
|
create.assert_called_once_with([{
|
||||||
'lot_p': 1,
|
'lot_p': 1,
|
||||||
'lot_s': 2,
|
'lot_s': 2,
|
||||||
'lot_quantity': Decimal('125'),
|
'lot_quantity': Decimal('125'),
|
||||||
@@ -689,6 +746,124 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
'planned_note': 'POL to POD',
|
'planned_note': 'POL to POD',
|
||||||
'lot_shipment_in': 40,
|
'lot_shipment_in': 40,
|
||||||
}])
|
}])
|
||||||
|
save.assert_called_once_with([lqt])
|
||||||
|
self.assertEqual(lqt.lot_quantity, 0)
|
||||||
|
self.assertIs(result, linked)
|
||||||
|
|
||||||
|
def test_add_physical_lot_defaults_transport_from_purchase(self):
|
||||||
|
'add physical lot proposes shipment creation from purchase values'
|
||||||
|
wizard = lot_module.LotAdding()
|
||||||
|
purchase = Mock(
|
||||||
|
party=Mock(id=30),
|
||||||
|
from_location=Mock(id=10),
|
||||||
|
to_location=Mock(id=20),
|
||||||
|
)
|
||||||
|
lqt = Mock(
|
||||||
|
lot_p=Mock(line=Mock(
|
||||||
|
purchase=purchase,
|
||||||
|
unit=Mock(id=3))),
|
||||||
|
lot_quantity=Decimal('125'),
|
||||||
|
lot_shipment_in=None,
|
||||||
|
lot_shipment_internal=None,
|
||||||
|
lot_shipment_out=None,
|
||||||
|
planned_from_location=None,
|
||||||
|
planned_to_location=None,
|
||||||
|
)
|
||||||
|
lqt.lot_p.lot_unit_line = Mock(id=3)
|
||||||
|
LotQt = Mock(return_value=lqt)
|
||||||
|
pool = Mock()
|
||||||
|
pool.get.side_effect = [Mock(), LotQt]
|
||||||
|
|
||||||
|
with patch('trytond.modules.purchase_trade.lot.Pool',
|
||||||
|
return_value=pool), patch(
|
||||||
|
'trytond.modules.purchase_trade.lot.Transaction'
|
||||||
|
) as TransactionMock:
|
||||||
|
TransactionMock.return_value.context = {
|
||||||
|
'active_ids': [10000007]}
|
||||||
|
values = wizard.default_add(None)
|
||||||
|
|
||||||
|
self.assertTrue(values['transport_missing'])
|
||||||
|
self.assertEqual(values['shipment_supplier'], 30)
|
||||||
|
self.assertEqual(values['transport_from_location'], 10)
|
||||||
|
self.assertEqual(values['transport_to_location'], 20)
|
||||||
|
|
||||||
|
def test_add_physical_lot_links_existing_shipment_before_add(self):
|
||||||
|
'add physical lot can link an existing shipment in one transaction'
|
||||||
|
wizard = lot_module.LotAdding()
|
||||||
|
shipment = Mock(id=40)
|
||||||
|
source = Mock(lot_quantity=Decimal('125'))
|
||||||
|
source.has_shipment.return_value = False
|
||||||
|
linked = Mock()
|
||||||
|
LotQt = Mock(return_value=source)
|
||||||
|
LotQt.link_to_transport.return_value = linked
|
||||||
|
Lot = Mock()
|
||||||
|
Lot.skip_quantity_consistency.return_value.__enter__ = Mock()
|
||||||
|
Lot.skip_quantity_consistency.return_value.__exit__ = Mock()
|
||||||
|
pool = Mock()
|
||||||
|
pool.get.side_effect = [LotQt, Lot]
|
||||||
|
wizard.records = [Mock(id=10000007)]
|
||||||
|
wizard.add = Mock(
|
||||||
|
shipment_in=shipment,
|
||||||
|
create_new_shipment=False,
|
||||||
|
lots=[Mock()],
|
||||||
|
unlink_remainder=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch('trytond.modules.purchase_trade.lot.Pool',
|
||||||
|
return_value=pool):
|
||||||
|
state = wizard.transition_adding()
|
||||||
|
|
||||||
|
self.assertEqual(state, 'end')
|
||||||
|
LotQt.link_to_transport.assert_called_once_with(
|
||||||
|
source, Decimal('125'), 'stock.shipment.in,40')
|
||||||
|
LotQt.add_physical_lots.assert_called_once_with(
|
||||||
|
linked, wizard.add.lots, True)
|
||||||
|
|
||||||
|
def test_add_physical_lot_creates_shipment_with_optional_bl(self):
|
||||||
|
'add physical lot creates and links a shipment with optional BL data'
|
||||||
|
wizard = lot_module.LotAdding()
|
||||||
|
shipment = Mock(id=40)
|
||||||
|
source = Mock(lot_quantity=Decimal('125'))
|
||||||
|
source.has_shipment.return_value = False
|
||||||
|
linked = Mock()
|
||||||
|
LotQt = Mock(return_value=source)
|
||||||
|
LotQt.link_to_transport.return_value = linked
|
||||||
|
ShipmentIn = Mock()
|
||||||
|
ShipmentIn.create.return_value = [shipment]
|
||||||
|
Lot = Mock()
|
||||||
|
Lot.skip_quantity_consistency.return_value.__enter__ = Mock()
|
||||||
|
Lot.skip_quantity_consistency.return_value.__exit__ = Mock()
|
||||||
|
pool = Mock()
|
||||||
|
pool.get.side_effect = [LotQt, ShipmentIn, Lot]
|
||||||
|
wizard.records = [Mock(id=10000007)]
|
||||||
|
wizard.add = Mock(
|
||||||
|
shipment_in=None,
|
||||||
|
create_new_shipment=True,
|
||||||
|
shipment_supplier=Mock(id=30),
|
||||||
|
transport_from_location=Mock(id=10),
|
||||||
|
transport_to_location=Mock(id=20),
|
||||||
|
shipment_bl_number='BL-2026-01',
|
||||||
|
shipment_bl_date=datetime.date(2026, 6, 22),
|
||||||
|
lots=[Mock()],
|
||||||
|
unlink_remainder=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch('trytond.modules.purchase_trade.lot.Pool',
|
||||||
|
return_value=pool):
|
||||||
|
state = wizard.transition_adding()
|
||||||
|
|
||||||
|
self.assertEqual(state, 'end')
|
||||||
|
ShipmentIn.create.assert_called_once_with([{
|
||||||
|
'supplier': 30,
|
||||||
|
'from_location': 10,
|
||||||
|
'to_location': 20,
|
||||||
|
'bl_number': 'BL-2026-01',
|
||||||
|
'bl_date': datetime.date(2026, 6, 22),
|
||||||
|
}])
|
||||||
|
LotQt.link_to_transport.assert_called_once_with(
|
||||||
|
source, Decimal('125'), 'stock.shipment.in,40')
|
||||||
|
LotQt.add_physical_lots.assert_called_once_with(
|
||||||
|
linked, wizard.add.lots, True)
|
||||||
|
|
||||||
@with_transaction()
|
@with_transaction()
|
||||||
def test_get_mtm_applies_component_ratio_as_percentage(self):
|
def test_get_mtm_applies_component_ratio_as_percentage(self):
|
||||||
@@ -2604,6 +2779,18 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
|
|
||||||
self.assertEqual(shipment.get_shipment_type(), 'inbound')
|
self.assertEqual(shipment.get_shipment_type(), 'inbound')
|
||||||
|
|
||||||
|
@with_transaction()
|
||||||
|
def test_shipment_in_location_domains_follow_trade_direction(self):
|
||||||
|
'shipment locations exclude invalid trade counterpart directions'
|
||||||
|
ShipmentIn = Pool().get('stock.shipment.in')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
ShipmentIn.from_location.domain,
|
||||||
|
[('type', '!=', 'customer')])
|
||||||
|
self.assertEqual(
|
||||||
|
ShipmentIn.to_location.domain,
|
||||||
|
[('type', '!=', 'supplier')])
|
||||||
|
|
||||||
@with_transaction()
|
@with_transaction()
|
||||||
def test_lot_report_shipment_type_uses_linked_shipment(self):
|
def test_lot_report_shipment_type_uses_linked_shipment(self):
|
||||||
'lot report shipment type mirrors the linked shipment_in'
|
'lot report shipment type mirrors the linked shipment_in'
|
||||||
|
|||||||
@@ -7,6 +7,19 @@
|
|||||||
<label name="unlink_remainder"/>
|
<label name="unlink_remainder"/>
|
||||||
<field name="unlink_remainder"/>
|
<field name="unlink_remainder"/>
|
||||||
<newline/>
|
<newline/>
|
||||||
|
<field name="transport_missing" invisible="1"/>
|
||||||
|
<label name="create_new_shipment"/>
|
||||||
|
<field name="create_new_shipment"/>
|
||||||
|
<label name="shipment_supplier"/>
|
||||||
|
<field name="shipment_supplier"/>
|
||||||
|
<label name="transport_from_location"/>
|
||||||
|
<field name="transport_from_location"/>
|
||||||
|
<label name="transport_to_location"/>
|
||||||
|
<field name="transport_to_location"/>
|
||||||
|
<label name="shipment_bl_number"/>
|
||||||
|
<field name="shipment_bl_number"/>
|
||||||
|
<label name="shipment_bl_date"/>
|
||||||
|
<field name="shipment_bl_date"/>
|
||||||
<label name="shipment_in"/>
|
<label name="shipment_in"/>
|
||||||
<field name="shipment_in"/>
|
<field name="shipment_in"/>
|
||||||
<label name="shipment_internal"/>
|
<label name="shipment_internal"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user