diff --git a/sale_lead_time_profile/README.rst b/sale_lead_time_profile/README.rst new file mode 100644 index 000000000000..94e8096a6c9f --- /dev/null +++ b/sale_lead_time_profile/README.rst @@ -0,0 +1,81 @@ +====================== +Sale Lead Time Profile +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:79be59475657d45c9881f4693df4e99894b904631029a20ab08f12cbfa64d303 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/16.0/sale_lead_time_profile + :alt: OCA/sale-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-workflow-16-0/sale-workflow-16-0-sale_lead_time_profile + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module enhances the sales order process by adding a delivery_lead_time that is determined based on the most closely matching lead time profile. +This time is then incorporated into the customer_lead of each sale order line, optimizing delivery scheduling based on specific lead time configurations. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure this module, you must first properly set up your lead time profiles: + +1. Navigate to Inventory > Configuration > Lead Time Profiles. +2. Create the records according to your specific requirements. + The system will use the most matching record to determine the delivery lead time for each sale order. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Quartile + +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/sale-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_lead_time_profile/__init__.py b/sale_lead_time_profile/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/sale_lead_time_profile/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_lead_time_profile/__manifest__.py b/sale_lead_time_profile/__manifest__.py new file mode 100644 index 000000000000..9fddd1b9b853 --- /dev/null +++ b/sale_lead_time_profile/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2025 Quartile +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Sale Lead Time Profile", + "version": "16.0.1.0.0", + "author": "Quartile, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/sale-workflow", + "license": "AGPL-3", + "depends": ["sale_stock"], + "data": [ + "security/ir.model.access.csv", + "views/lead_time_profile_views.xml", + "views/sale_order_views.xml", + ], + "installable": True, +} diff --git a/sale_lead_time_profile/models/__init__.py b/sale_lead_time_profile/models/__init__.py new file mode 100644 index 000000000000..f307f607274d --- /dev/null +++ b/sale_lead_time_profile/models/__init__.py @@ -0,0 +1,3 @@ +from . import lead_time_profile +from . import sale_order +from . import sale_order_line diff --git a/sale_lead_time_profile/models/lead_time_profile.py b/sale_lead_time_profile/models/lead_time_profile.py new file mode 100644 index 000000000000..cd371a875987 --- /dev/null +++ b/sale_lead_time_profile/models/lead_time_profile.py @@ -0,0 +1,45 @@ +# Copyright 2025 Quartile +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class LeadTimeProfile(models.Model): + _name = "lead.time.profile" + _description = "Lead Time Profile" + _order = "country_id, state_id, partner_id, warehouse_id" + + warehouse_id = fields.Many2one("stock.warehouse") + partner_id = fields.Many2one("res.partner", string="Delivery Address") + state_id = fields.Many2one( + "res.country.state", domain="[('country_id', '=?', country_id)]" + ) + country_id = fields.Many2one( + related="state_id.country_id", readonly=False, store=True, required=True + ) + lead_time = fields.Float(required=True) + + def _get_score(self, warehouse, partner): + self.ensure_one() + score = 0 + if self.warehouse_id: + if warehouse == self.warehouse_id: + score += 1 + else: + return -1 + if self.partner_id: + if partner == self.partner_id: + score += 3 + else: + return -1 + elif self.state_id: + if partner.state_id == self.state_id: + score += 2 + else: + return -1 + elif self.country_id: + if partner.country_id == self.country_id: + score += 1 + else: + return -1 + return score diff --git a/sale_lead_time_profile/models/sale_order.py b/sale_lead_time_profile/models/sale_order.py new file mode 100644 index 000000000000..4c5c2eb99df5 --- /dev/null +++ b/sale_lead_time_profile/models/sale_order.py @@ -0,0 +1,37 @@ +# Copyright 2025 Quartile +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + delivery_lead_time = fields.Float( + compute="_compute_delivery_lead_time", + store=True, + readonly=False, + help="Number of days expected for delivery from the warehouse to the delivery address.", + ) + + @api.depends("warehouse_id", "partner_shipping_id") + def _compute_delivery_lead_time(self): + for rec in self: + rec.delivery_lead_time = 0.0 + if not rec.partner_shipping_id: + continue + profiles = self.env["lead.time.profile"].search( + [ + "|", + ("warehouse_id", "=", rec.warehouse_id.id), + ("warehouse_id", "=", False), + ] + ) + if not profiles: + rec.delivery_lead_time = 0.0 + best_profile = max( + profiles, + default=None, + key=lambda r: r._get_score(rec.warehouse_id, rec.partner_shipping_id), + ) + rec.delivery_lead_time = best_profile.lead_time diff --git a/sale_lead_time_profile/models/sale_order_line.py b/sale_lead_time_profile/models/sale_order_line.py new file mode 100644 index 000000000000..4badad658205 --- /dev/null +++ b/sale_lead_time_profile/models/sale_order_line.py @@ -0,0 +1,25 @@ +# Copyright 2025 Quartile +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import timedelta + +from odoo import api, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + @api.depends("product_id", "order_id.delivery_lead_time") + def _compute_customer_lead(self): + super()._compute_customer_lead() + for line in self: + line.customer_lead += line.order_id.delivery_lead_time + return + + def _prepare_procurement_values(self, group_id=False): + values = super()._prepare_procurement_values(group_id) + if values.get("date_planned"): + values["date_planned"] = values["date_planned"] - timedelta( + days=self.order_id.delivery_lead_time + ) + return values diff --git a/sale_lead_time_profile/readme/CONFIGURE.rst b/sale_lead_time_profile/readme/CONFIGURE.rst new file mode 100644 index 000000000000..edbadeffc1bf --- /dev/null +++ b/sale_lead_time_profile/readme/CONFIGURE.rst @@ -0,0 +1,5 @@ +To configure this module, you must first properly set up your lead time profiles: + +1. Navigate to Inventory > Configuration > Lead Time Profiles. +2. Create the records according to your specific requirements. + The system will use the most matching record to determine the delivery lead time for each sale order. diff --git a/sale_lead_time_profile/readme/DESCRIPTION.rst b/sale_lead_time_profile/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..0a9b52d20100 --- /dev/null +++ b/sale_lead_time_profile/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module enhances the sales order process by adding a delivery_lead_time that is determined based on the most closely matching lead time profile. +This time is then incorporated into the customer_lead of each sale order line, optimizing delivery scheduling based on specific lead time configurations. diff --git a/sale_lead_time_profile/security/ir.model.access.csv b/sale_lead_time_profile/security/ir.model.access.csv new file mode 100644 index 000000000000..62bf3281c57a --- /dev/null +++ b/sale_lead_time_profile/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_lead_time_profile,lead.time.profile,model_lead_time_profile,sales_team.group_sale_manager,1,1,1,1 diff --git a/sale_lead_time_profile/static/description/index.html b/sale_lead_time_profile/static/description/index.html new file mode 100644 index 000000000000..ff4baf1fd0ac --- /dev/null +++ b/sale_lead_time_profile/static/description/index.html @@ -0,0 +1,424 @@ + + + + + +Sale Lead Time Profile + + + +
+

