[IMP] stock_picking_batch: display moves and move lines

* allow to directly edit the move lines of the batched transfers
* add a check availability button if any move is not assigned yet

task-2069646
This commit is contained in:
Simon Lejeune
2020-01-31 12:40:08 +00:00
committed by svs-odoo
parent 2105895bb6
commit 2b314aad5c
5 changed files with 290 additions and 55 deletions
@@ -4,10 +4,12 @@
<!-- Add batch picking -->
<record id="stock_picking_batch_dry_1" model="stock.picking.batch">
<field name="state">in_progress</field>
<field name="picking_type_id" ref="stock.picking_type_internal"/>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="stock_picking_batch_freeze_1" model="stock.picking.batch">
<field name="state">in_progress</field>
<field name="picking_type_id" ref="stock.picking_type_internal"/>
<field name="company_id" ref="base.main_company"/>
</record>
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, fields, models
class StockPicking(models.Model):
@@ -13,6 +13,21 @@ class StockPicking(models.Model):
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
help='Batch associated to this transfer', copy=False)
@api.model
def create(self, vals):
res = super().create(vals)
if vals.get('batch_id'):
self.batch_id._sanity_check()
return res
def write(self, vals):
res = super().write(vals)
if vals.get('batch_id'):
if not self.batch_id.picking_type_id:
self.batch_id.picking_type_id = self.picking_type_id[0]
self.batch_id._sanity_check()
return res
def _should_show_transfers(self):
if len(self.batch_id) == 1 and self == self.batch_id.picking_ids:
return False
@@ -17,48 +17,134 @@ class StockPickingBatch(models.Model):
help='Name of the batch transfer')
user_id = fields.Many2one(
'res.users', string='Responsible', tracking=True, check_company=True,
readonly=True, states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]},
help='Person responsible for this batch transfer')
company_id = fields.Many2one(
'res.company', string="Company", required=True, readonly=True,
index=True, default=lambda self: self.env.company)
picking_ids = fields.One2many(
'stock.picking', 'batch_id', string='Transfers',
domain="[('company_id', '=', company_id), ('state', 'not in', ('done', 'cancel'))]",
'stock.picking', 'batch_id', string='Transfers', readonly=True,
domain="[('id', 'in', allowed_picking_ids)]", check_company=True,
states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]},
help='List of transfers associated to this batch')
show_check_availability = fields.Boolean(
compute='_compute_move_ids',
help='Technical field used to compute whether the check availability button should be shown.')
allowed_picking_ids = fields.One2many('stock.picking', compute='_compute_allowed_picking_ids')
move_ids = fields.One2many(
'stock.move', string="Stock moves", compute='_compute_move_ids')
move_line_ids = fields.One2many(
'stock.move.line', string='Stock move lines',
compute='_compute_move_ids', inverse='_set_move_line_ids', readonly=True,
states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]})
state = fields.Selection([
('draft', 'Draft'),
('in_progress', 'In progress'),
('done', 'Done'),
('cancel', 'Cancelled')], default='draft',
copy=False, tracking=True, required=True, readonly=True)
picking_type_id = fields.Many2one(
'stock.picking.type', 'Operation Type', check_company=True, copy=False,
readonly=True, states={'draft': [('readonly', False)]})
@api.depends('company_id', 'picking_type_id', 'state')
def _compute_allowed_picking_ids(self):
allowed_picking_states = ['waiting', 'confirmed', 'assigned']
cancelled_batchs = self.env['stock.picking.batch'].search_read(
[('state', '=', 'cancel')], ['id']
)
cancelled_batch_ids = [batch['id'] for batch in cancelled_batchs]
for batch in self:
domain_states = list(allowed_picking_states)
# Allows to add draft pickings only if batch is in draft as well.
if batch.state == 'draft':
domain_states.append('draft')
domain = [
('company_id', '=', batch.company_id.id),
('immediate_transfer', '=', False),
('state', 'in', domain_states),
'|',
'|',
('batch_id', '=', False),
('batch_id', '=', batch.id),
('batch_id', 'in', cancelled_batch_ids),
]
if batch.picking_type_id:
domain += [('picking_type_id', '=', batch.picking_type_id.id)]
batch.allowed_picking_ids = self.env['stock.picking'].search(domain)
@api.depends('picking_ids', 'picking_ids.move_line_ids', 'picking_ids.move_lines', 'picking_ids.move_lines.state')
def _compute_move_ids(self):
for batch in self:
batch.move_ids = batch.picking_ids.move_lines
batch.move_line_ids = batch.picking_ids.move_line_ids
batch.show_check_availability = any(m.state != 'assigned' for m in batch.move_ids)
def _set_move_line_ids(self):
new_move_lines = self[0].move_line_ids
for picking in self.picking_ids:
old_move_lines = picking.move_line_ids
picking.move_line_ids = new_move_lines.filtered(lambda ml: ml.picking_id.id == picking.id)
move_lines_to_unlink = old_move_lines - new_move_lines
if move_lines_to_unlink:
move_lines_to_unlink.unlink()
# -------------------------------------------------------------------------
# CRUD
# -------------------------------------------------------------------------
@api.model
def create(self, vals):
if vals.get('name', '/') == '/':
vals['name'] = self.env['ir.sequence'].next_by_code('picking.batch') or '/'
return super(StockPickingBatch, self).create(vals)
return super().create(vals)
def confirm_picking(self):
def write(self, vals):
res = super().write(vals)
if vals.get('picking_type_id'):
self._sanity_check()
if vals.get('picking_ids'):
batch_without_picking_type = self.filtered(lambda batch: not batch.picking_type_id)
if batch_without_picking_type:
picking = self.picking_ids and self.picking_ids[0]
batch_without_picking_type.picking_type_id = picking.picking_type_id.id
return res
def unlink(self):
if any(batch.state != 'draft' for batch in self):
raise UserError(_("You can only delete draft batch transfers."))
return super().unlink()
# -------------------------------------------------------------------------
# Action methods
# -------------------------------------------------------------------------
def action_confirm(self):
"""Sanity checks, confirm the pickings and mark the batch as confirmed."""
self.ensure_one()
if not self.picking_ids:
raise UserError(_("You have to set some pickings to batch."))
self.picking_ids.action_confirm()
self._check_company()
pickings_todo = self.mapped('picking_ids')
self.write({'state': 'in_progress'})
return pickings_todo.action_assign()
self.state = 'in_progress'
return True
def cancel_picking(self):
self.mapped('picking_ids').action_cancel()
return self.write({'state': 'cancel'})
def action_cancel(self):
self.ensure_one()
self.state = 'cancel'
return True
def print_picking(self):
pickings = self.mapped('picking_ids')
if not pickings:
raise UserError(_('Nothing to print.'))
def action_print(self):
self.ensure_one()
return self.env.ref('stock_picking_batch.action_report_picking_batch').report_action(self)
def done(self):
def action_done(self):
self.ensure_one()
self._check_company()
pickings = self.mapped('picking_ids').filtered(lambda picking: picking.state not in ('cancel', 'done'))
if any(picking.state not in ('assigned') for picking in pickings):
raise UserError(_('Some transfers are still waiting for goods. Please check or force their availability before setting this batch to done.'))
for picking in pickings:
picking.message_post(
body="<b>%s:</b> %s <a href=#id=%s&view_type=form&model=stock.picking.batch>%s</a>" % (
@@ -70,8 +156,25 @@ class StockPickingBatch(models.Model):
self.write({'state': 'done'})
return self.picking_ids.button_validate()
def action_assign(self):
self.ensure_one()
self.picking_ids.action_assign()
# -------------------------------------------------------------------------
# Miscellaneous
# -------------------------------------------------------------------------
def _sanity_check(self):
for batch in self:
if not batch.picking_ids <= batch.allowed_picking_ids:
erroneous_pickings = batch.picking_ids - batch.allowed_picking_ids
raise UserError(_(
"The following transfers cannot be added to batch transfer %s. "
"Please check their states and operation types, if they aren't immediate " +
"transfers or if they're not already part of another batch transfer.\n\n"
"Incompatibilities: %s") % (batch.name, ', '.join(erroneous_pickings.mapped('name'))))
def _track_subtype(self, init_values):
if 'state' in init_values:
return self.env.ref('stock_picking_batch.mt_batch_state')
return super(StockPickingBatch, self)._track_subtype(init_values)
return super()._track_subtype(init_values)
@@ -71,14 +71,18 @@ class TestBatchPicking(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 10.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
# confirm batch, picking should be assigned
self.batch.confirm_picking()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be reserved')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be reserved')
# Confirm batch, pickings should not be automatically assigned.
self.batch.action_confirm()
self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
# Ask to assign, so pickings should be assigned now.
self.batch.action_assign()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
self.picking_client_1.move_lines.quantity_done = 10
self.picking_client_2.move_lines.quantity_done = 10
self.batch.done()
self.batch.action_done()
self.assertEqual(self.picking_client_1.state, 'done', 'Picking 1 should be done')
self.assertEqual(self.picking_client_2.state, 'done', 'Picking 2 should be done')
@@ -98,13 +102,17 @@ class TestBatchPicking(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 10.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
# confirm batch, picking should be assigned
self.batch.confirm_picking()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be reserved')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be reserved')
# Confirm batch, pickings should not be automatically assigned.
self.batch.action_confirm()
self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
# Ask to assign, so pickings should be assigned now.
self.batch.action_assign()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
# There should be a wizard asking to process picking without quantity done
immediate_transfer_wizard_dict = self.batch.done()
immediate_transfer_wizard_dict = self.batch.action_done()
self.assertTrue(immediate_transfer_wizard_dict)
immediate_transfer_wizard = Form(self.env[(immediate_transfer_wizard_dict.get('res_model'))].with_context(immediate_transfer_wizard_dict['context'])).save()
self.assertEqual(len(immediate_transfer_wizard.pick_ids), 2)
@@ -128,8 +136,12 @@ class TestBatchPicking(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
# confirm batch, picking should be assigned
self.batch.confirm_picking()
# Confirm batch, pickings should not be automatically assigned.
self.batch.action_confirm()
self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
# Ask to assign, so pickings should be assigned now.
self.batch.action_assign()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
@@ -137,7 +149,7 @@ class TestBatchPicking(TransactionCase):
self.picking_client_2.move_lines.quantity_done = 10
# There should be a wizard asking to process picking without quantity done
back_order_wizard_dict = self.batch.done()
back_order_wizard_dict = self.batch.action_done()
self.assertTrue(back_order_wizard_dict)
back_order_wizard = Form(self.env[(back_order_wizard_dict.get('res_model'))].with_context(back_order_wizard_dict['context'])).save()
self.assertEqual(len(back_order_wizard.pick_ids), 1)
@@ -164,13 +176,17 @@ class TestBatchPicking(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
# confirm batch, picking should be assigned
self.batch.confirm_picking()
# Confirm batch, pickings should not be automatically assigned.
self.batch.action_confirm()
self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
# Ask to assign, so pickings should be assigned now.
self.batch.action_assign()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
# There should be a wizard asking to process picking without quantity done
immediate_transfer_wizard_dict = self.batch.done()
immediate_transfer_wizard_dict = self.batch.action_done()
self.assertTrue(immediate_transfer_wizard_dict)
immediate_transfer_wizard = Form(self.env[(immediate_transfer_wizard_dict.get('res_model'))].with_context(immediate_transfer_wizard_dict['context'])).save()
self.assertEqual(len(immediate_transfer_wizard.pick_ids), 2)
@@ -200,14 +216,18 @@ class TestBatchPicking(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
# confirm batch, picking should be assigned
self.batch.confirm_picking()
# Confirm batch, pickings should not be automatically assigned.
self.batch.action_confirm()
self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
# Ask to assign, so pickings should be assigned now.
self.batch.action_assign()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
self.picking_client_1.move_lines.quantity_done = 5
# There should be a wizard asking to process picking without quantity done
immediate_transfer_wizard_dict = self.batch.done()
immediate_transfer_wizard_dict = self.batch.action_done()
self.assertTrue(immediate_transfer_wizard_dict)
immediate_transfer_wizard = Form(self.env[(immediate_transfer_wizard_dict.get('res_model'))].with_context(immediate_transfer_wizard_dict['context'])).save()
self.assertEqual(len(immediate_transfer_wizard.pick_ids), 1)
@@ -1,17 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_picking_form_inherited" model="ir.ui.view">
<field name="name">stock_picking_batch.picking.form</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="mode">primary</field>
<field name="priority" eval="1000"/>
<field name="arch" type="xml">
<xpath expr="//header" position="replace">
<header>
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,partially_available,assigned,done"/>
</header>
</xpath>
</field>
</record>
<record id="view_picking_move_tree_inherited" model="ir.ui.view">
<field name="name">stock_picking_batch.picking.move.tree</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_picking_move_tree"/>
<field name="mode">primary</field>
<field name="priority" eval="1000"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="delete">0</attribute>
</xpath>
<xpath expr="//field[@name='product_id']" position="after">
<field name="picking_id" required="1"
attrs="{'readonly': [('id', '!=', False)]}"
domain="[('id', 'in', parent.picking_ids)]"
options="{'no_create_edit': True}"/>
</xpath>
<xpath expr="//field[@name='quantity_done']" position="attributes">
<attribute name="readonly">1</attribute>
</xpath>
</field>
</record>
<record id="view_move_line_tree" model="ir.ui.view">
<field name="name">stock_picking_batch.move.line.tree</field>
<field name="model">stock.move.line</field>
<field name="arch" type="xml">
<tree editable="top" decoration-muted="state == 'cancel'" string="Move Lines" default_order="location_id">
<field name="tracking" invisible="1"/>
<field name="state" invisible="1"/>
<field name="product_id" required="1" attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="picking_id" required="1" attrs="{'readonly': [('id', '!=', False)]}"
options="{'no_create_edit': True}" domain="[('id', 'in', parent.picking_ids)]"/>
<field name="lot_id" groups="stock.group_production_lot" attrs="{'readonly': [('tracking', 'not in', ['lot', 'serial'])]}"/>
<field name="location_id"/>
<field name="location_dest_id"/>
<field name="product_uom_qty" readonly="1"/>
<field name="product_uom_id" options="{'no_create': True}"
groups="uom.group_uom" readonly="1" force_save="1"/>
<field name="qty_done"/>
<field name="company_id" groups="base.group_multi_company" force_save="1"/>
</tree>
</field>
</record>
<record id="stock_picking_batch_form" model="ir.ui.view">
<field name="name">stock.picking.batch.form</field>
<field name="model">stock.picking.batch</field>
<field name="arch" type="xml">
<form string="Stock Batch Transfer">
<field name="show_check_availability" invisible="1"/>
<header>
<button name="confirm_picking" states="draft" string="Confirm" type="object" class="oe_highlight"/>
<button name="print_picking" string="Print" type="object" class="oe_highlight"/>
<button name="done" states="in_progress" string="Done" type="object" class="oe_highlight"/>
<button name="cancel_picking" string="Cancel" type="object" states="in_progress"/>
<field name="state" widget="statusbar" statusbar_visible="in_progress,done"/>
<button name="action_confirm" states="draft" string="Confirm" type="object" class="oe_highlight"/>
<button name="action_done" string="Validate" type="object" class="oe_highlight"
attrs="{'invisible': [
'|',
('state', '!=', 'in_progress'),
('show_check_availability', '=', True)]}"/>
<button name="action_assign" string="Check Availability" type="object" class="oe_highlight"
attrs="{'invisible': [
'|',
('state', '!=', 'in_progress'),
('show_check_availability', '=', False)]}"/>
<button name="action_done" string="Validate" type="object"
attrs="{'invisible': [
'|',
('state', '!=', 'in_progress'),
('show_check_availability', '=', False)]}"/>
<button name="action_assign" string="Check Availability" type="object"
attrs="{'invisible': [
'|',
('state', '!=', 'draft'),
('show_check_availability', '=', False)]}"/>
<button name="action_print" states="in_progress,done" string="Print" type="object"/>
<button name="action_cancel" string="Cancel" type="object" states="in_progress"/>
<field name="state" widget="statusbar" statusbar_visible="draft,in_progress,done"/>
</header>
<sheet>
<div class="oe_title">
@@ -20,22 +99,31 @@
<group>
<group>
<field name="user_id"/>
<field name="picking_type_id"/>
<field name="company_id" groups="base.group_multi_company" attrs="{'readonly': [('picking_ids', '!=', [])]}" force_save="1"/>
</group>
</group>
<separator string="Transfers"/>
<field name="picking_ids" widget="many2many" options="{'not_delete': True}">
<tree>
<field name="name"/>
<field name="scheduled_date"/>
<field name="location_id"/>
<field name="backorder_id"/>
<field name="origin"/>
<field name="state"/>
<button name="action_assign" string="Confirm picking" type="object" icon="fa-check text-success" attrs="{'invisible': [('state', 'in', ('done', 'cancel', 'confirmed', 'assigned'))]}"/>
<button name="action_cancel" string="Cancel picking" type="object" icon="fa-times-circle text-danger" attrs="{'invisible': [('state', 'in', ('done', 'cancel'))]}"/>
</tree>
</field>
<notebook>
<page string="Detailed Operations" attrs="{'invisible': [('state', '=', 'draft')]}">
<field name="move_line_ids" context="{'tree_view_ref': 'stock_picking_batch.view_move_line_tree'}"/>
</page>
<page string="Operations" attrs="{'invisible': [('state', '=', 'draft')]}">
<field name="move_ids" context="{'tree_view_ref': 'stock_picking_batch.view_picking_move_tree_inherited'}"/>
</page>
<page string="Transfers">
<field name="allowed_picking_ids" invisible="1"/>
<field name="picking_ids" widget="many2many" 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>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
@@ -139,7 +227,14 @@
<field name="inherit_id" ref="stock.vpicktree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='state']" position="before">
<field name="batch_id" optional="show"/>
<field name="batch_id" optional="show"
domain="[
('state', 'in', ['draft', 'in_progress']),
'|',
('picking_type_id', '=', picking_type_id),
('picking_type_id', '=', False),
]"
context="{'default_picking_type_id': picking_type_id}"/>
</xpath>
</field>
</record>