Skip to content

Commit

Permalink
[ADD] stock_release_channel_auto_release: Add automatic release mode
Browse files Browse the repository at this point in the history
This addons define an automatic release mode on the release channels. By default
release channels manage the release of the transfers in batch mode. This means
that the transfers are released only when the user manually triggers the release
process by clicking on the release button.

When the automatic release mode is enabled, the transfers are released automatically
when a new transfer is added to the channel or as soon a product becomes available.

As for the batch mode, the automatic release process is only active on open channels.
When is locked, the automatic release process is stopped. Once the channel is unlocked,
the automatic release process is restarted and transfers into the channel are released
if they are ready to be released (IOW if quantities are available for moves not
yet released).
  • Loading branch information
lmignon authored and TDu committed Jul 4, 2023
1 parent 86dd089 commit 1761937
Show file tree
Hide file tree
Showing 23 changed files with 977 additions and 26 deletions.
6 changes: 6 additions & 0 deletions setup/stock_release_channel_auto_release/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
12 changes: 8 additions & 4 deletions stock_release_channel/i18n/stock_release_channel.pot
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ msgid "Assigned Total"
msgstr ""

#. module: stock_release_channel
#: model:ir.model.fields,field_description:stock_release_channel.field_stock_release_channel__auto_release
#: model:ir.model.fields,field_description:stock_release_channel.field_stock_release_channel__batch_mode
msgid "Auto Release"
msgstr ""

Expand Down Expand Up @@ -243,7 +243,7 @@ msgid "Full Progress"
msgstr ""

#. module: stock_release_channel
#: model:ir.model.fields.selection,name:stock_release_channel.selection__stock_release_channel__auto_release__group_commercial_partner
#: model:ir.model.fields.selection,name:stock_release_channel.selection__stock_release_channel__batch_mode__group_commercial_partner
msgid "Grouped by Commercial Partner"
msgstr ""

Expand Down Expand Up @@ -342,6 +342,7 @@ msgid "Lines]"
msgstr ""

#. module: stock_release_channel
<<<<<<< HEAD
#: model_terms:ir.ui.view,arch_db:stock_release_channel.stock_release_channel_form_view
#: model_terms:ir.ui.view,arch_db:stock_release_channel.stock_release_channel_kanban_view
msgid "Lock"
Expand All @@ -355,16 +356,19 @@ msgstr ""

#. module: stock_release_channel
#: model:ir.model.fields.selection,name:stock_release_channel.selection__stock_release_channel__auto_release__max
=======
#: model:ir.model.fields.selection,name:stock_release_channel.selection__stock_release_channel__batch_mode__max
>>>>>>> 0153d8aa ([ADD] stock_release_channel_auto_release: Add automatic release mode)
msgid "Max"
msgstr ""

#. module: stock_release_channel
#: model:ir.model.fields,field_description:stock_release_channel.field_stock_release_channel__max_auto_release
#: model:ir.model.fields,field_description:stock_release_channel.field_stock_release_channel__max_batch_mode
msgid "Max Transfers to release"
msgstr ""

#. module: stock_release_channel
#: model:ir.model.fields,help:stock_release_channel.field_stock_release_channel__auto_release
#: model:ir.model.fields,help:stock_release_channel.field_stock_release_channel__batch_mode
msgid ""
"Max: release N transfers to have a configured max of X deliveries in progress.\n"
"Grouped by Commercial Partner: release all transfers for acommercial partner at once."
Expand Down
13 changes: 13 additions & 0 deletions stock_release_channel/migrations/16.0.1.0.1/pre-migrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2022 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import logging

from odoo.tools import sql

_logger = logging.getLogger(__name__)


def migrate(cr, version):
sql.rename_column(cr, "stock_release_channel", "auto_release", "batch_mode")
sql.rename_column(cr, "stock_release_channel", "max_auto_release", "max_batch_mode")
51 changes: 37 additions & 14 deletions stock_release_channel/models/stock_release_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pytz import timezone

from odoo import _, api, exceptions, fields, models
from odoo.osv.expression import NEGATIVE_TERM_OPERATORS
from odoo.tools.safe_eval import (
datetime as safe_datetime,
dateutil as safe_dateutil,
Expand Down Expand Up @@ -64,8 +65,10 @@ class StockReleaseChannel(models.Model):
help="Write Python code to filter out pickings.",
)
active = fields.Boolean(default=True)

