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 0000000000..9e2130c24e --- /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 0000000000..28c57bb640 --- /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_available_to_promise_release/__manifest__.py b/stock_available_to_promise_release/__manifest__.py index f24c276e42..f941752f15 100644 --- a/stock_available_to_promise_release/__manifest__.py +++ b/stock_available_to_promise_release/__manifest__.py @@ -20,6 +20,7 @@ "wizards/stock_release_views.xml", "wizards/stock_unrelease_views.xml", ], + "demo": [], "installable": True, "license": "LGPL-3", "application": False, diff --git a/stock_available_to_promise_release/models/stock_picking.py b/stock_available_to_promise_release/models/stock_picking.py index 59f6ff6610..0b6545e859 100644 --- a/stock_available_to_promise_release/models/stock_picking.py +++ b/stock_available_to_promise_release/models/stock_picking.py @@ -38,6 +38,15 @@ class StockPicking(models.Model): help="It specifies how to release a transfer partially or all at once", ) + set_printed_at_release = fields.Boolean(compute="_compute_set_printed_at_release") + + @api.depends("move_lines") + def _compute_set_printed_at_release(self): + for picking in self: + picking.set_printed_at_release = not ( + any(picking.move_lines.mapped("rule_id.no_backorder_at_release")) + ) + @api.depends("move_lines.need_release") def _compute_need_release(self): data = self.env["stock.move"].read_group( diff --git a/stock_available_to_promise_release/tests/common.py b/stock_available_to_promise_release/tests/common.py index 8cf631dba9..85e020b88a 100644 --- a/stock_available_to_promise_release/tests/common.py +++ b/stock_available_to_promise_release/tests/common.py @@ -2,11 +2,14 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from odoo import fields -from odoo.tests import common, tagged +from odoo.tests import common -@tagged("post_install", "-at_install") class PromiseReleaseCommonCase(common.SavepointCase): + + at_install = False + post_install = True + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/stock_available_to_promise_release/tests/test_reservation.py b/stock_available_to_promise_release/tests/test_reservation.py index f45f2b000f..22a4de23e1 100644 --- a/stock_available_to_promise_release/tests/test_reservation.py +++ b/stock_available_to_promise_release/tests/test_reservation.py @@ -7,10 +7,17 @@ from dateutil.relativedelta import relativedelta from freezegun import freeze_time +from odoo.tests import tagged + from .common import PromiseReleaseCommonCase +@tagged("post_install", "-at_install") class TestAvailableToPromiseRelease(PromiseReleaseCommonCase): + + at_install = False + post_install = True + def test_horizon_date(self): move = self.env["stock.move"].create( { diff --git a/stock_available_to_promise_release/tests/test_unrelease.py b/stock_available_to_promise_release/tests/test_unrelease.py index 32728e7ed3..cc94677910 100644 --- a/stock_available_to_promise_release/tests/test_unrelease.py +++ b/stock_available_to_promise_release/tests/test_unrelease.py @@ -5,11 +5,17 @@ from datetime import datetime from odoo.exceptions import UserError +from odoo.tests import tagged from .common import PromiseReleaseCommonCase +@tagged("post_install", "-at_install") class TestAvailableToPromiseRelease(PromiseReleaseCommonCase): + + at_install = False + post_install = True + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/stock_available_to_promise_release/tests/test_unrelease_2steps.py b/stock_available_to_promise_release/tests/test_unrelease_2steps.py index 8019e5b9a2..b2163222e3 100644 --- a/stock_available_to_promise_release/tests/test_unrelease_2steps.py +++ b/stock_available_to_promise_release/tests/test_unrelease_2steps.py @@ -3,10 +3,17 @@ from datetime import datetime +from odoo.tests import tagged + from .common import PromiseReleaseCommonCase +@tagged("post_install", "-at_install") class TestAvailableToPromiseRelease(PromiseReleaseCommonCase): + + at_install = False + post_install = True + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/stock_available_to_promise_release/tests/test_unrelease_3steps.py b/stock_available_to_promise_release/tests/test_unrelease_3steps.py index d9b7c187dd..ebd1a009cf 100644 --- a/stock_available_to_promise_release/tests/test_unrelease_3steps.py +++ b/stock_available_to_promise_release/tests/test_unrelease_3steps.py @@ -3,10 +3,17 @@ from datetime import datetime +from odoo.tests import tagged + from .common import PromiseReleaseCommonCase +@tagged("post_install", "-at_install") class TestAvailableToPromiseRelease3steps(PromiseReleaseCommonCase): + + at_install = False + post_install = True + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/stock_release_channel/i18n/stock_release_channel.pot b/stock_release_channel/i18n/stock_release_channel.pot index 95cdf00964..820eb7990b 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 "" @@ -355,16 +355,17 @@ 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 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/14.0.1.3.0/pre-migrate.py b/stock_release_channel/migrations/14.0.1.3.0/pre-migrate.py new file mode 100644 index 0000000000..e370dceb5d --- /dev/null +++ b/stock_release_channel/migrations/14.0.1.3.0/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 72182657d6..d543d73d7e 100644 --- a/stock_release_channel/models/stock_release_channel.py +++ b/stock_release_channel/models/stock_release_channel.py @@ -9,6 +9,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, @@ -67,8 +68,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"), @@ -79,7 +82,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 " @@ -202,6 +205,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.", ) @@ -231,6 +235,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), @@ -706,17 +729,23 @@ def _pickings_sort_key(picking): ) 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()["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()["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( _( @@ -740,9 +769,7 @@ def _get_next_pickings_group_commercial_partner(self): # 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 f5a4433fd6..0220d4d073 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 4ffac94011..c4ba19593e 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" pickings = self.channel.picking_ids.filtered(lambda p: p.release_ready) for _i in range(0, len(pickings.partner_id.commercial_partner_id)): action = self.channel.release_next_batch() diff --git a/stock_release_channel/views/stock_release_channel_views.xml b/stock_release_channel/views/stock_release_channel_views.xml index 9493cb251d..5bfba064bf 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,8 @@ class="oe_background_grey o_kanban_dashboard o_emphasize_colors o_stock_release_channel" create="0" > + + @@ -238,7 +252,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 0000000000..0650744f6b --- /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 0000000000..60868d6e9f --- /dev/null +++ b/stock_release_channel_auto_release/__manifest__.py @@ -0,0 +1,20 @@ +# 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": "14.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", + ], +} 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 0000000000..05550a888d --- /dev/null +++ b/stock_release_channel_auto_release/data/queue_job_data.xml @@ -0,0 +1,15 @@ + + + + + stock_release_channel_auto_release + + + + + + auto_release_available_to_promise + + + + 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 0000000000..ebbc07de25 --- /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 0000000000..2aa7fa9e22 --- /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 0000000000..f7c13b576f --- /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_cache(["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 0000000000..ffd2426095 --- /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 0000000000..0ef1e9001a --- /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 0000000000..9228bb484d --- /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 0000000000..3a0328b516 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 0000000000..ad46444dd9 --- /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 0000000000..a42cf52214 --- /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 0000000000..16c5327328 --- /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["product.product"].invalidate_cache() + + @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 0000000000..3a5834fd29 --- /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 + + + + + +