odoo-addon-shopfloor 16.0.1.0.0.24__py3-none-any.whl → 16.0.2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. odoo/addons/shopfloor/README.rst +1 -1
  2. odoo/addons/shopfloor/__manifest__.py +1 -1
  3. odoo/addons/shopfloor/actions/data.py +69 -34
  4. odoo/addons/shopfloor/actions/data_detail.py +20 -0
  5. odoo/addons/shopfloor/actions/message.py +94 -2
  6. odoo/addons/shopfloor/actions/move_line_search.py +2 -2
  7. odoo/addons/shopfloor/actions/packaging.py +10 -0
  8. odoo/addons/shopfloor/actions/schema.py +11 -0
  9. odoo/addons/shopfloor/actions/schema_detail.py +14 -8
  10. odoo/addons/shopfloor/actions/search.py +9 -6
  11. odoo/addons/shopfloor/components/scan_handler_product.py +2 -0
  12. odoo/addons/shopfloor/data/shopfloor_scenario_data.xml +4 -2
  13. odoo/addons/shopfloor/docs/checkout_diag_seq.plantuml +19 -5
  14. odoo/addons/shopfloor/docs/checkout_diag_seq.png +0 -0
  15. odoo/addons/shopfloor/docs/single_pack_transfer_diag_seq.plantuml +4 -4
  16. odoo/addons/shopfloor/docs/single_pack_transfer_diag_seq.png +0 -0
  17. odoo/addons/shopfloor/i18n/ca.po +26 -8
  18. odoo/addons/shopfloor/i18n/de.po +26 -8
  19. odoo/addons/shopfloor/i18n/es_AR.po +36 -10
  20. odoo/addons/shopfloor/i18n/it.po +2015 -0
  21. odoo/addons/shopfloor/i18n/pt_BR.po +26 -8
  22. odoo/addons/shopfloor/i18n/shopfloor.pot +133 -8
  23. odoo/addons/shopfloor/migrations/16.0.2.0.0/post-migration.py +43 -0
  24. odoo/addons/shopfloor/models/shopfloor_menu.py +29 -5
  25. odoo/addons/shopfloor/models/stock_move_line.py +3 -0
  26. odoo/addons/shopfloor/models/stock_picking.py +11 -0
  27. odoo/addons/shopfloor/services/checkout.py +216 -61
  28. odoo/addons/shopfloor/services/cluster_picking.py +33 -18
  29. odoo/addons/shopfloor/services/delivery.py +25 -7
  30. odoo/addons/shopfloor/services/location_content_transfer.py +42 -28
  31. odoo/addons/shopfloor/services/single_pack_transfer.py +36 -13
  32. odoo/addons/shopfloor/services/zone_picking.py +187 -67
  33. odoo/addons/shopfloor/static/description/index.html +1 -1
  34. odoo/addons/shopfloor/tests/__init__.py +2 -0
  35. odoo/addons/shopfloor/tests/common.py +3 -1
  36. odoo/addons/shopfloor/tests/test_actions_data.py +46 -7
  37. odoo/addons/shopfloor/tests/test_actions_data_base.py +15 -0
  38. odoo/addons/shopfloor/tests/test_actions_data_detail.py +30 -8
  39. odoo/addons/shopfloor/tests/test_actions_packaging.py +43 -0
  40. odoo/addons/shopfloor/tests/test_checkout_base.py +15 -5
  41. odoo/addons/shopfloor/tests/test_checkout_done.py +40 -5
  42. odoo/addons/shopfloor/tests/test_checkout_list_delivery_packaging.py +1 -0
  43. odoo/addons/shopfloor/tests/test_checkout_list_package.py +3 -1
  44. odoo/addons/shopfloor/tests/test_checkout_scan.py +19 -0
  45. odoo/addons/shopfloor/tests/test_checkout_scan_dest_location.py +99 -0
  46. odoo/addons/shopfloor/tests/test_checkout_scan_line.py +3 -2
  47. odoo/addons/shopfloor/tests/test_checkout_scan_line_no_prefill_qty.py +48 -0
  48. odoo/addons/shopfloor/tests/test_checkout_scan_package_action.py +26 -0
  49. odoo/addons/shopfloor/tests/test_checkout_scan_package_action_no_prefill_qty.py +16 -0
  50. odoo/addons/shopfloor/tests/test_checkout_select_package_base.py +4 -1
  51. odoo/addons/shopfloor/tests/test_checkout_summary.py +1 -1
  52. odoo/addons/shopfloor/tests/test_cluster_picking_unload.py +37 -8
  53. odoo/addons/shopfloor/tests/test_delivery_list_stock_picking.py +5 -0
  54. odoo/addons/shopfloor/tests/test_location_content_transfer_base.py +4 -4
  55. odoo/addons/shopfloor/tests/test_location_content_transfer_set_destination_all.py +24 -2
  56. odoo/addons/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py +6 -4
  57. odoo/addons/shopfloor/tests/test_location_content_transfer_single.py +45 -0
  58. odoo/addons/shopfloor/tests/test_scan_anything.py +7 -0
  59. odoo/addons/shopfloor/tests/test_single_pack_transfer.py +59 -8
  60. odoo/addons/shopfloor/tests/test_zone_picking_base.py +36 -10
  61. odoo/addons/shopfloor/tests/test_zone_picking_change_pack_lot.py +2 -0
  62. odoo/addons/shopfloor/tests/test_zone_picking_complete_mix_pack_flux.py +59 -0
  63. odoo/addons/shopfloor/tests/test_zone_picking_select_line.py +33 -2
  64. odoo/addons/shopfloor/tests/test_zone_picking_select_line_first_scan_location.py +8 -3
  65. odoo/addons/shopfloor/tests/test_zone_picking_select_line_no_prefill_qty.py +19 -2
  66. odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination.py +88 -19
  67. odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination_package_not_allowed.py +94 -0
  68. odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py +1 -5
  69. odoo/addons/shopfloor/tests/test_zone_picking_start.py +4 -4
  70. odoo/addons/shopfloor/tests/test_zone_picking_unload_all.py +1 -1
  71. odoo/addons/shopfloor/tests/test_zone_picking_unload_set_destination.py +4 -4
  72. odoo/addons/shopfloor/views/shopfloor_menu.xml +30 -0
  73. {odoo_addon_shopfloor-16.0.1.0.0.24.dist-info → odoo_addon_shopfloor-16.0.2.1.0.dist-info}/METADATA +2 -2
  74. {odoo_addon_shopfloor-16.0.1.0.0.24.dist-info → odoo_addon_shopfloor-16.0.2.1.0.dist-info}/RECORD +76 -70
  75. {odoo_addon_shopfloor-16.0.1.0.0.24.dist-info → odoo_addon_shopfloor-16.0.2.1.0.dist-info}/WHEEL +0 -0
  76. {odoo_addon_shopfloor-16.0.1.0.0.24.dist-info → odoo_addon_shopfloor-16.0.2.1.0.dist-info}/top_level.txt +0 -0
