mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[ADD] module: product_route_profile
This commit is contained in:
2
product_route_profile/__init__.py
Normal file
2
product_route_profile/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from .hooks import post_init_hook
|
||||
25
product_route_profile/__manifest__.py
Normal file
25
product_route_profile/__manifest__.py
Normal 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",
|
||||
}
|
||||
43
product_route_profile/hooks.py
Normal file
43
product_route_profile/hooks.py
Normal 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})
|
||||
119
product_route_profile/i18n/fr.po
Normal file
119
product_route_profile/i18n/fr.po
Normal 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 ""
|
||||
2
product_route_profile/models/__init__.py
Normal file
2
product_route_profile/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import route_profile
|
||||
from . import product_template
|
||||
63
product_route_profile/models/product_template.py
Normal file
63
product_route_profile/models/product_template.py
Normal 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)],
|
||||
}
|
||||
23
product_route_profile/models/route_profile.py
Normal file
23
product_route_profile/models/route_profile.py
Normal 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)],
|
||||
)
|
||||
1
product_route_profile/readme/CONTRIBUTORS.rst
Normal file
1
product_route_profile/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
||||
* Kévin Roche <kevin.roche@akretion.com>
|
||||
1
product_route_profile/readme/DESCRIPTION.rst
Normal file
1
product_route_profile/readme/DESCRIPTION.rst
Normal 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.
|
||||
1
product_route_profile/readme/ROADMAP.rst
Normal file
1
product_route_profile/readme/ROADMAP.rst
Normal file
@@ -0,0 +1 @@
|
||||
Tests of this module are running separately than the other tests.
|
||||
9
product_route_profile/readme/USAGE.rst
Normal file
9
product_route_profile/readme/USAGE.rst
Normal 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.
|
||||
3
product_route_profile/security/ir.model.access.csv
Normal file
3
product_route_profile/security/ir.model.access.csv
Normal 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
product_route_profile/tests/__init__.py
Normal file
1
product_route_profile/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_product_route_profile
|
||||
72
product_route_profile/tests/test_product_route_profile.py
Normal file
72
product_route_profile/tests/test_product_route_profile.py
Normal 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,
|
||||
)
|
||||
26
product_route_profile/views/product_template.xml
Normal file
26
product_route_profile/views/product_template.xml
Normal 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>
|
||||
50
product_route_profile/views/route_profile.xml
Normal file
50
product_route_profile/views/route_profile.xml
Normal 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>
|
||||
1
setup/product_route_profile/odoo/addons/product_route_profile
Symbolic link
1
setup/product_route_profile/odoo/addons/product_route_profile
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../product_route_profile
|
||||
6
setup/product_route_profile/setup.py
Normal file
6
setup/product_route_profile/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
Reference in New Issue
Block a user