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
@@ -161,6 +161,13 @@ class ZonePicking(Component):
161
161
  def _pick_pack_same_time(self):
162
162
  return self.work.menu.pick_pack_same_time
163
163
 
164
+ def _handle_complete_mix_pack(self, package):
165
+ packaging = self._actions_for("packaging")
166
+ return (
167
+ packaging.is_complete_mix_pack(package)
168
+ and not self.work.menu.no_prefill_qty
169
+ )
170
+
164
171
  def _response_for_start(self, message=None):
165
172
  zones = self.work.menu.picking_type_ids.mapped(
166
173
  "default_location_src_id.child_ids"
@@ -197,7 +204,7 @@ class ZonePicking(Component):
197
204
  move_lines,
198
205
  message=None,
199
206
  popup=None,
200
- confirmation_required=False,
207
+ confirmation_required=None,
201
208
  product=False,
202
209
  sublocation=False,
203
210
  package=False,
@@ -220,7 +227,7 @@ class ZonePicking(Component):
220
227
  self,
221
228
  move_line,
222
229
  message=None,
223
- confirmation_required=False,
230
+ confirmation_required=None,
224
231
  **kw,
225
232
  ):
226
233
  if confirmation_required and not message:
@@ -228,6 +235,12 @@ class ZonePicking(Component):
228
235
  data = self._data_for_move_line(move_line)
229
236
  data["move_line"].update(kw)
230
237
  data["confirmation_required"] = confirmation_required
238
+ data[
239
+ "allow_alternative_destination_package"
240
+ ] = self.work.menu.allow_alternative_destination_package
241
+ data["handle_complete_mix_pack"] = self._handle_complete_mix_pack(
242
+ move_line.package_id
243
+ )
231
244
  return self._response(
232
245
  next_state="set_line_destination", data=data, message=message
233
246
  )
@@ -252,7 +265,7 @@ class ZonePicking(Component):
252
265
  self,
253
266
  move_lines,
254
267
  message=None,
255
- confirmation_required=False,
268
+ confirmation_required=None,
256
269
  ):
257
270
  if confirmation_required and not message:
258
271
  message = self.msg_store.need_confirmation()
@@ -275,7 +288,7 @@ class ZonePicking(Component):
275
288
  self,
276
289
  move_line,
277
290
  message=None,
278
- confirmation_required=False,
291
+ confirmation_required=None,
279
292
  ):
280
293
  if confirmation_required and not message:
281
294
  message = self.msg_store.need_confirmation()
@@ -346,11 +359,19 @@ class ZonePicking(Component):
346
359
  # to retrieve if location will be empty.
347
360
  # Maybe group lines by location and compute only once.
348
361
  move_line = self.env["stock.move.line"].browse(data_move_line["id"])
362
+ handle_complete_mix_pack = self._handle_complete_mix_pack(
363
+ move_line.package_id
364
+ )
365
+ data_move_line["handle_complete_mix_pack"] = handle_complete_mix_pack
349
366
  # `location_will_be_empty` flag states if, by processing this move line
350
367
  # and picking the product, the location will be emptied.
351
368
  data_move_line[
352
369
  "location_will_be_empty"
353
- ] = move_line.location_id.planned_qty_in_location_is_empty(move_line)
370
+ ] = move_line.location_id.planned_qty_in_location_is_empty(
371
+ move_line.package_id.move_line_ids
372
+ if handle_complete_mix_pack
373
+ else move_line
374
+ )
354
375
  return data
355
376
 
356
377
  def _data_for_location(self, location, zone_location=None, picking_type=None):
