Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0][IMP] stock_picking_group_by_partner_by_carrier: Control the original_group_id propagation. #1751

Open
wants to merge 6 commits into
base: 16.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion stock_picking_group_by_partner_by_carrier/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ Contributors

* Phuc Tran Thanh <[email protected]>

* Denis Roussel <[email protected]>
* ACSONE SA/NV:
* Denis Roussel <[email protected]>
* Laurent Mignon <[email protected]>

Other credits
~~~~~~~~~~~~~
Expand Down
3 changes: 2 additions & 1 deletion stock_picking_group_by_partner_by_carrier/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{
"name": "Stock Picking: group by partner and carrier",
"Summary": "Group sales deliveries moves in 1 picking per partner and carrier",
"version": "16.0.1.2.0",
"version": "16.0.2.0.0",
"author": "Camptocamp, BCIM, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/stock-logistics-workflow",
"category": "Warehouse",
Expand All @@ -15,6 +15,7 @@
"data": [
"views/res_partner.xml",
"views/stock_picking_type.xml",
"views/stock_rule.xml",
"views/stock_warehouse.xml",
"report/report_delivery_slip.xml",
"wizards/stock_picking_merge_wiz.xml",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright 2024 ACSONE SA/NV (https://www.acsone.eu)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import logging

from openupgradelib import openupgrade

_logger = logging.getLogger(__name__)


@openupgrade.migrate(use_env=True)
def migrate(env, version):
# Get the list of picking types to group.
picking_types = env["stock.picking.type"].search([("group_pickings", "=", True), ("code", "=", "outgoing")])

# Declare procurement group linked to more than one sale order as
# merged for procurement groups linked to stock pickings of type outgoing
SQL = """
UPDATE
procurement_group
SET
is_merged = TRUE
WHERE
id IN (
SELECT
procurement_group_id
FROM
procurement_group_sale_order_rel
JOIN stock_picking ON stock_picking.group_id = procurement_group_id
WHERE
stock_picking.state NOT IN ('cancel', 'done')
AND stock_picking.picking_type_id in %s
GROUP BY
procurement_group_id
HAVING
COUNT(DISTINCT sale_order_id) > 1
)
"""
env.cr.execute(SQL, (tuple(picking_types.ids),))
_logger.info("Declared %d procurement groups as merged", env.cr.rowcount)

# Get all procurement group linked to only one sale order for stock pickings
# of type outgoing with state not done or cancel and not set as merged.
SQL = """
SELECT
id
FROM stock_picking
WHERE
state NOT IN ('cancel', 'done')
AND picking_type_id in %s
AND group_id IN (
SELECT
procurement_group_id
FROM
procurement_group_sale_order_rel
JOIN stock_picking ON stock_picking.group_id = procurement_group_id
JOIN procurement_group ON procurement_group.id = procurement_group_id
WHERE
stock_picking.state NOT IN ('cancel', 'done')
AND stock_picking.picking_type_id in %s
AND (not procurement_group.is_merged or procurement_group.is_merged IS NULL)
GROUP BY
procurement_group_id
HAVING
COUNT(DISTINCT sale_order_id) = 1
)
"""
env.cr.execute(SQL, (tuple(picking_types.ids), tuple(picking_types.ids)))
ids = [row[0] for row in env.cr.fetchall()]
# generate a merged procurement group for each group of stock pickings
pickings = env["stock.picking"].browse(ids)
total = len(pickings)
cpt = 0
for picking in pickings:
cpt += 1
base_group = picking.group_id
moves = picking.move_ids.filtered(
lambda move: move.state not in ("done", "cancel")
)
_logger.info(
"Generating a merged procurement group for stock picking %s (%d of %d)",
picking.name,
cpt,
total,
)
assert len(moves.group_id) == 1
group_pickings = moves.group_id.picking_ids.filtered(
lambda picking: not (picking.printed or picking.state == "done")
)
moves = group_pickings.move_ids
new_group = base_group.copy(
picking._prepare_merge_procurement_group_values(moves.original_group_id)
)
moves.group_id = new_group
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ class ProcurementGroup(models.Model):
inverse_name="group_id",
readonly=True,
)
is_merged = fields.Boolean()
44 changes: 26 additions & 18 deletions stock_picking_group_by_partner_by_carrier/models/stock_picking.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,23 @@ def _create_backorder(self):
picking = picking.with_context(picking_no_copy_if_can_group=1)
backorder = super(StockPicking, picking)._create_backorder()
if backorder and not picking._is_grouping_disabled():
backorder._merge_procurement_groups()
backorder._update_merged_origin()
if backorder.group_id != picking.group_id:
# the backorder is an existing picking where the remaining
# moves have been moved to. We need to update the origin
backorder._update_merged_origin()
else:
# Create a new procurement group to remove the link from moves
# done in the original group and therefore avoid that a sale order
# linked to the picking and entirely delivered is no more linked
# to the backorder.
base_group = backorder.group_id
moves = backorder.move_ids
new_group = base_group.copy(
self._prepare_merge_procurement_group_values(
moves.original_group_id
)
)
moves.group_id = new_group
backorders |= backorder
return backorders

Expand All @@ -119,32 +134,25 @@ def _prepare_merge_procurement_group_values(self, move_groups):
"Merged procurement for partners: %(partners_name)s",
partners_name=", ".join(partners.mapped("display_name")),
)
return {"sale_ids": [(6, 0, sales.ids)], "name": name}
return {"sale_ids": [(6, 0, sales.ids)], "name": name, "is_merged": True}

def _merge_procurement_groups(self):
self.ensure_one()
if self._is_grouping_disabled():
return False
if self.picking_type_id.code != "outgoing":
return False
group_pickings = self.move_ids.group_id.picking_ids.filtered(
# Do no longer modify a printed or done transfer: they are
# started and their group is now fixed. It prevents keeping
# old, done sales orders in new groups forever
lambda picking: not (picking.printed or picking.state == "done")
)
moves = group_pickings.move_ids
base_group = self.group_id

# If we have moves of different procurement groups, it means moves
# have been merged in the same picking. In this case a new
# procurement group is required
if len(moves.original_group_id) > 1 and base_group in moves.original_group_id:
# Create a new procurement group
moves = self.move_ids
base_group = self.group_id
# When grouping is allowed, we create a "merged" procurement group
# that will be used to group the moves every time a new move is
# added to the picking from a different procurement group.
if not base_group.is_merged:
new_group = base_group.copy(
self._prepare_merge_procurement_group_values(moves.original_group_id)
)
group_pickings.move_ids.group_id = new_group
moves.group_id = new_group
return True

new_moves = moves.filtered(lambda move: move.group_id != base_group)
Expand All @@ -164,7 +172,7 @@ def _merge_procurement_groups(self):
moves.original_group_id
)
)
group_pickings.move_ids.group_id = new_group
self.move_ids.group_id = new_group
return True

