Improve moves synchronisation on shipments
By using the origin as synchronisation key, the code is simplified but also it allows to follow when products are changed. The same design is applied to the internal shipment which allows to synchronise also the lots. issue8363 review261811002
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Improve moves synchronisation on shipments
|
||||
* Allow move as move's origin
|
||||
* Open product quantities by warehouse to show moves
|
||||
|
||||
|
||||
+128
-105
@@ -1132,7 +1132,7 @@ class ShipmentOut(Workflow, ModelSQL, ModelView):
|
||||
@Workflow.transition('assigned')
|
||||
@set_employee('assigned_by')
|
||||
def assign(cls, shipments):
|
||||
cls._sync_inventory_to_outgoing(shipments, create=True, write=False)
|
||||
cls._sync_inventory_to_outgoing(shipments, quantity=False)
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@@ -1142,95 +1142,84 @@ class ShipmentOut(Workflow, ModelSQL, ModelView):
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
Move.do([m for s in shipments for m in s.inventory_moves])
|
||||
cls._sync_inventory_to_outgoing(shipments)
|
||||
cls._sync_inventory_to_outgoing(shipments, quantity=True)
|
||||
Move.assign([m for s in shipments for m in s.outgoing_moves])
|
||||
|
||||
def _get_outgoing_move(self, move):
|
||||
'Return outgoing move for the inventory move'
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
return Move(
|
||||
from_location=move.to_location,
|
||||
to_location=self.customer.customer_location,
|
||||
product=move.product,
|
||||
uom=move.uom,
|
||||
quantity=move.quantity,
|
||||
shipment=self,
|
||||
planned_date=self.planned_date,
|
||||
company=move.company,
|
||||
currency=move.company.currency,
|
||||
unit_price=move.unit_price,
|
||||
def _sync_move_key(self, move):
|
||||
return (
|
||||
('product', move.product),
|
||||
('uom', move.uom),
|
||||
)
|
||||
|
||||
def _sync_outgoing_move(self, template=None):
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
move = Move(
|
||||
from_location=self.warehouse_output,
|
||||
to_location=self.customer_location,
|
||||
quantity=0,
|
||||
shipment=self,
|
||||
planned_date=self.planned_date,
|
||||
company=self.company,
|
||||
)
|
||||
if template:
|
||||
move.origin = template.origin
|
||||
move.currency = template.currency
|
||||
move.unit_price = template.unit_price
|
||||
else:
|
||||
move.currency = self.company.currency
|
||||
move.unit_price = 0
|
||||
return move
|
||||
|
||||
@classmethod
|
||||
def _sync_inventory_to_outgoing(cls, shipments, create=True, write=True):
|
||||
'Synchronise outgoing moves with inventory moves'
|
||||
def _sync_inventory_to_outgoing(cls, shipments, quantity=True):
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
def active(move):
|
||||
return move.state != 'cancel'
|
||||
|
||||
moves = []
|
||||
for shipment in shipments:
|
||||
if shipment.warehouse_storage == shipment.warehouse_output:
|
||||
# Do not create inventory moves
|
||||
# Do not have inventory moves
|
||||
continue
|
||||
# Sum all outgoing quantities
|
||||
outgoing_qty = {}
|
||||
for move in shipment.outgoing_moves:
|
||||
if move.state == 'cancel':
|
||||
continue
|
||||
quantity = Uom.compute_qty(move.uom, move.quantity,
|
||||
move.product.default_uom, round=False)
|
||||
outgoing_qty.setdefault(move.product.id, 0.0)
|
||||
outgoing_qty[move.product.id] += quantity
|
||||
|
||||
to_create = []
|
||||
for move in shipment.inventory_moves:
|
||||
if move.state == 'cancel':
|
||||
continue
|
||||
qty_default_uom = Uom.compute_qty(move.uom, move.quantity,
|
||||
outgoing_moves = {m: m for m in shipment.outgoing_moves}
|
||||
inventory_qty = defaultdict(lambda: defaultdict(float))
|
||||
for move in filter(active, shipment.outgoing_moves):
|
||||
key = shipment._sync_move_key(move)
|
||||
inventory_qty[move][key] = 0
|
||||
for move in filter(active, shipment.inventory_moves):
|
||||
key = shipment._sync_move_key(move)
|
||||
outgoing_move = outgoing_moves.get(move.origin)
|
||||
qty_default_uom = Uom.compute_qty(
|
||||
move.uom, move.quantity,
|
||||
move.product.default_uom, round=False)
|
||||
# Check if the outgoing move doesn't exist already
|
||||
if outgoing_qty.get(move.product.id):
|
||||
# If it exist, decrease the sum
|
||||
if qty_default_uom <= outgoing_qty[move.product.id]:
|
||||
outgoing_qty[move.product.id] -= qty_default_uom
|
||||
inventory_qty[outgoing_move][key] += qty_default_uom
|
||||
|
||||
for outgoing_move in inventory_qty:
|
||||
if outgoing_move:
|
||||
outgoing_key = shipment._sync_move_key(outgoing_move)
|
||||
for key, qty in inventory_qty[outgoing_move].items():
|
||||
if not quantity and outgoing_move:
|
||||
# Do not create outgoing move with origin
|
||||
# to allow to reset to draft
|
||||
continue
|
||||
# Else create the complement
|
||||
if outgoing_move and key == outgoing_key:
|
||||
move = outgoing_move
|
||||
else:
|
||||
out_quantity = (qty_default_uom
|
||||
- outgoing_qty[move.product.id])
|
||||
out_quantity = Uom.compute_qty(
|
||||
move.product.default_uom, out_quantity, move.uom)
|
||||
outgoing_qty[move.product.id] = 0.0
|
||||
else:
|
||||
out_quantity = move.quantity
|
||||
|
||||
if not out_quantity:
|
||||
continue
|
||||
unit_price = Uom.compute_price(move.product.default_uom,
|
||||
move.product.list_price, move.uom)
|
||||
to_create.append(shipment._get_outgoing_move(move))
|
||||
to_create[-1].quantity = out_quantity
|
||||
to_create[-1].unit_price = unit_price
|
||||
if create and to_create:
|
||||
Move.save(to_create)
|
||||
|
||||
# Re-read the shipment and remove exceeding quantities
|
||||
for move in shipment.outgoing_moves:
|
||||
if move.state == 'cancel':
|
||||
continue
|
||||
if outgoing_qty.get(move.product.id, 0.0) > 0.0:
|
||||
exc_qty = Uom.compute_qty(move.product.default_uom,
|
||||
outgoing_qty[move.product.id], move.uom)
|
||||
removed_qty = Uom.compute_qty(move.uom,
|
||||
min(exc_qty, move.quantity), move.product.default_uom,
|
||||
round=False)
|
||||
if write:
|
||||
Move.write([move], {
|
||||
'quantity': max(
|
||||
0.0,
|
||||
move.uom.round(move.quantity - exc_qty)),
|
||||
})
|
||||
outgoing_qty[move.product.id] -= removed_qty
|
||||
move = shipment._sync_outgoing_move(outgoing_move)
|
||||
for name, value in key:
|
||||
setattr(move, name, value)
|
||||
qty = Uom.compute_qty(
|
||||
move.product.default_uom, qty,
|
||||
move.uom)
|
||||
if quantity and move.quantity != qty:
|
||||
move.quantity = qty
|
||||
moves.append(move)
|
||||
Move.save(moves)
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@@ -2192,44 +2181,76 @@ class ShipmentInternal(Workflow, ModelSQL, ModelView):
|
||||
default.setdefault('done_by', None)
|
||||
return super().copy(shipments, default=default)
|
||||
|
||||
def _sync_move_key(self, move):
|
||||
return (
|
||||
('product', move.product),
|
||||
('uom', move.uom),
|
||||
)
|
||||
|
||||
def _sync_incoming_move(self, template=None):
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
move = Move(
|
||||
from_location=self.transit_location,
|
||||
to_location=self.to_location,
|
||||
quantity=0,
|
||||
shipment=self,
|
||||
planned_date=self.planned_date,
|
||||
company=self.company,
|
||||
)
|
||||
if template:
|
||||
move.origin = template.origin
|
||||
move.currency = template.currency
|
||||
move.unit_price = template.unit_price
|
||||
else:
|
||||
move.currency = self.company.currency
|
||||
move.unit_price = 0
|
||||
return move
|
||||
|
||||
@classmethod
|
||||
def _sync_moves(cls, shipments):
|
||||
'Synchronise incoming moves with outgoing moves'
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
Uom = pool.get('product.uom')
|
||||
to_delete = []
|
||||
to_save = []
|
||||
|
||||
def active(move):
|
||||
return move.state != 'cancel'
|
||||
|
||||
moves = []
|
||||
for shipment in shipments:
|
||||
if not shipment.transit_location:
|
||||
continue
|
||||
product_qty = defaultdict(int)
|
||||
for move in shipment.outgoing_moves:
|
||||
if move.state == 'cancel':
|
||||
continue
|
||||
product_qty[move.product] += Uom.compute_qty(
|
||||
move.uom, move.quantity, move.product.default_uom,
|
||||
round=False)
|
||||
|
||||
for move in shipment.incoming_moves:
|
||||
if move.state == 'cancel':
|
||||
continue
|
||||
if product_qty[move.product] <= 0:
|
||||
to_delete.append(move)
|
||||
else:
|
||||
quantity = Uom.compute_qty(
|
||||
move.uom, move.quantity, move.product.default_uom,
|
||||
round=False)
|
||||
quantity = min(product_qty[move.product], quantity)
|
||||
move.quantity = Uom.compute_qty(
|
||||
move.product.default_uom, quantity, move.uom)
|
||||
product_qty[move.product] -= quantity
|
||||
to_save.append(move)
|
||||
incoming_moves = {m: m for m in shipment.incoming_moves}
|
||||
outgoing_qty = defaultdict(lambda: defaultdict(lambda: 0))
|
||||
for move in filter(active, shipment.incoming_moves):
|
||||
key = shipment._sync_move_key(move)
|
||||
outgoing_qty[move][key] = 0
|
||||
for move in filter(active, shipment.outgoing_moves):
|
||||
key = shipment._sync_move_key(move)
|
||||
incoming_move = incoming_moves.get(move.origin)
|
||||
qty_default_uom = Uom.compute_qty(
|
||||
move.uom, move.quantity,
|
||||
move.product.default_uom, round=False)
|
||||
outgoing_qty[incoming_move][key] += qty_default_uom
|
||||
|
||||
if to_save:
|
||||
Move.save(to_save)
|
||||
if to_delete:
|
||||
Move.delete(to_delete)
|
||||
for incoming_move in outgoing_qty:
|
||||
if incoming_move:
|
||||
incoming_key = shipment._sync_move_key(incoming_move)
|
||||
for key, qty in outgoing_qty[incoming_move].items():
|
||||
if incoming_move and key == incoming_key:
|
||||
move = incoming_move
|
||||
else:
|
||||
move = shipment._sync_incoming_move(incoming_move)
|
||||
for name, value in key:
|
||||
setattr(move, name, value)
|
||||
qty = Uom.compute_qty(
|
||||
move.product.default_uom, qty,
|
||||
move.uom)
|
||||
if move.quantity != qty:
|
||||
move.quantity = qty
|
||||
moves.append(move)
|
||||
Move.save(moves)
|
||||
|
||||
@classmethod
|
||||
def _set_transit(cls, shipments):
|
||||
@@ -2245,12 +2266,14 @@ class ShipmentInternal(Workflow, ModelSQL, ModelView):
|
||||
and m.from_location != shipment.transit_location
|
||||
and m.to_location != shipment.transit_location]
|
||||
Move.copy(moves, default={
|
||||
'from_location': shipment.transit_location.id,
|
||||
'to_location': shipment.transit_location.id,
|
||||
'planned_date': shipment.planned_date,
|
||||
'origin': lambda data: '%s,%s' % (
|
||||
Move.__name__, data['id']),
|
||||
})
|
||||
to_write.append(moves)
|
||||
to_write.append({
|
||||
'to_location': shipment.transit_location.id,
|
||||
'from_location': shipment.transit_location.id,
|
||||
'planned_date': shipment.planned_start_date,
|
||||
})
|
||||
if to_write:
|
||||
|
||||
@@ -152,7 +152,7 @@ Create Internal Shipment with lead time::
|
||||
True
|
||||
>>> move = shipment.moves.new()
|
||||
>>> move.product = product
|
||||
>>> move.quantity = 1
|
||||
>>> move.quantity = 2
|
||||
>>> move.from_location = internal_loc
|
||||
>>> move.to_location = storage_loc
|
||||
>>> shipment.click('wait')
|
||||
@@ -160,22 +160,28 @@ Create Internal Shipment with lead time::
|
||||
2
|
||||
>>> outgoing_move, = shipment.outgoing_moves
|
||||
>>> outgoing_move.quantity
|
||||
1.0
|
||||
2.0
|
||||
>>> outgoing_move.from_location == internal_loc
|
||||
True
|
||||
>>> outgoing_move.to_location == shipment.transit_location
|
||||
True
|
||||
>>> incoming_move, = shipment.incoming_moves
|
||||
>>> incoming_move.quantity
|
||||
1.0
|
||||
2.0
|
||||
>>> incoming_move.from_location == shipment.transit_location
|
||||
True
|
||||
>>> incoming_move.to_location == storage_loc
|
||||
True
|
||||
|
||||
>>> outgoing_move.quantity = 1
|
||||
>>> outgoing_move.save()
|
||||
|
||||
>>> shipment.click('assign_try')
|
||||
True
|
||||
>>> shipment.click('ship')
|
||||
>>> incoming_move, = shipment.incoming_moves
|
||||
>>> incoming_move.quantity
|
||||
1.0
|
||||
>>> shipment.outgoing_moves[0].state
|
||||
'done'
|
||||
>>> shipment.click('done')
|
||||
|
||||
Reference in New Issue
Block a user