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
@@ -41,7 +41,7 @@ class Checkout(Component):
41
41
  _description = __doc__
42
42
 
43
43
  def _response_for_select_line(
44
- self, picking, message=None, need_confirm_pack_all=False
44
+ self, picking, message=None, need_confirm_pack_all=""
45
45
  ):
46
46
  if all(line.shopfloor_checkout_done for line in picking.move_line_ids):
47
47
  return self._response_for_summary(picking, message=message)
@@ -53,7 +53,7 @@ class Checkout(Component):
53
53
  message=message,
54
54
  )
55
55
 
56
- def _data_for_select_line(self, picking, need_confirm_pack_all=False):
56
+ def _data_for_select_line(self, picking, need_confirm_pack_all=""):
57
57
  return {
58
58
  "picking": self._data_for_stock_picking(picking),
59
59
  "group_lines_by_location": True,
@@ -71,8 +71,20 @@ class Checkout(Component):
71
71
  message=message,
72
72
  )
73
73
 
74
+ def _response_for_select_child_location(self, picking, message=None):
75
+ return self._response(
76
+ next_state="select_child_location",
77
+ data={
78
+ "picking": self._data_for_stock_picking(
79
+ picking, done=True, with_lines=False, with_location=True
80
+ ),
81
+ },
82
+ message=message,
83
+ )
84
+
74
85
  def _response_for_select_document(self, message=None):
75
- return self._response(next_state="select_document", message=message)
86
+ data = {"restrict_scan_first": self.work.menu.scan_location_or_pack_first}
87
+ return self._response(next_state="select_document", message=message, data=data)
76
88
 
77
89
  def _response_for_manual_selection(self, message=None):
78
90
  pickings = self.env["stock.picking"].search(
@@ -82,17 +94,20 @@ class Checkout(Component):
82
94
  data = {"pickings": self.data.pickings(pickings)}
83
95
  return self._response(next_state="manual_selection", data=data, message=message)
84
96
 
97
+ def _data_response_for_select_package(self, picking, lines):
98
+ return {
99
+ "selected_move_lines": self._data_for_move_lines(lines.sorted()),
100
+ "picking": self.data.picking(picking),
101
+ "packing_info": self._data_for_packing_info(picking),
102
+ "no_package_enabled": not self.options.get("checkout__disable_no_package"),
103
+ # Used by inheriting module
104
+ "package_allowed": True,
105
+ }
106
+
85
107
  def _response_for_select_package(self, picking, lines, message=None):
86
108
  return self._response(
87
109
  next_state="select_package",
88
- data={
89
- "selected_move_lines": self._data_for_move_lines(lines.sorted()),
90
- "picking": self.data.picking(picking),
91
- "packing_info": self._data_for_packing_info(picking),
92
- "no_package_enabled": not self.options.get(
93
- "checkout__disable_no_package"
94
- ),
95
- },
110
+ data=self._data_response_for_select_package(picking, lines),
96
111
  message=message,
97
112
  )
98
113
 
@@ -124,6 +139,7 @@ class Checkout(Component):
124
139
  packages.with_context(picking_id=picking.id).sorted(),
125
140
  picking=picking,
126
141
  with_packaging=True,
142
+ with_package_move_line_count=True,
127
143
  )
128
144
  return self._response(
129
145
  next_state="select_dest_package",
@@ -203,9 +219,8 @@ class Checkout(Component):
203
219
  "picking",
204
220
  "location",
205
221
  "package",
206
- "product",
207
222
  "packaging",
208
- )
223
+ ) + (("product",) if not self.work.menu.scan_location_or_pack_first else ())
209
224
  return search.find(
210
225
  barcode,
211
226
  types=search_types,
@@ -236,7 +251,7 @@ class Checkout(Component):
236
251
  def _select_document_from_package(self, package, **kw):
237
252
  pickings = package.move_line_ids.filtered(
238
253
  lambda ml: ml.state not in ("cancel", "done")
239
- ).mapped("picking_id")
254
+ ).picking_id
240
255
  if len(pickings) > 1:
241
256
  # Filter only if we find several pickings to narrow the
242
257
  # selection to one of the good type. If we have one picking
@@ -247,9 +262,7 @@ class Checkout(Component):
247
262
  pickings = pickings.filtered(
248
263
  lambda p: p.picking_type_id in self.picking_types
249
264
  )
250
- if len(pickings) == 1:
251
- picking = pickings
252
- return self._select_picking(picking, "select_document")
265
+ return self._select_picking(fields.first(pickings), "select_document")
253
266
 
254
267
  def _select_document_from_product(self, product, line_domain=None, **kw):
255
268
  line_domain = line_domain or []
@@ -317,17 +330,22 @@ class Checkout(Component):
317
330
  def _data_for_delivery_packaging(self, packaging, **kw):
318
331
  return self.data.delivery_packaging_list(packaging, **kw)
319
332
 
320
- def _data_for_stock_picking(self, picking, done=False):
333
+ def _data_for_stock_picking(
334
+ self, picking, done=False, with_lines=True, with_location=False
335
+ ):
321
336
  data = self.data.picking(picking)
322
337
  line_picker = self._lines_checkout_done if done else self._lines_to_pack
323
- data.update(
324
- {
325
- "move_lines": self._data_for_move_lines(
326
- self._lines_prepare(picking, line_picker(picking)),
327
- with_packaging=done,
328
- )
329
- }
330
- )
338
+ if with_lines:
339
+ data.update(
340
+ {
341
+ "move_lines": self._data_for_move_lines(
342
+ self._lines_prepare(picking, line_picker(picking)),
343
+ with_packaging=done,
344
+ )
345
+ }
346
+ )
347
+ if with_location:
348
+ data.update({"location_dest": self.data.location(picking.location_dest_id)})
331
349
  return data
332
350
 
333
351
  def _lines_checkout_done(self, picking):
@@ -347,7 +365,7 @@ class Checkout(Component):
347
365
  ]
348
366
 
349
367
  def _order_for_list_stock_picking(self):
350
- return "scheduled_date asc, id asc"
368
+ return "priority desc, scheduled_date asc, id asc"
351
369
 
352
370
  def list_stock_picking(self):
353
371
  """List stock.picking records available
@@ -407,7 +425,7 @@ class Checkout(Component):
407
425
  {"qty_done": 0, "shopfloor_user_id": False}
408
426
  )
409
427
 
410
- def scan_line(self, picking_id, barcode, confirm_pack_all=False):
428
+ def scan_line(self, picking_id, barcode, confirm_pack_all=None):
411
429
  """Scan move lines of the stock picking
412
430
 
413
431
  It allows to select move lines of the stock picking for the next
@@ -436,6 +454,7 @@ class Checkout(Component):
436
454
  if not selection_lines:
437
455
  return self._response_for_summary(picking)
438
456
 
457
+ # Search of the destination package
439
458
  search_result = self._scan_line_find(picking, barcode)
440
459
  result_handler = getattr(self, "_select_lines_from_" + search_result.type)
441
460
  kw = {"confirm_pack_all": confirm_pack_all}
@@ -466,7 +485,9 @@ class Checkout(Component):
466
485
  picking, message=self.msg_store.barcode_not_found()
467
486
  )
468
487
 
469
- def _select_lines_from_package(self, picking, selection_lines, package, **kw):
488
+ def _select_lines_from_package(
489
+ self, picking, selection_lines, package, prefill_qty=0, **kw
490
+ ):
470
491
  lines = selection_lines.filtered(
471
492
  lambda l: l.package_id == package and not l.shopfloor_checkout_done
472
493
  )
@@ -480,15 +501,15 @@ class Checkout(Component):
480
501
  ),
481
502
  },
482
503
  )
483
- self._select_lines(lines)
504
+ self._select_lines(lines, prefill_qty=prefill_qty)
484
505
  if self.work.menu.no_prefill_qty:
485
506
  lines = picking.move_line_ids
486
507
  return self._response_for_select_package(picking, lines)