@@ -144,10 +144,10 @@ class ClusterPicking(Component):
144
144
  message=message,
145
145
  )
146
146
 
147
- def _response_for_confirm_unload_all(self, batch, message=None):
147
+ def _response_for_confirm_unload_all(self, batch, message=None, confirmation=None):
148
148
  return self._response(
149
149
  next_state="confirm_unload_all",
150
- data=self._data_for_unload_all(batch),
150
+ data=self._data_for_unload_all(batch, confirmation=confirmation),
151
151
  message=message,
152
152
  )
153
153
 
@@ -166,10 +166,14 @@ class ClusterPicking(Component):
166
166
  message=message,
167
167
  )
168
168
 
169
- def _response_for_confirm_unload_set_destination(self, batch, package):
169
+ def _response_for_confirm_unload_set_destination(
170
+ self, batch, package, confirmation=None
171
+ ):
170
172
  return self._response(
171
173
  next_state="confirm_unload_set_destination",
172
- data=self._data_for_unload_single(batch, package),
174
+ data=self._data_for_unload_single(
175
+ batch, package, confirmation=confirmation
176
+ ),
173
177
  )
174
178
 
175
179
  def find_batch(self):
@@ -776,8 +780,11 @@ class ClusterPicking(Component):
776
780
  qty_done=quantity,
777
781
  )
778
782
  move_line.write({"qty_done": quantity, "result_package_id": bin_package.id})
779
-
780
- zero_check = move_line.picking_id.picking_type_id.shopfloor_zero_check
783
+ # Only apply zero check if the product is of type "product".
784
+ zero_check = (
785
+ move_line.product_id.type == "product"
786
+ and move_line.picking_id.picking_type_id.shopfloor_zero_check
787
+ )
781
788
  if zero_check and move_line.location_id.planned_qty_in_location_is_empty():
782
789
  return self._response_for_zero_check(batch, move_line)
783
790
 
@@ -814,20 +821,24 @@ class ClusterPicking(Component):
814
821
  # the lines have different destinations
815
822
  return self._unload_next_package(batch)
816
823
 
