Merge PR #808 into 13.0

Signed-off-by guewen
This commit is contained in:
OCA-git-bot
2020-07-17 06:50:34 +00:00
13 changed files with 495 additions and 7 deletions

View File

@@ -0,0 +1 @@
../../../../stock_picking_completion_info

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@@ -10,17 +10,12 @@ class StockMove(models.Model):
common_dest_move_ids = fields.Many2many(
"stock.move",
compute="_compute_common_dest_move_ids",
search="_search_compute_dest_move_ids",
help="All the stock moves having a chained destination move sharing the"
" same picking as the actual move's destination move",
)
@api.depends(
"move_dest_ids",
"move_dest_ids.picking_id",
"move_dest_ids.picking_id.move_lines",
"move_dest_ids.picking_id.move_lines.move_orig_ids",
)
def _compute_common_dest_move_ids(self):
def _common_dest_move_query(self):
sql = """SELECT smmr.move_orig_id move_id
, array_agg(smmr2.move_orig_id) common_move_dest_ids
FROM stock_move_move_rel smmr
@@ -36,6 +31,16 @@ class StockMove(models.Model):
AND smmr.move_orig_id IN %s
GROUP BY smmr.move_orig_id;
"""
return sql
@api.depends(
"move_dest_ids",
"move_dest_ids.picking_id",
"move_dest_ids.picking_id.move_lines",
"move_dest_ids.picking_id.move_lines.move_orig_ids",
)
def _compute_common_dest_move_ids(self):
sql = self._common_dest_move_query()
self.env.cr.execute(sql, (tuple(self.ids),))
res = {
row.get("move_id"): row.get("common_move_dest_ids")
@@ -47,3 +52,16 @@ class StockMove(models.Model):
move.common_dest_move_ids = [(6, 0, common_move_ids)]
else:
move.common_dest_move_ids = [(5, 0, 0)]
def _search_compute_dest_move_ids(self, operator, value):
moves = self.search([("id", operator, value)])
if not moves:
return [("id", "=", 0)]
sql = self._common_dest_move_query()
self.env.cr.execute(sql, (tuple(moves.ids),))
res = [
move_dest_id
for row in self.env.cr.dictfetchall()
for move_dest_id in row.get("common_move_dest_ids") or []
]
return [("id", "in", res)]

View File

@@ -154,3 +154,21 @@ class TestCommonMoveDest(SavepointCase):
self.assertEqual(pack_move_1b.common_dest_move_ids, pack_move_1a)
self.assertFalse(ship_move_1a.common_dest_move_ids)
self.assertFalse(ship_move_1b.common_dest_move_ids)
self.assertEqual(
self.env["stock.move"].search(
[("common_dest_move_ids", "=", pick_move_1b.id)]
),
pick_move_1a,
)
self.assertEqual(
self.env["stock.move"].search(
[("common_dest_move_ids", "=", pick_move_1a.id)]
),
pick_move_1b,
)
self.assertEqual(
self.env["stock.move"].search(
[("common_dest_move_ids", "in", (pick_move_1a | pick_move_1b).ids)]
),
pick_move_1a | pick_move_1b,
)

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,17 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
{
"name": "Stock Picking Completion Info",
"summary": "Display on current document completion information according "
"to next operations",
"version": "13.0.1.0.0",
"development_status": "Alpha",
"category": "Warehouse Management",
"website": "https://github.com/OCA/stock-logistics-warehouse",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": ["stock_move_common_dest"],
"data": ["views/stock_picking.xml"],
}

View File

@@ -0,0 +1 @@
from . import stock_picking

View File

@@ -0,0 +1,86 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import api, fields, models
class PickingType(models.Model):
_inherit = "stock.picking.type"
display_completion_info = fields.Boolean(
help="Inform operator of a completed operation at processing and at"
" completion"
)
class StockPicking(models.Model):
_inherit = "stock.picking"
completion_info = fields.Selection(
[
("no", "No"),
(
"last_picking",
"Last picking: Completion of this operation allows next "
"operations to be processed.",
),
("next_picking_ready", "Next operations are ready to be processed."),
(
"full_order_picking",
"Full order picking: You are processing a full order picking "
"that will allow next operation to be processed",
),
],
compute="_compute_completion_info",
)
@api.depends(
"picking_type_id.display_completion_info",
"move_lines.common_dest_move_ids.state",
)
def _compute_completion_info(self):
for picking in self:
if (
picking.state == "draft"
or not picking.picking_type_id.display_completion_info
):
picking.completion_info = "no"
continue
# Depending moves are all the origin moves linked to the
# destination pickings' moves
depending_moves = picking.move_lines.mapped("common_dest_move_ids")
# If all the depending moves are done or canceled then next picking
# is ready to be processed
if picking.state == "done" and all(
m.state in ("done", "cancel") for m in depending_moves
):
picking.completion_info = "next_picking_ready"
continue
# If all the depending moves are the moves on the actual picking
# then it's a full order and next picking is ready to be processed
if depending_moves == picking.move_lines:
picking.completion_info = "full_order_picking"
continue
# If there aren't any depending move from another picking that is
# not done, then actual picking is the last to process
other_depending_moves = (depending_moves - picking.move_lines).filtered(
lambda m: m.state not in ("done", "cancel")
)
if not other_depending_moves:
picking.completion_info = "last_picking"
continue
picking.completion_info = "no"
class StockMove(models.Model):
_inherit = "stock.move"
def write(self, vals):
super().write(vals)
if "state" in vals:
# invalidate cache, the api.depends do not allow to find all
# the conditions to invalidate the field
self.env["stock.picking"].invalidate_cache(fnames=["completion_info"])
return True

View File

@@ -0,0 +1 @@
* Akim Juillerat <akim.juillerat@camptocamp.com>

View File

@@ -0,0 +1,9 @@
This module adds completion information on stock picking.
If activated on the picking type, completion information is computed according
to the next chained pickings related to the stock moves of the actual picking.
In other words, if all the previous moves linked to the destination pickings
moves are done, the completion of the actual picking allows the destination
pickings to be processed. In such case, a ribbon will appear on the stock
picking form view, to inform the stock operator.

View File

@@ -0,0 +1 @@
from . import test_stock_picking_completion_info

View File

@@ -0,0 +1,285 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo.tests import SavepointCase
class TestStockPickingCompletionInfo(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.partner_delta = cls.env.ref("base.res_partner_4")
cls.warehouse = cls.env.ref("stock.warehouse0")
cls.warehouse.write({"delivery_steps": "pick_pack_ship"})
cls.customers_location = cls.env.ref("stock.stock_location_customers")
cls.output_location = cls.env.ref("stock.stock_location_output")
cls.packing_location = cls.env.ref("stock.location_pack_zone")
cls.stock_shelf_location = cls.env.ref("stock.stock_location_components")
cls.stock_shelf_2_location = cls.env.ref("stock.stock_location_14")
cls.out_type = cls.warehouse.out_type_id
cls.pack_type = cls.warehouse.pack_type_id
cls.pick_type = cls.warehouse.pick_type_id
cls.pick_type.write({"display_completion_info": True})
cls.product_1 = cls.env["product.product"].create(
{"name": "Product 1", "type": "product"}
)
cls.product_2 = cls.env["product.product"].create(
{"name": "Product 2", "type": "product"}
)
def _init_inventory(self, same_location=True):
# Product 1 on shelf 1
# Product 2 on shelf 2
inventory = self.env["stock.inventory"].create({"name": "Test init"})
inventory.action_start()
if not same_location:
product_location_list = [
(self.product_1, self.stock_shelf_location),
(self.product_2, self.stock_shelf_2_location),
]
else:
product_location_list = [
(self.product_1, self.stock_shelf_location),
(self.product_2, self.stock_shelf_location),
]
lines_vals = list()
for product, location in product_location_list:
lines_vals.append(
(
0,
0,
{
"product_id": product.id,
"product_uom_id": product.uom_id.id,
"product_qty": 10.0,
"location_id": location.id,
},
)
)
inventory.write({"line_ids": lines_vals})
inventory.action_validate()
def _create_pickings(self, same_pick_location=True):
# Create delivery order
ship_order = self.env["stock.picking"].create(
{
"partner_id": self.partner_delta.id,
"location_id": self.output_location.id,
"location_dest_id": self.customers_location.id,
"picking_type_id": self.out_type.id,
}
)
pack_order = self.env["stock.picking"].create(
{
"partner_id": self.partner_delta.id,
"location_id": self.packing_location.id,
"location_dest_id": self.output_location.id,
"picking_type_id": self.pack_type.id,
}
)
pick_order = self.env["stock.picking"].create(
{
"partner_id": self.partner_delta.id,
"location_id": self.stock_shelf_location.id,
"location_dest_id": self.packing_location.id,
"picking_type_id": self.pick_type.id,
}
)
if same_pick_location:
return ship_order, pack_order, pick_order
pick_order_2 = self.env["stock.picking"].create(
{
"partner_id": self.partner_delta.id,
"location_id": self.stock_shelf_2_location.id,
"location_dest_id": self.packing_location.id,
"picking_type_id": self.pick_type.id,
}
)
return ship_order, pack_order, pick_order, pick_order_2
def _create_move(
self,
picking,
product,
state="waiting",
procure_method="make_to_order",
move_dest=None,
):
move_vals = {
"name": product.name,
"product_id": product.id,
"product_uom_qty": 2.0,
"product_uom": product.uom_id.id,
"picking_id": picking.id,
"location_id": picking.location_id.id,
"location_dest_id": picking.location_dest_id.id,
"state": state,
"procure_method": procure_method,
}
if move_dest:
move_vals["move_dest_ids"] = [(4, move_dest.id, False)]
return self.env["stock.move"].create(move_vals)
def test_picking_all_at_once(self):
self._init_inventory()
ship_order, pack_order, pick_order = self._create_pickings()
ship_move_1 = self._create_move(ship_order, self.product_1)
pack_move_1 = self._create_move(
pack_order, self.product_1, move_dest=ship_move_1
)
pick_move_1 = self._create_move(
pick_order,
self.product_1,
state="confirmed",
procure_method="make_to_stock",
move_dest=pack_move_1,
)
ship_move_2 = self._create_move(ship_order, self.product_2)
pack_move_2 = self._create_move(
pack_order, self.product_2, move_dest=ship_move_2
)
pick_move_2 = self._create_move(
pick_order,
self.product_2,
state="confirmed",
procure_method="make_to_stock",
move_dest=pack_move_2,
)
self.assertEqual(pick_move_1.state, "confirmed")
self.assertEqual(pick_move_2.state, "confirmed")
self.assertEqual(pick_order.state, "confirmed")
self.assertEqual(pick_order.completion_info, "full_order_picking")
pick_order.action_assign()
self.assertEqual(pick_move_1.state, "assigned")
self.assertEqual(pick_move_2.state, "assigned")
self.assertEqual(pick_order.state, "assigned")
self.assertEqual(pick_order.completion_info, "full_order_picking")
wiz = self.env["stock.immediate.transfer"].create(
{"pick_ids": [(4, pick_order.id)]}
)
wiz.process()
self.assertEqual(pick_move_1.state, "done")
self.assertEqual(pick_move_2.state, "done")
self.assertEqual(pick_order.state, "done")
self.assertEqual(pick_order.completion_info, "next_picking_ready")
def test_picking_from_different_locations(self):
self._init_inventory(same_location=False)
ship_order, pack_order, pick_order_1, pick_order_2 = self._create_pickings(
same_pick_location=False
)
ship_move_1 = self._create_move(ship_order, self.product_1)
pack_move_1 = self._create_move(
pack_order, self.product_1, move_dest=ship_move_1
)
pick_move_1 = self._create_move(
pick_order_1,
self.product_1,
state="confirmed",
procure_method="make_to_stock",
move_dest=pack_move_1,
)
ship_move_2 = self._create_move(ship_order, self.product_2)
pack_move_2 = self._create_move(
pack_order, self.product_2, move_dest=ship_move_2
)
pick_move_2 = self._create_move(
pick_order_2,
self.product_2,
state="confirmed",
procure_method="make_to_stock",
move_dest=pack_move_2,
)
self.assertEqual(pick_move_1.state, "confirmed")
self.assertEqual(pick_move_2.state, "confirmed")
self.assertEqual(pick_order_1.state, "confirmed")
self.assertEqual(pick_order_1.completion_info, "no")
self.assertEqual(pick_order_2.state, "confirmed")
self.assertEqual(pick_order_2.completion_info, "no")
pick_order_1.action_assign()
self.assertEqual(pick_move_1.state, "assigned")
self.assertEqual(pick_order_1.state, "assigned")
self.assertEqual(pick_order_1.completion_info, "no")
pick_order_2.action_assign()
self.assertEqual(pick_move_2.state, "assigned")
self.assertEqual(pick_order_2.state, "assigned")
self.assertEqual(pick_order_2.completion_info, "no")
wiz = self.env["stock.immediate.transfer"].create(
{"pick_ids": [(4, pick_order_1.id)]}
)
wiz.process()
self.assertEqual(pick_move_1.state, "done")
self.assertEqual(pick_order_1.state, "done")
self.assertEqual(pick_order_1.completion_info, "no")
self.assertNotEqual(pick_move_2.state, "done")
self.assertNotEqual(pick_order_2.state, "done")
self.assertEqual(pick_order_2.completion_info, "last_picking")
wiz = self.env["stock.immediate.transfer"].create(
{"pick_ids": [(4, pick_order_2.id)]}
)
wiz.process()
self.assertEqual(pick_move_2.state, "done")
self.assertEqual(pick_order_2.state, "done")
self.assertEqual(pick_order_2.completion_info, "next_picking_ready")
self.assertEqual(pick_order_1.completion_info, "next_picking_ready")
def test_picking_with_backorder(self):
self._init_inventory()
ship_order, pack_order, pick_order = self._create_pickings()
ship_move_1 = self._create_move(ship_order, self.product_1)
pack_move_1 = self._create_move(
pack_order, self.product_1, move_dest=ship_move_1
)
pick_move_1 = self._create_move(
pick_order,
self.product_1,
state="confirmed",
procure_method="make_to_stock",
move_dest=pack_move_1,
)
ship_move_2 = self._create_move(ship_order, self.product_2)
pack_move_2 = self._create_move(
pack_order, self.product_2, move_dest=ship_move_2
)
pick_move_2 = self._create_move(
pick_order,
self.product_2,
state="confirmed",
procure_method="make_to_stock",
move_dest=pack_move_2,
)
self.assertEqual(pick_move_1.state, "confirmed")
self.assertEqual(pick_move_2.state, "confirmed")
self.assertEqual(pick_order.state, "confirmed")
self.assertEqual(pick_order.completion_info, "full_order_picking")
pick_order.action_assign()
self.assertEqual(pick_move_1.state, "assigned")
self.assertEqual(pick_move_2.state, "assigned")
self.assertEqual(pick_order.state, "assigned")
self.assertEqual(pick_order.completion_info, "full_order_picking")
# Process partially to create backorder
pick_move_1.move_line_ids.qty_done = 1.0
pick_move_2.move_line_ids.qty_done = pick_move_2.move_line_ids.product_uom_qty
pick_order.action_done()
pick_backorder = self.env["stock.picking"].search(
[("backorder_id", "=", pick_order.id)]
)
pick_backorder_move = pick_backorder.move_lines
self.assertEqual(pick_move_1.state, "done")
self.assertEqual(pick_move_2.state, "done")
self.assertEqual(pick_order.state, "done")
self.assertEqual(pick_backorder_move.state, "assigned")
self.assertEqual(pick_backorder.state, "assigned")
self.assertEqual(pick_order.completion_info, "no")
self.assertEqual(pick_backorder.completion_info, "last_picking")
# Process backorder
pick_backorder_move.move_line_ids.qty_done = (
pick_backorder_move.move_line_ids.product_uom_qty
)
pick_backorder.action_done()
self.assertEqual(pick_backorder_move.state, "done")
self.assertEqual(pick_backorder.state, "done")
self.assertEqual(pick_order.completion_info, "next_picking_ready")
self.assertEqual(pick_backorder.completion_info, "next_picking_ready")

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_picking_type_form_inherit" model="ir.ui.view">
<field name="name">Operation Types inherit</field>
<field name="inherit_id" ref="stock.view_picking_type_form" />
<field name="model">stock.picking.type</field>
<field name="arch" type="xml">
<field name="show_reserved" position="after">
<field name="display_completion_info" />
</field>
</field>
</record>
<record id="view_picking_form_inherit" model="ir.ui.view">
<field name="name">stock.picking.form.inherit</field>
<field name="inherit_id" ref="stock.view_picking_form" />
<field name="model">stock.picking</field>
<field name="arch" type="xml">
<xpath expr="//h1" position="after">
<label for="completion_info" invisible="1" />
<div
class="alert alert-success"
attrs="{'invisible': [('completion_info', '!=', 'next_picking_ready')]}"
role="alert"
>
<field name="completion_info" nolabel="1" />
</div>
<div
class="alert alert-warning"
attrs="{'invisible': [('completion_info', '!=', 'last_picking')]}"
role="alert"
>
<field name="completion_info" nolabel="1" />
</div>
<div
class="alert alert-primary"
attrs="{'invisible': [('completion_info', '!=', 'full_order_picking')]}"
role="alert"
>
<field name="completion_info" nolabel="1" />
</div>
</xpath>
</field>
</record>
</odoo>