From 6ab4b0d4967490ed1ee0559ec296da6637705125 Mon Sep 17 00:00:00 2001
From: Simon Lejeune
Date: Thu, 24 Oct 2019 14:57:26 +0000
Subject: [PATCH 1/3] [REF] stock: make picking button_validate multi
So we have to adapt
- the sanity checks
- the immediate/backorder wizards
- split the call to _action_done with and without backorder
This is a preliminary work to remove all the override in
stock_picking_batch and to allow choosing the pickings to immediate or
backorder directly in the wizards.
Before this commit, both wizards were calling _action_done and one
another. We make them go back through `button_validate` so that it is
more sane to handle and allow future extensions to add pre-action done
wizards.
Also the first part of button_validate is some sanity check. We adapt
them to be multi (inside of a loop over self) without any other
changes.
Now the wizard can work on the records (immediate: write the done
quantities) or work with the context (backorder: picking ids to not
backorder in the context).
Also we adapt the stock sms weird wizard (a confirmation wizard but the
feature is auto installed? wth). Now there it isn't manually called in
the middle of button_validate but it uses the pre_action_done_hook.
task-2069646
---
addons/stock/models/stock_picking.py | 164 ++++++++++--------
addons/stock/views/stock_picking_views.xml | 13 ++
.../wizard/stock_backorder_confirmation.py | 14 +-
.../stock/wizard/stock_immediate_transfer.py | 27 +--
.../wizard/stock_immediate_transfer_views.xml | 5 +-
.../tests/test_stockvaluation.py | 2 +-
addons/stock_sms/models/stock_picking.py | 59 ++++---
addons/stock_sms/wizard/confirm_stock_sms.py | 26 +--
.../wizard/confirm_stock_sms_views.xml | 4 -
9 files changed, 183 insertions(+), 131 deletions(-)
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index 32a492a8aa7..e6f9fec8fd1 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -782,66 +782,62 @@ class Picking(models.Model):
picking.move_lines._do_unreserve()
picking.package_level_ids.filtered(lambda p: not p.move_ids).unlink()
- def _check_sms_confirmation_popup(self):
- return False
-
def button_validate(self):
- self.ensure_one()
- if not self.move_lines and not self.move_line_ids:
- raise UserError(_('Please add some items to move.'))
+ # Sanity checks.
+ for picking in self:
+ if not picking.move_lines and not picking.move_line_ids:
+ raise UserError(_('Please add some items to move.'))
- # If no lots when needed, raise error
- picking_type = self.picking_type_id
- precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
- no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in self.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel')))
- no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
- if no_reserved_quantities and no_quantities_done:
- raise UserError(_('You cannot validate a transfer if no quantites are reserved nor done. To force the transfer, switch in edit more and encode the done quantities.'))
+ # If no lots when needed, raise error
+ picking_type = picking.picking_type_id
+ precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in picking.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel')))
+ no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in picking.move_line_ids)
+ if no_reserved_quantities and no_quantities_done:
+ raise UserError(_('You cannot validate a transfer if no quantites are reserved nor done. To force the transfer, switch in edit more and encode the done quantities.'))
- if picking_type.use_create_lots or picking_type.use_existing_lots:
- lines_to_check = self.move_line_ids
- if not no_quantities_done:
- lines_to_check = lines_to_check.filtered(
- lambda line: float_compare(line.qty_done, 0,
- precision_rounding=line.product_uom_id.rounding)
- )
+ if picking_type.use_create_lots or picking_type.use_existing_lots:
+ lines_to_check = picking.move_line_ids
+ if not no_quantities_done:
+ lines_to_check = lines_to_check.filtered(lambda line: float_compare(line.qty_done, 0, precision_rounding=line.product_uom_id.rounding))
+ for line in lines_to_check:
+ product = line.product_id
+ if product and product.tracking != 'none':
+ if not line.lot_name and not line.lot_id:
+ raise UserError(_('You need to supply a Lot/Serial number for product %s.') % product.display_name)
- for line in lines_to_check:
- product = line.product_id
- if product and product.tracking != 'none':
- if not line.lot_name and not line.lot_id:
- raise UserError(_('You need to supply a Lot/Serial number for product %s.') % product.display_name)
+ # Run the pre-validation wizards. Processing a pre-validation wizard should work on the
+ # moves and/or the context and never call `_action_done`.
+ if not self.env.context.get('button_validate_picking_ids'):
+ self = self.with_context(button_validate_picking_ids=self.ids)
+ res = self._pre_action_done_hook()
+ if res is not True:
+ return res
- # Propose to use the sms mechanism the first time a delivery
- # picking is validated. Whatever the user's decision (use it or not),
- # the method button_validate is called again (except if it's cancel),
- # so the checks are made twice in that case, but the flow is not broken
- sms_confirmation = self._check_sms_confirmation_popup()
- if sms_confirmation:
- return sms_confirmation
+ # Call `_action_done`.
+ if self.env.context.get('picking_ids_not_to_backorder'):
+ pickings_not_to_backorder = self.browse(self.env.context['picking_ids_not_to_backorder'])
+ pickings_to_backorder = self - pickings_not_to_backorder
+ else:
+ pickings_not_to_backorder = self.env['stock.picking']
+ pickings_to_backorder = self
+ pickings_not_to_backorder.with_context(cancel_backorder=True)._action_done()
+ pickings_to_backorder.with_context(cancel_backorder=False)._action_done()
+ return True
- if no_quantities_done:
- view = self.env.ref('stock.view_immediate_transfer')
- wiz = self.env['stock.immediate.transfer'].create({'pick_ids': [(4, self.id)]})
- return {
- 'name': _('Immediate Transfer?'),
- 'type': 'ir.actions.act_window',
- 'view_mode': 'form',
- 'res_model': 'stock.immediate.transfer',
- 'views': [(view.id, 'form')],
- 'view_id': view.id,
- 'target': 'new',
- 'res_id': wiz.id,
- 'context': self.env.context,
- }
+ def _pre_action_done_hook(self):
+ if not self.env.context.get('skip_immediate'):
+ pickings_to_immediate = self._check_immediate()
+ if pickings_to_immediate:
+ return pickings_to_immediate._action_generate_immediate_wizard()
- # Check backorder should check for other barcodes
- if self._check_backorder():
- return self.action_generate_backorder_wizard()
- self._action_done()
- return
+ if not self.env.context.get('skip_backorder'):
+ pickings_to_backorder = self._check_backorder()
+ if pickings_to_backorder:
+ return pickings_to_backorder._action_generate_backorder_wizard()
+ return True
- def action_generate_backorder_wizard(self):
+ def _action_generate_backorder_wizard(self):
view = self.env.ref('stock.view_backorder_confirmation')
wiz = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, p.id) for p in self]})
return {
@@ -856,42 +852,60 @@ class Picking(models.Model):
'context': self.env.context,
}
+ def _action_generate_immediate_wizard(self):
+ view = self.env.ref('stock.view_immediate_transfer')
+ wiz = self.env['stock.immediate.transfer'].create({'pick_ids': [(4, p.id) for p in self]})
+ return {
+ 'name': _('Immediate Transfer?'),
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'stock.immediate.transfer',
+ 'views': [(view.id, 'form')],
+ 'view_id': view.id,
+ 'target': 'new',
+ 'res_id': wiz.id,
+ 'context': self.env.context,
+ }
+
def action_toggle_is_locked(self):
self.ensure_one()
self.is_locked = not self.is_locked
return True
def _check_backorder(self):
- """ This method will loop over all the move lines of self and
- check if creating a backorder is necessary. This method is
- called during button_validate if the user has already processed
- some quantities and in the immediate transfer wizard that is
- displayed if the user has not processed any quantities.
+ backorder_pickings = self.browse()
+ for picking in self:
+ quantity_todo = {}
+ quantity_done = {}
+ for move in picking.mapped('move_lines'):
+ quantity_todo.setdefault(move.product_id.id, 0)
+ quantity_done.setdefault(move.product_id.id, 0)
+ quantity_todo[move.product_id.id] += move.product_uom_qty
+ quantity_done[move.product_id.id] += move.quantity_done
+ for ops in picking.mapped('move_line_ids').filtered(lambda x: x.package_id and not x.product_id and not x.move_id):
+ for quant in ops.package_id.quant_ids:
+ quantity_done.setdefault(quant.product_id.id, 0)
+ quantity_done[quant.product_id.id] += quant.qty
+ for pack in picking.mapped('move_line_ids').filtered(lambda x: x.product_id and not x.move_id):
+ quantity_done.setdefault(pack.product_id.id, 0)
+ quantity_done[pack.product_id.id] += pack.product_uom_id._compute_quantity(pack.qty_done, pack.product_id.uom_id)
+ if any(quantity_done[x] < quantity_todo.get(x, 0) for x in quantity_done):
+ backorder_pickings |= picking
+ return backorder_pickings
- :return: True if a backorder is necessary else False
- """
- quantity_todo = {}
- quantity_done = {}
- for move in self.mapped('move_lines'):
- quantity_todo.setdefault(move.product_id.id, 0)
- quantity_done.setdefault(move.product_id.id, 0)
- quantity_todo[move.product_id.id] += move.product_uom_qty
- quantity_done[move.product_id.id] += move.quantity_done
- for ops in self.mapped('move_line_ids').filtered(lambda x: x.package_id and not x.product_id and not x.move_id):
- for quant in ops.package_id.quant_ids:
- quantity_done.setdefault(quant.product_id.id, 0)
- quantity_done[quant.product_id.id] += quant.qty
- for pack in self.mapped('move_line_ids').filtered(lambda x: x.product_id and not x.move_id):
- quantity_done.setdefault(pack.product_id.id, 0)
- quantity_done[pack.product_id.id] += pack.product_uom_id._compute_quantity(pack.qty_done, pack.product_id.uom_id)
- return any(quantity_done[x] < quantity_todo.get(x, 0) for x in quantity_done)
+ def _check_immediate(self):
+ immediate_pickings = self.browse()
+ precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ for picking in self:
+ if all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in picking.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel'))):
+ immediate_pickings |= picking
+ return immediate_pickings
def _autoconfirm_picking(self):
for picking in self.filtered(lambda picking: picking.immediate_transfer and picking.state not in ('done', 'cancel') and (picking.move_lines or picking.package_level_ids)):
picking.action_confirm()
def _create_backorder(self):
-
""" This method is called when the user chose to create a backorder. It will create a new
picking, the backorder, and move the stock.moves that are not `done` or `cancel` into it.
"""
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 1fcc1b55e7c..73d5713ce5d 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -462,6 +462,19 @@
+
+ Validate
+
+
+ list
+ code
+
+ if records:
+ res = records.button_validate()
+ action = res
+
+
+
diff --git a/addons/stock/wizard/stock_backorder_confirmation.py b/addons/stock/wizard/stock_backorder_confirmation.py
index 183acba1ecc..add3912077c 100644
--- a/addons/stock/wizard/stock_backorder_confirmation.py
+++ b/addons/stock/wizard/stock_backorder_confirmation.py
@@ -22,10 +22,18 @@ class StockBackorderConfirmation(models.TransientModel):
precision_rounding=move.product_uom.rounding) > 0:
moves_to_log[move] = (move.quantity_done, move.product_uom_qty)
pick_id._log_less_quantities_than_expected(moves_to_log)
- confirmation.pick_ids.with_context(cancel_backorder=cancel_backorder)._action_done()
+
+ pickings_to_validate = self.env.context.get('button_validate_picking_ids')
+ if pickings_to_validate:
+ pickings_to_validate = self.env['stock.picking'].browse(pickings_to_validate).with_context(skip_backorder=True)
+ if cancel_backorder:
+ pickings_to_validate = pickings_to_validate.with_context(picking_ids_not_to_backorder=self.pick_ids.ids)
+ return pickings_to_validate.button_validate()
+ return True
+
def process(self):
- self._process()
+ return self._process()
def process_cancel_backorder(self):
- self._process(cancel_backorder=True)
+ return self._process(cancel_backorder=True)
diff --git a/addons/stock/wizard/stock_immediate_transfer.py b/addons/stock/wizard/stock_immediate_transfer.py
index caa24d2d067..4a0435d168d 100644
--- a/addons/stock/wizard/stock_immediate_transfer.py
+++ b/addons/stock/wizard/stock_immediate_transfer.py
@@ -12,8 +12,6 @@ class StockImmediateTransfer(models.TransientModel):
pick_ids = fields.Many2many('stock.picking', 'stock_picking_transfer_rel')
def process(self):
- pick_to_backorder = self.env['stock.picking']
- pick_to_do = self.env['stock.picking']
for picking in self.pick_ids:
# If still in draft => confirm and assign
if picking.state == 'draft':
@@ -25,13 +23,18 @@ class StockImmediateTransfer(models.TransientModel):
for move in picking.move_lines.filtered(lambda m: m.state not in ['done', 'cancel']):
for move_line in move.move_line_ids:
move_line.qty_done = move_line.product_uom_qty
- if picking._check_backorder():
- pick_to_backorder |= picking
- continue
- pick_to_do |= picking
- # Process every picking that do not require a backorder, then return a single backorder wizard for every other ones.
- if pick_to_do:
- pick_to_do._action_done()
- if pick_to_backorder:
- return pick_to_backorder.action_generate_backorder_wizard()
- return False
+
+ pickings_to_validate = self.env.context.get('button_validate_picking_ids')
+ if pickings_to_validate:
+ return self.env['stock.picking'].browse(pickings_to_validate).with_context(skip_immediate=True).button_validate()
+ return True
+
+ def process_no_immediate(self):
+ # Remove `self.pick_ids` from `button_validate_picking_ids` and call `button_validate` with
+ # the subset (if any).
+ pickings_to_validate = self.env['stock.picking'].browse(self.env.context.get('button_validate_picking_ids'))
+ pickings_to_validate = pickings_to_validate - self.pick_ids
+ if pickings_to_validate:
+ return pickings_to_validate.with_context(skip_immediate=True).button_validate()
+ return True
+
diff --git a/addons/stock/wizard/stock_immediate_transfer_views.xml b/addons/stock/wizard/stock_immediate_transfer_views.xml
index dbe96d0dac4..25e6e07fb0d 100644
--- a/addons/stock/wizard/stock_immediate_transfer_views.xml
+++ b/addons/stock/wizard/stock_immediate_transfer_views.xml
@@ -12,10 +12,11 @@
-
\ No newline at end of file
+
diff --git a/addons/stock_dropshipping/tests/test_stockvaluation.py b/addons/stock_dropshipping/tests/test_stockvaluation.py
index 14fad4e6316..2f8f9bc65aa 100644
--- a/addons/stock_dropshipping/tests/test_stockvaluation.py
+++ b/addons/stock_dropshipping/tests/test_stockvaluation.py
@@ -61,7 +61,7 @@ class TestStockValuation(AccountingTestCase):
self.assertEqual(len(self.sale_order1.picking_ids), 1)
#self.assertEqual(self.sale_order1.picking_ids.move_lines._is_dropshipped(), True)
wizard = self.sale_order1.picking_ids.button_validate()
- immediate_transfer = self.env[wizard['res_model']].browse(wizard['res_id'])
+ immediate_transfer = self.env[wizard['res_model']].browse(wizard['res_id']).with_context(wizard['context'])
immediate_transfer.process()
self.assertEqual(self.sale_order1.picking_ids.state, 'done')
diff --git a/addons/stock_sms/models/stock_picking.py b/addons/stock_sms/models/stock_picking.py
index c31736cf4e1..853256194ad 100644
--- a/addons/stock_sms/models/stock_picking.py
+++ b/addons/stock_sms/models/stock_picking.py
@@ -9,34 +9,47 @@ import threading
class Picking(models.Model):
_inherit = 'stock.picking'
+ def _pre_action_done_hook(self):
+ res = super()._pre_action_done_hook()
+ if res is True:
+ pickings_to_warn_sms = self._check_warn_sms()
+ if pickings_to_warn_sms:
+ return pickings_to_warn_sms._action_generate_warn_sms_wizard()
+ return res
+
+ def _check_warn_sms(self):
+ warn_sms_pickings = self.browse()
+ for picking in self:
+ is_delivery = picking.company_id.stock_move_sms_validation \
+ and picking.picking_type_id.code == 'outgoing' \
+ and (picking.partner_id.mobile or picking.partner_id.phone)
+ if is_delivery and not getattr(threading.currentThread(), 'testing', False) \
+ and not self.env.registry.in_test_mode() \
+ and not picking.company_id.has_received_warning_stock_sms \
+ and picking.company_id.stock_move_sms_validation:
+ warn_sms_pickings |= picking
+ return warn_sms_pickings
+
+ def _action_generate_warn_sms_wizard(self):
+ view = self.env.ref('stock_sms.view_confirm_stock_sms')
+ wiz = self.env['confirm.stock.sms'].create({'pick_ids': [(4, p.id) for p in self]})
+ return {
+ 'name': _('SMS'),
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'confirm.stock.sms',
+ 'views': [(view.id, 'form')],
+ 'view_id': view.id,
+ 'target': 'new',
+ 'res_id': wiz.id,
+ 'context': self.env.context,
+ }
+
def _sms_get_number_fields(self):
""" This method returns the fields to use to find the number to use to
send an SMS on a record. """
return ['mobile', 'phone']
- def _check_sms_confirmation_popup(self):
- is_delivery = self.company_id.stock_move_sms_validation \
- and self.picking_type_id.code == 'outgoing' \
- and (self.partner_id.mobile or self.partner_id.phone)
- if is_delivery and not getattr(threading.currentThread(), 'testing', False) \
- and not self.env.registry.in_test_mode() \
- and not self.company_id.has_received_warning_stock_sms \
- and self.company_id.stock_move_sms_validation:
- view = self.env.ref('stock_sms.view_confirm_stock_sms')
- wiz = self.env['confirm.stock.sms'].create({'picking_id': self.id})
- return {
- 'name': _('SMS'),
- 'type': 'ir.actions.act_window',
- 'view_mode': 'form',
- 'res_model': 'confirm.stock.sms',
- 'views': [(view.id, 'form')],
- 'view_id': view.id,
- 'target': 'new',
- 'res_id': wiz.id,
- 'context': self.env.context,
- }
- return False
-
def _send_confirmation_email(self):
super(Picking, self)._send_confirmation_email()
if not getattr(threading.currentThread(), 'testing', False) and not self.env.registry.in_test_mode():
diff --git a/addons/stock_sms/wizard/confirm_stock_sms.py b/addons/stock_sms/wizard/confirm_stock_sms.py
index b89a9e613f0..2417a854b0d 100644
--- a/addons/stock_sms/wizard/confirm_stock_sms.py
+++ b/addons/stock_sms/wizard/confirm_stock_sms.py
@@ -8,20 +8,24 @@ class ConfirmStockSms(models.TransientModel):
_name = 'confirm.stock.sms'
_description = 'Confirm Stock SMS'
- picking_id = fields.Many2one('stock.picking', required=True)
- company_id = fields.Many2one('res.company', string='Company', required=True, related='picking_id.company_id')
+ pick_ids = fields.Many2many('stock.picking', 'stock_picking_sms_rel')
def send_sms(self):
self.ensure_one()
- if not self.company_id.has_received_warning_stock_sms:
- self.company_id.sudo().write({'has_received_warning_stock_sms': True})
- return self.picking_id.button_validate()
+ for company in self.pick_ids.company_id:
+ if not company.has_received_warning_stock_sms:
+ company.sudo().write({'has_received_warning_stock_sms': True})
+ pickings_to_validate = self.env['stock.picking'].browse(self.env.context.get('button_validate_picking_ids'))
+ return pickings_to_validate.button_validate()
def dont_send_sms(self):
self.ensure_one()
- if not self.company_id.has_received_warning_stock_sms:
- self.company_id.sudo().write({
- 'has_received_warning_stock_sms': True,
- 'stock_move_sms_validation': False,
- })
- return self.picking_id.button_validate()
+ for company in self.pick_ids.company_id:
+ if not company.has_received_warning_stock_sms:
+ company.sudo().write({
+ 'has_received_warning_stock_sms': True,
+ 'stock_move_sms_validation': False,
+ })
+ pickings_to_validate = self.env['stock.picking'].browse(self.env.context.get('button_validate_picking_ids'))
+ return pickings_to_validate.button_validate()
+
diff --git a/addons/stock_sms/wizard/confirm_stock_sms_views.xml b/addons/stock_sms/wizard/confirm_stock_sms_views.xml
index f696f1cafc1..001304c50d0 100644
--- a/addons/stock_sms/wizard/confirm_stock_sms_views.xml
+++ b/addons/stock_sms/wizard/confirm_stock_sms_views.xml
@@ -7,10 +7,6 @@