817
- def _data_for_unload_all(self, batch):
824
+ def _data_for_unload_all(self, batch, confirmation=None):
818
825
  lines = self._lines_to_unload(batch)
819
826
  # all the lines destinations are the same here, it looks
820
827
  # only for the first one
821
828
  first_line = fields.first(lines)
822
829
  data = self.data.picking_batch(batch)
823
830
  data.update({"location_dest": self.data.location(first_line.location_dest_id)})
831
+ if confirmation:
832
+ data.update({"confirmation": confirmation})
824
833
  return data
825
834
 
826
- def _data_for_unload_single(self, batch, package):
835
+ def _data_for_unload_single(self, batch, package, confirmation=None):
827
836
  line = fields.first(
828
837
  package.planned_move_line_ids.filtered(self._filter_for_unload)
829
838
  )
830
839
  data = self.data.picking_batch(batch)
840
+ if confirmation:
841
+ data.update({"confirmation": confirmation})
831
842
  data.update(
832
843
  {
833
844
  "package": self.data.package(package),
@@ -1068,7 +1079,7 @@ class ClusterPicking(Component):
1068
1079
  message=self.msg_store.no_package_or_lot_for_barcode(barcode),
1069
1080
  )
1070
1081
 
1071
- def set_destination_all(self, picking_batch_id, barcode, confirmation=False):
1082
+ def set_destination_all(self, picking_batch_id, barcode, confirmation=None):
1072
1083
  """Set the destination for all the lines of the batch with a dest. package
1073
1084
 
1074
1085
  This method must be used only if all the move lines which have a destination
@@ -1109,10 +1120,10 @@ class ClusterPicking(Component):
1109
1120
  batch, message=self.msg_store.dest_location_not_allowed()
1110
1121
  )
1111
1122
 
1112
- if not confirmation and self.is_dest_location_to_confirm(
1123
+ if confirmation != barcode and self.is_dest_location_to_confirm(
1113
1124
  first_line.location_dest_id, scanned_location
1114
1125
  ):
1115
- return self._response_for_confirm_unload_all(batch)
1126
+ return self._response_for_confirm_unload_all(batch, confirmation=barcode)
1116
1127
 
1117
1128
  self._unload_write_destination_on_lines(lines, scanned_location)
1118
1129
  completion_info = self._actions_for("completion.info")
@@ -1220,7 +1231,7 @@ class ClusterPicking(Component):
1220
1231
  return self._response_for_unload_set_destination(batch, package)
1221
1232
 
1222
1233
  def unload_scan_destination(
1223
- self, picking_batch_id, package_id, barcode, confirmation=False
1234
+ self, picking_batch_id, package_id, barcode, confirmation=None
1224
1235
  ):
1225
1236
  """Scan the final destination for all the move lines moved with the Bin
1226
1237
 
@@ -1232,7 +1243,7 @@ class ClusterPicking(Component):
1232
1243
  * unload_single: line is processed and the next bin can be unloaded
1233
1244
  * confirm_unload_set_destination: the destination is valid but not the
1234
1245
  expected, ask a confirmation. This state has to call again the
1235
- endpoint with confirmation=True
1246
+ endpoint with confirmation=barcode
1236
1247
  * start_line: if the batch still has lines to pick
1237
1248
  * start: if the batch is done. In this case, this method *has*
1238
1249
  to handle the closing of the batch to create backorders.
@@ -1262,7 +1273,7 @@ class ClusterPicking(Component):
1262
1273
  self._actions_for("lock").for_update(lines)
1263
1274
 
1264
1275
  def _unload_scan_destination_lines(
1265
- self, batch, package, lines, barcode, confirmation=False
1276
+ self, batch, package, lines, barcode, confirmation=None
1266
1277
  ):
1267
1278
  # Lock move lines that will be updated
1268
1279
  self._lock_lines(lines)
@@ -1276,10 +1287,12 @@ class ClusterPicking(Component):
1276
1287
  return self._response_for_unload_set_destination(
1277
1288
  batch, package, message=self.msg_store.dest_location_not_allowed()
1278
1289
  )
1279
- if not confirmation and self.is_dest_location_to_confirm(
1290
+ if confirmation != barcode and self.is_dest_location_to_confirm(
1280
1291
  first_line.location_dest_id, scanned_location
1281
1292
  ):
1282
- return self._response_for_confirm_unload_set_destination(batch, package)
1293
+ return self._response_for_confirm_unload_set_destination(
1294
+ batch, package, confirmation=barcode
1295
+ )
1283
1296
 
1284
1297
  self._unload_write_destination_on_lines(lines, scanned_location)
1285
1298
 
@@ -1384,7 +1397,7 @@ class ShopfloorClusterPickingValidator(Component):
1384
1397
  return {
1385
1398
  "picking_batch_id": {"coerce": to_int, "required": True, "type": "integer"},
1386
1399
  "barcode": {"required": True, "type": "string"},
1387
- "confirmation": {"type": "boolean", "nullable": True, "required": False},
1400
+ "confirmation": {"type": "string", "nullable": True, "required": False},
1388
1401
  }
1389
1402
 
1390
1403
  def unload_split(self):
@@ -1404,7 +1417,7 @@ class ShopfloorClusterPickingValidator(Component):
1404
1417
  "picking_batch_id": {"coerce": to_int, "required": True, "type": "integer"},
1405
1418
  "package_id": {"coerce": to_int, "required": True, "type": "integer"},
1406
1419
  "barcode": {"required": True, "type": "string"},
1407
- "confirmation": {"type": "boolean", "nullable": True, "required": False},
1420
+ "confirmation": {"type": "string", "nullable": True, "required": False},
1408
1421
  }
1409
1422
 
1410
1423
 
@@ -1599,6 +1612,7 @@ class ShopfloorClusterPickingValidatorResponse(Component):
1599
1612
  def _schema_for_unload_all(self):
1600
1613
  schema = self.schemas.picking_batch()
1601
1614
  schema["location_dest"] = self.schemas._schema_dict_of(self.schemas.location())
1615
+ schema["confirmation"] = {"type": "string", "nullable": True, "required": False}
1602
1616
  return schema
1603
1617
 
1604
1618
  @property
@@ -1606,6 +1620,7 @@ class ShopfloorClusterPickingValidatorResponse(Component):
1606
1620
  schema = self.schemas.picking_batch()
1607
1621
  schema["package"] = self.schemas._schema_dict_of(self.schemas.package())
1608
1622
  schema["location_dest"] = self.schemas._schema_dict_of(self.schemas.location())
1623
+ schema["confirmation"] = {"type": "string", "nullable": True, "required": False}
1609
1624
  return schema
1610
1625
 
1611
1626
  @property
@@ -259,13 +259,14 @@ class Delivery(Component):
259
259
  # we added auto_join for this, otherwise, the ORM would search all pickings
260
260
  # in the picking type, and then use IN (ids)
261
261
  ("picking_id.picking_type_id", "in", self.picking_types.ids),
262
+ ("picking_id.state", "not in", ("done", "cancel")),
262
263
  ]