Sale Lead Time Profile

+ + +

Beta License: AGPL-3 OCA/sale-workflow Translate me on Weblate Try me on Runboat

+

This module enhances the sales order process by adding a delivery_lead_time that is determined based on the most closely matching lead time profile. +This time is then incorporated into the customer_lead of each sale order line, optimizing delivery scheduling based on specific lead time configurations.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you must first properly set up your lead time profiles:

+
    +
  1. Navigate to Inventory > Configuration > Lead Time Profiles.
  2. +
  3. Create the records according to your specific requirements. +The system will use the most matching record to determine the delivery lead time for each sale order.
  4. +
+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Quartile
  • +
+
+
+

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

+

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

+
+
+
+ + diff --git a/sale_lead_time_profile/tests/__init__.py b/sale_lead_time_profile/tests/__init__.py new file mode 100644 index 000000000000..7fd6e7118776 --- /dev/null +++ b/sale_lead_time_profile/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_lead_time_profile diff --git a/sale_lead_time_profile/tests/test_sale_lead_time_profile.py b/sale_lead_time_profile/tests/test_sale_lead_time_profile.py new file mode 100644 index 000000000000..20b4177ca220 --- /dev/null +++ b/sale_lead_time_profile/tests/test_sale_lead_time_profile.py @@ -0,0 +1,91 @@ +# Copyright 2025 Quartile +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import timedelta + +from odoo.tests.common import TransactionCase + + +class TestSaleLeadTimeProfile(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create( + { + "name": "Partner", + } + ) + cls.delivery_address = cls.env["res.partner"].create( + { + "name": "Delivery Address", + "parent_id": cls.partner.id, + } + ) + cls.warehouse = cls.env["stock.warehouse"].create( + {"name": "Main Warehouse", "code": "MW"} + ) + cls.lead_time_profile_1 = cls.env["lead.time.profile"].create( + { + "country_id": cls.env.ref("base.us").id, + "lead_time": 3.0, + } + ) + cls.lead_time_profile_2 = cls.env["lead.time.profile"].create( + { + "warehouse_id": cls.warehouse.id, + "country_id": cls.env.ref("base.us").id, + "lead_time": 5.0, + } + ) + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "type": "product", + } + ) + + def create_and_confirm_sale_order(self): + sale_order = self.env["sale.order"].create( + { + "partner_id": self.partner.id, + "warehouse_id": self.warehouse.id, + "order_line": [ + ( + 0, + 0, + { + "name": self.product.name, + "product_id": self.product.id, + "product_uom_qty": 2, + "product_uom": self.product.uom_id.id, + "price_unit": self.product.list_price, + }, + ) + ], + } + ) + sale_order.action_confirm() + return sale_order + + def test_sale_order_lead_time_profile(self): + sale_order = self.create_and_confirm_sale_order() + self.assertTrue(sale_order.picking_ids) + self.assertEqual(sale_order.delivery_lead_time, 5.0) + self.assertEqual(sale_order.order_line.customer_lead, 5.0) + picking = sale_order.picking_ids + self.assertEqual( + picking.scheduled_date.date() + timedelta(days=5.0), + picking.date_deadline.date(), + ) + + # assign partner_id to profile + self.lead_time_profile_1.partner_id = self.delivery_address.id + sale_order = self.create_and_confirm_sale_order() + self.assertTrue(sale_order.picking_ids) + self.assertEqual(sale_order.delivery_lead_time, 3.0) + self.assertEqual(sale_order.order_line.customer_lead, 3.0) + picking = sale_order.picking_ids + self.assertEqual( + picking.scheduled_date.date() + timedelta(days=3.0), + picking.date_deadline.date(), + ) diff --git a/sale_lead_time_profile/views/lead_time_profile_views.xml b/sale_lead_time_profile/views/lead_time_profile_views.xml new file mode 100644 index 000000000000..5c332edabbe7 --- /dev/null +++ b/sale_lead_time_profile/views/lead_time_profile_views.xml @@ -0,0 +1,48 @@ + + + + lead.time.profile.search + lead.time.profile + + + + + + + + + + + + + + + lead.time.profile.tree + lead.time.profile + + + + + + + + + + + + Lead Time Profiles + lead.time.profile + tree + + + diff --git a/sale_lead_time_profile/views/sale_order_views.xml b/sale_lead_time_profile/views/sale_order_views.xml new file mode 100644 index 000000000000..2c57b78f5711 --- /dev/null +++ b/sale_lead_time_profile/views/sale_order_views.xml @@ -0,0 +1,13 @@ + + + + sale.order.form + sale.order + + + + + + + + diff --git a/setup/sale_lead_time_profile/odoo/addons/sale_lead_time_profile b/setup/sale_lead_time_profile/odoo/addons/sale_lead_time_profile new file mode 120000 index 000000000000..ece7e2e72ade --- /dev/null +++ b/setup/sale_lead_time_profile/odoo/addons/sale_lead_time_profile @@ -0,0 +1 @@ +../../../../sale_lead_time_profile \ No newline at end of file diff --git a/setup/sale_lead_time_profile/setup.py b/setup/sale_lead_time_profile/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/sale_lead_time_profile/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)