changeset 38e2a76db719 in modules/stock:default
details: https://hg.tryton.org/modules/stock?cmd=changeset;node=38e2a76db719
description:
        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
diffstat:

 CHANGELOG                                  |    1 +
 shipment.py                                |  227 +++++++++++++++-------------
 tests/scenario_stock_shipment_internal.rst |   12 +-
 3 files changed, 135 insertions(+), 105 deletions(-)

diffs (341 lines):

diff -r cfecd4a9939a -r 38e2a76db719 CHANGELOG
--- a/CHANGELOG Fri Aug 09 16:40:04 2019 +0200
+++ b/CHANGELOG Sun Aug 18 19:22:43 2019 +0200
@@ -1,3 +1,4 @@
+* Improve moves synchronisation on shipments
 * Allow move as move's origin
 * Open product quantities by warehouse to show moves
 
diff -r cfecd4a9939a -r 38e2a76db719 shipment.py
--- a/shipment.py       Fri Aug 09 16:40:04 2019 +0200
+++ b/shipment.py       Sun Aug 18 19:22:43 2019 +0200
@@ -1132,7 +1132,7 @@
     @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 @@
         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'
+    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')
-        return Move(
-            from_location=move.to_location,
-            to_location=self.customer.customer_location,
-            product=move.product,
-            uom=move.uom,
-            quantity=move.quantity,
+        move = Move(
+            from_location=self.warehouse_output,
+            to_location=self.customer_location,
+            quantity=0,
             shipment=self,
             planned_date=self.planned_date,
-            company=move.company,
-            currency=move.company.currency,
-            unit_price=move.unit_price,
+            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 @@
         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)
+
+            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
 
-            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)
-
-        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 @@
                 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:
diff -r cfecd4a9939a -r 38e2a76db719 tests/scenario_stock_shipment_internal.rst
--- a/tests/scenario_stock_shipment_internal.rst        Fri Aug 09 16:40:04 
2019 +0200
+++ b/tests/scenario_stock_shipment_internal.rst        Sun Aug 18 19:22:43 
2019 +0200
@@ -152,7 +152,7 @@
     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 @@
     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')

Reply via email to