odoo-addon-stock-barcodes 15.0.3.1.6__py3-none-any.whl → 15.0.3.2.0.1__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 (24) hide show
  1. odoo/addons/stock_barcodes/README.rst +1 -1
  2. odoo/addons/stock_barcodes/__manifest__.py +1 -1
  3. odoo/addons/stock_barcodes/hooks.py +15 -13
  4. odoo/addons/stock_barcodes/i18n/es.po +64 -124
  5. odoo/addons/stock_barcodes/i18n/it.po +61 -8
  6. odoo/addons/stock_barcodes/i18n/stock_barcodes.pot +55 -2
  7. odoo/addons/stock_barcodes/models/__init__.py +1 -0
  8. odoo/addons/stock_barcodes/models/stock_barcodes_action.py +1 -1
  9. odoo/addons/stock_barcodes/models/stock_move.py +37 -0
  10. odoo/addons/stock_barcodes/models/stock_move_line.py +6 -12
  11. odoo/addons/stock_barcodes/models/stock_picking.py +9 -36
  12. odoo/addons/stock_barcodes/models/stock_picking_type.py +5 -6
  13. odoo/addons/stock_barcodes/static/description/index.html +1 -1
  14. odoo/addons/stock_barcodes/tests/test_stock_barcodes_picking.py +1 -0
  15. odoo/addons/stock_barcodes/wizard/stock_barcodes_read.py +3 -20
  16. odoo/addons/stock_barcodes/wizard/stock_barcodes_read_picking.py +230 -24
  17. odoo/addons/stock_barcodes/wizard/stock_barcodes_read_picking_views.xml +12 -1
  18. odoo/addons/stock_barcodes/wizard/stock_barcodes_read_todo.py +42 -105
  19. odoo/addons/stock_barcodes/wizard/stock_barcodes_read_todo_view.xml +45 -3
  20. odoo/addons/stock_barcodes/wizard/stock_barcodes_read_views.xml +0 -1
  21. {odoo_addon_stock_barcodes-15.0.3.1.6.dist-info → odoo_addon_stock_barcodes-15.0.3.2.0.1.dist-info}/METADATA +4 -4
  22. {odoo_addon_stock_barcodes-15.0.3.1.6.dist-info → odoo_addon_stock_barcodes-15.0.3.2.0.1.dist-info}/RECORD +24 -23
  23. {odoo_addon_stock_barcodes-15.0.3.1.6.dist-info → odoo_addon_stock_barcodes-15.0.3.2.0.1.dist-info}/WHEEL +1 -1
  24. {odoo_addon_stock_barcodes-15.0.3.1.6.dist-info → odoo_addon_stock_barcodes-15.0.3.2.0.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  from . import stock_barcodes_action
2
2
  from . import stock_barcodes_option
3
3
  from . import stock_barcodes_read_log
4
+ from . import stock_move
4
5
  from . import stock_move_line
5
6
  from . import stock_picking
6
7
  from . import stock_picking_type
@@ -43,7 +43,7 @@ class StockBarcodesAction(models.Model):
43
43
  }
44
44
  if option_group.get_option_value("location_id", "filled_default"):
45
45
  vals["location_id"] = (
46
- self.env["stock.warehouse"].search([])[:1].lot_stock_id.id
46
+ self.env["stock.warehouse"].search([], limit=1).lot_stock_id.id
47
47
  )
48
48
  wiz = self.env["wiz.stock.barcodes.read.inventory"].create(vals)