487
508
 
488
509
  def _select_lines_from_product(
489
- self, picking, selection_lines, product, prefill_qty=1, **kw
510
+ self, picking, selection_lines, product, prefill_qty=1, check_lot=True, **kw
490
511
  ):
491
- if product.tracking in ("lot", "serial"):
512
+ if product.tracking in ("lot", "serial") and check_lot:
492
513
  return self._response_for_select_line(
493
514
  picking, message=self.msg_store.scan_lot_on_product_tracked_by_lot()
494
515
  )
@@ -516,7 +537,9 @@ class Checkout(Component):
516
537
  elif packages:
517
538
  # Select all the lines of the package when we scan a product in a
518
539
  # package and we have only one.
519
- return self._select_lines_from_package(picking, selection_lines, packages)
540
+ return self._select_lines_from_package(
541
+ picking, selection_lines, packages, prefill_qty=prefill_qty
542
+ )
520
543
  else:
521
544
  # There is no package on selected lines, so also select all other lines
522
545
  # not in a package. But only the quantity on first selected lines
@@ -535,7 +558,9 @@ class Checkout(Component):
535
558
  picking, selection_lines, packaging.product_id, prefill_qty=packaging.qty
536
559
  )
537
560
 
538
- def _select_lines_from_lot(self, picking, selection_lines, lot, **kw):
561
+ def _select_lines_from_lot(
562
+ self, picking, selection_lines, lot, prefill_qty=1, **kw
563
+ ):
539
564
  lines = selection_lines.filtered(lambda l: l.lot_id == lot)
540
565
  if not lines:
541
566
  return self._response_for_select_line(
@@ -563,28 +588,36 @@ class Checkout(Component):
563
588
  # Select all the lines of the package when we scan a lot in a
564
589
  # package and we have only one.
565
590
  return self._select_lines_from_package(
566
- picking, selection_lines, packages, **kw
591
+ picking, selection_lines, packages, prefill_qty=prefill_qty, **kw
567
592
  )
568
593
 
569
- self._select_lines(lines, prefill_qty=1)
570
- return self._response_for_select_package(picking, lines)
594
+ first_allowed_line = fields.first(lines)
595
+ return self._select_lines_from_product(
596
+ picking,
597
+ selection_lines,
598
+ first_allowed_line.product_id,
599
+ prefill_qty=prefill_qty,
600
+ check_lot=False,
601
+ )
571
602
 
572
603
  def _select_lines_from_serial(self, picking, selection_lines, lot, **kw):
573
604
  # Search for serial number is actually the same as searching for lot (as of v14...)
574
605
  return self._select_lines_from_lot(picking, selection_lines, lot, **kw)
575
606
 
607
+ # Handling of the destination package scanned
576
608
  def _select_lines_from_delivery_packaging(
577
- self, picking, selection_lines, packaging, confirm_pack_all=False, **kw
609
+ self, picking, selection_lines, packaging, confirm_pack_all=None, **kw
578
610
  ):
579
611
  """Handle delivery packaging.
580
612
 
581
-
582
- If a delivery pkg has been scanned:
613
+ A delivery pkg has been scanned:
583
614
 
584
615
  1. validate it
585
- 2. ask for confirmation to place all lines left into the same package
586
- 3. if scanned twice for confirmation,
587
- assign new package and skip `select_package` state
616
+ 2. no lines to process (no quantities set to done)
617
+ 2.a Option no prefill qty, ask to set some quantities
618
+ 2.b Otherwise ask confirmation to pack everything if not yet done
619
+ 3. if confirmation to pack everything set all quantities.
620
+ 4. assign new package and skip `select_package` state
588
621
 
589
622
  """
590
623
  carrier = self._get_carrier(picking)
@@ -600,16 +633,29 @@ class Checkout(Component):
600
633
  packaging, carrier
601
634
  ),
602
635
  )
