diff --git a/setup/stock_partner_delivery_window/odoo/addons/stock_partner_delivery_window b/setup/stock_partner_delivery_window/odoo/addons/stock_partner_delivery_window new file mode 120000 index 000000000000..e6205a6f6de0 --- /dev/null +++ b/setup/stock_partner_delivery_window/odoo/addons/stock_partner_delivery_window @@ -0,0 +1 @@ +../../../../stock_partner_delivery_window \ No newline at end of file diff --git a/setup/stock_partner_delivery_window/setup.py b/setup/stock_partner_delivery_window/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/stock_partner_delivery_window/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_partner_delivery_window/README.rst b/stock_partner_delivery_window/README.rst new file mode 100644 index 000000000000..51db7cf6d008 --- /dev/null +++ b/stock_partner_delivery_window/README.rst @@ -0,0 +1,103 @@ +============================= +Stock Partner Delivery Window +============================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/14.0/stock_partner_delivery_window + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-14-0/stock-logistics-workflow-14-0-stock_partner_delivery_window + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/154/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to define time scheduling preference for delivery orders on +partners, in order to raise a warning when changing a scheduled date to a time +window that is not preferred by this customer. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +On partners form view, under the "Sales & Purchases" tab, one can define a +"Delivery schedule preference" for each partner. + +Possible configurations are: + +* Any time: Do not postpone deliveries +* Fixed time windows: Postpone deliveries to the next preferred time window +* Weekdays: Postpone deliveries to the next weekday + +After selecting "Fixed time windows", one can define the preferred delivery +windows in the embedded tree view below. + +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 +~~~~~~~ + +* Camptocamp +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Akim Juillerat +* Matthieu Méquignon + +Trobz + +* Dung Tran + +Other credits +~~~~~~~~~~~~~ + +The development of this module has been financially supported by: + +* Camptocamp + +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/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_partner_delivery_window/__init__.py b/stock_partner_delivery_window/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/stock_partner_delivery_window/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_partner_delivery_window/__manifest__.py b/stock_partner_delivery_window/__manifest__.py new file mode 100644 index 000000000000..422df64fb2f2 --- /dev/null +++ b/stock_partner_delivery_window/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Stock Partner Delivery Window", + "summary": "Define preferred delivery time windows for partners", + "version": "16.0.1.0.0", + "category": "Inventory", + "author": "Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/stock-logistics-workflow", + "depends": ["base_time_window", "partner_tz", "stock"], + "data": ["security/ir.model.access.csv", "views/res_partner.xml"], + "demo": ["demo/delivery_time_window.xml"], + "installable": True, +} diff --git a/stock_partner_delivery_window/demo/delivery_time_window.xml b/stock_partner_delivery_window/demo/delivery_time_window.xml new file mode 100644 index 000000000000..f359d9cf58d2 --- /dev/null +++ b/stock_partner_delivery_window/demo/delivery_time_window.xml @@ -0,0 +1,15 @@ + + + + + 10.0 + 18.0 + + + + time_windows + + diff --git a/stock_partner_delivery_window/i18n/stock_partner_delivery_window.pot b/stock_partner_delivery_window/i18n/stock_partner_delivery_window.pot new file mode 100644 index 000000000000..383d59c55b91 --- /dev/null +++ b/stock_partner_delivery_window/i18n/stock_partner_delivery_window.pot @@ -0,0 +1,183 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_partner_delivery_window +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-14 09:01+0000\n" +"PO-Revision-Date: 2023-09-14 09:01+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_partner_delivery_window +#: code:addons/stock_partner_delivery_window/models/res_partner.py:0 +#, python-format +msgid "{}: {}" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields.selection,name:stock_partner_delivery_window.selection__res_partner__delivery_time_preference__anytime +msgid "Any time" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model,name:stock_partner_delivery_window.model_res_partner +msgid "Contact" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__create_uid +msgid "Created by" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__create_date +msgid "Created on" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,help:stock_partner_delivery_window.field_res_partner__delivery_time_preference +#: model:ir.model.fields,help:stock_partner_delivery_window.field_res_users__delivery_time_preference +msgid "" +"Define the scheduling preference for delivery orders:\n" +"\n" +"* Any time: Do not postpone deliveries\n" +"* Fixed time windows: Postpone deliveries to the next preferred time window\n" +"* Weekdays: Postpone deliveries to the next weekday" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_res_partner__delivery_time_preference +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_res_users__delivery_time_preference +msgid "Delivery time schedule preference" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_res_partner__delivery_time_window_ids +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_res_users__delivery_time_window_ids +msgid "Delivery time windows" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__display_name +msgid "Display Name" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields.selection,name:stock_partner_delivery_window.selection__res_partner__delivery_time_preference__time_windows +msgid "Fixed time windows" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__time_window_start +msgid "From" +msgstr "" + +#. module: stock_partner_delivery_window +#: code:addons/stock_partner_delivery_window/models/res_partner.py:0 +#, python-format +msgid "From {} to {}" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__id +msgid "ID" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window____last_update +msgid "Last Modified on" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__write_date +msgid "Last Updated on" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__partner_id +msgid "Partner" +msgstr "" + +#. module: stock_partner_delivery_window +#. odoo-python +#: code:addons/stock_partner_delivery_window/models/res_partner.py:0 +#, python-format +msgid "" +"Please define at least one delivery time window or change preference to Any " +"time" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model,name:stock_partner_delivery_window.model_partner_delivery_time_window +msgid "Preferred delivery time windows" +msgstr "" + +#. module: stock_partner_delivery_window +#. odoo-python +#: code:addons/stock_partner_delivery_window/models/stock_picking.py:0 +#, python-format +msgid "Scheduled date does not match partner's Delivery window preference." +msgstr "" + +#. module: stock_partner_delivery_window +#. odoo-python +#: code:addons/stock_partner_delivery_window/models/stock_picking.py:0 +#, python-format +msgid "" +"The scheduled date is {date} ({tz}), but the partner is set to prefer deliveries on following time windows:\n" +"{window}" +msgstr "" + +#. module: stock_partner_delivery_window +#. odoo-python +#: code:addons/stock_partner_delivery_window/models/stock_picking.py:0 +#, python-format +msgid "" +"The scheduled date is {} ({}), but the partner is set to prefer deliveries " +"on working days." +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__time_window_weekday_ids +msgid "Time Window Weekday" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__tz +msgid "Timezone" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,field_description:stock_partner_delivery_window.field_partner_delivery_time_window__time_window_end +msgid "To" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model,name:stock_partner_delivery_window.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields.selection,name:stock_partner_delivery_window.selection__res_partner__delivery_time_preference__workdays +msgid "Weekdays (Monday to Friday)" +msgstr "" + +#. module: stock_partner_delivery_window +#: model:ir.model.fields,help:stock_partner_delivery_window.field_partner_delivery_time_window__tz +msgid "" +"When printing documents and exporting/importing data, time values are computed according to this timezone.\n" +"If the timezone is not set, UTC (Coordinated Universal Time) is used.\n" +"Anywhere else, time values are computed according to the time offset of your web client." +msgstr "" \ No newline at end of file diff --git a/stock_partner_delivery_window/models/__init__.py b/stock_partner_delivery_window/models/__init__.py new file mode 100644 index 000000000000..2b3ebc04d451 --- /dev/null +++ b/stock_partner_delivery_window/models/__init__.py @@ -0,0 +1,3 @@ +from . import delivery_time_window +from . import res_partner +from . import stock_picking diff --git a/stock_partner_delivery_window/models/delivery_time_window.py b/stock_partner_delivery_window/models/delivery_time_window.py new file mode 100644 index 000000000000..8739938d2a99 --- /dev/null +++ b/stock_partner_delivery_window/models/delivery_time_window.py @@ -0,0 +1,20 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class DeliveryTimeWindow(models.Model): + _name = "partner.delivery.time.window" + _inherit = "time.window.mixin" + _description = "Preferred delivery time windows" + _time_window_overlap_check_field = "partner_id" + + partner_id = fields.Many2one( + "res.partner", required=True, index=True, ondelete="cascade" + ) + tz = fields.Selection(related="partner_id.tz", readonly=True) + + @api.constrains("partner_id") + def check_window_no_overlaps(self): + return super().check_window_no_overlaps() diff --git a/stock_partner_delivery_window/models/res_partner.py b/stock_partner_delivery_window/models/res_partner.py new file mode 100644 index 000000000000..b54c205e5770 --- /dev/null +++ b/stock_partner_delivery_window/models/res_partner.py @@ -0,0 +1,184 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import warnings +from collections import defaultdict +from datetime import time + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.misc import format_time + +from odoo.addons.partner_tz.tools import tz_utils + +WORKDAYS = list(range(5)) + + +class ResPartner(models.Model): + + _inherit = "res.partner" + + delivery_time_preference = fields.Selection( + [ + ("anytime", "Any time"), + ("time_windows", "Fixed time windows"), + ("workdays", "Weekdays (Monday to Friday)"), + ], + string="Delivery time schedule preference", + default="anytime", + required=True, + help="Define the scheduling preference for delivery orders:\n\n" + "* Any time: Do not postpone deliveries\n" + "* Fixed time windows: Postpone deliveries to the next preferred " + "time window\n" + "* Weekdays: Postpone deliveries to the next weekday", + ) + + delivery_time_window_ids = fields.One2many( + "partner.delivery.time.window", "partner_id", string="Delivery time windows" + ) + + @api.constrains("delivery_time_preference", "delivery_time_window_ids") + def _check_delivery_time_preference(self): + for partner in self: + if ( + partner.delivery_time_preference == "time_windows" + and not partner.delivery_time_window_ids + ): + raise ValidationError( + _( + "Please define at least one delivery time window or change" + " preference to Any time" + ) + ) + + def get_delivery_windows(self, day_name=None): + """ + Return the list of delivery windows by partner id for the given day + + :param day: The day name (see time.weekday, ex: 0,1,2,...) + :return: dict partner_id: delivery_window recordset + """ + res = {} + domain = [("partner_id", "in", self.ids)] + if day_name is not None: + week_day_id = self.env["time.weekday"]._get_id_by_name(day_name) + domain.append(("time_window_weekday_ids", "in", week_day_id)) + windows = self.env["partner.delivery.time.window"].search(domain) + for window in windows: + if not res.get(window.partner_id.id): + res[window.partner_id.id] = self.env[ + "partner.delivery.time.window" + ].browse() + res[window.partner_id.id] |= window + return res + + def is_in_delivery_window(self, date_time): + """ + Checks if provided date_time is in a delivery window for actual partner + + :param date_time: Datetime object + :return: Boolean + """ + self.ensure_one() + if self.delivery_time_preference == "workdays": + if date_time.weekday() > 4: + return False + return True + windows = self.get_delivery_windows(date_time.weekday()).get(self.id) + if windows: + for w in windows: + start_time = w.get_time_window_start_time() + end_time = w.get_time_window_end_time() + if self.tz: + utc_start = tz_utils.tz_to_utc_time(self.tz, start_time) + utc_end = tz_utils.tz_to_utc_time(self.tz, end_time) + else: + utc_start = start_time + utc_end = end_time + if utc_start <= date_time.time() <= utc_end: + return True + return False + + def _get_delivery_time_format_string(self): + warnings.warn( + "Method `_get_delivery_time_format_string` will be deprecated in the next version", + DeprecationWarning, + stacklevel=2, + ) + return _("From {} to {}") + + def get_delivery_time_description(self): + warnings.warn( + "Method `get_delivery_time_description` will be deprecated in the next version", + DeprecationWarning, + stacklevel=2, + ) + res = dict() + day_translated_values = dict( + self.env["time.weekday"]._fields["name"]._description_selection(self.env) + ) + + def short_format_time(time): + return format_time(self.env, time, time_format="short") + + weekdays = self.env["time.weekday"].search([]) + for partner in self: + opening_times = defaultdict(list) + time_format_string = self._get_delivery_time_format_string() + if partner.delivery_time_preference == "time_windows": + for day in weekdays: + day_windows = partner.delivery_time_window_ids.filtered( + lambda d: day in d.time_window_weekday_ids + ) + for win in day_windows: + start = win.get_time_window_start_time() + end = win.get_time_window_end_time() + translated_day = day_translated_values[day.name] + value = time_format_string % ( + short_format_time(start), + short_format_time(end), + ) + opening_times[translated_day].append(value) + elif partner.delivery_time_preference == "workdays": + day_windows = weekdays.filtered(lambda d: d.name in WORKDAYS) + for day in day_windows: + translated_day = day_translated_values[day.name] + value = time_format_string % ( + short_format_time(time(hour=0, minute=0)), + short_format_time(time(hour=23, minute=59)), + ) + opening_times[translated_day].append(value) + else: + for day in weekdays: + translated_day = day_translated_values[day.name] + value = time_format_string % ( + short_format_time(time(hour=0, minute=0)), + short_format_time(time(hour=23, minute=59)), + ) + opening_times[translated_day].append(value) + opening_times_description = list() + for day_name, time_list in opening_times.items(): + opening_times_description.append( + _("{}: {}").format(day_name, _(", ").join(time_list)) + ) + res[partner.id] = "\n".join(opening_times_description) + return res + + def copy_data(self, default=None): + result = super().copy_data(default=default)[0] + not_time_windows = self.delivery_time_preference != "time_windows" + not_copy_windows = not_time_windows or "delivery_time_window_ids" in result + if not_copy_windows: + return [result] + values = [ + { + "time_window_start": window_id.time_window_start, + "time_window_end": window_id.time_window_end, + "time_window_weekday_ids": [ + (4, wd_id.id, 0) for wd_id in window_id.time_window_weekday_ids + ], + } + for window_id in self.delivery_time_window_ids + ] + result["delivery_time_window_ids"] = [(0, 0, val) for val in values] + return [result] diff --git a/stock_partner_delivery_window/models/stock_picking.py b/stock_partner_delivery_window/models/stock_picking.py new file mode 100644 index 000000000000..0d2aacdf154b --- /dev/null +++ b/stock_partner_delivery_window/models/stock_picking.py @@ -0,0 +1,54 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import _, api, models +from odoo.tools.misc import format_datetime + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def _planned_delivery_date(self): + return self.scheduled_date + + @api.onchange("scheduled_date") + def _onchange_scheduled_date(self): + self.ensure_one() + partner = self.partner_id + anytime_delivery = partner and partner.delivery_time_preference == "anytime" + outgoing_picking = self.picking_type_id.code == "outgoing_picking" + # Return nothing if partner delivery preference is anytime + if not partner or anytime_delivery or outgoing_picking: + return + if not partner.is_in_delivery_window(self._planned_delivery_date()): + return {"warning": self._scheduled_date_no_delivery_window_match_msg()} + + def _scheduled_date_no_delivery_window_match_msg(self): + scheduled_date = self.scheduled_date + formatted_scheduled_date = format_datetime(self.env, scheduled_date) + partner = self.partner_id + if partner.delivery_time_preference == "workdays": + message = _( + "The scheduled date is {} ({}), but the partner is " + "set to prefer deliveries on working days." + ).format(formatted_scheduled_date, scheduled_date.weekday()) + else: + delivery_windows_strings = [] + if partner: + for w in partner.get_delivery_windows().get(partner.id): + delivery_windows_strings.append( + " * {} ({})".format(w.display_name, partner.tz) + ) + message = _( + "The scheduled date is {date} ({tz}), but the partner is " + "set to prefer deliveries on following time windows:\n{window}" + ).format( + date=format_datetime(self.env, self.scheduled_date), + tz=self.env.context.get("tz"), + window="\n".join(delivery_windows_strings), + ) + return { + "title": _( + "Scheduled date does not match partner's Delivery window preference." + ), + "message": message, + } diff --git a/stock_partner_delivery_window/readme/CONFIGURE.rst b/stock_partner_delivery_window/readme/CONFIGURE.rst new file mode 100644 index 000000000000..afb25e3fa348 --- /dev/null +++ b/stock_partner_delivery_window/readme/CONFIGURE.rst @@ -0,0 +1,11 @@ +On partners form view, under the "Sales & Purchases" tab, one can define a +"Delivery schedule preference" for each partner. + +Possible configurations are: + +* Any time: Do not postpone deliveries +* Fixed time windows: Postpone deliveries to the next preferred time window +* Weekdays: Postpone deliveries to the next weekday + +After selecting "Fixed time windows", one can define the preferred delivery +windows in the embedded tree view below. diff --git a/stock_partner_delivery_window/readme/CONTRIBUTORS.rst b/stock_partner_delivery_window/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..30ba8315e3fe --- /dev/null +++ b/stock_partner_delivery_window/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +* Akim Juillerat +* Matthieu Méquignon + +Trobz + +* Dung Tran diff --git a/stock_partner_delivery_window/readme/CREDITS.rst b/stock_partner_delivery_window/readme/CREDITS.rst new file mode 100644 index 000000000000..f5cc070c78ea --- /dev/null +++ b/stock_partner_delivery_window/readme/CREDITS.rst @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +* Camptocamp diff --git a/stock_partner_delivery_window/readme/DESCRIPTION.rst b/stock_partner_delivery_window/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..9008816817f0 --- /dev/null +++ b/stock_partner_delivery_window/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module allows to define time scheduling preference for delivery orders on +partners, in order to raise a warning when changing a scheduled date to a time +window that is not preferred by this customer. diff --git a/stock_partner_delivery_window/security/ir.model.access.csv b/stock_partner_delivery_window/security/ir.model.access.csv new file mode 100644 index 000000000000..096e52b6582f --- /dev/null +++ b/stock_partner_delivery_window/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +access_partner_delivery_time_window_user,access_partner_delivery_time_window_user,model_partner_delivery_time_window,base.group_user,1,0,0,0 +access_partner_delivery_time_window_manager,access_partner_delivery_time_window_manager,model_partner_delivery_time_window,stock.group_stock_manager,1,1,1,1 diff --git a/stock_partner_delivery_window/static/description/icon.png b/stock_partner_delivery_window/static/description/icon.png new file mode 100644 index 000000000000..3a0328b516c4 Binary files /dev/null and b/stock_partner_delivery_window/static/description/icon.png differ diff --git a/stock_partner_delivery_window/static/description/index.html b/stock_partner_delivery_window/static/description/index.html new file mode 100644 index 000000000000..449dedb6830d --- /dev/null +++ b/stock_partner_delivery_window/static/description/index.html @@ -0,0 +1,449 @@ + + + + + + +Stock Partner Delivery Window + + + +
+