49
49
  action = self.env["ir.actions.actions"]._for_xml_id(
@@ -0,0 +1,37 @@
1
+ # Copyright 2024 Tecnativa - Sergio Teruel
2
+ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
3
+
4
+ from odoo import fields, models
5
+
6
+
7
+ class StockMove(models.Model):
8
+ _inherit = "stock.move"
9
+
10
+ barcode_backorder_action = fields.Selection(
11
+ [
12
+ ("pending", "Pending"),
13
+ ("create_backorder", "Create Backorder"),
14
+ ("skip_backorder", "No Backorder"),
15
+ ],
16
+ string="Backorder action",
17
+ default="pending",
18
+ )
19
+
20
+ def _action_done(self, cancel_backorder=False):
21
+ moves_cancel_backorder = self.browse()
22
+ if not cancel_backorder:
23
+ moves_cancel_backorder = self.filtered(
24
+ lambda sm: sm.barcode_backorder_action == "skip_backorder"
25
+ )
26
+ super(StockMove, moves_cancel_backorder)._action_done(cancel_backorder=True)
27
+ moves_backorder = self - moves_cancel_backorder
28
+ moves_backorder.barcode_backorder_action = "pending"
29
+ return super(StockMove, moves_backorder)._action_done(
30
+ cancel_backorder=cancel_backorder
31
+ )
32
+
33
+ def copy_data(self, default=None):
34
+ vals_list = super().copy_data(default=default)
35
+ for vals in vals_list:
36
+ vals.pop("barcode_backorder_action", None)
37
+ return vals_list
@@ -28,19 +28,13 @@ class StockMoveLine(models.Model):
28
28
 
29
29
  def action_barcode_detailed_operation_unlink(self):
30
30
  for sml in self:
31
- if sml.product_uom_qty:
32
- sml._barcodes_process_line_to_unlink()
33
- else:
34
- sml.unlink()
31
+ stock_move = sml.move_id
32
+ stock_move.barcode_backorder_action = "pending"
33
+ sml.unlink()
35
34
  # HACK: To force refresh wizard values
36
35
  wiz_barcode = self.env["wiz.stock.barcodes.read.picking"].browse(
37
36
  self.env.context.get("wiz_barcode_id", False)
38
37
  )
39
- if wiz_barcode.option_group_id.barcode_guided_mode == "guided":
40
- wiz_barcode.todo_line_id.line_ids = wiz_barcode.todo_line_id.line_ids
41
- if not any(wiz_barcode.todo_line_id.line_ids.mapped("qty_done")):
42
- wiz_barcode.fill_todo_records()
43
- wiz_barcode.determine_todo_action()
44
- else:
45
- wiz_barcode.fill_todo_records()
46
- wiz_barcode.todo_line_id.line_ids = wiz_barcode.todo_line_id.line_ids
38
+ stock_move._action_assign()
39
+ wiz_barcode.fill_todo_records()
40
+ wiz_barcode.determine_todo_action()
@@ -1,7 +1,6 @@
1
1
  # Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
2
2
  # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
3
  from odoo import models
4
- from odoo.tools.float_utils import float_compare
5
4
 
6
5
 
7
6
  class StockPicking(models.Model):
@@ -19,7 +18,7 @@ class StockPicking(models.Model):
19
18
  }
20
19
  if self.picking_type_id.code == "outgoing":
21
20
  vals["location_dest_id"] = self.location_dest_id.id
22
- if self.picking_type_id.code == "incoming":
21
+ elif self.picking_type_id.code == "incoming":
23
22
  vals["location_id"] = self.location_id.id
24
23
 
25
24
  if option_group.get_option_value("location_id", "filled_default"):
@@ -33,8 +32,8 @@ class StockPicking(models.Model):
33
32
  wiz = self.env["wiz.stock.barcodes.read.picking"].create(
34
33
  self._prepare_barcode_wiz_vals(option_group)
35
34
  )
36
- wiz.determine_todo_action()
37
35
  wiz.fill_pending_moves()
36
+ wiz.determine_todo_action()
38
37
  action = self.env["ir.actions.actions"]._for_xml_id(
39
38
  "stock_barcodes.action_stock_barcodes_read_picking"
40
39
  )
@@ -42,41 +41,15 @@ class StockPicking(models.Model):
42
41
  return action
43
42
 
44
43
  def button_validate(self):