603
- if confirm_pack_all:
604
- # Select all lines and pack them all w/o passing for select_package state
636
+ message = None
637
+ need_confirm_pack_all = ""
638
+ has_lines_to_pack = any(
639
+ self._filter_lines_to_pack(ml) for ml in selection_lines
640
+ )
641
+ if not has_lines_to_pack:
642
+ if self.work.menu.no_prefill_qty:
643
+ message = self.msg_store.no_lines_to_process_set_quantities()
644
+ elif confirm_pack_all != packaging.barcode:
645
+ need_confirm_pack_all = packaging.barcode
646
+ message = self.msg_store.confirm_put_all_goods_in_delivery_package(
647
+ packaging
648
+ )
649
+ if message:
650
+ return self._response_for_select_line(
651
+ picking,
652
+ message=message,
653
+ need_confirm_pack_all=need_confirm_pack_all,
654
+ )
655
+ if confirm_pack_all == packaging.barcode:
605
656
  self._select_lines(selection_lines)
606
- return self._create_and_assign_new_packaging(
607
- picking, selection_lines, packaging=packaging
608
- )
609
- return self._response_for_select_line(
610
- picking,
611
- message=self.msg_store.confirm_put_all_goods_in_delivery_package(packaging),
612
- need_confirm_pack_all=True,
657
+ return self._create_and_assign_new_packaging(
658
+ picking, selection_lines, packaging=packaging
613
659
  )
614
660
 
615
661
  def _select_line_package(self, picking, selection_lines, package):
@@ -630,8 +676,12 @@ class Checkout(Component):
630
676
  return self._select_lines_from_package(
631
677
  picking, selection_lines, move_line.package_id
632
678
  )
633
- self._select_lines(move_line)
634
- return self._response_for_select_package(picking, move_line)
679
+
680
+ related_lines = selection_lines.filtered(
681
+ lambda l: not l.package_id and l.product_id != move_line.product_id
682
+ )
683
+ lines = self._select_lines(move_line, related_lines=related_lines)
684
+ return self._response_for_select_package(picking, lines)
635
685
 
636
686
  def select_line(self, picking_id, package_id=None, move_line_id=None):
637
687
  """Select move lines of the stock picking
@@ -915,6 +965,13 @@ class Checkout(Component):
915
965
 
916
966
  selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists()
917
967
  search_result = self._scan_package_find(picking, barcode)
968
+ message = self._check_scan_package_find(picking, search_result)
969
+ if message:
970
+ return self._response_for_select_package(
971
+ picking,
972
+ selected_lines,
973
+ message=message,
974
+ )
918
975
  result_handler = getattr(
919
976
  self, "_scan_package_action_from_" + search_result.type
920
977
  )
@@ -939,6 +996,22 @@ class Checkout(Component):
939
996
  ),
940
997
  )
941
998
 
999
+ def _check_scan_package_find(self, picking, search_result):
1000
+ # Used by inheriting modules
1001
+ return False
1002
+
1003
+ def _find_line_to_increment(self, product_lines):
1004
+ """Find which line should have its qty incremented.
1005
+
1006
+ Return the first line for the scanned product
1007
+ which still has some qty todo.
1008
+ If none are found, return the first line for that product.
1009
+ """
1010
+ return next(
1011
+ (line for line in product_lines if line.qty_done < line.reserved_uom_qty),
1012
+ fields.first(product_lines),
1013
+ )
1014
+
942
1015
  def _scan_package_action_from_product(
943
1016
  self, picking, selected_lines, product, packaging=None, **kw
944
1017
  ):
@@ -954,7 +1027,7 @@ class Checkout(Component):
954
1027
  return self._increment_custom_qty(
955
1028
  picking,
956
1029
  selected_lines,
957
- fields.first(product_lines),
1030
+ self._find_line_to_increment(product_lines),
958
1031
  quantity_increment,
959
1032
  )
960
1033
  return self._switch_line_qty_done(picking, selected_lines, product_lines)
@@ -970,7 +1043,7 @@ class Checkout(Component):
970
1043
  lot_lines = selected_lines.filtered(lambda l: l.lot_id == lot)
971
1044
  if self.work.menu.no_prefill_qty:
972
1045
  return self._increment_custom_qty(
973
- picking, selected_lines, fields.first(lot_lines), 1
1046
+ picking, selected_lines, self._find_line_to_increment(lot_lines), 1
974
1047
  )
975
1048
  return self._switch_line_qty_done(picking, selected_lines, lot_lines)
976
1049
 
@@ -1097,7 +1170,10 @@ class Checkout(Component):
1097
1170
  if message:
1098
1171
  return self._response_for_select_document(message=message)
1099
1172
  selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists()
1100
- selected_lines.write(
1173
+ selected_lines_with_qty_done = selected_lines.filtered(
1174
+ lambda line: line.qty_done > 0
1175
+ )
1176
+ selected_lines_with_qty_done.write(
1101
1177
  {"shopfloor_checkout_done": True, "result_package_id": False}
1102
1178
  )
1103
1179
  response = self._check_allowed_qty_done(picking, selected_lines)
@@ -1337,6 +1413,7 @@ class Checkout(Component):
1337
1413
  * summary: in case of error
1338
1414
  * select_document: after done, goes back to start
1339
1415
  * confirm_done: confirm a partial
1416
+ * select_child_location: there are child destination locations
1340
1417
  """
1341
1418
  picking = self.env["stock.picking"].browse(picking_id)
1342
1419
  message = self._check_picking_status(picking)
@@ -1359,8 +1436,51 @@ class Checkout(Component):
1359
1436
  "body": _("Remaining raw product not packed, proceed anyway?"),
1360
1437
  },
