[IMP] stock: wave pickings

This commit introduce Wave Pickings. This new object is a group of stock
move lines related to different pickings. The goal is to group stock
movements having similar characteristics like same source location, same
product category, ...

Wave pickings are created from stock move lines. Those are removed from
their picking to a copy of it. All the new picking are grouped together
in a new batch picking. This batch is the wave.
To differentiate the batch pickings and the wave pickings. This commit
introduces a new menu and a new sequence.

Task : 2411102

Part-of: odoo/odoo#63291
This commit is contained in:
William Henrotin
2021-09-02 14:14:09 +00:00
parent bf9944c100
commit dd53f5323f
17 changed files with 369 additions and 20 deletions
+3 -1
View File
@@ -23,7 +23,9 @@ class ResConfigSettings(models.TransientModel):
help="Add and customize route operations to process product moves in your warehouse(s): e.g. unload > quality control > stock for incoming products, pick > pack > ship for outgoing products. \n You can also set putaway strategies on warehouse locations in order to send incoming products into specific child locations straight away (e.g. specific bins, racks).")
group_warning_stock = fields.Boolean("Warnings for Stock", implied_group='stock.group_warning_stock')
group_stock_sign_delivery = fields.Boolean("Signature", implied_group='stock.group_stock_sign_delivery')
module_stock_picking_batch = fields.Boolean("Batch Pickings")
module_stock_picking_batch = fields.Boolean("Batch Transfers")
group_stock_picking_wave = fields.Boolean('Wave Transfers', implied_group='stock.group_stock_picking_wave',
help="Group your move operations in wave transfer to process them together")
module_stock_barcode = fields.Boolean("Barcode Scanner")
stock_move_email_validation = fields.Boolean(related='company_id.stock_move_email_validation', readonly=False)
stock_mail_confirmation_template_id = fields.Many2one(related='company_id.stock_mail_confirmation_template_id', readonly=False)
+4
View File
@@ -78,6 +78,10 @@
<field name="name">Display Reception Report at Validation</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<record id="group_stock_picking_wave" model="res.groups">
<field name="name">Use wave pickings</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
</data>
<data noupdate="1">
<record id="base.default_user" model="res.users">
@@ -34,6 +34,13 @@
<div class="text-muted">
Process transfers in batch per worker
</div>
<div class="row mt-2" attrs="{'invisible': [('module_stock_picking_batch','=',False)]}">
<field name="group_stock_picking_wave" widget="upgrade_boolean" class="col-lg-1 ml16 mr0"/>
<div class="col pl-0">
<label for="group_stock_picking_wave"/>
<div class="text-muted">Process operations in wave transfers</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="warning_info">
@@ -13,14 +13,22 @@ This module adds the batch transfer option in warehouse management
'data': [
'security/ir.model.access.csv',
'views/stock_picking_batch_views.xml',
'views/stock_picking_wave_views.xml',
'views/stock_move_line_views.xml',
'data/stock_picking_batch_data.xml',
'wizard/stock_picking_to_batch_views.xml',
'wizard/stock_add_to_wave_views.xml',
'report/stock_picking_batch_report_views.xml',
'report/report_picking_batch.xml',
],
'demo': [
'data/stock_picking_batch_demo.xml',
],
'assets': {
'web.assets_backend': [
'stock_picking_batch/static/src/js/*',
]
},
'installable': True,
'license': 'LGPL-3',
}
@@ -15,4 +15,12 @@
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_picking_wave" model="ir.sequence">
<field name="name">Wave Transfer</field>
<field name="code">picking.wave</field>
<field name="prefix">WAVE/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</data></odoo>
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import stock_move_line
from . import stock_picking
from . import stock_picking_batch
@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import _, fields, models
from odoo import Command
class StockMoveLine(models.Model):
_inherit = "stock.move.line"
batch_id = fields.Many2one(related='picking_id.batch_id')
def action_open_add_to_wave(self):
# This action can be called from the move line list view or from the 'Add to wave' wizard
if 'active_wave_id' in self.env.context:
wave = self.env['stock.picking.batch'].browse(self.env.context.get('active_wave_id'))
return self._add_to_wave(wave)
view = self.env.ref('stock_picking_batch.stock_add_to_wave_form')
return {
'name': _('Add to Wave'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'stock.add.to.wave',
'views': [(view.id, 'form')],
'view_id': view.id,
'target': 'new',
}
def _add_to_wave(self, wave=False):
""" Detach lines (and corresponding stock move from a picking to another). If wave is
passed, attach new picking into it. If not attach line to their original picking.
:param int wave: id of the wave picking on which to put the move lines. """
if not wave:
wave = self.env['stock.picking.batch'].create({
'is_wave': True
})
line_by_picking = defaultdict(lambda: self.env['stock.move.line'])
for line in self:
# if not wave and line.orig_picking_id.is_wave:
# continue
line_by_picking[line.picking_id] |= line
picking_to_wave_vals_list = []
for picking, lines in line_by_picking.items():
# Move the entire picking if all the line are taken
if lines == picking.move_line_ids:
wave.picking_ids = [Command.link(picking.id)]
continue
# Split the picking in two part to extract only line that are taken on the wave
picking_to_wave_vals = picking.copy_data({
'move_lines': [],
'move_line_ids': [],
'batch_id': wave.id,
})[0]
line_by_move = defaultdict(lambda: self.env['stock.move.line'])
for line in lines:
line_by_move[line.move_id] |= line
for move, move_lines in line_by_move.items():
picking_to_wave_vals['move_line_ids'] += [Command.link(line.id) for line in lines]
# if all the line of a stock move are taken we change the picking on the stock move
if move_lines == move.move_line_ids:
picking_to_wave_vals['move_lines'] += [Command.link(move.id)]
continue
# Split the move
qty = sum(lines.mapped('product_qty'))
new_move = move._split(qty)
new_move[0]['move_line_ids'] = [Command.set(move_lines.ids)]
picking_to_wave_vals['move_lines'] += [Command.create(new_move)]
picking_to_wave_vals_list.append(picking_to_wave_vals)
if picking_to_wave_vals_list:
self.env['stock.picking'].create(picking_to_wave_vals_list)
wave.action_confirm()
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import _, api, fields, models
class StockPicking(models.Model):
@@ -30,6 +30,27 @@ class StockPicking(models.Model):
self.batch_id._sanity_check()
return res
def action_add_operations(self):
view = self.env.ref('stock_picking_batch.view_move_line_tree_detailed_wave')
return {
'name': _('Add Operations'),
'type': 'ir.actions.act_window',
'view_mode': 'list',
'view': view,
'views': [(view.id, 'tree')],
'res_model': 'stock.move.line',
'target': 'new',
'domain': [
('picking_id', 'in', self.ids),
('state', '!=', 'done')
],
'context': dict(
self.env.context,
picking_to_wave=self.ids,
active_wave_id=self.env.context.get('active_wave_id').id,
search_default_by_location=True,
)}
def _should_show_transfers(self):
if len(self.batch_id) == 1 and self == self.batch_id.picking_ids:
return False
@@ -59,6 +59,7 @@ class StockPickingBatch(models.Model):
- If manually set then scheduled date for all transfers in batch will automatically update to this date.
- If not manually changed and transfers are added/removed/updated then this will be their earliest scheduled date
but this scheduled date will not be set for all transfers in batch.""")
is_wave = fields.Boolean('This batch is a wave')
@api.depends('company_id', 'picking_type_id', 'state')
def _compute_allowed_picking_ids(self):
@@ -127,7 +128,10 @@ class StockPickingBatch(models.Model):
@api.model
def create(self, vals):
if vals.get('name', '/') == '/':
vals['name'] = self.env['ir.sequence'].next_by_code('picking.batch') or '/'
if vals.get('is_wave'):
vals['name'] = self.env['ir.sequence'].next_by_code('picking.wave') or '/'
else:
vals['name'] = self.env['ir.sequence'].next_by_code('picking.batch') or '/'
return super().create(vals)
def write(self, vals):
@@ -1,3 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_stock_picking_batch,stock.picking.batch stock users,model_stock_picking_batch,stock.group_stock_user,1,1,1,1
access_stock_picking_to_batch,access.stock.picking.to.batch,model_stock_picking_to_batch,stock.group_stock_user,1,1,1,0
access_stock_add_to_wave,access.stock.picking.to.wave,model_stock_add_to_wave,stock.group_stock_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_picking_batch stock.picking.batch stock users model_stock_picking_batch stock.group_stock_user 1 1 1 1
3 access_stock_picking_to_batch access.stock.picking.to.batch model_stock_picking_to_batch stock.group_stock_user 1 1 1 0
4 access_stock_add_to_wave access.stock.picking.to.wave model_stock_add_to_wave stock.group_stock_user 1 1 1 0
@@ -0,0 +1,43 @@
odoo.define('stock_picking_wave.choose_lines_widget', function (require) {
const relationalFields = require('web.relational_fields');
const FieldsRegistry = require('web.field_registry');
const core = require('web.core');
var ChoosePickingWidget = relationalFields.FieldOne2Many.extend({
init: function (parent, name, record, options) {
this._super.apply(this, arguments);
this.isMany2Many = true;
this.isWave = record.data.is_wave;
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Opens a SelectCreateDialog
*/
_setValue: function (value, options) {
return this._super(value, options).then(() => {
if (value.operation === 'ADD_M2M') {
core.bus.trigger('add_move_lines', value.ids);
}
});
},
});
var ChooseLinesWidget = relationalFields.FieldMany2Many.extend({
start: function () {
core.bus.on('add_move_lines', this, this.onAddRecordOpenDialog);
return this._super();
},
});
FieldsRegistry.add('choose_lines_widget', ChooseLinesWidget);
FieldsRegistry.add('choose_picking_widget', ChoosePickingWidget);
return ChooseLinesWidget, ChoosePickingWidget;
});
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_move_line_tree_detailed_wave" model="ir.ui.view">
<field name="name">stock_picking_wave.move.line.tree.wave</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock.view_move_line_tree_detailed"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="inside">
<header>
<button name="action_open_add_to_wave" type="object" string="Add to Wave" groups="stock.group_stock_picking_wave"/>
</header>
</xpath>
<xpath expr="//field[@name='picking_id']" position="after">
<field name="batch_id" optional="hide"/>
</xpath>
</field>
</record>
</odoo>
@@ -14,7 +14,22 @@
</xpath>
</field>
</record>
<record id="view_picking_tree_batch" model="ir.ui.view">
<field name="name">stock_picking_batch.picking.tree.batch</field>
<field name="model">stock.picking</field>
<field name="mode">primary</field>
<field name="priority" eval="1000"/>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="scheduled_date"/>
<field name="location_id"/>
<field name="backorder_id"/>
<field name="origin"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="view_picking_move_tree_inherited" model="ir.ui.view">
<field name="name">stock_picking_batch.picking.move.tree</field>
@@ -70,6 +85,7 @@
<field name="show_check_availability" invisible="1"/>
<field name="show_validate" invisible="1"/>
<field name="picking_type_code" invisible="1"/>
<field name="is_wave" invisible="1"/>
<header>
<button name="action_confirm" states="draft" string="Confirm" type="object" class="oe_highlight"/>
<button name="action_done" string="Validate" type="object" class="oe_highlight"
@@ -137,16 +153,7 @@
<page string="Transfers">
<field name="allowed_picking_ids" invisible="1"/>
<field name="picking_ids" widget="many2many" mode="tree,kanban"
context="{'form_view_ref': 'stock_picking_batch.view_picking_form_inherited'}">
<tree>
<field name="name"/>
<field name="scheduled_date"/>
<field name="location_id"/>
<field name="backorder_id"/>
<field name="origin"/>
<field name="state"/>
</tree>
</field>
context="{'form_view_ref': 'stock_picking_batch.view_picking_form_inherited', 'tree_view_ref': 'stock_picking_batch.view_picking_tree_batch'}"/>
</page>
</notebook>
</sheet>
@@ -166,6 +173,7 @@
<field name="name" decoration-bf="1"/>
<field name="scheduled_date"/>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="picking_type_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state in ('draft', 'in_progress')"/>
<field name="activity_exception_decoration" widget="activity_exception"/>
@@ -210,6 +218,7 @@
<field name="arch" type="xml">
<search string="Search Batch Transfer">
<field name="name" string="Batch Transfer"/>
<field name="picking_type_id" invisible="1"/>
<field name="user_id"/>
<filter name="draft" string="Draft" domain="[('state', '=', 'draft')]"/>
<filter name="in_progress" string="In Progress" domain="[('state', '=', 'in_progress')]" help="Batch Transfers not finished"/>
@@ -234,11 +243,8 @@
<field name="type">ir.actions.act_window</field>
<field name="res_model">stock.picking.batch</field>
<field name="view_mode">tree,kanban,form</field>
<field name="context">{
"search_default_draft" : True,
"search_default_in_progress" : True,
'default_company_id': allowed_company_ids[0]
}</field>
<field name="domain">[('is_wave', '=', False)]</field>
<field name="context">{'search_default_draft': True, 'search_default_in_progress': True}</field>
<field name="search_view_id" ref="stock_picking_batch_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -251,7 +257,7 @@
</p>
</field>
</record>
<menuitem action="stock_picking_batch_action" id="stock_picking_batch_menu" parent="stock.menu_stock_warehouse_mgmt" sequence="10"/>
<menuitem action="stock_picking_batch_action" id="stock_picking_batch_menu" parent="stock.menu_stock_warehouse_mgmt" sequence="6"/>
<record id="vpicktree_inherit_stock_picking_batch" model="ir.ui.view">
<field name="name">stock.picking.tree</field>
@@ -266,6 +272,7 @@
('picking_type_id', '=', picking_type_id),
('picking_type_id', '=', False),
]"
widget="many2onebutton"
context="{'default_picking_type_id': picking_type_id}"/>
</xpath>
</field>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="stock_picking_wave_tree" model="ir.ui.view">
<field name="name">stock.picking.wave.tree</field>
<field name="model">stock.picking.batch</field>
<field name="priority">25</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="stock_picking_batch.stock_picking_batch_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="create">0</attribute>
</xpath>
</field>
</record>
<record id="action_picking_tree_wave" model="ir.actions.act_window">
<field name="name">Waves Transfers</field>
<field name="res_model">stock.picking.batch</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">tree,kanban,form</field>
<field name="context">{'search_default_draft': True, 'search_default_in_progress': True}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('stock_picking_batch.stock_picking_wave_tree')})]"/>
<field name="domain">[('is_wave', '=', True)]</field>
<field name="search_view_id" ref="stock_picking_batch_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new wave transfer
</p><p>
The goal of the wave transfer is to group operations from differents transfer
together in order to increase their efficiency.
It may also be useful to assign jobs (one person = one batch) or
help the timing management of operations (tasks to be done at 1pm).
</p>
</field>
</record>
<menuitem
action="action_picking_tree_wave"
id="stock_picking_wave_menu"
parent="stock.menu_stock_warehouse_mgmt"
groups="stock.group_stock_picking_wave"
sequence="8"/>
</odoo>
@@ -2,4 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import stock_picking_to_batch
from . import stock_add_to_wave
from . import stock_package_destination
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import UserError
class StockPickingToWave(models.TransientModel):
_name = 'stock.add.to.wave'
_description = 'Wave Transfer Lines'
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
if self.env.context.get('active_model') == 'stock.move.line':
lines = self.env['stock.move.line'].browse(self.env.context.get('active_ids'))
res['line_ids'] = self.env.context.get('active_ids')
picking_types = lines.picking_type_id
elif self.env.context.get('active_model') == 'stock.picking':
pickings = self.env['stock.picking'].browse(self.env.context.get('active_ids'))
res['picking_ids'] = self.env.context.get('active_ids')
picking_types = pickings.picking_type_id
else:
return res
if len(picking_types) > 1:
raise UserError(_("The selected transfers should belong to the same operation type"))
return res
wave_id = fields.Many2one('stock.picking.batch', string='Wave Transfer', domain="[('is_wave', '=', True), ('state', '!=', 'done')]")
picking_ids = fields.Many2many('stock.picking')
line_ids = fields.Many2many('stock.move.line')
mode = fields.Selection([('existing', 'an existing wave transfer'), ('new', 'a new wave transfer')], default='existing')
user_id = fields.Many2one('res.users', string='Responsible', help='Person responsible for this wave transfer')
def attach_pickings(self):
self.ensure_one()
if self.line_ids:
company = self.line_ids.company_id
if len(company) > 1:
raise UserError(_("The selected operations should belong to a unique company."))
return self.line_ids._add_to_wave(self.wave_id)
if self.picking_ids:
company = self.picking_ids.company_id
if len(company) > 1:
raise UserError(_("The selected transfers should belong to a unique company."))
else:
raise UserError(_('Cannot create wave transfers'))
view = self.env.ref('stock_picking_batch.view_move_line_tree_detailed_wave')
return {
'name': _('Add Operations'),
'type': 'ir.actions.act_window',
'view_mode': 'list',
'views': [(view.id, 'tree')],
'res_model': 'stock.move.line',
'target': 'new',
'domain': [
('picking_id', 'in', self.picking_ids.ids),
('state', '!=', 'done')
],
'context': dict(
self.env.context,
picking_to_wave=self.picking_ids.ids,
active_wave_id=self.wave_id.id,
)}
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- add picking to wave -->
<record id="stock_add_to_wave_form" model="ir.ui.view">
<field name="name">stock.add.to.wave.form</field>
<field name="model">stock.add.to.wave</field>
<field name="arch" type="xml">
<form string="Add to Wave">
<group>
<group>
<label for="mode" string="Add to"/>
<field name="mode" widget="radio" nolabel="1"/>
<field name="wave_id" options="{'no_create_edit': True, 'no_open': True}" attrs="{'invisible': [('mode', '=', 'new')], 'required': [('mode', '=', 'existing')]}"/>
<field name="user_id" options="{'no_create_edit': True}" attrs="{'invisible': [('mode', '=', 'existing')]}"/>
</group>
</group>
<footer>
<button name="attach_pickings" type="object" string="Confirm" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="stock_add_to_wave_action_stock_picking" model="ir.actions.act_window">
<field name="name">Add to wave</field>
<field name="res_model">stock.add.to.wave</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="groups_id" eval="[(4, ref('stock.group_stock_picking_wave'))]"/>
<field name="binding_model_id" ref="stock.model_stock_picking"/>
<field name="binding_view_types">list</field>
</record>
</odoo>