45
- if (
46
- self.picking_type_id.barcode_option_group_id.auto_put_in_pack
47
- and not self.move_line_ids.mapped("result_package_id")
48
- ):
49
- self.action_put_in_pack()
50
- create_backorder = False
51
- # Variable initialized as True to optimize break loop
52
- skip_backorder = True
44
+ put_in_pack_picks = self.filtered(
45
+ lambda p: p.picking_type_id.barcode_option_group_id.auto_put_in_pack
46
+ and not p.move_line_ids.result_package_id
47
+ )
48
+ if put_in_pack_picks:
49
+ put_in_pack_picks.action_put_in_pack()
53
50
  if self.env.context.get("stock_barcodes_validate_picking", False):
54
- # Avoid backorder when all move lines are processed (done or done_forced)
55
- prec = self.env["decimal.precision"].precision_get(
56
- "Product Unit of Measure"
57
- )
58
- for move in self.move_lines.filtered(lambda sm: sm.state != "cancel"):
59
- if (
60
- float_compare(
61
- move.quantity_done, move.product_uom_qty, precision_digits=prec
62
- )
63
- < 0
64
- ):
65
- # In normal conditions backorder will be created
66
- create_backorder = True
67
- if not move.move_line_ids or any(
68
- sml.barcode_scan_state in ["pending"]
69
- for sml in move.move_line_ids
70
- ):
71
- # If any move are not processed we can not skip backorder
72
- skip_backorder = False
73
- break
74
- if create_backorder and skip_backorder:
75
51
  res = super(
76
- StockPicking,
77
- self.with_context(
78
- picking_ids_not_to_backorder=self.ids, skip_backorder=True
79
- ),
52
+ StockPicking, self.with_context(skip_backorder=True)
80
53
  ).button_validate()
81
54
  else:
82
55
  res = super().button_validate()
@@ -26,17 +26,15 @@ class StockPickingType(models.Model):
26
26
  "picking_mode": "picking",
27
27
  }
28
28
  if self.code == "outgoing":