1361
1438
  )
1439
+ lines_done = self._lines_checkout_done(picking)
1440
+ dest_location = lines_done.move_id.location_dest_id
1441
+ if len(dest_location) != 1 or dest_location.usage == "view":
1442
+ return self._response_for_select_child_location(
1443
+ picking,
1444
+ )
1362
1445
  stock = self._actions_for("stock")
1446
+ stock.validate_moves(lines_done.move_id)
1447
+ return self._response_for_select_document(
1448
+ message=self.msg_store.transfer_done_success(lines_done.picking_id)
1449
+ )
1450
+
1451
+ def scan_dest_location(self, picking_id, barcode):
1452
+ """Select a location destination
1453
+
1454
+ When setting the move as done, if the destination location
1455
+ has children locations, ask the user to scan one of them.
1456
+
1457
+ Transitions:
1458
+ * select_document: after done, goes back to start
1459
+ * select_child_location: in case of error
1460
+ """
1461
+ picking = self.env["stock.picking"].browse(picking_id)
1462
+ message = self._check_picking_status(picking)
1463
+ if message:
1464
+ return self._response_for_select_document(message=message)
1465
+ search = self._actions_for("search")
1466
+ scanned_location = search.location_from_scan(barcode)
1467
+ if not scanned_location:
1468
+ return self._response_for_select_child_location(
1469
+ picking,
1470
+ message=self.msg_store.location_not_found(),
1471
+ )
1472
+ allowed_locations = self.env["stock.location"].search(
1473
+ [("id", "child_of", picking.location_dest_id.id), ("usage", "!=", "view")]
1474
+ )
1475
+ if scanned_location not in allowed_locations:
1476
+ return self._response_for_select_child_location(
1477
+ picking,
1478
+ message=self.msg_store.dest_location_not_allowed(),
1479
+ )
1363
1480
  lines_done = self._lines_checkout_done(picking)
1481
+ for line in lines_done:
1482
+ line.update({"location_dest_id": scanned_location.id})
1483
+ stock = self._actions_for("stock")
1364
1484
  stock.validate_moves(lines_done.move_id)
1365
1485
  return self._response_for_select_document(
1366
1486
  message=self.msg_store.transfer_done_success(lines_done.picking_id)
@@ -1388,7 +1508,7 @@ class ShopfloorCheckoutValidator(Component):
1388
1508
  "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1389
1509
  "barcode": {"required": True, "type": "string"},
1390
1510
  "confirm_pack_all": {
1391
- "type": "boolean",
1511
+ "type": "string",
1392
1512
  "nullable": True,
1393
1513
  "required": False,
1394
1514
  },
@@ -1550,6 +1670,12 @@ class ShopfloorCheckoutValidator(Component):
1550
1670
  "confirmation": {"type": "boolean", "nullable": True, "required": False},
1551
1671
  }