263
264
  if no_qty_done:
264
265
  domain.append(("qty_done", "=", 0))
265
266
  return domain
266
267
 
267
268
  def _lines_from_lot_domain(
268
- self, lot, no_qty_done=True, product_qty=None, location=None
269
+ self, lot, no_qty_done=True, product_qty=None, location=None, picking=None
269
270
  ):
270
271
  location_domain = (
271
272
  [("picking_id.location_id", "=", location.id)] if location else []
@@ -283,10 +284,12 @@ class Delivery(Component):
283
284
  ("reserved_qty", ">=", product_qty),
284
285
  ]
285
286
  )
287
+ if picking:
288
+ domain.extend([("picking_id", "=", picking.id)])
286
289
  return domain
287
290
 
288
291
  def _lines_from_product_domain(
289
- self, product, no_qty_done=True, product_qty=None, location=None
292
+ self, product, no_qty_done=True, product_qty=None, location=None, picking=None
290
293
  ):
291
294
  # TODO: searching lines is common to other scenario, to refactor
292
295
  domain = expression.AND(
@@ -300,12 +303,17 @@ class Delivery(Component):
300
303
  ("reserved_qty", ">=", product_qty),
301
304
  ]
302
305
  )
306
+ if picking:
307
+ domain.extend([("picking_id", "=", picking.id)])
303
308
  return domain
304
309
 