Stock Partner Delivery Window

+ + +

Beta License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runbot

+

This module allows to define time scheduling preference for delivery orders on +partners, in order to raise a warning when changing a scheduled date to a time +window that is not preferred by this customer.

+

Table of contents

+ +
+

Configuration

+

On partners form view, under the “Sales & Purchases” tab, one can define a +“Delivery schedule preference” for each partner.

+

Possible configurations are:

+
    +
  • Any time: Do not postpone deliveries
  • +
  • Fixed time windows: Postpone deliveries to the next preferred time window
  • +
  • Weekdays: Postpone deliveries to the next weekday
  • +
+

After selecting “Fixed time windows”, one can define the preferred delivery +windows in the embedded tree view below.

+
+
+

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

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

Contributors

+ +

Trobz

+ +
+
+

Other credits

+

The development of this module has been financially supported by:

+
    +
  • Camptocamp
  • +
+
+
+

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/stock-logistics-workflow project on GitHub.

+

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

+
+
+
+ + diff --git a/stock_partner_delivery_window/tests/__init__.py b/stock_partner_delivery_window/tests/__init__.py new file mode 100644 index 000000000000..cbbca6e51166 --- /dev/null +++ b/stock_partner_delivery_window/tests/__init__.py @@ -0,0 +1 @@ +from . import test_delivery_window diff --git a/stock_partner_delivery_window/tests/test_delivery_window.py b/stock_partner_delivery_window/tests/test_delivery_window.py new file mode 100644 index 000000000000..49f57b1261ba --- /dev/null +++ b/stock_partner_delivery_window/tests/test_delivery_window.py @@ -0,0 +1,180 @@ +# Copyright 2020 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from freezegun import freeze_time + +from odoo.tests.common import TransactionCase + + +class TestPartnerDeliveryWindow(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.customer_anytime = cls.env["res.partner"].create( + {"name": "Anytime", "delivery_time_preference": "anytime"} + ) + cls.customer_working_days = cls.env["res.partner"].create( + {"name": "Working Days", "delivery_time_preference": "workdays"} + ) + cls.customer_time_window = cls.env["res.partner"].create( + { + "name": "Time Window", + "delivery_time_preference": "time_windows", + "delivery_time_window_ids": [ + ( + 0, + 0, + { + "time_window_start": 0.00, + "time_window_end": 23.99, + "time_window_weekday_ids": [ + ( + 6, + 0, + [ + cls.env.ref( + "base_time_window.time_weekday_thursday" + ).id, + cls.env.ref( + "base_time_window.time_weekday_saturday" + ).id, + ], + ) + ], + }, + ) + ], + } + ) + cls.product = cls.env.ref("product.product_product_9") + cls.picking_type_delivery = cls.env.ref("stock.picking_type_out") + cls.location_stock = cls.env.ref("stock.stock_location_stock") + cls.location_customers = cls.env.ref("stock.stock_location_customers") + + def _create_delivery_picking(self, partner): + return self.env["stock.picking"].create( + { + "partner_id": partner.id, + "location_id": self.location_stock.id, + "location_dest_id": self.location_customers.id, + "picking_type_id": self.picking_type_delivery.id, + } + ) + + @freeze_time("2020-04-02") # Thursday + def test_delivery_window_warning(self): + # No warning with anytime + anytime_picking = self._create_delivery_picking(self.customer_anytime) + anytime_picking.scheduled_date = "2020-04-03" # Friday + onchange_res = anytime_picking._onchange_scheduled_date() + self.assertIsNone(onchange_res) + # No warning on friday + workdays_picking = self._create_delivery_picking(self.customer_working_days) + workdays_picking.scheduled_date = "2020-04-03" # Friday + onchange_res = workdays_picking._onchange_scheduled_date() + self.assertIsNone(onchange_res) + # But warning on saturday + workdays_picking.scheduled_date = "2020-04-04" # Saturday + onchange_res = workdays_picking._onchange_scheduled_date() + self.assertIn("warning", onchange_res) + self.assertIn( + "the partner is set to prefer deliveries on working days", + onchange_res["warning"]["message"], + ) + # No warning on preferred time window + time_window_picking = self._create_delivery_picking(self.customer_time_window) + time_window_picking.scheduled_date = "2020-04-04" # Saturday + onchange_res = time_window_picking._onchange_scheduled_date() + self.assertIsNone(onchange_res) + time_window_picking.scheduled_date = "2020-04-03" # Friday + onchange_res = time_window_picking._onchange_scheduled_date() + self.assertTrue("warning" in onchange_res.keys()) + + @freeze_time("2020-04-02 07:59:59") # Thursday + def test_with_timezone_dst(self): + # Define customer to allow shipping only between 10.00am and 4.00pm + # in tz 'Europe/Brussels' (GMT+1 or GMT+2 during DST) + self.customer_time_window.tz = "Europe/Brussels" + self.customer_time_window.delivery_time_window_ids.write( + {"time_window_start": 10.0, "time_window_end": 16.0} + ) + # Test DST + # + # Frozen time is in UTC so 2020-04-02 07:59:59 == 2020-04-02 09:59:59 + # in Brussels which is preferred + picking = self._create_delivery_picking(self.customer_time_window) + onchange_res = picking._onchange_scheduled_date() + self.assertTrue( + isinstance(onchange_res, dict) and "warning" in onchange_res.keys() + ) + # Scheduled date is in UTC so 2020-04-02 08:00:00 == 2020-04-02 10:00:00 + # in Brussels which is preferred + picking.scheduled_date = "2020-04-02 08:00:00" + onchange_res = picking._onchange_scheduled_date() + self.assertIsNone(onchange_res) + # Scheduled date is in UTC so 2020-04-02 13:59:59 == 2020-04-02 15:59:59 + # in Brussels which is preferred + picking.scheduled_date = "2020-04-02 13:59:59" + onchange_res = picking._onchange_scheduled_date() + self.assertIsNone(onchange_res) + # Scheduled date is in UTC so 2020-04-02 14:00:00 == 2020-04-02 16:00:00 + # in Brussels which is preferred + picking.scheduled_date = "2020-04-02 14:00:00" + onchange_res = picking._onchange_scheduled_date() + self.assertIsNone(onchange_res) + # Scheduled date is in UTC so 2020-04-02 14:00:01 == 2020-04-02 16:00:01 + # in Brussels which is preferred + picking.scheduled_date = "2020-04-02 14:00:01" + onchange_res = picking._onchange_scheduled_date() + self.assertTrue( + isinstance(onchange_res, dict) and "warning" in onchange_res.keys() + ) + + @freeze_time("2020-03-26 08:59:59") # Thursday + def test_with_timezone_no_dst(self): + # Define customer to allow shipping only between 10.00am and 4.00pm + # in tz 'Europe/Brussels' (GMT+1 or GMT+2 during DST) + self.customer_time_window.tz = "Europe/Brussels" + self.customer_time_window.delivery_time_window_ids.write( + {"time_window_start": 10.0, "time_window_end": 16.0} + ) + # Test No-DST + # + # Frozen time is in UTC so 2020-03-26 08:59:59 == 2020-04-02 09:59:59 + # in Brussels which is preferred + picking = self._create_delivery_picking(self.customer_time_window) + onchange_res = picking._onchange_scheduled_date() + self.assertTrue( + isinstance(onchange_res, dict) and "warning" in onchange_res.keys() + ) + # Scheduled date is in UTC so 2020-03-26 09:00:00 == 2020-04-02 10:00:00 + # in Brussels which is preferred + picking.scheduled_date = "2020-03-26 09:00:00" + onchange_res = picking._onchange_scheduled_date() + # No warning since we're in the timeframe + self.assertIsNone(onchange_res) + # Scheduled date is in UTC so 2020-03-26 14:59:59 == 2020-04-02 15:59:59 + # in Brussels which is preferred + picking.scheduled_date = "2020-03-26 14:59:59" + onchange_res = picking._onchange_scheduled_date() + # No warning since we're in the timeframe + self.assertIsNone(onchange_res) + # Scheduled date is in UTC so 2020-03-26 15:00:00 == 2020-04-02 16:00:00 + # in Brussels which is preferred + picking.scheduled_date = "2020-03-26 15:00:00" + onchange_res = picking._onchange_scheduled_date() + self.assertIsNone(onchange_res) + # Scheduled date is in UTC so 2020-03-26 15:00:01 == 2020-04-02 16:00:01 + # in Brussels which is not preferred + picking.scheduled_date = "2020-03-26 15:00:01" + onchange_res = picking._onchange_scheduled_date() + self.assertTrue( + isinstance(onchange_res, dict) and "warning" in onchange_res.keys() + ) + + def test_copy_partner_with_time_window_ids(self): + copied_partner = self.customer_time_window.copy() + expecting = len(self.customer_time_window.delivery_time_window_ids) + self.assertEqual(len(copied_partner.delivery_time_window_ids), expecting) + copied_partner = self.customer_working_days.copy() + self.assertFalse(copied_partner.delivery_time_window_ids) diff --git a/stock_partner_delivery_window/views/res_partner.xml b/stock_partner_delivery_window/views/res_partner.xml new file mode 100644 index 000000000000..23913976ee50 --- /dev/null +++ b/stock_partner_delivery_window/views/res_partner.xml @@ -0,0 +1,32 @@ + + + + res.partner.form.inherit + res.partner + + + + +
+
+
+
+
+