auto_release = fields.Selection(
release_mode = fields.Selection(
[("batch", "Batch (Manual)")], required=True, default="batch"
)
batch_mode = fields.Selection(
selection=[
("max", "Max"),
("group_commercial_partner", "Grouped by Commercial Partner"),
Expand All @@ -76,7 +79,7 @@ class StockReleaseChannel(models.Model):
" in progress.\nGrouped by Commercial Partner: release all transfers for a"
"commercial partner at once.",
)
max_auto_release = fields.Integer(
max_batch_mode = fields.Integer(
string="Max Transfers to release",
default=10,
help="When clicking on the package icon, it releases X transfers minus "
Expand Down Expand Up @@ -199,6 +202,7 @@ class StockReleaseChannel(models.Model):
)
is_release_allowed = fields.Boolean(
compute="_compute_is_release_allowed",
search="_search_is_release_allowed",
help="Technical field to check if the "
"action 'Release Next Batch' is allowed.",
)
Expand Down Expand Up @@ -228,6 +232,25 @@ def _compute_is_release_allowed(self):
for rec in self:
rec.is_release_allowed = rec.state == "open" and not rec.release_forbidden

@api.model
def _get_is_release_allowed_domain(self):
return [("state", "=", "open"), ("release_forbidden", "=", False)]

@api.model
def _get_is_release_not_allowed_domain(self):
return ["|", ("state", "!=", "open"), ("release_forbidden", "=", True)]

@api.model
def _search_is_release_allowed(self, operator, value):
if "in" in operator:
raise ValueError(f"Invalid operator {operator}")
negative_op = operator in NEGATIVE_TERM_OPERATORS
is_release_allowed = (value and not negative_op) or (not value and negative_op)
domain = self._get_is_release_allowed_domain()
if not is_release_allowed:
domain = self._get_is_release_not_allowed_domain()
return domain

def _get_picking_to_unassign_domain(self):
return [
("release_channel_id", "in", self.ids),
Expand Down Expand Up @@ -652,43 +675,43 @@ def _pickings_sort_key(picking):
return (-int(picking.priority or 1), picking.date_priority, picking.id)

def _get_next_pickings(self):
return getattr(self, "_get_next_pickings_{}".format(self.auto_release))()
return getattr(self, "_get_next_pickings_{}".format(self.batch_mode))()

def _get_pickings_to_release(self):
"""Get the pickings to release."""
domain = self._field_picking_domains()["count_picking_release_ready"]
domain += [("release_channel_id", "in", self.ids)]
return self.env["stock.picking"].search(domain)

def _get_next_pickings_max(self):
if not self.max_auto_release:
if not self.max_batch_mode:
raise exceptions.UserError(_("No Max transfers to release is configured."))

waiting_domain = self._field_picking_domains()["count_picking_waiting"]
waiting_domain += [("release_channel_id", "=", self.id)]
released_in_progress = self.env["stock.picking"].search_count(waiting_domain)

release_limit = max(self.max_auto_release - released_in_progress, 0)
release_limit = max(self.max_batch_mode - released_in_progress, 0)
if not release_limit:
raise exceptions.UserError(
_(
"The number of released transfers in"
" progress is already at the maximum."
)
)
domain = self._field_picking_domains()["count_picking_release_ready"]
domain += [("release_channel_id", "=", self.id)]
next_pickings = self.env["stock.picking"].search(domain)
next_pickings = self._get_pickings_to_release()
# We have to use a python sort and not a order + limit on the search
# because "date_priority" is computed and not stored. If needed, we
# should evaluate making it a stored field in the module
# "stock_available_to_promise_release".
return next_pickings.sorted(self._pickings_sort_key)[:release_limit]

def _get_next_pickings_group_commercial_partner(self):
domain = self._field_picking_domains()["count_picking_release_ready"]
domain += [("release_channel_id", "=", self.id)]
# We have to use a python sort and not a order + limit on the search
# because "date_priority" is computed and not stored. If needed, we
# should evaluate making it a stored field in the module
# "stock_available_to_promise_release".
next_pickings = (
self.env["stock.picking"].search(domain).sorted(self._pickings_sort_key)
)
next_pickings = self._get_pickings_to_release().sorted(self._pickings_sort_key)
if not next_pickings:
return self.env["stock.picking"].browse()
first_picking = next_pickings[0]
Expand Down
17 changes: 17 additions & 0 deletions stock_release_channel/tests/common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright 2020 Camptocamp (https://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)

import logging

from odoo import fields
from odoo.tests import common

Expand All @@ -20,6 +22,21 @@ def setUpClass(cls):
)
cls._create_base_data()

def setUp(self):
super(ReleaseChannelCase, self).setUp()
loggers = ["odoo.addons.stock_release_channel.models.stock_release_channel"]
for logger in loggers:
logging.getLogger(logger).addFilter(self)

# pylint: disable=unused-variable
@self.addCleanup
def un_mute_logger():
for logger_ in loggers:
logging.getLogger(logger_).removeFilter(self)

def filter(self, record):
return 0

@classmethod
def _create_base_data(cls):
cls.wh = cls.env["stock.warehouse"].create(
Expand Down
8 changes: 4 additions & 4 deletions stock_release_channel/tests/test_channel_release_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ def test_release_auto_forbidden(self):
self.channel.release_next_batch()

def test_release_auto_max_next_batch_no_config(self):
self.channel.max_auto_release = 0
self.channel.max_batch_mode = 0
with self.assertRaises(exceptions.UserError):
self.channel.release_next_batch()

def test_release_auto_max_next_batch(self):
self.channel.max_auto_release = 2
self.channel.max_batch_mode = 2
self.channel.release_next_batch()
# 2 have been released
self.assertEqual(
Expand All @@ -60,15 +60,15 @@ def test_release_auto_max_no_next_batch(self):
self._assert_action_nothing_in_the_queue(action)

def test_release_auto_group_commercial_partner(self):
self.channel.auto_release = "group_commercial_partner"
self.channel.batch_mode = "group_commercial_partner"
self.channel.release_next_batch()
self.assertFalse(self.picking.need_release)
self.assertFalse(self.picking2.need_release)
other_pickings = self.pickings - (self.picking | self.picking2)
self.assertTrue(all(p.need_release) for p in other_pickings)

def test_release_auto_group_commercial_partner_no_next_batch(self):
self.channel.auto_release = "group_commercial_partner"
self.channel.batch_mode = "group_commercial_partner"
self.pickings.need_release = False # cheat for getting the right condition
action = self.channel.release_next_batch()
self._assert_action_nothing_in_the_queue(action)
Expand Down
21 changes: 17 additions & 4 deletions stock_release_channel/views/stock_release_channel_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,16 @@
<group name="options" groups="stock.group_stock_manager">
<field name="release_forbidden" />
<field
name="auto_release"
name="release_mode"
attrs="{'invisible': [('release_forbidden', '=', True)]}"
/>
<field
name="max_auto_release"
attrs="{'invisible': ['|', ('release_forbidden', '=', True), ('auto_release', '!=', 'max')]}"
name="batch_mode"
attrs="{'invisible': ['|', ('release_mode','!=', 'batch'), ('release_forbidden', '=', True)]}"
/>
<field
name="max_batch_mode"
attrs="{'invisible': ['|', '|', ('release_mode','!=', 'batch'), ('release_forbidden', '=', True), ('batch_mode', '!=', 'max')]}"
/>
<field name="warehouse_id" />
<field name="picking_type_ids" options="{'no_create': True}">
Expand Down Expand Up @@ -142,7 +146,15 @@
<field name="arch" type="xml">
<tree>
<field name="sequence" widget="handle" />
<field
name="state"
widget="badge"
decoration-muted="state == 'asleep'"
decoration-info="state == 'locked'"
decoration-success="state == 'open'"
/>
<field name="name" />
<field name="release_mode" optional="hide" />
<field name="count_picking_all" sum="All Total" />
<field name="count_picking_release_ready" sum="Release Ready Total" />
<field name="count_picking_released" sum="Released Total" />
Expand All @@ -168,6 +180,7 @@
class="oe_background_grey o_kanban_dashboard o_emphasize_colors o_stock_release_channel"
create="0"
>
<field name="release_mode" invisible="1" />
<field name="color" />
<field name="name" readonly="1" />
<field name="count_picking_release_ready" />
Expand Down Expand Up @@ -238,7 +251,7 @@
class="btn btn-primary"
name="release_next_batch"
type="object"
attrs="{'invisible': [('is_release_allowed', '=', False)]}"
attrs="{'invisible': ['|', ('is_release_allowed', '=', False), ('release_mode', '!=', 'batch')]}"
>
<i
class="fa fa-dropbox fa-4x"
Expand Down
Loading

0 comments on commit 1761937

Please sign in to comment.