1552
1672
 
1673
+ def scan_dest_location(self):
1674
+ return {
1675
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1676
+ "barcode": {"required": True, "type": "string"},
1677
+ }
1678
+
1553
1679
 
1554
1680
  class ShopfloorCheckoutValidatorResponse(Component):
1555
1681
  """Validators for the Checkout endpoints responses"""
@@ -1567,7 +1693,7 @@ class ShopfloorCheckoutValidatorResponse(Component):
1567
1693
  to the next state.
1568
1694
  """
1569
1695
  return {
1570
- "select_document": {},
1696
+ "select_document": self._schema_for_select_document,
1571
1697
  "manual_selection": self._schema_selection_list,
1572
1698
  "select_line": self._schema_stock_picking_details,
1573
1699
  "select_package": dict(
@@ -1578,6 +1704,11 @@ class ShopfloorCheckoutValidatorResponse(Component):
1578
1704
  "nullable": True,
1579
1705
  "required": False,
1580
1706
  },
1707
+ package_allowed={
1708
+ "type": "boolean",
1709
+ "nullable": True,
1710
+ "required": False,
1711
+ },
1581
1712
  ),
1582
1713
  "change_quantity": self._schema_selected_lines,
1583
1714
  "select_dest_package": self._schema_select_package,
@@ -1585,6 +1716,17 @@ class ShopfloorCheckoutValidatorResponse(Component):
1585
1716
  "summary": self._schema_summary,
1586
1717
  "change_packaging": self._schema_select_packaging,
1587
1718
  "confirm_done": self._schema_confirm_done,
1719
+ "select_child_location": self._schema_select_child_location,
1720
+ }
1721
+
1722
+ @property
1723
+ def _schema_for_select_document(self):
1724
+ return {
1725
+ "restrict_scan_first": {
1726
+ "type": "boolean",
1727
+ "nullable": False,
1728
+ "required": True,
1729
+ },
1588
1730
  }
1589
1731
 
1590
1732
  def _schema_stock_picking(self, lines_with_packaging=False):
@@ -1604,7 +1746,7 @@ class ShopfloorCheckoutValidatorResponse(Component):
1604
1746
  self._schema_stock_picking(),
1605
1747
  group_lines_by_location={"type": "boolean"},
1606
1748
  show_oneline_package_content={"type": "boolean"},
1607
- need_confirm_pack_all={"type": "boolean"},
1749
+ need_confirm_pack_all={"type": "string"},
1608
1750
  )
1609
1751
 
1610
1752
  @property
@@ -1618,6 +1760,12 @@ class ShopfloorCheckoutValidatorResponse(Component):
1618
1760
  def _schema_confirm_done(self):
1619
1761
  return self._schema_stock_picking(lines_with_packaging=True)
1620
1762
 
1763
+ @property
1764
+ def _schema_select_child_location(self):
1765
+ return {
1766
+ "picking": {"type": "dict", "schema": self.schemas.picking()},
1767
+ }
1768
+
1621
1769
  @property
1622
1770
  def _schema_selection_list(self):
1623
1771
  return {
@@ -1760,4 +1908,11 @@ class ShopfloorCheckoutValidatorResponse(Component):
1760
1908
  return self._response_schema(next_states={"summary", "select_line"})
1761
1909
 
1762
1910
  def done(self):
1763
- return self._response_schema(next_states={"summary", "confirm_done"})
1911
+ return self._response_schema(
1912
+ next_states={"summary", "confirm_done", "select_child_location"}
1913
+ )
1914
+
1915
+ def scan_dest_location(self):
1916
+ return self._response_schema(
1917
+ next_states={"confirm_done", "select_document", "select_child_location"}
1918
+ )