diff --git a/setup/stock_release_channel_auto_release/odoo/addons/stock_release_channel_auto_release b/setup/stock_release_channel_auto_release/odoo/addons/stock_release_channel_auto_release new file mode 120000 index 00000000000..9e2130c24e6 --- /dev/null +++ b/setup/stock_release_channel_auto_release/odoo/addons/stock_release_channel_auto_release @@ -0,0 +1 @@ +../../../../stock_release_channel_auto_release \ No newline at end of file diff --git a/setup/stock_release_channel_auto_release/setup.py b/setup/stock_release_channel_auto_release/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_release_channel_auto_release/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_release_channel/i18n/stock_release_channel.pot b/stock_release_channel/i18n/stock_release_channel.pot index 95cdf00964b..2c9b580bfd3 100644 --- a/stock_release_channel/i18n/stock_release_channel.pot +++ b/stock_release_channel/i18n/stock_release_channel.pot @@ -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 "" @@ -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 "" @@ -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" @@ -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." diff --git a/stock_release_channel/migrations/16.0.1.0.1/pre-migrate.py b/stock_release_channel/migrations/16.0.1.0.1/pre-migrate.py new file mode 100644 index 00000000000..e370dceb5d2 --- /dev/null +++ b/stock_release_channel/migrations/16.0.1.0.1/pre-migrate.py @@ -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") diff --git a/stock_release_channel/models/stock_release_channel.py b/stock_release_channel/models/stock_release_channel.py index 144e7e5e5c2..cb22e0760b3 100644 --- a/stock_release_channel/models/stock_release_channel.py +++ b/stock_release_channel/models/stock_release_channel.py @@ -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, @@ -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"), @@ -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 " @@ -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.", ) @@ -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), @@ -652,17 +675,23 @@ 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( _( @@ -670,9 +699,7 @@ def _get_next_pickings_max(self): " 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 @@ -680,15 +707,11 @@ def _get_next_pickings_max(self): 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] diff --git a/stock_release_channel/tests/common.py b/stock_release_channel/tests/common.py index f5a4433fd67..0220d4d0732 100644 --- a/stock_release_channel/tests/common.py +++ b/stock_release_channel/tests/common.py @@ -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 @@ -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( diff --git a/stock_release_channel/tests/test_channel_release_batch.py b/stock_release_channel/tests/test_channel_release_batch.py index a1ec2ca3c6b..1320be26e9f 100644 --- a/stock_release_channel/tests/test_channel_release_batch.py +++ b/stock_release_channel/tests/test_channel_release_batch.py @@ -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( @@ -60,7 +60,7 @@ 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) @@ -68,7 +68,7 @@ def test_release_auto_group_commercial_partner(self): 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) diff --git a/stock_release_channel/views/stock_release_channel_views.xml b/stock_release_channel/views/stock_release_channel_views.xml index 9493cb251d0..aca1558cd9a 100644 --- a/stock_release_channel/views/stock_release_channel_views.xml +++ b/stock_release_channel/views/stock_release_channel_views.xml @@ -68,12 +68,16 @@ + @@ -142,7 +146,15 @@ + + @@ -168,6 +180,7 @@ class="oe_background_grey o_kanban_dashboard o_emphasize_colors o_stock_release_channel" create="0" > + @@ -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')]}" > Operations > Release Channels to access to the dashboard. + +Edit a channel and select 'Automatic' into the list of available release mode. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Laurent Mignon +* Jacques-Etienne Baudoux + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_release_channel_auto_release/__init__.py b/stock_release_channel_auto_release/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stock_release_channel_auto_release/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_release_channel_auto_release/__manifest__.py b/stock_release_channel_auto_release/__manifest__.py new file mode 100644 index 00000000000..59d41a2fb3d --- /dev/null +++ b/stock_release_channel_auto_release/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2022 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Stock Release Channel Auto Release", + "summary": """ + Add an automatic release mode to the release channel""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/wms", + "depends": [ + "stock_release_channel", + "stock_move_auto_assign_auto_release", + ], + "data": [ + "data/queue_job_data.xml", + "views/stock_release_channel.xml", + ], + "demo": [], +} diff --git a/stock_release_channel_auto_release/data/queue_job_data.xml b/stock_release_channel_auto_release/data/queue_job_data.xml new file mode 100644 index 00000000000..248d47bb077 --- /dev/null +++ b/stock_release_channel_auto_release/data/queue_job_data.xml @@ -0,0 +1,15 @@ + + + + + stock_release_channel + + + + + + auto_release + + + + diff --git a/stock_release_channel_auto_release/models/__init__.py b/stock_release_channel_auto_release/models/__init__.py new file mode 100644 index 00000000000..ebbc07de258 --- /dev/null +++ b/stock_release_channel_auto_release/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_release_channel +from . import stock_picking diff --git a/stock_release_channel_auto_release/models/stock_picking.py b/stock_release_channel_auto_release/models/stock_picking.py new file mode 100644 index 00000000000..2aa7fa9e22f --- /dev/null +++ b/stock_release_channel_auto_release/models/stock_picking.py @@ -0,0 +1,49 @@ +# Copyright 2022 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, models +from odoo.osv.expression import AND + +from odoo.addons.queue_job.job import identity_exact + + +class StockPicking(models.Model): + + _inherit = "stock.picking" + + @api.model + def _is_auto_release_allowed_depends(self): + depends = super()._is_auto_release_allowed_depends() + depends.append("release_channel_id.is_auto_release_allowed") + return depends + + @property + def _is_auto_release_allowed_domain(self): + domain = super()._is_auto_release_allowed_domain + return AND( + [ + domain, + [("release_channel_id.is_auto_release_allowed", "=", True)], + ] + ) + + def _delay_auto_release_available_to_promise(self): + for picking in self: + picking.with_delay( + identity_key=identity_exact, + description=_( + "Auto release available to promise %(name)s", name=picking.name + ), + ).auto_release_available_to_promise() + + def auto_release_available_to_promise(self): + to_release = self.filtered("is_auto_release_allowed") + to_release.release_available_to_promise() + return to_release + + def assign_release_channel(self): + res = super().assign_release_channel() + self.filtered( + "is_auto_release_allowed" + )._delay_auto_release_available_to_promise() + return res diff --git a/stock_release_channel_auto_release/models/stock_release_channel.py b/stock_release_channel_auto_release/models/stock_release_channel.py new file mode 100644 index 00000000000..19458212b89 --- /dev/null +++ b/stock_release_channel_auto_release/models/stock_release_channel.py @@ -0,0 +1,73 @@ +# Copyright 2022 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from odoo.osv.expression import AND, NEGATIVE_TERM_OPERATORS, OR + + +class StockReleaseChannel(models.Model): + + _inherit = "stock.release.channel" + + release_mode = fields.Selection( + selection_add=[("auto", "Automatic")], + ondelete={"auto": "set default"}, + ) + + is_auto_release_allowed = fields.Boolean( + compute="_compute_is_auto_release_allowed", + search="_search_is_auto_release_allowed", + ) + + @api.depends("release_mode", "is_release_allowed") + def _compute_is_auto_release_allowed(self): + for channel in self: + channel.is_auto_release_allowed = ( + channel.release_mode == "auto" and channel.is_release_allowed + ) + + @api.model + def _get_is_auto_release_allowed_domain(self): + return AND( + [self._get_is_release_allowed_domain(), [("release_mode", "=", "auto")]] + ) + + @api.model + def _get_is_auto_release_not_allowed_domain(self): + return OR( + [ + self._get_is_release_not_allowed_domain(), + [("release_mode", "!=", "auto")], + ] + ) + + @api.model + def _search_is_auto_release_allowed(self, operator, value): + if "in" in operator: + raise ValueError(f"Invalid operator {operator}") + negative_op = operator in NEGATIVE_TERM_OPERATORS + is_auto_release_allowed = (value and not negative_op) or ( + not value and negative_op + ) + domain = self._get_is_auto_release_allowed_domain() + if not is_auto_release_allowed: + domain = self._get_is_auto_release_not_allowed_domain() + return domain + + def write(self, vals): + res = super().write(vals) + release_mode = vals.get("release_mode") + if release_mode == "auto": + self.invalidate_recordset(["is_auto_release_allowed"]) + self.auto_release_all() + return res + + def action_unlock(self): + res = super().action_unlock() + if not self.env.context.get("no_auto_release"): + self.auto_release_all() + return res + + def auto_release_all(self): + pickings = self.filtered("is_auto_release_allowed")._get_pickings_to_release() + pickings._delay_auto_release_available_to_promise() diff --git a/stock_release_channel_auto_release/readme/CONTRIBUTORS.rst b/stock_release_channel_auto_release/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..ffd24260952 --- /dev/null +++ b/stock_release_channel_auto_release/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Laurent Mignon +* Jacques-Etienne Baudoux diff --git a/stock_release_channel_auto_release/readme/DESCRIPTION.rst b/stock_release_channel_auto_release/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..0ef1e9001a2 --- /dev/null +++ b/stock_release_channel_auto_release/readme/DESCRIPTION.rst @@ -0,0 +1,13 @@ +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). diff --git a/stock_release_channel_auto_release/readme/USAGE.rst b/stock_release_channel_auto_release/readme/USAGE.rst new file mode 100644 index 00000000000..9228bb484d3 --- /dev/null +++ b/stock_release_channel_auto_release/readme/USAGE.rst @@ -0,0 +1,3 @@ +Use Inventory > Operations > Release Channels to access to the dashboard. + +Edit a channel and select 'Automatic' into the list of available release mode. diff --git a/stock_release_channel_auto_release/static/description/icon.png b/stock_release_channel_auto_release/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/stock_release_channel_auto_release/static/description/icon.png differ diff --git a/stock_release_channel_auto_release/static/description/index.html b/stock_release_channel_auto_release/static/description/index.html new file mode 100644 index 00000000000..ad46444dd93 --- /dev/null +++ b/stock_release_channel_auto_release/static/description/index.html @@ -0,0 +1,436 @@ + + + + + + +Stock Release Channel Auto Release + + + +
+

Stock Release Channel Auto Release

+ + +

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runbot

+

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).

+

Table of contents

+ +
+

Usage

+

Use Inventory > Operations > Release Channels to access to the dashboard.

+

Edit a channel and select ‘Automatic’ into the list of available release mode.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/wms project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/stock_release_channel_auto_release/tests/__init__.py b/stock_release_channel_auto_release/tests/__init__.py new file mode 100644 index 00000000000..a42cf522144 --- /dev/null +++ b/stock_release_channel_auto_release/tests/__init__.py @@ -0,0 +1 @@ +from . import test_channel_release_auto diff --git a/stock_release_channel_auto_release/tests/test_channel_release_auto.py b/stock_release_channel_auto_release/tests/test_channel_release_auto.py new file mode 100644 index 00000000000..73e7e0009c6 --- /dev/null +++ b/stock_release_channel_auto_release/tests/test_channel_release_auto.py @@ -0,0 +1,134 @@ +# Copyright 2022 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager + +from odoo.addons.queue_job.job import identity_exact +from odoo.addons.queue_job.tests.common import trap_jobs +from odoo.addons.stock_release_channel.tests.common import ChannelReleaseCase + + +class TestChannelReleaseAuto(ChannelReleaseCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.channel.release_mode = "auto" + + cls._update_qty_in_location(cls.loc_bin1, cls.product1, 1000.0) + cls._update_qty_in_location(cls.loc_bin1, cls.product2, 1000.0) + + # invalidate cache for computed fields bases on qty in stock + cls.env.invalidate_all() + + @contextmanager + def assert_release_job_enqueued(self, channel): + pickings_to_release = channel._get_pickings_to_release() + self.assertTrue(pickings_to_release) + with trap_jobs() as trap: + yield + trap.assert_jobs_count(len(pickings_to_release)) + for pick in pickings_to_release: + trap.assert_enqueued_job( + pick.auto_release_available_to_promise, + args=(), + kwargs={}, + properties=dict( + identity_key=identity_exact, + ), + ) + + def test_channel_auto_release_forbidden(self): + self.assertTrue(self.channel.is_auto_release_allowed) + self.channel.release_forbidden = True + self.assertFalse(self.channel.is_auto_release_allowed) + + def test_channel_search_is_auto_release_allowed(self): + self.assertTrue(self.channel.is_auto_release_allowed) + self.assertIn( + self.channel, + self.env["stock.release.channel"].search( + [("is_auto_release_allowed", "=", True)] + ), + ) + self.assertNotIn( + self.channel, + self.env["stock.release.channel"].search( + [("is_auto_release_allowed", "=", False)] + ), + ) + self.channel.release_forbidden = True + self.assertFalse(self.channel.is_auto_release_allowed) + self.assertNotIn( + self.channel, + self.env["stock.release.channel"].search( + [("is_auto_release_allowed", "=", True)] + ), + ) + self.assertIn( + self.channel, + self.env["stock.release.channel"].search( + [("is_auto_release_allowed", "=", False)] + ), + ) + + def test_picking_auto_release_forbidden(self): + self.assertTrue(self.picking.is_auto_release_allowed) + self.channel.release_forbidden = True + self.assertFalse(self.picking.is_auto_release_allowed) + + def test_picking_search_is_auto_release_allowed(self): + self.assertTrue(self.picking.is_auto_release_allowed) + self.assertIn( + self.picking, + self.env["stock.picking"].search([("is_auto_release_allowed", "=", True)]), + ) + self.assertNotIn( + self.picking, + self.env["stock.picking"].search([("is_auto_release_allowed", "=", False)]), + ) + self.channel.release_forbidden = True + self.assertFalse(self.picking.is_auto_release_allowed) + self.assertNotIn( + self.picking, + self.env["stock.picking"].search([("is_auto_release_allowed", "=", True)]), + ) + self.assertIn( + self.picking, + self.env["stock.picking"].search([("is_auto_release_allowed", "=", False)]), + ) + + def test_channel_auto_release_all_launch_release_job(self): + with self.assert_release_job_enqueued(self.channel): + self.channel.auto_release_all() + + def test_picking_assign_release_channel_launch_release_job(self): + self.picking.release_channel_id = None + self.channel.release_mode = "batch" + with trap_jobs() as trap: + self.picking.assign_release_channel() + self.assertEqual(self.picking.release_channel_id, self.channel) + trap.assert_jobs_count(0) + self.picking.release_channel_id = None + self.channel.release_mode = "auto" + with trap_jobs() as trap: + self.picking.assign_release_channel() + self.assertEqual(self.picking.release_channel_id, self.channel) + trap.assert_jobs_count(1) + trap.assert_enqueued_job( + self.picking.auto_release_available_to_promise, + args=(), + kwargs={}, + properties=dict( + identity_key=identity_exact, + ), + ) + + def test_release_channel_change_mode_launch_release_job(self): + self.channel.release_mode = "batch" + with self.assert_release_job_enqueued(self.channel): + self.channel.release_mode = "auto" + + def test_release_channel_action_unlock_launch_release_job(self): + self.channel.action_lock() + with self.assert_release_job_enqueued(self.channel): + self.channel.action_unlock() diff --git a/stock_release_channel_auto_release/views/stock_release_channel.xml b/stock_release_channel_auto_release/views/stock_release_channel.xml new file mode 100644 index 00000000000..3a5834fd299 --- /dev/null +++ b/stock_release_channel_auto_release/views/stock_release_channel.xml @@ -0,0 +1,31 @@ + + + + + stock.release.channel.kanban (in stock_release_channel_auto_release) + stock.release.channel + + + + + +