305
- def _lines_from_package_domain(self, package, no_qty_done=True):
306
- return expression.AND(
310
+ def _lines_from_package_domain(self, package, no_qty_done=True, picking=None):
311
+ domain = expression.AND(
307
312
  [self._lines_base_domain(no_qty_done), [("package_id", "=", package.id)]]
308
313
  )
314
+ if picking:
315
+ domain.extend([("picking_id", "=", picking.id)])
316
+ return domain
309
317
 
310
318
  def _deliver_product(self, picking, product, product_qty=None, location=None):
311
319
  """Handle the scan_deliver end point for a product."""
@@ -318,7 +326,11 @@ class Delivery(Component):
318
326
 
319
327
  lines = self.env["stock.move.line"].search(
320
328
  self._lines_from_product_domain(
321
- product, no_qty_done=False, product_qty=product_qty, location=location
329
+ product,
330
+ no_qty_done=False,
331
+ product_qty=product_qty,
332
+ location=location,
333
+ picking=picking,
322
334
  ),
323
335
  order="date_planned",
324
336
  )
@@ -403,7 +415,11 @@ class Delivery(Component):
403
415
  def _deliver_lot(self, picking, lot, product_qty=None, location=None):
404
416
  lines = self.env["stock.move.line"].search(
405
417
  self._lines_from_lot_domain(
406
- lot, no_qty_done=False, product_qty=product_qty, location=location
418
+ lot,
419
+ no_qty_done=False,
420
+ product_qty=product_qty,
421
+ location=location,
422
+ picking=picking,
407
423
  )
408
424
  )
409
425
  if not lines:
@@ -608,7 +624,9 @@ class Delivery(Component):
608
624
  package = self.env["stock.quant.package"].browse(package_id).exists()
609
625
  if package:
610
626
  lines = self.env["stock.move.line"].search(
611
- self._lines_from_package_domain(package, no_qty_done=False)
627
+ self._lines_from_package_domain(
628
+ package, no_qty_done=False, picking=picking
629
+ )
612
630
  )
613
631
  if not lines:
614
632
  return self._response_for_deliver(
@@ -80,7 +80,7 @@ class LocationContentTransfer(Component):
80
80
  )
81
81
 
82
82
  def _response_for_scan_destination_all(
83
- self, pickings, message=None, confirmation_required=False
83
+ self, pickings, message=None, confirmation_required=None
84
84
  ):
85
85
  """Transition to the 'scan_destination_all' state
86
86
 
@@ -116,7 +116,7 @@ class LocationContentTransfer(Component):
116
116
  )
117
117
 
118
118
  def _response_for_scan_destination(
119
- self, location, next_content, message=None, confirmation_required=False
119
+ self, location, next_content, message=None, confirmation_required=None
120
120
  ):
121
121
  """Transition to the 'scan_destination' state
122
122
 
@@ -461,7 +461,7 @@ class LocationContentTransfer(Component):
461
461
  """Lock move lines"""
462
462
  self._actions_for("lock").for_update(lines)
463
463
 
464
- def set_destination_all(self, location_id, barcode, confirmation=False):
464
+ def set_destination_all(self, location_id, barcode, confirmation=None):
465
465
  """Scan destination location for all the moves of the location
466
466
 
467
467
  barcode is a stock.location for the destination
@@ -489,11 +489,11 @@ class LocationContentTransfer(Component):
489
489
  return self._response_for_scan_destination_all(
490
490
  pickings, message=self.msg_store.dest_location_not_allowed()
491
491
  )
492
- if not confirmation and self.is_dest_location_to_confirm(
492
+ if confirmation != barcode and self.is_dest_location_to_confirm(
493
493
  move_lines.location_dest_id, scanned_location
494
494
  ):
495
495
  return self._response_for_scan_destination_all(
496
- pickings, confirmation_required=True
496
+ pickings, confirmation_required=barcode
497
497
  )
498
498
  self._lock_lines(move_lines)
499
499
 
@@ -663,7 +663,7 @@ class LocationContentTransfer(Component):
663
663
  )
664
664
 
665
665
  def set_destination_package(
666
- self, location_id, package_level_id, barcode, confirmation=False
666
+ self, location_id, package_level_id, barcode, confirmation=None
667
667
  ):
668
668
  """Scan destination location for package level
669
669
 
@@ -697,11 +697,11 @@ class LocationContentTransfer(Component):
697
697
  package_level,
698
698
  message=self.msg_store.dest_location_not_allowed(),
699
699
  )
700
- if not confirmation and self.is_dest_location_to_confirm(
700
+ if confirmation != barcode and self.is_dest_location_to_confirm(
701
701
  package_level.location_dest_id, scanned_location
702
702
  ):
703
703
  return self._response_for_scan_destination(
704
- location, package_level, confirmation_required=True
704
+ location, package_level, confirmation_required=barcode
705
705
  )
706
706
  package_move_lines = package_level.move_line_ids
707
707
  self._lock_lines(package_move_lines)
@@ -722,7 +722,7 @@ class LocationContentTransfer(Component):
722
722
  )
723
723
 
724
724
  def set_destination_line(
725
- self, location_id, move_line_id, quantity, barcode, confirmation=False
725
+ self, location_id, move_line_id, quantity, barcode, confirmation=None
726
726
  ):