29
- location_dest_id = (
29
+ vals["location_dest_id"] = (
30
30
  self.default_location_dest_id.id
31
31
  or self.env.ref("stock.stock_location_customers").id
32
32
  )
33
- vals["location_dest_id"] = location_dest_id
34
- if self.code == "incoming":
35
- location_src_id = (
33
+ elif self.code == "incoming":
34
+ vals["location_id"] = (
36
35
  self.default_location_src_id.id
37
36
  or self.env.ref("stock.stock_location_suppliers").id
38
37
  )
39
- vals["location_id"] = location_src_id
40
38
  if self.barcode_option_group_id.get_option_value(
41
39
  "location_id", "filled_default"
42
40
  ):
@@ -46,8 +44,8 @@ class StockPickingType(models.Model):
46
44
  ):
47
45
  vals["location_dest_id"] = self.default_location_dest_id.id
48
46
  wiz = self.env["wiz.stock.barcodes.read.picking"].create(vals)
49
- wiz.determine_todo_action()
50
47
  wiz.fill_pending_moves()
48
+ wiz.determine_todo_action()
51
49
  action = self.env["ir.actions.actions"]._for_xml_id(
52
50
  "stock_barcodes.action_stock_barcodes_read_picking"
53
51
  )
@@ -55,6 +53,7 @@ class StockPickingType(models.Model):
55
53
  return action
56
54
 
57
55
  def action_barcode_new_picking(self):
56
+ self.ensure_one()
58
57
  picking = (
59
58
  self.env["stock.picking"]
60
59
  .with_context(default_immediate_transfer=True)
@@ -367,7 +367,7 @@ ul.auto-toc {
367
367
  !! This file is generated by oca-gen-addon-readme !!
368
368
  !! changes will be overwritten. !!
369
369
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
370
- !! source digest: sha256:6658299983dc274c7a7bbc0ed6f36e8a9b679249c86b3295a92c78fdf305c00f
370
+ !! source digest: sha256:aaf3270f55f43b616a2ceb165378f0909ba3cc1a3d0d36d21a65e4c04c06998f
371
371
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
372
372
  <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/stock-logistics-barcode/tree/15.0/stock_barcodes"><img alt="OCA/stock-logistics-barcode" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--barcode-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/stock-logistics-barcode-15-0/stock-logistics-barcode-15-0-stock_barcodes"><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/stock-logistics-barcode&amp;target_branch=15.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
373
373
  <p>This module provides a barcode reader interface for stock module.</p>
@@ -294,6 +294,7 @@ class TestStockBarcodesPicking(TestStockBarcodes):
294
294
  self.product_tracking.categ_id.removal_strategy_id = self.env.ref(
295
295
  "stock.removal_lifo"
296
296
  )
297
+ self.wiz_scan_picking_out.action_clean_values()
297
298
  self.action_barcode_scanned(self.wiz_scan_picking_out, "8433281006850")
298
299
  self.assertEqual(self.wiz_scan_picking_out.lot_id, lot_3)
299
300
 
@@ -76,7 +76,6 @@ class WizStockBarcodesRead(models.AbstractModel):
76
76
  show_scan_log = fields.Boolean(compute="_compute_is_manual_qty")
77
77
  # Technical field to allow use in attrs
78
78
  display_menu = fields.Boolean()
79
- qty_available = fields.Float(compute="_compute_qty_available")
80
79
  auto_lot = fields.Boolean(
81
80
  string="Get lots automatically",
82
81
  help="If checked the lot will be set automatically with the same "
@@ -122,24 +121,6 @@ class WizStockBarcodesRead(models.AbstractModel):
122
121
  for rec in self:
123
122
  rec.create_lot = rec.option_group_id.create_lot
124
123
 
125
- @api.depends("location_id", "product_id", "lot_id")
126
- def _compute_qty_available(self):
127
- if not self.product_id or self.location_id.usage != "internal":
128
- self.qty_available = 0.0
129
- return
130
- domain_quant = [
131
- ("product_id", "=", self.product_id.id),
132
- ("location_id", "=", self.location_id.id),
133
- ]
134
- if self.lot_id:
135
- domain_quant.append(("lot_id", "=", self.lot_id.id))
136
- # if self.package_id:
137
- # domain_quant.append(('package_id', '=', self.package_id.id))
138
- groups = self.env["stock.quant"].read_group(
139
- domain_quant, ["quantity"], [], orderby="id"
140
- )
141
- self.qty_available = groups[0]["quantity"]
142
-
143
124
  @api.depends("product_id")
144
125
  def _compute_display_assign_serial(self):
145
126
  for rec in self:
@@ -630,7 +611,9 @@ class WizStockBarcodesRead(models.AbstractModel):
630
611
 
631
612
  def action_clean_values(self):
632
613
  options = self.option_group_id.option_ids
633
- options_to_clean = options.filtered("clean_after_done")
614
+ options_to_clean = options.filtered(
615
+ lambda op: op.clean_after_done and op.field_name in self
616
+ )
634
617
  for option in options_to_clean:
635
618
  if option.field_name == "result_package_id" and self.keep_result_package:
636
619
  continue
@@ -1,11 +1,13 @@
1
1
  # Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
2
2
  # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
3
  import logging
4
+ from collections import OrderedDict, defaultdict
4
5
 
5
6
  from odoo import _, api, fields, models
6
7
  from odoo.exceptions import ValidationError
7
8
  from odoo.fields import first
8
- from odoo.tools.float_utils import float_compare
9
+ from odoo.tools.float_utils import float_compare, float_round
10
+ from odoo.tools.safe_eval import safe_eval
9
11
 
10
12
  _logger = logging.getLogger(__name__)
11
13
 
@@ -74,6 +76,9 @@ class WizStockBarcodesReadPicking(models.TransientModel):
74
76
  picking_location_id = fields.Many2one(related="picking_id.location_id")
75
77
  picking_location_dest_id = fields.Many2one(related="picking_id.location_dest_id")
76
78
  company_id = fields.Many2one(related="picking_id.company_id")
79
+ todo_line_is_extra_line = fields.Boolean(related="todo_line_id.is_extra_line")
80
+ forced_todo_key = fields.Char()
81
+ qty_available = fields.Float(compute="_compute_qty_available")
77
82
 
78
83
  @api.depends("todo_line_id")
79
84
  def _compute_todo_line_display_ids(self):
@@ -85,6 +90,9 @@ class WizStockBarcodesReadPicking(models.TransientModel):
85
90
  if self.option_group_id.show_pending_moves:
86
91
  self.pending_move_ids = self.todo_line_ids.filtered(
87
92
  lambda t: t.state == "pending"
93
+ and any(
94
+ sm.barcode_backorder_action == "pending" for sm in t.stock_move_ids
95
+ )
88
96
  )
89
97
  else:
90
98
  self.pending_move_ids = False
@@ -108,6 +116,40 @@ class WizStockBarcodesReadPicking(models.TransientModel):
108
116
  rec.total_product_uom_qty += line.product_uom_qty
109
117
  rec.total_product_qty_done += line.quantity_done
110
118
 
119
+ @api.depends("location_id", "product_id", "lot_id")
120
+ def _compute_qty_available(self):
121
+ if not self.product_id or self.location_id.usage != "internal":
122
+ self.qty_available = 0.0
123
+ return
124
+ domain_quant = [
125
+ ("product_id", "=", self.product_id.id),
126
+ ("location_id", "=", self.location_id.id),
127
+ ]
128
+ if self.lot_id:
129
+ domain_quant.append(("lot_id", "=", self.lot_id.id))
130
+ # if self.package_id:
131
+ # domain_quant.append(('package_id', '=', self.package_id.id))
132
+ groups = self.env["stock.quant"].read_group(
133
+ domain_quant, ["quantity"], [], orderby="id"
134
+ )
135
+ self.qty_available = groups[0]["quantity"]
136
+ # Unexpected done quantities must reduce qty_available
137
+ if self.lot_id:
138
+ done_move_lines = self.move_line_ids.filtered(
139
+ lambda m: m.product_id == self.product_id and m.lot_id == self.lot_id
140
+ )
141
+ else:
142
+ done_move_lines = self.move_line_ids.filtered(
143
+ lambda m: m.product_id == self.product_id
144
+ )
145
+ for sml in done_move_lines:
146
+ over_done_qty = float_round(
147
+ sml.qty_done - sml.product_uom_qty,
148
+ precision_rounding=sml.product_uom_id.rounding,
149
+ )
150
+ if over_done_qty > 0.0:
151
+ self.qty_available -= over_done_qty
152
+
111
153
  def name_get(self):
112
154
  return [
113
155
  (
@@ -141,8 +183,8 @@ class WizStockBarcodesReadPicking(models.TransientModel):
141
183
  # view, so for create a candidate picking with the same default picking
142
184
  # we need create it in this onchange
143
185
  self._set_default_picking()
144
- self.determine_todo_action()
145
186
  self.fill_pending_moves()
187
+ self.determine_todo_action()
146
188
 
147
189
  def get_sorted_move_lines(self, move_lines):
148
190
  location_field = self.option_group_id.location_field_to_sort
@@ -180,12 +222,8 @@ class WizStockBarcodesReadPicking(models.TransientModel):
180
222
  return move_lines
181
223
 
182
224
  def fill_pending_moves(self):
183
- if (
184
- self.option_group_id.barcode_guided_mode != "guided"
185
- and self.option_group_id.show_pending_moves
186
- and not self.todo_line_ids
187
- ):
188
- self.fill_todo_records()
225
+ # TODO: Unify method
226
+ self.fill_todo_records()
189
227
 
190
228
  def get_moves_or_move_lines(self):
191
229
  if self.option_group_id.source_pending_moves == "move_line_ids":
@@ -193,9 +231,12 @@ class WizStockBarcodesReadPicking(models.TransientModel):
193
231
  else:
194
232
  return self.picking_id.move_lines
195
233
 
234
+ def get_moves(self):
235
+ return self.picking_id.move_lines
236
+
196
237
  def fill_todo_records(self):
197
238
  move_lines = self.get_sorted_move_lines(self.get_moves_or_move_lines())
198
- self.env["wiz.stock.barcodes.read.todo"].fill_records(self, [move_lines])
239
+ self.fill_records([move_lines])
199
240
 
200
241
  @api.model
201
242
  def _get_fields_filled_special(self):
@@ -211,12 +252,6 @@ class WizStockBarcodesReadPicking(models.TransientModel):
211
252
  self.visible_force_done = self.env.context.get("visible_force_done", False)
212
253
  if not self.option_group_id.barcode_guided_mode == "guided":
213
254
  return False
214
- if not self.todo_line_ids:
215
- self.fill_todo_records()
216
- # When scanning all information in one step (e.g. using GS-1), the
217
- # status and qty processed might have not been update, we ensure it
218
- # invalidating the cache.
219
- self.todo_line_ids.invalidate_cache()
220
255
  self.todo_line_id = (
221
256
  forced_todo_line
222
257
  or self.todo_line_ids.filtered(lambda t: t._origin.state == "pending")[:1]
@@ -278,9 +313,24 @@ class WizStockBarcodesReadPicking(models.TransientModel):
278
313
  if not self.keep_screen_values or self.todo_line_id.state != "pending":
279
314
  if not self.env.context.get("skip_clean_values", False):
280
315
  self.action_clean_values()
281
- self.determine_todo_action()
316
+ keep_vals = {}
282
317
  else:
283
- self.action_show_step()
318
+ keep_vals = self._convert_to_write(self._cache)
319
+ self.fill_todo_records()
320
+ if self.forced_todo_key:
321
+ self.todo_line_id = self.pending_move_ids.filtered(
322
+ lambda ln: str(self._group_key(ln)) == self.forced_todo_key
323
+ )[:1]
324
+ self.selected_pending_move_id = self.todo_line_id
325
+ self.determine_todo_action(self.todo_line_id)
326
+ else:
327
+ self.determine_todo_action()
328
+ self.action_show_step()
329
+ if keep_vals:
330
+ self.update_keep_values(keep_vals)
331
+ # Force refresh candidate pickings to show green if not pending moves
332
+ if not self.pending_move_ids:
333
+ self._set_candidate_pickings(self.picking_id)
284
334
  # Now we can add read log with details.
285
335
  _logger.info("Add scanned log barcode:{}".format(self.barcode))
286
336
  self._add_read_log(log_detail=move_dic)
@@ -290,6 +340,13 @@ class WizStockBarcodesReadPicking(models.TransientModel):
290
340
  self._add_read_log()
291
341
  return res
292
342
 
343
+ def update_keep_values(self, keep_vals):
344
+ options = self.option_group_id.option_ids
345
+ fields_to_keep = options.filtered(
346
+ lambda op: self._fields[op.field_name].type != "float"
347
+ ).mapped("field_name")
348
+ self.update({f_name: keep_vals[f_name] for f_name in fields_to_keep})
349
+
293
350
  def action_manual_entry(self):
294
351
  result = super().action_manual_entry()
295
352
  if result:
@@ -456,6 +513,10 @@ class WizStockBarcodesReadPicking(models.TransientModel):
456
513
  domain = self._prepare_stock_moves_domain()
457
514
  if self.option_group_id.barcode_guided_mode == "guided":
458
515
  moves_todo = self.todo_line_id.stock_move_ids
516
+ elif self.picking_id:
517
+ moves_todo = self.picking_id.move_lines.filtered(
518
+ lambda sm: sm.product_id == self.product_id
519
+ )
459
520
  else:
460
521
  moves_todo = StockMove.search(domain)
461
522
  if not getattr(
@@ -615,13 +676,14 @@ class WizStockBarcodesReadPicking(models.TransientModel):
615
676
  # link this new lines to the todo line details
616
677
  # If user scan a product distinct of the todo line we need link to other
617
678
  # alternative move
618
- if move_to_link_in_todo_line and self.todo_line_id:
619
- todo_line = self.todo_line_id
620
- else:
621
- todo_line = self.todo_line_ids.filtered(
622
- lambda ln: ln.product_id == self.product_id
623
- )
624
- todo_line.line_ids = [(4, sml.id) for sml in stock_move_lines]
679
+ if self.option_group_id.source_pending_moves != "move_line_ids":
680
+ if move_to_link_in_todo_line and self.todo_line_id:
681
+ todo_line = self.todo_line_id
682
+ else:
683
+ todo_line = self.todo_line_ids.filtered(
684
+ lambda ln: ln.product_id == self.product_id
685
+ )
686
+ todo_line.line_ids = [(4, sml.id) for sml in stock_move_lines]
625
687
  self.update_fields_after_process_stock(moves_todo)
626
688
  return move_lines_dic
627
689
 
@@ -784,6 +846,150 @@ class WizStockBarcodesReadPicking(models.TransientModel):
784
846
  return bool(self.location_dest_id)
785
847
  return super()._option_required_hook(option_required)
786
848
 
849
+ def _group_key(self, line):
850
+ group_key_for_todo_records = self.option_group_id.group_key_for_todo_records
851
+ if group_key_for_todo_records:
852
+ return safe_eval(group_key_for_todo_records, globals_dict={"object": line})
853
+ if self.option_group_id.source_pending_moves == "move_line_ids":
854
+ return (
855
+ line.location_id.id,
856
+ line.product_id.id,
857
+ line.lot_id.id,
858
+ line.package_id.id,
859
+ )
860
+ else:
861
+ return (line.location_id.id, line.product_id.id)
862
+
863
+ def _get_all_products_quantities_in_package(self, package):
864
+ res = {}
865
+ for quant in package._get_contained_quants():
866
+ if quant.product_id not in res:
867
+ res[quant.product_id] = 0
868
+ res[quant.product_id] += quant.quantity
869
+ return res
870
+
871
+ def _prepare_fill_record_values(self, line, position):
872
+ vals = {
873
+ "wiz_barcode_id": self.id,
874
+ "product_id": line.product_id.id,
875
+ "product_uom_qty": line.product_uom_qty,
876
+ "name": "To do action",
877
+ "position_index": position,
878
+ "picking_code": line.picking_code,
879
+ }
880
+ if line._name == "stock.move.line":
881
+ package_product_dic = self._get_all_products_quantities_in_package(
882
+ line.package_id
883
+ )
884
+ vals.update(
885
+ {
886
+ "location_id": line.location_id.id,
887
+ "location_dest_id": line.location_dest_id.id,
888
+ "lot_id": line.lot_id.id,
889
+ "package_id": line.package_id.id,
890
+ "result_package_id": line.result_package_id.id,
891
+ "uom_id": line.product_uom_id.id,
892
+ "product_qty_reserved": line.product_qty,
893
+ "line_ids": [(6, 0, line.ids)],
894
+ "stock_move_ids": [(6, 0, line.move_id.ids)],
895
+ "package_product_qty": package_product_dic
896
+ and package_product_dic[line.product_id]
897
+ or 0.0,
898
+ "is_stock_move_line_origin": True,
899
+ }
900
+ )
901
+ else:
902
+ vals.update(
903
+ {
904
+ "location_id": (line.move_line_ids[:1] or line).location_id.id,
905
+ "location_dest_id": (
906
+ line.move_line_ids[:1] or line
907
+ ).location_dest_id.id,
908
+ "uom_id": line.product_uom.id,
909
+ "product_qty_reserved": line.move_line_ids
910
+ and sum(line.move_line_ids.mapped("product_qty"))
911
+ or line.product_uom_qty,
912
+ "line_ids": [(6, 0, line.move_line_ids.ids)],
913
+ "stock_move_ids": [(6, 0, line.ids)],
914
+ "is_stock_move_line_origin": False,
915
+ }
916
+ )
917
+ return vals
918
+
919
+ def _update_fill_record_values(self, line, vals):
920
+ vals["product_uom_qty"] += line.product_uom_qty
921
+ if vals["is_stock_move_line_origin"]:
922
+ vals["product_qty_reserved"] += line.product_qty
923
+ vals["line_ids"][0][2].append(line.id)
924
+ vals["stock_move_ids"][0][2].append(line.move_id.id)
925
+ else:
926
+ vals["product_qty_reserved"] += (
927
+ line.move_line_ids
928
+ and sum(line.move_line_ids.mapped("product_qty"))
929
+ or line.product_uom_qty
930
+ )
931
+ vals["line_ids"][0][2].extend(line.move_line_ids.ids)
932
+ vals["stock_move_ids"][0][2].extend(line.ids)
933
+ return vals
934
+
935
+ @api.model
936
+ def fill_records(self, lines_list):
937
+ """
938
+ :param lines_list: browse list
939
+ :return:
940
+ """
941
+ self.forced_todo_key = str(
942
+ self._group_key(self.todo_line_id or self.selected_pending_move_id)
943
+ )
944
+ self.todo_line_ids.unlink()
945
+ self.todo_line_id = False
946
+ # self.position_index = 0
947
+ todo_vals = OrderedDict()
948
+ position = 0
949
+ move_qty_dic = defaultdict(float)
950
+ is_stock_move_line_origin = lines_list[0]._name == "stock.move.line"
951
+ for lines in lines_list:
952
+ for line in lines:
953
+ key = self._group_key(line)
954
+ if key not in todo_vals:
955
+ todo_vals[key] = self._prepare_fill_record_values(line, position)
956
+ position += 1
957
+ else:
958
+ todo_vals[key] = self._update_fill_record_values(
959
+ line, todo_vals[key]
960
+ )
961
+ if is_stock_move_line_origin:
962
+ move_qty_dic[line.move_id] += max(
963
+ line.product_uom_qty, line.qty_done
964
+ )
965
+ else:
966
+ move_qty_dic[line] += max(line.product_uom_qty, line.quantity_done)
967
+ for move in self.get_moves():
968
+ qty = move_qty_dic[move]
969
+ if (
970
+ move.barcode_backorder_action == "pending"
971
+ and move.product_uom_qty > qty
972
+ ):
973
+ vals = self._prepare_fill_record_values(move, position)
974
+ vals.update(
975
+ {
976
+ "product_uom_qty": move.product_uom_qty - qty,
977
+ "product_qty_reserved": 0.0,
978
+ "line_ids": False,
979
+ "is_extra_line": True,
980
+ }
981
+ )
982
+ todo_vals[
983
+ (
984
+ move,
985
+ "M",
986
+ )
987
+ ] = vals
988
+ position += 1
989
+ self.todo_line_ids = self.env["wiz.stock.barcodes.read.todo"].create(
990
+ list(todo_vals.values())
991
+ )
992
+
787
993
 
788
994
  class WizCandidatePicking(models.TransientModel):
789
995
  """
@@ -127,6 +127,8 @@
127
127
  <field name="picking_location_id" invisible="1" />
128
128
  <field name="picking_location_dest_id" invisible="1" />
129
129
  <field name="company_id" invisible="1" />
130
+ <field name="todo_line_is_extra_line" invisible="1" />
131
+ <field name="qty_available" invisible="1" />
130
132
  </field>
131
133
  <field name="location_id" position="attributes">
132
134
  <attribute
@@ -161,6 +163,12 @@
161
163
  </div>
162
164
  </group>
163
165
  </group>
166
+ <group name="scan_fields" position="attributes">
167
+ <!-- hide group scan_fields for extra todo lines -->
168
+ <attribute
169
+ name="attrs"
170
+ >{'invisible': [('todo_line_is_extra_line', '!=', False)]}</attribute>
171
+ </group>
164
172
  <group name="scan_fields" position="after">
165
173
  <group
166
174
  string="Pending moves"
@@ -173,11 +181,14 @@
173
181
  force_save="1"
174
182
  mode="tree,kanban"
175
183
  >
176
- <tree>
184
+ <tree decoration-warning="is_extra_line">
177
185
  <field name="state" invisible="1" />
186
+ <field name="is_extra_line" invisible="1" />
178
187
  <field name="product_id" />
179
188
  <field name="product_uom_qty" />
180
189
  <field name="line_ids" invisible="1" />
190
+ <field name="location_id" optional="hide" />
191
+ <field name="location_dest_id" optional="hide" />
181
192
  <field
182
193
  name="lot_id"
183
194
  groups="stock.group_production_lot"