Skip to content

Commit

Permalink
[ADD] module: product_route_profile
Browse files Browse the repository at this point in the history
  • Loading branch information
Kev-Roche authored and astirpe committed Nov 15, 2024
1 parent d1533d4 commit b88cb1d
Show file tree
Hide file tree
Showing 16 changed files with 441 additions and 0 deletions.
2 changes: 2 additions & 0 deletions product_route_profile/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from .hooks import post_init_hook
25 changes: 25 additions & 0 deletions product_route_profile/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <[email protected]>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "Product Route Profile",
"summary": "Add Route profile concept on product",
"version": "14.0.1.0.0",
"category": "Warehouse",
"website": "https://github.com/OCA/stock-logistics-warehouse",
"author": "Akretion, Odoo Community Association (OCA)",
"maintainers": ["Kev-Roche"],
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"stock",
],
"data": [
"views/route_profile.xml",
"views/product_template.xml",
"security/ir.model.access.csv",
],
"post_init_hook": "post_init_hook",
}
43 changes: 43 additions & 0 deletions product_route_profile/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright (C) 2022 Akretion (<http://www.akretion.com>).
# @author Kévin Roche <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from collections import defaultdict

from odoo import SUPERUSER_ID, api


def post_init_hook(cr, registry):
def get_profile(route_ids):
route_ids = tuple(set(route_ids))
profile = route2profile.get(route_ids)
if not profile:
profile_name = ""
route_names = [
rec.name for rec in env["stock.location.route"].browse(route_ids)
]
profile_name = " / ".join(route_names)
profile = env["route.profile"].create(
{
"name": profile_name,
"route_ids": [(6, 0, route_ids)],
}
)
route2profile[route_ids] = profile
return profile

env = api.Environment(cr, SUPERUSER_ID, {})
query = """
SELECT product_id, array_agg(route_id)
FROM stock_route_product group by product_id;
"""
cr.execute(query)
results = cr.fetchall()
route2profile = {}
profile2product = defaultdict(lambda: env["product.template"])
for row in results:
profile = get_profile(row[1])
profile2product[profile.id] |= env["product.template"].browse(row[0])

for profile in profile2product:
profile2product[profile].write({"route_profile_id": profile})
119 changes: 119 additions & 0 deletions product_route_profile/i18n/fr.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * product_route_profile
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-27 18:10+0000\n"
"PO-Revision-Date: 2022-04-27 20:13+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: \n"
"X-Generator: Poedit 3.0.1\n"

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_route_profile__create_uid
msgid "Created by"
msgstr ""

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_route_profile__create_date
msgid "Created on"
msgstr ""

#. module: product_route_profile
#: model:ir.model.fields,help:product_route_profile.field_product_product__route_ids
#: model:ir.model.fields,help:product_route_profile.field_product_template__route_ids
msgid ""
"Depending on the modules installed, this will allow you to define the route of the product: whether it will be bought, "
"manufactured, replenished on order, etc."
msgstr ""
"En fonction des modules installés, cela va vous permettre de définir les routes sur l'article: acheter, fabriquer, "
"réapprovisionner sur commande, etc."

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_product_template__display_name
#: model:ir.model.fields,field_description:product_route_profile.field_route_profile__display_name
msgid "Display Name"
msgstr "Nom"

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_product_template__id
#: model:ir.model.fields,field_description:product_route_profile.field_route_profile__id
msgid "ID"
msgstr ""

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_product_template____last_update
#: model:ir.model.fields,field_description:product_route_profile.field_route_profile____last_update
msgid "Last Modified on"
msgstr "Dernière modification le"

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_route_profile__write_uid
msgid "Last Updated by"
msgstr ""

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_route_profile__write_date
msgid "Last Updated on"
msgstr ""

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_route_profile__name
msgid "Name"
msgstr ""

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_product_product__force_route_profile_id
#: model:ir.model.fields,field_description:product_route_profile.field_product_template__force_route_profile_id
msgid "Priority Route Profile"
msgstr "Profil de Routes Prioritaires"

#. module: product_route_profile
#: model:ir.model,name:product_route_profile.model_product_template
msgid "Product Template"
msgstr "Modèle de produit"

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_product_product__route_profile_id
#: model:ir.model.fields,field_description:product_route_profile.field_product_template__route_profile_id
msgid "Route Profile"
msgstr "Profil de routes"

#. module: product_route_profile
#: model:ir.model.fields,field_description:product_route_profile.field_product_product__route_ids
#: model:ir.model.fields,field_description:product_route_profile.field_product_template__route_ids
#: model:ir.model.fields,field_description:product_route_profile.field_route_profile__route_ids
msgid "Routes"
msgstr "Routes"

#. module: product_route_profile
#: model:ir.actions.act_window,name:product_route_profile.action_route_profile_form
#: model:ir.ui.menu,name:product_route_profile.menu_route_profile_config
#: model_terms:ir.ui.view,arch_db:product_route_profile.route_profile_form
#: model_terms:ir.ui.view,arch_db:product_route_profile.route_profile_tree
msgid "Routes Profiles"
msgstr "Profils de Routes"

#. module: product_route_profile
#: model_terms:ir.actions.act_window,help:product_route_profile.action_route_profile_form
msgid ""
"You can define here the routes profiles that run through\n"
" your warehouses and that define the flows of your products.\n"
" A route profile can be set on each product as \"Route Profile\" or \"Priority Route Profile\" (company dependent)."
msgstr ""
"Vous pouvez définir ici les routes qui régissent les mouvements de vos produits dans vos entrepôts. \n"
"Un profil de route peut être défini pour chaque produit en tant que \"Profil de Routes\" ou \"Profil de Routes Prioritaires"
"\" (société dépendant)."

#. module: product_route_profile
#: model:ir.model,name:product_route_profile.model_route_profile
msgid "route.profile"
msgstr ""
2 changes: 2 additions & 0 deletions product_route_profile/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import route_profile
from . import product_template
63 changes: 63 additions & 0 deletions product_route_profile/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <[email protected]>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import api, fields, models


class ProductTemplate(models.Model):
_inherit = "product.template"

route_profile_id = fields.Many2one("route.profile", string="Route Profile")
force_route_profile_id = fields.Many2one(
"route.profile",
string="Priority Route Profile",
company_dependent=True,
help="If defined, the "
"priority route profile will be used and will replace the "
"route profile, only for this company.",
)

route_ids = fields.Many2many(
compute="_compute_route_ids",
inverse="_inverse_route_ids",
search="_search_route_ids",
store=False,
)

@api.depends("route_profile_id", "force_route_profile_id")
@api.depends_context("company")
def _compute_route_ids(self):
for rec in self:
if rec.force_route_profile_id:
rec.route_ids = [(6, 0, rec.force_route_profile_id.route_ids.ids)]
elif rec.route_profile_id:
rec.route_ids = [(6, 0, rec.route_profile_id.route_ids.ids)]
else:
rec.route_ids = False

def _search_route_ids(self, operator, value):
return [
"|",
("force_route_profile_id.route_ids", operator, value),
"&",
("force_route_profile_id", "=", False),
("route_profile_id.route_ids", operator, value),
]

def _inverse_route_ids(self):
profiles = self.env["route.profile"].search([])
for rec in self:
for profile in profiles:
if rec.route_ids == profile.route_ids:
rec.route_profile_id = profile
break
else:
vals = rec._prepare_profile()
rec.route_profile_id = self.env["route.profile"].create(vals)

def _prepare_profile(self):
return {
"name": " / ".join(self.route_ids.mapped("name")),
"route_ids": [(6, 0, self.route_ids.ids)],
}
23 changes: 23 additions & 0 deletions product_route_profile/models/route_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <[email protected]>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import fields, models


class RouteProfile(models.Model):
_name = "route.profile"
_description = "Route Profile"

name = fields.Char("Name")
company_id = fields.Many2one(
comodel_name="res.company",
default=lambda self: self.env.company.id,
required=False,
string="Company",
)
route_ids = fields.Many2many(
"stock.location.route",
string="Routes",
domain=[("product_selectable", "=", True)],
)
1 change: 1 addition & 0 deletions product_route_profile/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Kévin Roche <[email protected]>
1 change: 1 addition & 0 deletions product_route_profile/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This module replaces the initial concept of route_ids with a new concept of "route profile", coming with a company-specific and priority route profile.
1 change: 1 addition & 0 deletions product_route_profile/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tests of this module are running separately than the other tests.
9 changes: 9 additions & 0 deletions product_route_profile/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
**Route profile**
In Inventory > Configuration > Settings > Routes Profiles
- Create some Route profile depending on your needs


**On product**
On each template product, in inventory page, we can select:
- **Route Profile**: a default profile, common to all companies
- **Priority Route Profile**: a profile specific to each company and priority if existing.
3 changes: 3 additions & 0 deletions product_route_profile/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_route_profile_manager,access_route_profile_manager,model_route_profile,stock.group_stock_manager,1,1,1,1
access_route_profile_user,access_route_profile_user,model_route_profile,stock.group_stock_user,1,0,0,0
1 change: 1 addition & 0 deletions product_route_profile/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_product_route_profile
72 changes: 72 additions & 0 deletions product_route_profile/tests/test_product_route_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <[email protected]>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo.tests.common import SavepointCase


class TestProductRouteProfile(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestProductRouteProfile, cls).setUpClass()

cls.company_bis = cls.env["res.company"].create(
{
"name": "company 2",
"parent_id": cls.env.ref("base.main_company").id,
}
)

cls.route_1 = cls.env.ref("stock.route_warehouse0_mto")
cls.route_1.active = True
cls.route_2 = cls.route_1.copy({"name": "route 2"})

cls.route_profile_1 = cls.env["route.profile"].create(
{
"name": "profile 1",
"route_ids": [(6, 0, [cls.route_1.id])],
}
)
cls.route_profile_2 = cls.env["route.profile"].create(
{
"name": "profile 2",
"route_ids": [(6, 0, [cls.route_2.id])],
}
)

cls.product = cls.env["product.template"].create(
{
"name": "Template 1",
"company_id": False,
}
)

def test_1_route_profile(self):
self.product.route_profile_id = self.route_profile_1.id
self.assertEqual(self.product.route_ids, self.route_profile_1.route_ids)
# In other company, no change
self.assertEqual(
self.product.with_company(self.company_bis).route_ids,
self.route_profile_1.route_ids,
)

def test_2_force_route_profile(self):
self.product.route_profile_id = self.route_profile_1.id
self.product.with_company(
self.env.company
).force_route_profile_id = self.route_profile_2.id
self.assertEqual(
self.product.with_company(self.env.company).route_ids,
self.route_profile_2.route_ids,
)
# In other company, no change
self.assertEqual(
self.product.with_company(self.company_bis).route_ids,
self.route_profile_1.route_ids,
)
# Return to route_profile_id if no force_route_profile_id
self.product.with_company(self.env.company).force_route_profile_id = False
self.assertEqual(
self.product.with_company(self.env.company).route_ids,
self.route_profile_1.route_ids,
)
Loading

0 comments on commit b88cb1d

Please sign in to comment.