727
727
  """Scan destination location for move line
728
728
 
@@ -754,11 +754,11 @@ class LocationContentTransfer(Component):
754
754
  return self._response_for_scan_destination(
755
755
  location, move_line, message=self.msg_store.dest_location_not_allowed()
756
756
  )
757
- if not confirmation and self.is_dest_location_to_confirm(
757
+ if confirmation != barcode and self.is_dest_location_to_confirm(
758
758
  move_line.location_dest_id, scanned_location
759
759
  ):
760
760
  return self._response_for_scan_destination(
761
- location, move_line, confirmation_required=True
761
+ location, move_line, confirmation_required=barcode
762
762
  )
763
763
 
764
764
  self._lock_lines(move_line)
@@ -833,9 +833,10 @@ class LocationContentTransfer(Component):
833
833
  splits the move to have no side-effect on the other package levels/move
834
834
  lines.
835
835
 
836
- It unreserves the move, create an inventory at 0 in the move's source
837
- location, create a second draft inventory (if none exists) to check later.
838
- Finally, it cancels the move.
836
+ If the move has been created by the shopfloor user it will be canceled
837
+ otherwise it is unreserved.
838
+ Then create an inventory at 0 in the move's source location, create a
839
+ second draft inventory (if none exists) to check later.
839
840
 
840
841
  Transitions:
841
842
  * start: no more content to move
@@ -861,15 +862,23 @@ class LocationContentTransfer(Component):
861
862
  # We need to set qty_done at 0 because otherwise
862
863
  # the move_line will not be deleted
863
864
  package_move.move_line_ids.write({"qty_done": 0})
864
- package_move._do_unreserve()
865
- package_move._recompute_state()
865
+ package = package_level.package_id
866
+ if (
867
+ self.is_allow_move_create()
868
+ and self.env.user == package_move.picking_id.create_uid
869
+ ):
870
+ # Owned by the user deleting the move
871
+ package_move._action_cancel()
872
+ else:
873
+ # Not owned only unreserved
874
+ package_move._do_unreserve()
875
+ package_move._recompute_state()
866
876
  # Create an inventory at 0 in the move's source location
867
877
  inventory.create_stock_issue(package_move, location, package, lot)
868
878
  # Create a draft inventory to control stock
869
879
  inventory.create_control_stock(
870
880
  location, package_move.product_id, package, lot
871
881
  )
872
- package_move._action_cancel()
873
882
  # remove the package level (this is what does the `picking.do_unreserve()`
874
883
  # method, but here we want to unreserve+unlink this package alone)
875
884
  move_lines = self._find_transfer_move_lines(location)
@@ -882,9 +891,10 @@ class LocationContentTransfer(Component):
882
891
  splits the move to have no side-effect on the other package levels/move
883
892
  lines.
884
893
 
885
- It unreserves the move, create an inventory at 0 in the move's source
886
- location, create a second draft inventory (if none exists) to check later.
887
- Finally, it cancels the move.
894
+ If the move has been created by the shopfloor user it will be canceled
895
+ otherwise it will be unreserved.
896
+ Then an inventory is created at 0 in the move's source location,
897
+ create a second draft inventory (if none exists) to check later.
888
898
 
889
899
  Transitions:
890
900
  * start: no more content to move
@@ -906,15 +916,19 @@ class LocationContentTransfer(Component):
906
916
  # We need to set qty_done at 0 because otherwise
907
917
  # the move_line will not be deleted
908
918
  move_line.qty_done = 0
909
- move._do_unreserve()
910
- move._recompute_state()
919
+ if self.is_allow_move_create() and self.env.user == move.picking_id.create_uid:
920
+ # Owned by the user deleting the move
921
+ move._action_cancel()
922
+ else:
923
+ # Not owned unreserve
924
+ move._do_unreserve()
925
+ move._recompute_state()
911
926
  # Create an inventory at 0 in the move's source location
912
927
  inventory.create_stock_issue(move, move_line_src_location, package, lot)
913
928
  # Create a draft inventory to control stock
914
929
  inventory.create_control_stock(
915
930
  move_line_src_location, move.product_id, package, lot
916
931
  )
917
- move._action_cancel()
918
932
  move_lines = self._find_transfer_move_lines(location)
919
933
  return self._response_for_start_single(move_lines.mapped("picking_id"))
920
934
 
@@ -976,7 +990,7 @@ class ShopfloorLocationContentTransferValidator(Component):
976
990
  return {
977
991
  "location_id": {"coerce": to_int, "required": True, "type": "integer"},
978
992
  "barcode": {"required": True, "type": "string"},
979
- "confirmation": {"type": "boolean", "nullable": True, "required": False},
993
+ "confirmation": {"type": "string", "nullable": True, "required": False},
980
994
  }
981
995
 
982
996
  def go_to_single(self):
@@ -1001,7 +1015,7 @@ class ShopfloorLocationContentTransferValidator(Component):
1001
1015
  "location_id": {"coerce": to_int, "required": True, "type": "integer"},
1002
1016
  "package_level_id": {"coerce": to_int, "required": True, "type": "integer"},
1003
1017
  "barcode": {"required": True, "type": "string"},
1004
- "confirmation": {"type": "boolean", "nullable": True, "required": False},
1018
+ "confirmation": {"type": "string", "nullable": True, "required": False},
1005
1019
  }
1006
1020
 
1007
1021
  def set_destination_line(self):
@@ -1010,7 +1024,7 @@ class ShopfloorLocationContentTransferValidator(Component):
1010
1024
  "move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
1011
1025
  "quantity": {"coerce": to_float, "required": True, "type": "float"},
1012
1026
  "barcode": {"required": True, "type": "string"},
1013
- "confirmation": {"type": "boolean", "nullable": True, "required": False},
1027
+ "confirmation": {"type": "string", "nullable": True, "required": False},
1014
1028
  }
1015
1029
 
1016
1030
  def postpone_package(self):
@@ -1077,7 +1091,7 @@ class ShopfloorLocationContentTransferValidatorResponse(Component):
1077
1091
  "package_levels": self.schemas._schema_list_of(package_level_schema),
1078
1092
  "move_lines": self.schemas._schema_list_of(move_line_schema),
1079
1093
  "confirmation_required": {
1080
- "type": "boolean",
1094
+ "type": "string",
1081
1095
  "nullable": True,
1082
1096
  "required": False,
1083
1097
  },
@@ -1092,7 +1106,7 @@ class ShopfloorLocationContentTransferValidatorResponse(Component):
1092
1106
  "package_level": self.schemas._schema_dict_of(schema_package_level),
1093
1107
  "move_line": self.schemas._schema_dict_of(schema_move_line),
1094
1108
  "confirmation_required": {
1095
- "type": "boolean",
1109
+ "type": "string",
1096
1110
  "nullable": True,
1097
1111
  "required": False,
1098
1112
  },
@@ -29,6 +29,9 @@ class SinglePackTransfer(Component):
29
29
  return {
30
30
  "id": package_level.id,
31
31
  "name": package.name,
32
+ "weight_uom": package.weight_uom_id.name,
33
+ "weight": package.pack_weight,
34
+ "estimated_weight_kg": package.estimated_pack_weight_kg,
32
35
  "location_src": self.data.location(package.location_id),
33
36
  "location_dest": self.data.location(package_level.location_dest_id),
34
37
  "products": self.data.products(move_lines.product_id),
@@ -38,9 +41,9 @@ class SinglePackTransfer(Component):
38
41
  def _response_for_start(self, message=None, popup=None):
39
42
  return self._response(next_state="start", message=message, popup=popup)
40
43
 
41
- def _response_for_confirm_start(self, package_level, message=None):
44
+ def _response_for_confirm_start(self, package_level, message=None, barcode=""):
42
45
  data = self._data_after_package_scanned(package_level)
43
- data["confirmation_required"] = True
46
+ data["confirmation_required"] = barcode
44
47
  return self._response(
45
48
  next_state="start",
46
49
  data=data,
@@ -48,7 +51,7 @@ class SinglePackTransfer(Component):
48
51
  )
49
52
 
50
53
  def _response_for_scan_location(
51
- self, package_level, message=None, confirmation_required=False
54
+ self, package_level, message=None, confirmation_required=None
52
55
  ):
53
56
  data = self._data_after_package_scanned(package_level)
54
57
  data["confirmation_required"] = confirmation_required
@@ -58,7 +61,7 @@ class SinglePackTransfer(Component):
58
61
  message=message,
59
62
  )
60
63
 
61
- def _scan_source(self, barcode, confirmation=False):
64
+ def _scan_source(self, barcode, confirmation=None):
62
65
  """Search a package"""
63
66
  search = self._actions_for("search")
64
67
  location = search.location_from_scan(barcode)
@@ -90,7 +93,7 @@ class SinglePackTransfer(Component):
90
93
 
91
94
  return (None, package)
92
95
 
93
- def start(self, barcode, confirmation=False):
96
+ def start(self, barcode, confirmation=None):
94
97
  picking_types = self.picking_types
95
98
  message, package = self._scan_source(barcode, confirmation)
96
99
  if message:
@@ -161,9 +164,11 @@ class SinglePackTransfer(Component):
161
164
  message=self.msg_store.no_putaway_destination_available()
162
165
  )
163
166
 
164
- if package_level.is_done and not confirmation:
167
+ if package_level.is_done and confirmation != barcode:
165
168
  return self._response_for_confirm_start(
166
- package_level, message=self.msg_store.already_running_ask_confirmation()
169
+ package_level,
170
+ message=self.msg_store.already_running_ask_confirmation(),
171
+ barcode=barcode,
167
172
  )
168
173
  if not package_level.is_done:
169
174
  package_level.is_done = True
@@ -202,7 +207,7 @@ class SinglePackTransfer(Component):
202
207
  def _is_move_state_valid(self, moves):
203
208
  return all(move.state != "cancel" for move in moves)
204
209
 
205
- def validate(self, package_level_id, location_barcode, confirmation=False):
210
+ def validate(self, package_level_id, location_barcode, confirmation=None):
206
211
  """Validate the transfer"""
207
212
  search = self._actions_for("search")
208
213
 
@@ -232,12 +237,12 @@ class SinglePackTransfer(Component):
232
237
  package_level, message=self.msg_store.dest_location_not_allowed()
233
238
  )
234
239
 
235
- if not confirmation and self.is_dest_location_to_confirm(
240
+ if confirmation != location_barcode and self.is_dest_location_to_confirm(
236
241
  package_level.location_dest_id, scanned_location
237
242
  ):
238
243
  return self._response_for_scan_location(
239
244
  package_level,
240
- confirmation_required=True,
245
+ confirmation_required=location_barcode,
241
246
  message=self.msg_store.confirm_location_changed(
242
247
  package_level.location_dest_id, scanned_location
243
248
  ),
@@ -280,6 +285,17 @@ class SinglePackTransfer(Component):
280
285
  return self._response_for_start(message=self.msg_store.already_done())
281
286
 
282
287
  package_level.is_done = False
288
+ if (
289
+ self.is_allow_move_create()
290
+ and package_level.picking_id.create_uid == self.env.user
291
+ ):
292
+ # Cancel the transfer when it has been created by the shopfloor user
293
+ moves.picking_id.action_cancel()
294
+ else:
295
+ # Not owned only unassign the user
296
+ stock = self._actions_for("stock")
297
+ stock.unmark_move_line_as_picked(moves.move_line_ids)
298
+
283
299
  return self._response_for_start(
284
300
  message=self.msg_store.confirm_canceled_scan_next_pack()
285
301
  )
@@ -295,7 +311,7 @@ class SinglePackTransferValidator(Component):
295
311
  def start(self):
296
312
  return {
297
313
  "barcode": {"type": "string", "nullable": False, "required": True},
298
- "confirmation": {"type": "boolean", "required": False},
314
+ "confirmation": {"type": "string", "required": False},
299
315
  }
300
316
 
301
317
  def cancel(self):
@@ -307,7 +323,7 @@ class SinglePackTransferValidator(Component):
307
323
  return {
308
324
  "package_level_id": {"coerce": to_int, "required": True, "type": "integer"},
309
325
  "location_barcode": {"type": "string", "nullable": False, "required": True},
310
- "confirmation": {"type": "boolean", "required": False},
326
+ "confirmation": {"type": "string", "required": False},
311
327
  }
312
328
 
313
329
 
@@ -347,6 +363,13 @@ class SinglePackTransferValidatorResponse(Component):
347
363
  return {
348
364
  "id": {"required": required, "type": "integer"},
349
365
  "name": {"type": "string", "nullable": False, "required": required},
366
+ "weight_uom": {"type": "string", "nullable": False, "required": required},
367
+ "weight": {"type": "float", "nullable": False, "required": required},
368
+ "estimated_weight_kg": {
369
+ "type": "float",
370
+ "nullable": False,
371
+ "required": required,
372
+ },
350
373
  "location_src": {"type": "dict", "schema": self.schemas.location()},
351
374
  "location_dest": {"type": "dict", "schema": self.schemas.location()},
352
375
  "products": {
@@ -359,7 +382,7 @@ class SinglePackTransferValidatorResponse(Component):
359
382
  def _schema_confirmation_required(self):
360
383
  return {
361
384
  "confirmation_required": {
362
- "type": "boolean",
385
+ "type": "string",
363
386
  "nullable": True,
364
387
  "required": False,
365
388
  },