base_group.write(
Expand Down
23 changes: 15 additions & 8 deletions stock_picking_group_by_partner_by_carrier/models/stock_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@
# Copyright 2020 Jacques-Etienne Baudoux (BCIM) <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import models
from odoo import fields, models


class StockRule(models.Model):
_inherit = "stock.rule"

propagate_original_group = fields.Boolean(
help="Propagate the original group on the created moves. This allows to "
"prevent merging pulled moves. This allows to cancel a SO without "
"canceling pulled moves from other SO (as we ensure they are not "
"merged). You likely need this option if you don't propagate the merged "
"procurement group to the pulled moves.",
default=True,
)

def _get_stock_move_values(
self,
product_id,
Expand All @@ -29,11 +38,9 @@ def _get_stock_move_values(
company_id,
values,
)
# We propagate the original_group_id on pull moves to allow to prevent
# merging pulled moves. This allows to cancel a SO without canceling
# pulled moves from other SO (as we ensure they are not merged).
move_values["original_group_id"] = (
values.get("move_dest_ids", self.env["stock.move"]).original_group_id.id
or move_values["group_id"]
)
if self.propagate_original_group:
move_values["original_group_id"] = (
values.get("move_dest_ids", self.env["stock.move"]).original_group_id.id
or move_values["group_id"]
)
return move_values
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@

* Phuc Tran Thanh <[email protected]>

* Denis Roussel <[email protected]>
* ACSONE SA/NV:
* Denis Roussel <[email protected]>
* Laurent Mignon <[email protected]>
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,9 @@ <h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<li>BCIM:
* Jacques-Etienne Baudoux &lt;<a class="reference external" href="mailto:je&#64;bcim.be">je&#64;bcim.be</a>&gt;</li>
<li>Phuc Tran Thanh &lt;<a class="reference external" href="mailto:phuc&#64;trobz.com">phuc&#64;trobz.com</a>&gt;</li>
<li>Denis Roussel &lt;<a class="reference external" href="mailto:denis.roussel&#64;acsone.eu">denis.roussel&#64;acsone.eu</a>&gt;</li>
<li>ACSONE SA/NV:
* Denis Roussel &lt;<a class="reference external" href="mailto:denis.roussel&#64;acsone.eu">denis.roussel&#64;acsone.eu</a>&gt;
* Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
Expand Down
79 changes: 62 additions & 17 deletions stock_picking_group_by_partner_by_carrier/tests/test_grouping.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2020 Camptocamp (https://www.camptocamp.com)
# Copyright 2020 Jacques-Etienne Baudoux (BCIM) <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo.fields import first
from odoo.tests.common import Form, TransactionCase

Expand All @@ -14,10 +15,13 @@ def test_sale_stock_merge_same_partner_no_carrier(self):
-> the pickings are merged"""
so1 = self._get_new_sale_order()
so2 = self._get_new_sale_order(amount=11)
so3 = self._get_new_sale_order(amount=12)
so1.action_confirm()
so2.action_confirm()
so3.action_confirm()
self.assertTrue(so1.picking_ids)
self.assertEqual(so1.picking_ids, so2.picking_ids)
self.assertEqual(so1.picking_ids, so2.picking_ids, so3.picking_ids)
self.assertEqual(1, len(so1.picking_ids.move_ids.group_id))

def test_sale_stock_merge_same_carrier(self):
"""2 sale orders for the same partner, with same carrier
Expand Down Expand Up @@ -193,43 +197,51 @@ def test_delivery_multi_step(self):
so1.action_confirm()
so2 = self._get_new_sale_order(amount=11, carrier=self.carrier1)
so2.action_confirm()
self.assertEqual(len(so1.picking_ids), 3)
self.assertEqual(len(so2.picking_ids), 3)
self.assertEqual(so1.picking_ids, so2.picking_ids)
self.assertEqual(len(so1.picking_ids), 2)
self.assertEqual(len(so2.picking_ids), 2)
# ship should be shared between so1 and so2
ships = (so1.picking_ids | so2.picking_ids).filtered(
lambda p: p.picking_type_code == "outgoing"
)
ships = so1.picking_ids & so2.picking_ids
self.assertEqual(len(ships), 1)
self.assertEqual(ships.picking_type_id, self.warehouse.out_type_id)
# but not picks
# Note: When grouping the ships, all pulled internal moves should also
# be regrouped but this is currently not supported by this module. You
# need the stock_available_to_promise_release module to have this
# feature
picks = so1.picking_ids - ships
picks = so1.picking_ids - ships | so2.picking_ids - ships
self.assertEqual(len(picks), 2)
self.assertEqual(picks.picking_type_id, self.warehouse.pick_type_id)
# the group is the same on the move lines and picking
self.assertEqual(len(so1.picking_ids.group_id), 1)
self.assertEqual(so1.picking_ids.group_id, so1.picking_ids.move_ids.group_id)

# the group is the same on the move lines in every picks and on the ships
for pick in picks | ships:
self.assertEqual(pick.group_id, pick.move_ids.group_id)

# Add a line to so1
self.assertEqual(len(ships.move_ids), 2)
sale_form = Form(so1)
self._set_line(sale_form, 4)
sale_form.save()
self.assertEqual(len(ships.move_ids), 3)
# the group is the same on the move lines and picking
self.assertEqual(len(so1.picking_ids.group_id), 1)
self.assertEqual(so1.picking_ids.group_id, so1.picking_ids.move_ids.group_id)
# the group is the same on the move lines in every picks and on the ships
ships = so1.picking_ids & so2.picking_ids
self.assertEqual(len(ships), 1)
picks = so1.picking_ids - ships | so2.picking_ids - ships
self.assertEqual(len(picks), 2)
for pick in picks | ships:
self.assertEqual(pick.group_id, pick.move_ids.group_id)

# Add a line to so2
self.assertEqual(len(ships.move_ids), 3)
self._set_line(sale_form, 4)
sale_form.save()
self.assertEqual(len(ships.move_ids), 4)
# the group is the same on the move lines and picking
self.assertEqual(len(so2.picking_ids.group_id), 1)
self.assertEqual(so1.picking_ids.group_id, so2.picking_ids.move_ids.group_id)
# the group is the same on the move lines in every picks and on the ships
ships = so1.picking_ids & so2.picking_ids
self.assertEqual(len(ships), 1)
picks = so1.picking_ids - ships | so2.picking_ids - ships
self.assertEqual(len(picks), 2)
for pick in picks | ships:
self.assertEqual(pick.group_id, pick.move_ids.group_id)

def test_delivery_multi_step_group_pick(self):
"""the warehouse uses pick + ship (with grouping enabled on pick)
Expand Down Expand Up @@ -497,3 +509,36 @@ def test_create_backorder(self):
self.assertEqual(picking.state, "done")
self.assertTrue(picking.backorder_ids)
self.assertNotEqual(picking, picking.backorder_ids)

def test_create_backorder_new_procurement_group(self):
"""Ensure a new procurement group is created when a backorder is created
and that the backorder will reference only pickings from remaining moves.
"""
so1 = self._get_new_sale_order()
so2 = self._get_new_sale_order(amount=11)
so3 = self._get_new_sale_order(amount=12)
so1.action_confirm()
so2.action_confirm()
so3.action_confirm()

picking = so1.picking_ids | so2.picking_ids | so3.picking_ids
line = so1.order_line.filtered(lambda line: not line.is_delivery)

self._update_qty_in_location(
picking.location_id,
line.product_id,
line.product_uom_qty,
)

self.assertEqual(len(picking), 1)
picking.action_assign()

# partially process the picking for line from so1
move = picking.move_ids.filtered(lambda m: m.sale_line_id.order_id == so1)
move.move_line_ids.qty_done = move.move_line_ids.reserved_uom_qty
picking._action_done()
backorder = picking.backorder_ids
self.assertTrue(backorder)
self.assertEqual(len(backorder), 1)
self.assertNotEqual(picking.group_id, backorder.group_id)
self.assertEqual(backorder.sale_ids, so2 | so3)
Loading
Loading