[ADD] module: product_route_profile

This commit is contained in:
Kevin.roche
2022-04-28 01:32:25 +02:00
committed by Kev-Roche
parent a742b82b62
commit b50bd2c746
18 changed files with 448 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
from . import models
from .hooks import post_init_hook

View File

@@ -0,0 +1,25 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <kevin.roche@akretion.com>
# 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",
}

View File

@@ -0,0 +1,43 @@
# Copyright (C) 2022 Akretion (<http://www.akretion.com>).
# @author Kévin Roche <kevin.roche@akretion.com>
# 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})

View File

@@ -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 ""

View File

@@ -0,0 +1,2 @@
from . import route_profile
from . import product_template

View File

@@ -0,0 +1,63 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <kevin.roche@akretion.com>
# 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)],
}

View File

@@ -0,0 +1,23 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <kevin.roche@akretion.com>
# 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)],
)

View File

@@ -0,0 +1 @@
* Kévin Roche <kevin.roche@akretion.com>

View File

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

View File

@@ -0,0 +1 @@
Tests of this module are running separately than the other tests.

View File

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

View File

@@ -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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_route_profile_manager access_route_profile_manager model_route_profile stock.group_stock_manager 1 1 1 1
3 access_route_profile_user access_route_profile_user model_route_profile stock.group_stock_user 1 0 0 0

View File

@@ -0,0 +1 @@
from . import test_product_route_profile

View File

@@ -0,0 +1,72 @@
# Copyright 2022 Akretion (https://www.akretion.com).
# @author Kévin Roche <kevin.roche@akretion.com>
# 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,
)

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright (C) 2022 Akretion (<http://www.akretion.com>).
@author Kévin Roche <kevin.roche@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_template_route_profile_form" model="ir.ui.view">
<field name="name">product.template.route.profile.form</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view" />
<field name="arch" type="xml">
<xpath expr="//field[@name='route_ids']/parent::div" position="attributes">
<attribute name="attrs">{'invisible': True}</attribute>
</xpath>
<xpath expr="//group[@name='operations']" position="inside">
<field
name="route_profile_id"
attrs="{'invisible': [('type', 'in', ['service', 'digital'])]}"
/>
<field
name="force_route_profile_id"
attrs="{'invisible': [('type', 'in', ['service', 'digital'])]}"
/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright (C) 2022 Akretion (<http://www.akretion.com>).
@author Kévin Roche <kevin.roche@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="route_profile_tree" model="ir.ui.view">
<field name="name">route.profile.tree</field>
<field name="model">route.profile</field>
<field name="arch" type="xml">
<tree string="Routes Profiles">
<field name="name" />
<field name="route_ids" widget="many2many_tags" />
</tree>
</field>
</record>
<record id="route_profile_form" model="ir.ui.view">
<field name="name">route.profile.form</field>
<field name="model">route.profile</field>
<field name="arch" type="xml">
<form>
<group string="Routes Profiles">
<field name="name" />
<field name="route_ids" widget="many2many_checkboxes" />
</group>
</form>
</field>
</record>
<record id="action_route_profile_form" model="ir.actions.act_window">
<field name="name">Routes Profiles</field>
<field name="res_model">route.profile</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="route_profile_tree" />
<field name="help" type="html">
<p>
You can define here the routes profiles that run through
your warehouses and that define the flows of your products.
A route profile can be set on each product as "Route Profile" or "Priority Route Profile" (company dependent).
</p>
</field>
</record>
<menuitem
id="menu_route_profile_config"
action="action_route_profile_form"
name="Routes Profiles"
parent="stock.menu_warehouse_config"
sequence="4"
groups="stock.group_adv_location"
/>
</odoo>