@@ -506,7 +527,7 @@ class ZonePicking(Component):
506
527
  def _scan_source_location(
507
528
  self,
508
529
  barcode,
509
- confirmation=False,
530
+ confirmation=None,
510
531
  product_id=False,
511
532
  sublocation=False,
512
533
  package=False,
@@ -576,7 +597,7 @@ class ZonePicking(Component):
576
597
  def _scan_source_package(
577
598
  self,
578
599
  barcode,
579
- confirmation=False,
600
+ confirmation=None,
580
601
  product_id=False,
581
602
  sublocation=False,
582
603
  package=False,
@@ -594,21 +615,28 @@ class ZonePicking(Component):
594
615
  package = search.package_from_scan(barcode)
595
616
  if not package:
596
617
  return response, message
597
- if not package.location_id.is_sublocation_of(self.zone_location):
598
- # Package is not in an allowed location
599
- response = self._list_move_lines(self.zone_location)
600
- message = self.msg_store.location_not_allowed()
601
- return response, message
618
+ if package.location_id:
619
+ if not package.location_id.is_sublocation_of(self.zone_location):
620
+ # Package is not in an allowed location
621
+ response = self._list_move_lines(self.zone_location)
622
+ message = self.msg_store.location_not_allowed()
623
+ return response, message
602
624
 
603
625
  move_lines = self._find_location_move_lines(
604
626
  locations=sublocation, package=package
605
627
  )
628
+ handle_complete_mix_pack = self._handle_complete_mix_pack(package)
606
629
  if move_lines:
630
+ if handle_complete_mix_pack:
631
+ response = self._response_for_set_line_destination(
632
+ first(move_lines), qty_done=0
633
+ )
634
+ return response, message
607
635
  if packaging.package_has_several_products(package):
608
636
  message = self.msg_store.several_products_in_package(package)
609
637
  if packaging.package_has_several_lots(package):
610
638
  message = self.msg_store.several_lots_in_package(package)
611
- if message:
639
+ if message or self.work.menu.no_prefill_qty:
612
640
  return (
613
641
  self._list_move_lines(
614
642
  self.zone_location,
@@ -617,8 +645,8 @@ class ZonePicking(Component):
617
645
  ),
618
646
  message,
619
647
  )
648
+
620
649
  move_line = first(move_lines)
621
- # Fix me for a package prefill qty is zero ?
622
650
  qty_done = self._get_prefill_qty(move_line)
623
651
  response = self._response_for_set_line_destination(
624
652
  move_line, qty_done=qty_done
@@ -632,19 +660,22 @@ class ZonePicking(Component):
632
660
  product=product,
633
661
  )
634
662
  if move_lines:
635
- if not confirmation:
663
+ if confirmation != barcode:
636
664
  message = self.msg_store.package_different_change()
637
665
  response = self._response_for_select_line(
638
- move_lines, message, confirmation_required=True
666
+ move_lines, message, confirmation_required=barcode
639
667
  )
640
668
  else:
641
669
  change_package_lot = self._actions_for("change.package.lot")
670
+ move_line = first(move_lines)
642
671
  response = change_package_lot.change_package(
643
- first(move_lines),
672
+ move_line,
644
673
  package,
645
- # FIXME we may need to pass the quantity being done
646
- self._response_for_set_line_destination,
647
- self._response_for_change_pack_lot,
674
+ response_ok_func=functools.partial(
675
+ self._response_for_set_line_destination,
676
+ qty_done=self._get_prefill_qty(move_line, qty=0),
677
+ ),
678
+ response_error_func=self._response_for_change_pack_lot,
648
679
  )
649
680
  else:
650
681
  response = self._list_move_lines(sublocation or self.zone_location)
@@ -668,7 +699,7 @@ class ZonePicking(Component):
668
699
  def _scan_source_product(
669
700
  self,
670
701
  barcode,
671
- confirmation=False,
702
+ confirmation=None,
672
703
  product_id=False,
673
704
  sublocation=False,
674
705
  package=False,
@@ -730,7 +761,7 @@ class ZonePicking(Component):
730
761
  def _scan_source_lot(
731
762
  self,
732
763
  barcode,
733
- confirmation=False,
764
+ confirmation=None,
734
765
  product_id=False,
735
766
  sublocation=False,
736
767
  package=False,
@@ -783,7 +814,7 @@ class ZonePicking(Component):
783
814
  def scan_source(
784
815
  self,
785
816
  barcode,
786
- confirmation=False,
817
+ confirmation=None,
787
818
  product_id=None,
788
819
  sublocation_id=None,
789
820
  package_id=None,
@@ -860,13 +891,15 @@ class ZonePicking(Component):
860
891
  base_response=response, message=self.msg_store.barcode_not_found()
861
892
  )
862
893
 
863
- def _set_destination_location(self, move_line, quantity, confirmation, location):
894
+ def _set_destination_location(
895
+ self, move_line, package, quantity, confirmation, location, barcode
896
+ ):
864
897
  location_changed = False
865
898
  response = None
866
899
 
867
900
  # A valid location is a sub-location of the original destination, or a
868
901
  # any sub-location of the picking type's default destination location
869
- # if `confirmation is True
902
+ # if `confirmation is equal to the barcode scanned.
870
903
  # Ask confirmation to the user if the scanned location is not in the
871
904
  # expected ones but is valid (in picking type's default destination)
872
905
  if not self.is_dest_location_valid(move_line.move_id, location):
@@ -877,7 +910,7 @@ class ZonePicking(Component):
877
910
  )
878
911
  return (location_changed, response)
879
912
 
880
- if not confirmation and self.is_dest_location_to_confirm(
913
+ if confirmation != barcode and self.is_dest_location_to_confirm(
881
914
  move_line.location_dest_id, location
882
915
  ):
883
916
  response = self._response_for_set_line_destination(
@@ -885,7 +918,7 @@ class ZonePicking(Component):
885
918
  message=self.msg_store.confirm_location_changed(
886
919
  move_line.location_dest_id, location
887
920
  ),
888
- confirmation_required=True,
921
+ confirmation_required=barcode,
889
922
  qty_done=quantity,
890
923
  )
891
924
  return (location_changed, response)
@@ -899,24 +932,41 @@ class ZonePicking(Component):
899
932
  )
900
933
  return (location_changed, response)
901
934
  # destination location set to the scanned one
902
- self._write_destination_on_lines(move_line, location)
935
+
903
936
  stock = self._actions_for("stock")
937
+ move_lines = move_line
938
+ if package:
939
+ quantity = None
940
+ # Handling all move lines from a complete mix pack
941
+ for _move_line in package.move_line_ids:
942
+ if _move_line.state not in ("assigned", "partially_available"):
943
+ continue
944
+ _move_line.qty_done = move_line.reserved_uom_qty
945
+ move_lines |= _move_line
946
+ self._write_destination_on_lines(move_lines, location)
947
+
904
948
  try:
905
- stock.mark_move_line_as_picked(move_line, quantity, check_user=True)
949
+ stock.mark_move_line_as_picked(move_lines, quantity, check_user=True)
906
950
  except ConcurentWorkOnTransfer as error:
951
+ values = {"qty_done": quantity} if quantity is not None else {}
907
952
  response = self._response_for_set_line_destination(
908
953
  move_line,
909
954
  message={
910
955
  "message_type": "error",
911
956
  "body": str(error),
912
957
  },
913
- qty_done=quantity,
958
+ **values,
914
959
  )
915
960
  return (location_changed, response)
916
- stock.validate_moves(move_line.move_id)
961
+ stock.validate_moves(move_lines.move_id)
962
+
917
963
  location_changed = True
918
964
  # Zero check
919
- zero_check = self.picking_type.shopfloor_zero_check
965
+ # Only apply zero check if the product is of type "product".
966
+ zero_check = (
967
+ move_line.product_id.type == "product"
968
+ and self.picking_type.shopfloor_zero_check
969
+ )
920
970
  if zero_check and move_line.location_id.planned_qty_in_location_is_empty():
921
971
  response = self._response_for_zero_check(move_line)
922
972
  return (location_changed, response)
@@ -940,25 +990,34 @@ class ZonePicking(Component):
940
990
  move_line.reserved_uom_qty - qty, precision_rounding=rounding
941
991
  )
942
992
 
943
- def _set_destination_package(self, move_line, quantity, package):
944
- package_changed = False
945
- response = None
993
+ def _is_package_not_valid(self, package):
994
+ message = False
946
995
  # A valid package is:
947
996
  # * an empty package
948
997
  # * not used as destination for another move line
998
+ # * not contains move lines with different operation type
949
999
  if not self._is_package_empty(package):
1000
+ message = self.msg_store.package_not_empty(package)
1001
+ elif package.planned_move_line_ids:
1002
+ if not self.work.menu.multiple_move_single_pack:
1003
+ message = self.msg_store.package_already_used(package)
1004
+ else:
1005
+ for line in package.planned_move_line_ids:
1006
+ if line.picking_id.picking_type_id.id in self.picking_types.ids:
1007
+ continue
1008
+ message = self.msg_store.package_different_picking_type(
1009
+ package, line.picking_id.picking_type_id
1010
+ )
1011
+ break
1012
+ return message
1013
+
1014
+ def _set_destination_package(self, move_line, quantity, package):
1015
+ package_changed = False
1016
+ response = None
1017
+ package_invalid_message = self._is_package_not_valid(package)
1018
+ if package_invalid_message:
950
1019
  response = self._response_for_set_line_destination(
951
- move_line,
952
- message=self.msg_store.package_not_empty(package),
953
- qty_done=quantity,
954
- )
955
- return (package_changed, response)
956
- multiple_move_allowed = self.work.menu.multiple_move_single_pack
957
- if package.planned_move_line_ids and not multiple_move_allowed:
958
- response = self._response_for_set_line_destination(
959
- move_line,
960
- message=self.msg_store.package_already_used(package),
961
- qty_done=quantity,
1020
+ move_line, message=package_invalid_message, qty_done=quantity
962
1021
  )
963
1022
  return (package_changed, response)
964
1023
  # the quantity done is set to the passed quantity
@@ -973,6 +1032,7 @@ class ZonePicking(Component):
973
1032
  )
974
1033
  return (package_changed, response)
975
1034
  stock = self._actions_for("stock")
1035
+ self._lock_lines(move_line)
976
1036
  try:
977
1037
  stock.mark_move_line_as_picked(
978
1038
  move_line, quantity, package, check_user=True
@@ -989,7 +1049,11 @@ class ZonePicking(Component):
989
1049
  return (package_changed, response)
990
1050
  package_changed = True
991
1051
  # Zero check
992
- zero_check = self.picking_type.shopfloor_zero_check
1052
+ # Only apply zero check if the product is of type "product".
1053
+ zero_check = (
1054
+ move_line.product_id.type == "product"
1055
+ and self.picking_type.shopfloor_zero_check
1056
+ )
993
1057
  if zero_check and move_line.location_id.planned_qty_in_location_is_empty():
994
1058
  response = self._response_for_zero_check(move_line)
995
1059
  return (package_changed, response)
@@ -1028,7 +1092,8 @@ class ZonePicking(Component):
1028
1092
  move_line_id,
1029
1093
  barcode,
1030
1094
  quantity,
1031
- confirmation=False,
1095
+ handle_complete_mix_pack=False,
1096
+ confirmation=None,
1032
1097
  ):
1033
1098
  """Set a destination location (and done) or a destination package (in buffer)
1034
1099
 
@@ -1068,6 +1133,9 @@ class ZonePicking(Component):
1068
1133
  When the barcode is the product (or its packaging) or the lot on the line:
1069
1134
  * The done quantity is incremented by one or the packaging quantity.
1070
1135
 
1136
+ The `handle_complete_mix_pack` option, when it is set to true. Will move all they
1137
+ lines contained in the package of the move line passed in parameter.
1138
+
1071
1139
  Transitions:
1072
1140
  * select_line: destination has been set, showing the next lines to pick
1073
1141
  * zero_check: if the option is active and if the quantity of product
@@ -1086,12 +1154,12 @@ class ZonePicking(Component):
1086
1154
 
1087
1155
  pkg_moved = False
1088
1156
  search = self._actions_for("search")
1089
- accept_only_package = not self._move_line_full_qty(move_line, quantity)
1157
+
1158
+ moving_full_quantity = self._move_line_full_qty(move_line, quantity)
1090
1159
 
1091
1160
  response = self._set_destination_update_quantity(move_line, quantity, barcode)
1092
1161
  if response:
1093
1162
  return response
1094
-
1095
1163
  if quantity <= 0:
1096
1164
  message = self.msg_store.picking_zero_quantity()
1097
1165
  return self._response_for_set_line_destination(
@@ -1101,10 +1169,14 @@ class ZonePicking(Component):
1101
1169
  )
1102
1170
 
1103
1171
  extra_message = ""
1104
- if not accept_only_package:
1105
- # When the barcode is a location
1172
+ if moving_full_quantity:
1173
+ # When the barcode is a location,
1174
+ # only allow it if moving the full qty.
1106
1175
  location = search.location_from_scan(barcode)
1107
1176
  if location:
1177
+ package = None
1178
+ if handle_complete_mix_pack:
1179
+ package = move_line.package_id
1108
1180
  if self._pick_pack_same_time():
1109
1181
  (
1110
1182
  good_for_packing,
@@ -1121,9 +1193,11 @@ class ZonePicking(Component):
1121
1193
  )
1122
1194
  pkg_moved, response = self._set_destination_location(
1123
1195
  move_line,
1196
+ package,
1124
1197
  quantity,
1125
1198
  confirmation,
1126
1199
  location,
1200
+ barcode,
1127
1201
  )
1128
1202
  if response:
1129
1203
  if extra_message:
@@ -1136,6 +1210,28 @@ class ZonePicking(Component):
1136
1210
  # When the barcode is a package
1137
1211
  package = search.package_from_scan(barcode)
1138
1212
  if package:
1213
+
1214
+ if not moving_full_quantity and move_line.package_id == package:
1215
+ # Check we're not using the source package as transfer package.
1216
+ message = self.msg_store.dest_package_not_valid(package)
1217
+ return self._response_for_set_line_destination(
1218
+ move_line, message=message, qty_done=quantity
1219
+ )
1220
+
1221
+ allow_alternative_package = (
1222
+ self.work.menu.allow_alternative_destination_package
1223
+ )
1224
+ if (
1225
+ not allow_alternative_package
1226
+ and move_line.result_package_id
1227
+ and move_line.result_package_id != package
1228
+ ):
1229
+ # Check whether the user can move a whole package to a different package.
1230
+ message = self.msg_store.package_transfer_not_allowed_scan_location()
1231
+ return self._response_for_set_line_destination(
1232
+ move_line, message=message, qty_done=quantity
1233
+ )
1234
+
1139
1235
  if self._pick_pack_same_time():
1140
1236
  (
1141
1237
  good_for_packing,
@@ -1155,7 +1251,7 @@ class ZonePicking(Component):
1155
1251
  message = None
1156
1252
 
1157
1253
  if not pkg_moved and not package:
1158
- if accept_only_package:
1254
+ if not moving_full_quantity:
1159
1255
  message = self.msg_store.package_not_found_for_barcode(barcode)
1160
1256
  else:
1161
1257
  # we don't know if user wanted to scan a location or a package
@@ -1341,7 +1437,10 @@ class ZonePicking(Component):
1341
1437
  # pre-configured callable used to generate the response as the
1342
1438
  # change.package.lot component is not aware of the needed response type
1343
1439
  # and related parameters for zone picking scenario
1344
- response_ok_func = functools.partial(self._response_for_set_line_destination)
1440
+ response_ok_func = functools.partial(
1441
+ self._response_for_set_line_destination,
1442
+ qty_done=self._get_prefill_qty(move_line),
1443
+ )
1345
1444
  response_error_func = functools.partial(self._response_for_change_pack_lot)
1346
1445
  response = None
1347
1446
  change_package_lot = self._actions_for("change.package.lot")
@@ -1416,7 +1515,7 @@ class ZonePicking(Component):
1416
1515
  return self._response_for_select_line(move_lines, message=message)
1417
1516
  return self._response_for_start(message=message)
1418
1517
 
1419
- def set_destination_all(self, barcode, confirmation=False):
1518
+ def set_destination_all(self, barcode, confirmation=None):
1420
1519
  """Set the destination for all the lines in the buffer
1421
1520
 
1422
1521
  Look in ``prepare_unload`` for the definition of the buffer.
@@ -1458,7 +1557,7 @@ class ZonePicking(Component):
1458
1557
  # destination set on buffer lines
1459
1558
  # - To confirm if the scanned destination is not a child of the
1460
1559
  # current destination set on buffer lines
1461
- if not confirmation and self.is_dest_location_to_confirm(
1560
+ if confirmation != barcode and self.is_dest_location_to_confirm(
1462
1561
  buffer_lines.location_dest_id, location
1463
1562
  ):
1464
1563
  return self._response_for_unload_all(
@@ -1466,7 +1565,7 @@ class ZonePicking(Component):
1466
1565
  message=self.msg_store.confirm_location_changed(
1467
1566
  first(buffer_lines.location_dest_id), location
1468
1567
  ),
1469
- confirmation_required=True,
1568
+ confirmation_required=barcode,
1470
1569
  )
1471
1570
  # the scanned location is still valid, use it
1472
1571
  self._write_destination_on_lines(buffer_lines, location)
@@ -1568,7 +1667,7 @@ class ZonePicking(Component):
1568
1667
  """Lock move lines"""
1569
1668
  self._actions_for("lock").for_update(lines)
1570
1669
 
1571
- def unload_set_destination(self, package_id, barcode, confirmation=False):
1670
+ def unload_set_destination(self, package_id, barcode, confirmation=None):
1572
1671
  """Scan the final destination for move lines in the buffer with the
1573
1672
  destination package
1574
1673
 
@@ -1610,7 +1709,7 @@ class ZonePicking(Component):
1610
1709
  # destination set on buffer lines
1611
1710
  # - To confirm if the scanned destination is not a child of the
1612
1711
  # current destination set on buffer lines
1613
- if not confirmation and self.is_dest_location_to_confirm(
1712
+ if not confirmation == barcode and self.is_dest_location_to_confirm(
1614
1713
  buffer_lines.location_dest_id, location
1615
1714
  ):
1616
1715
  return self._response_for_unload_set_destination(
@@ -1618,7 +1717,7 @@ class ZonePicking(Component):
1618
1717
  message=self.msg_store.confirm_location_changed(
1619
1718
  first(buffer_lines.location_dest_id), location
1620
1719
  ),
1621
- confirmation_required=True,
1720
+ confirmation_required=barcode,
1622
1721
  )
1623
1722
  # the scanned location is valid, use it
1624
1723
  self._write_destination_on_lines(buffer_lines, location)
@@ -1674,7 +1773,7 @@ class ShopfloorZonePickingValidator(Component):
1674
1773
  def scan_source(self):
1675
1774
  return {
1676
1775
  "barcode": {"required": False, "nullable": True, "type": "string"},
1677
- "confirmation": {"type": "boolean", "nullable": True, "required": False},
1776
+ "confirmation": {"type": "string", "nullable": True, "required": False},
1678
1777
  "product_id": {"required": False, "nullable": True, "type": "integer"},
1679
1778
  "sublocation_id": {"required": False, "nullable": True, "type": "integer"},
1680
1779
  "package_id": {"required": False, "nullable": True, "type": "integer"},
@@ -1689,7 +1788,12 @@ class ShopfloorZonePickingValidator(Component):
1689
1788
  "required": True,
1690
1789
  "type": "float",
1691
1790
  },
1692
- "confirmation": {"type": "boolean", "nullable": True, "required": False},
1791
+ "confirmation": {"type": "string", "nullable": True, "required": False},
1792
+ "handle_complete_mix_pack": {
1793
+ "type": "boolean",
1794
+ "nullable": True,
1795
+ "required": False,
1796
+ },
1693
1797
  }
1694
1798
 
1695
1799
  def is_zero(self):
@@ -1715,7 +1819,7 @@ class ShopfloorZonePickingValidator(Component):
1715
1819
  def set_destination_all(self):
1716
1820
  return {
1717
1821
  "barcode": {"required": False, "nullable": True, "type": "string"},
1718
- "confirmation": {"type": "boolean", "nullable": True, "required": False},
1822
+ "confirmation": {"type": "string", "nullable": True, "required": False},
1719
1823
  }
1720
1824
 
1721
1825
  def unload_split(self):
@@ -1731,7 +1835,7 @@ class ShopfloorZonePickingValidator(Component):
1731
1835
  return {
1732
1836
  "package_id": {"coerce": to_int, "required": True, "type": "integer"},
1733
1837
  "barcode": {"required": False, "nullable": True, "type": "string"},
1734
- "confirmation": {"type": "boolean", "nullable": True, "required": False},
1838
+ "confirmation": {"type": "string", "nullable": True, "required": False},
1735
1839
  }
1736
1840
 
1737
1841
 
@@ -1872,10 +1976,11 @@ class ShopfloorZonePickingValidatorResponse(Component):
1872
1976
  "zone_location": self.schemas._schema_dict_of(self.schemas.location()),
1873
1977
  "picking_type": self.schemas._schema_dict_of(self.schemas.picking_type()),
1874
1978
  "move_line": self.schemas._schema_dict_of(
1875
- self.schemas.move_line(with_picking=True)
1979
+ self.schemas.move_line(with_picking=True),
1980
+ required=False,
1876
1981
  ),
1877
1982
  "confirmation_required": {
1878
- "type": "boolean",
1983
+ "type": "string",
1879
1984
  "nullable": True,
1880
1985
  "required": False,
1881
1986
  },
@@ -1884,6 +1989,16 @@ class ShopfloorZonePickingValidatorResponse(Component):
1884
1989
  "nullable": True,
1885
1990
  "required": False,
1886
1991
  },
1992
+ "allow_alternative_destination_package": {
1993
+ "type": "boolean",
1994
+ "nullable": True,
1995
+ "required": False,
1996
+ },
1997
+ "handle_complete_mix_pack": {
1998
+ "type": "boolean",
1999
+ "nullable": True,
2000
+ "required": False,
2001
+ },
1887
2002
  }
1888
2003
  return schema
1889
2004
 
@@ -1896,7 +2011,7 @@ class ShopfloorZonePickingValidatorResponse(Component):
1896
2011
  self.schemas.move_line(with_picking=True)
1897
2012
  ),
1898
2013
  "confirmation_required": {
1899
- "type": "boolean",
2014
+ "type": "string",
1900
2015
  "nullable": True,
1901
2016
  "required": False,
1902
2017
  },
@@ -1925,6 +2040,11 @@ class ShopfloorZonePickingValidatorResponse(Component):
1925
2040
  "nullable": False,
1926
2041
  "required": True,
1927
2042
  }
2043
+ schema["move_lines"]["schema"]["schema"]["handle_complete_mix_pack"] = {
2044
+ "type": "boolean",
2045
+ "nullable": False,
2046
+ "required": True,
2047
+ }
1928
2048
  return schema
1929
2049
 
1930
2050
  @property
@@ -366,7 +366,7 @@ ul.auto-toc {
366
366
  !! This file is generated by oca-gen-addon-readme !!
367
367
  !! changes will be overwritten. !!
368
368
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
369
- !! source digest: sha256:993547f893f6715d97989e36897fae01ba59faacf8a2a9f66f202d3130bba967
369
+ !! source digest: sha256:835c681f1a1e049cfb7792d6dd69c0214bfbaad4775d134f6e5a47900e8aeb4a
370
370
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
371
371
  <p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/wms/tree/16.0/shopfloor"><img alt="OCA/wms" src="https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-shopfloor"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/wms&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
372
372
  <p>Shopfloor is a barcode scanner application for internal warehouse operations.</p>
@@ -26,6 +26,7 @@ from . import test_checkout_select
26
26
  from . import test_checkout_scan_line
27
27
  from . import test_checkout_scan_line_no_prefill_qty
28
28
  from . import test_checkout_scan_line_base
29
+ from . import test_checkout_scan_dest_location
29
30
  from . import test_checkout_select_line
30
31
  from . import test_checkout_select_package_base
31
32
  from . import test_checkout_set_qty
@@ -60,6 +61,7 @@ from . import test_location_content_transfer_set_destination_package_or_line
60
61
  from . import test_location_content_transfer_putaway
61
62
  from . import test_location_content_transfer_mix
62
63
  from . import test_zone_picking_base
64
+ from . import test_zone_picking_complete_mix_pack_flux
63
65
  from . import test_zone_picking_start
64
66
  from . import test_zone_picking_select_picking_type
65
67
  from . import test_zone_picking_select_line
@@ -27,6 +27,7 @@ class CommonCase(BaseCommonCase):
27
27
  cls.input_location = cls.env.ref("stock.stock_location_company")
28
28
  cls.shelf1 = cls.env.ref("stock.stock_location_components")
29
29
  cls.shelf2 = cls.env.ref("stock.stock_location_14")
30
+ cls.shelf3 = cls.shelf2.sudo().copy({"barcode": "26019853"})
30
31
 
31
32
  @classmethod
32
33
  def _shopfloor_user_values(cls):
@@ -220,6 +221,7 @@ class CommonCase(BaseCommonCase):
220
221
  package = cls.env["stock.quant.package"].create({})
221
222
  product_packages[key] = package
222
223
  for (product, location), qty in product_locations.items():
224
+ product_package = product_packages.get((product, location))
223
225
  lot = None
224
226
  if in_lot:
225
227
  if isinstance(in_lot, models.BaseModel):
@@ -235,7 +237,7 @@ class CommonCase(BaseCommonCase):
235
237
  # of units to pick a package
236
238
  qty *= 2
237
239
  cls._update_qty_in_location(
238
- location, product, qty, package=package, lot=lot
240
+ location, product, qty, package=product_package, lot=lot
239
241
  )
240
242
 
241
243
  # used by _create_package_in_location