mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[ADD] stock_available_portal: Module added.
This commit is contained in:
3
stock_available_portal/README.rst
Normal file
3
stock_available_portal/README.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
=========================
|
||||
Stock available to Portal
|
||||
=========================
|
||||
2
stock_available_portal/__init__.py
Normal file
2
stock_available_portal/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
17
stock_available_portal/__manifest__.py
Normal file
17
stock_available_portal/__manifest__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Stock Available In Portal",
|
||||
"version": "14.0.1.0.0",
|
||||
"author": "Cetmix, Odoo Community Association (OCA)",
|
||||
"summary": "Show product quantity available to promise in portal",
|
||||
"website": "https://github.com/OCA/stock-logistics-warehouse",
|
||||
"development_status": "Production/Stable",
|
||||
"category": "Hidden",
|
||||
"depends": ["stock_available", "portal"],
|
||||
"maintainers": ["geomer198", "CetmixGitDrone"],
|
||||
"license": "AGPL-3",
|
||||
"data": [
|
||||
"views/product_portal_templates.xml",
|
||||
"views/res_config_settings_views.xml",
|
||||
],
|
||||
"installable": True,
|
||||
}
|
||||
1
stock_available_portal/controllers/__init__.py
Normal file
1
stock_available_portal/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import portal
|
||||
156
stock_available_portal/controllers/portal.py
Normal file
156
stock_available_portal/controllers/portal.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.http import request
|
||||
from odoo.osv.expression import OR
|
||||
|
||||
from odoo.addons.portal.controllers.portal import (
|
||||
CustomerPortal,
|
||||
get_records_pager,
|
||||
pager as portal_pager,
|
||||
)
|
||||
|
||||
|
||||
class PortalProduct(CustomerPortal):
|
||||
def _get_product_searchbar_sorting(self):
|
||||
return {
|
||||
"date": {"label": _("Create Date"), "order": "create_date desc"},
|
||||
"name": {"label": _("Name"), "order": "name"},
|
||||
"default_code": {"label": _("SKU"), "order": "default_code"},
|
||||
}
|
||||
|
||||
def _get_product_searchbar_filters(self):
|
||||
return {
|
||||
"all": {"label": _("All"), "domain": []},
|
||||
}
|
||||
|
||||
def _get_searchbar_inputs(self):
|
||||
return {
|
||||
"all": {"input": "all", "label": _("Search in All")},
|
||||
"default_code": {"input": "default_code", "label": _("Search in SKU")},
|
||||
"name": {"input": "display_name", "label": _("Search in Product Name")},
|
||||
}
|
||||
|
||||
def _get_search_domain(self, search_in, search):
|
||||
search_domain = []
|
||||
if search_in in ("name", "all"):
|
||||
search_domain = OR([search_domain, [("name", "ilike", search)]])
|
||||
if search_in in ("default_code", "all"):
|
||||
search_domain = OR([search_domain, [("default_code", "ilike", search)]])
|
||||
return search_domain
|
||||
|
||||
def _prepare_portal_layout_values(self):
|
||||
values = super(PortalProduct, self)._prepare_portal_layout_values()
|
||||
Product = request.env["product.product"]
|
||||
values.update(access_to_products=Product.check_product_portal_access())
|
||||
return values
|
||||
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
values = super(PortalProduct, self)._prepare_home_portal_values(counters)
|
||||
if "product_count" in counters:
|
||||
Product = request.env["product.product"]
|
||||
values.update(product_count=len(Product.get_portal_products(False)))
|
||||
return values
|
||||
|
||||
def _product_get_page_view_values(self, product, access_token, **kwargs):
|
||||
values = {"page_name": "product", "product": product}
|
||||
history = request.session.get("my_products_history", [])
|
||||
values.update(get_records_pager(history, product.sudo()))
|
||||
return self._get_page_view_values(
|
||||
product, access_token, values, "my_products_history", False, **kwargs
|
||||
)
|
||||
|
||||
@http.route(
|
||||
["/my/products", "/my/products/page/<int:page>"],
|
||||
type="http",
|
||||
auth="user",
|
||||
website=True,
|
||||
)
|
||||
def portal_my_products(
|
||||
self,
|
||||
page=1,
|
||||
date_begin=None,
|
||||
date_end=None,
|
||||
sortby=None,
|
||||
filterby=None,
|
||||
search=None,
|
||||
search_in="all",
|
||||
**kw
|
||||
):
|
||||
values = self._prepare_portal_layout_values()
|
||||
if not values.get("access_to_products", False):
|
||||
return request.redirect("/my/home")
|
||||
Product = request.env["product.product"]
|
||||
|
||||
searchbar_sorting = self._get_product_searchbar_sorting()
|
||||
searchbar_filters = self._get_product_searchbar_filters()
|
||||
searchbar_inputs = self._get_searchbar_inputs()
|
||||
|
||||
domain = []
|
||||
|
||||
if not sortby:
|
||||
sortby = "date"
|
||||
order = searchbar_sorting[sortby]["order"]
|
||||
if not filterby:
|
||||
filterby = "all"
|
||||
domain += searchbar_filters[filterby]["domain"]
|
||||
if search and search_in:
|
||||
domain += self._get_search_domain(search_in, search)
|
||||
if date_begin and date_end:
|
||||
domain += [
|
||||
("create_date", ">", date_begin),
|
||||
("create_date", "<=", date_end),
|
||||
]
|
||||
|
||||
product_count = len(Product.get_portal_products(domain))
|
||||
|
||||
pager = portal_pager(
|
||||
url="/my/products",
|
||||
url_args={
|
||||
"date_begin": date_begin,
|
||||
"date_end": date_end,
|
||||
"search_in": search_in,
|
||||
"search": search,
|
||||
"filterby": filterby,
|
||||
"sortby": sortby,
|
||||
},
|
||||
total=product_count,
|
||||
page=page,
|
||||
step=self._items_per_page,
|
||||
)
|
||||
|
||||
products = Product.get_portal_products(
|
||||
domain, order=order, limit=self._items_per_page, offset=pager["offset"]
|
||||
)
|
||||
|
||||
request.session["my_products_history"] = products.ids[:100]
|
||||
|
||||
values.update(
|
||||
{
|
||||
"date": date_begin,
|
||||
"products": products,
|
||||
"page_name": "product",
|
||||
"pager": pager,
|
||||
"default_url": "/my/products",
|
||||
"searchbar_sortings": searchbar_sorting,
|
||||
"search_in": search_in,
|
||||
"search": search,
|
||||
"filterby": filterby,
|
||||
"searchbar_inputs": searchbar_inputs,
|
||||
"sortby": sortby,
|
||||
"searchbar_filters": OrderedDict(sorted(searchbar_filters.items())),
|
||||
"date_end": date_end,
|
||||
}
|
||||
)
|
||||
return request.render("stock_available_portal.portal_my_products", values)
|
||||
|
||||
@http.route(
|
||||
["/my/products/<int:product_id>"], type="http", auth="user", website=True
|
||||
)
|
||||
def portal_product_page(self, product_id, access_token=None, **kw):
|
||||
Product = request.env["product.product"]
|
||||
product = Product.get_portal_products([("id", "=", product_id)])
|
||||
if Product.check_product_portal_access() and product:
|
||||
values = self._product_get_page_view_values(product, access_token, **kw)
|
||||
return request.render("stock_available_portal.portal_my_product", values)
|
||||
return request.redirect("/my/home")
|
||||
2
stock_available_portal/models/__init__.py
Normal file
2
stock_available_portal/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import product_product
|
||||
from . import res_config_settings
|
||||
54
stock_available_portal/models/product_product.py
Normal file
54
stock_available_portal/models/product_product.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from odoo import api, models
|
||||
from odoo.osv import expression
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_name = "product.product"
|
||||
_inherit = ["product.product", "portal.mixin"]
|
||||
|
||||
def _compute_access_url(self):
|
||||
"""Compute access url for portal view"""
|
||||
for rec in self:
|
||||
rec.access_url = "/my/products/{}".format(rec.id)
|
||||
|
||||
@api.model
|
||||
def get_config_domain(self, key):
|
||||
"""Get domain using config key"""
|
||||
try:
|
||||
return safe_eval(self.env["ir.config_parameter"].sudo().get_param(key, []))
|
||||
except (TypeError, SyntaxError, NameError, ValueError):
|
||||
return []
|
||||
|
||||
@api.model
|
||||
def check_product_portal_access(self):
|
||||
"""Check access to portal products for user"""
|
||||
self = self.sudo()
|
||||
user_has_group = self.env.user.has_group
|
||||
if not (
|
||||
user_has_group("base.group_system") or user_has_group("base.group_portal")
|
||||
):
|
||||
return False
|
||||
domain = self.get_config_domain("stock_available_portal.portal_visible_users")
|
||||
return (
|
||||
bool(
|
||||
self.env["res.users"].search(
|
||||
expression.AND([domain, [("id", "=", self.env.user.id)]]), limit=1
|
||||
)
|
||||
)
|
||||
if domain or domain == []
|
||||
else False
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_portal_products(self, filter_domain, **kwargs):
|
||||
"""Get portal products"""
|
||||
self = self.sudo()
|
||||
if not self.check_product_portal_access():
|
||||
return self
|
||||
domain = self.get_config_domain(
|
||||
"stock_available_portal.portal_visible_products_domain"
|
||||
)
|
||||
if not domain and domain != []:
|
||||
return self
|
||||
return self.search(expression.AND([domain, filter_domain]), **kwargs)
|
||||
17
stock_available_portal/models/res_config_settings.py
Normal file
17
stock_available_portal/models/res_config_settings.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
portal_visible_products_domain = fields.Char(
|
||||
string="Portal Visible Products",
|
||||
help="Domain which defines which products can be visible on portal. "
|
||||
"By default all products.",
|
||||
config_parameter="stock_available_portal.portal_visible_products_domain",
|
||||
)
|
||||
portal_visible_users = fields.Char(
|
||||
string="Visible to Users",
|
||||
help="Domain which defines which users can see products. By default all users.",
|
||||
config_parameter="stock_available_portal.portal_visible_users",
|
||||
)
|
||||
6
stock_available_portal/readme/CONFIGURE.rst
Normal file
6
stock_available_portal/readme/CONFIGURE.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
Open **General Settings** > **Inventory**.
|
||||
|
||||
Configure the following filters in **Stock available to promise** section:
|
||||
|
||||
* **Portal Visible Products** defines which products will be visible in portal
|
||||
* **Visible to Users** defines which users have access to products in portal
|
||||
1
stock_available_portal/readme/CONTRIBUTORS.rst
Normal file
1
stock_available_portal/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
||||
* Cetmix <https://cetmix.com>
|
||||
4
stock_available_portal/readme/DESCRIPTION.rst
Normal file
4
stock_available_portal/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,4 @@
|
||||
This module extends the functionality of **Stock available to promise**.
|
||||
It allows to show "Quantity available to promise" of your products in customer portal.
|
||||
A new menu "Products" is added to the Portal view. So your portal users can see your current inventory.
|
||||
You can configure which products are visible in portal and which users have access to these products.
|
||||
1
stock_available_portal/readme/USAGE.rst
Normal file
1
stock_available_portal/readme/USAGE.rst
Normal file
@@ -0,0 +1 @@
|
||||
Log into portal. Products are listed under the **Products** menu.
|
||||
2
stock_available_portal/tests/__init__.py
Normal file
2
stock_available_portal/tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import test_portal_product
|
||||
from . import test_product_product
|
||||
152
stock_available_portal/tests/test_portal_product.py
Normal file
152
stock_available_portal/tests/test_portal_product.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserPortal
|
||||
from odoo.addons.stock_available_portal.controllers.portal import PortalProduct
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestWebsitePriceListHttp(HttpCaseWithUserPortal):
|
||||
def test_access_portal_products(self):
|
||||
product = self.env["product.product"].search([], limit=1)
|
||||
session = self.authenticate("portal", "portal")
|
||||
r = self.url_open("/my/products")
|
||||
self.assertEqual(r.status_code, 200, msg="Status code must be equal to 200")
|
||||
self.assertEqual(
|
||||
r.request.path_url,
|
||||
"/my/products",
|
||||
msg="Url path must be equal to '/my/products'",
|
||||
)
|
||||
r = self.url_open("/my/products?search_in=all&search=test")
|
||||
self.assertEqual(r.status_code, 200, msg="Status code must be equal to 200")
|
||||
self.assertEqual(
|
||||
r.request.path_url,
|
||||
"/my/products?search_in=all&search=test",
|
||||
msg="Url path must be equal to '/my/products?search_in=all&search=test'",
|
||||
)
|
||||
r = self.url_open("/my/products/{}".format(product.id))
|
||||
self.assertEqual(r.status_code, 200, msg="Status code must be equal to 200")
|
||||
self.assertEqual(
|
||||
r.request.path_url,
|
||||
"/my/products/{}".format(product.id),
|
||||
msg="Url path must be equal to '/my/products/{}'".format(product.id),
|
||||
)
|
||||
self.env["ir.config_parameter"].set_param(
|
||||
"stock_available_portal.portal_visible_users", "[('id', '=', 1)]"
|
||||
)
|
||||
r = self.url_open("/my/products")
|
||||
self.assertEqual(r.status_code, 200, msg="Status code must be equal to 200")
|
||||
self.assertEqual(
|
||||
r.request.path_url, "/my/home", msg="Url path must be equal to '/my/home'"
|
||||
)
|
||||
r = self.url_open("/my/products/{}".format(product.id))
|
||||
self.assertEqual(r.status_code, 200, msg="Status code must be equal to 200")
|
||||
self.assertEqual(
|
||||
r.request.path_url, "/my/home", msg="Url path must be equal to '/my/home'"
|
||||
)
|
||||
self.env["ir.config_parameter"].set_param(
|
||||
"stock_available_portal.portal_visible_users",
|
||||
"[('id', '=', {})]".format(session.uid),
|
||||
)
|
||||
r = self.url_open("/my/products")
|
||||
self.assertEqual(r.status_code, 200, msg="Status code must be equal to 200")
|
||||
self.assertEqual(
|
||||
r.request.path_url,
|
||||
"/my/products",
|
||||
msg="Url path must be equal to '/my/products'",
|
||||
)
|
||||
|
||||
def test_get_product_searchbar_sorting(self):
|
||||
obj = PortalProduct()
|
||||
searched_bar = obj._get_product_searchbar_sorting()
|
||||
keys = list(searched_bar.keys())
|
||||
expected_keys = ["date", "name", "default_code"]
|
||||
self.assertListEqual(keys, expected_keys, msg="Keys list must be the same")
|
||||
|
||||
date_obj = searched_bar.get("date")
|
||||
self.assertEqual(
|
||||
date_obj.get("order"),
|
||||
"create_date desc",
|
||||
msg="Order must be equal to 'create_date desc'",
|
||||
)
|
||||
name_obj = searched_bar.get("name")
|
||||
self.assertEqual(
|
||||
name_obj.get("order"), "name", msg="Order must be equal to 'name'"
|
||||
)
|
||||
default_code_obj = searched_bar.get("default_code")
|
||||
self.assertEqual(
|
||||
default_code_obj.get("order"),
|
||||
"default_code",
|
||||
msg="Order must be equal to 'default_code'",
|
||||
)
|
||||
|
||||
def test_get_product_searchbar_filters(self):
|
||||
obj = PortalProduct()
|
||||
searchbar_filter = obj._get_product_searchbar_filters()
|
||||
keys = list(searchbar_filter.keys())
|
||||
expected_keys = ["all"]
|
||||
self.assertListEqual(keys, expected_keys, msg="Keys list must be the same")
|
||||
all_obj = searchbar_filter.get("all")
|
||||
self.assertEqual(all_obj.get("domain"), [], msg="Domain must be empty")
|
||||
|
||||
def test_get_searchbar_inputs(self):
|
||||
obj = PortalProduct()
|
||||
searchbar_input = obj._get_searchbar_inputs()
|
||||
keys = list(searchbar_input.keys())
|
||||
expected_keys = ["all", "default_code", "name"]
|
||||
self.assertListEqual(keys, expected_keys, msg="Keys list must be the same")
|
||||
all_obj = searchbar_input.get("all")
|
||||
self.assertEqual(
|
||||
all_obj.get("input"), "all", msg="Input value must be equal to 'all'"
|
||||
)
|
||||
default_code_obj = searchbar_input.get("default_code")
|
||||
self.assertEqual(
|
||||
default_code_obj.get("input"),
|
||||
"default_code",
|
||||
msg="Input value must be equal to 'default_code'",
|
||||
)
|
||||
name_obj = searchbar_input.get("name")
|
||||
self.assertEqual(
|
||||
name_obj.get("input"),
|
||||
"display_name",
|
||||
msg="Input value must be equal to 'display_name'",
|
||||
)
|
||||
|
||||
def test_get_search_domain(self):
|
||||
obj = PortalProduct()
|
||||
self.assertEqual(
|
||||
obj._get_search_domain(None, None),
|
||||
[],
|
||||
msg="Function must be return empty list",
|
||||
)
|
||||
self.assertEqual(
|
||||
obj._get_search_domain("other", "other"),
|
||||
[],
|
||||
msg="Function must br return empty list",
|
||||
)
|
||||
expected_domain = [("name", "ilike", "test")]
|
||||
self.assertEqual(
|
||||
obj._get_search_domain("name", "test"),
|
||||
expected_domain,
|
||||
msg="Domains must be the same",
|
||||
)
|
||||
expected_domain = [("default_code", "ilike", "test")]
|
||||
self.assertEqual(
|
||||
obj._get_search_domain("default_code", "test"),
|
||||
expected_domain,
|
||||
msg="Domains must be the same",
|
||||
)
|
||||
expected_domain = [
|
||||
"|",
|
||||
("name", "ilike", "test"),
|
||||
("default_code", "ilike", "test"),
|
||||
]
|
||||
self.assertEqual(
|
||||
obj._get_search_domain("all", "test"),
|
||||
expected_domain,
|
||||
msg="Domains must be the same",
|
||||
)
|
||||
|
||||
def test_prepare_home_portal_values(self):
|
||||
obj = PortalProduct()
|
||||
result = obj._prepare_home_portal_values([])
|
||||
self.assertEqual(result, {}, msg="Function must be return empty dict")
|
||||
112
stock_available_portal/tests/test_product_product.py
Normal file
112
stock_available_portal/tests/test_product_product.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from odoo.tests import TransactionCase
|
||||
|
||||
|
||||
class TestProductProducts(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestProductProducts, self).setUp()
|
||||
ProductProduct = self.env["product.product"]
|
||||
ResPartner = self.env["res.partner"]
|
||||
ResUser = self.env["res.users"]
|
||||
self.portal_group_id = self.ref("base.group_portal")
|
||||
self._icp_sudo = self.env["ir.config_parameter"].sudo()
|
||||
self.visible_product_key = (
|
||||
"stock_available_portal.portal_visible_products_domain"
|
||||
)
|
||||
self.visible_user_key = "stock_available_portal.portal_visible_users"
|
||||
self.test_user = ResUser.create(
|
||||
{
|
||||
"login": "test@odoo.com",
|
||||
"partner_id": ResPartner.create({"name": "test partner"}).id,
|
||||
}
|
||||
)
|
||||
self.product_test = ProductProduct.create(
|
||||
{
|
||||
"name": "Test Product",
|
||||
}
|
||||
)
|
||||
self.Product = ProductProduct.with_user(self.test_user)
|
||||
|
||||
def test_computing_access_url(self):
|
||||
Product = self.env["product.product"]
|
||||
product_test_1 = Product.create({"name": "Test Product #1"})
|
||||
format_text = "/my/products/{}"
|
||||
self.assertEqual(
|
||||
product_test_1.access_url,
|
||||
format_text.format(product_test_1.id),
|
||||
msg="Product access url must be equal to '/my/products/{}'".format(
|
||||
product_test_1.id
|
||||
),
|
||||
)
|
||||
product_test_2 = Product.create({"name": "Test Product #2"})
|
||||
self.assertEqual(
|
||||
product_test_2.access_url,
|
||||
format_text.format(product_test_2.id),
|
||||
msg="Product access url must be equal to '/my/products/{}'".format(
|
||||
product_test_2.id
|
||||
),
|
||||
)
|
||||
|
||||
def test_get_config_domain(self):
|
||||
self._icp_sudo.set_param(self.visible_user_key, "")
|
||||
value = self.Product.get_config_domain(self.visible_user_key)
|
||||
self.assertEqual(value, [], msg="Value must be equal to empty list")
|
||||
self.assertIsInstance(value, list, msg="Value type must be 'list' type")
|
||||
|
||||
self._icp_sudo.set_param(self.visible_user_key, "[]")
|
||||
value = self.Product.get_config_domain(self.visible_user_key)
|
||||
self.assertEqual(value, [], msg="Value must be equal to []")
|
||||
self.assertIsInstance(value, list, msg="Value type must be 'list' type")
|
||||
|
||||
self._icp_sudo.set_param(self.visible_user_key, '[("id", "=", 1)]')
|
||||
value = self.Product.get_config_domain(self.visible_user_key)
|
||||
self.assertTrue(value, msg="Value must be True")
|
||||
self.assertIsInstance(value, list, msg="Value type must be 'list' type")
|
||||
|
||||
def test_access_user_to_portal_products(self):
|
||||
self._icp_sudo.set_param(self.visible_user_key, "")
|
||||
access = self.Product.check_product_portal_access()
|
||||
self.assertFalse(access, msg="Access must be False")
|
||||
self._icp_sudo.set_param(self.visible_user_key, "[]")
|
||||
access = self.Product.check_product_portal_access()
|
||||
self.assertFalse(access, msg="Access must be False")
|
||||
|
||||
self.test_user.write({"groups_id": [(6, 0, [self.portal_group_id])]})
|
||||
|
||||
access = self.Product.check_product_portal_access()
|
||||
self.assertTrue(access, msg="Access must be True")
|
||||
|
||||
self._icp_sudo.set_param(
|
||||
self.visible_user_key, "[('id', '=', {})]".format(self.test_user.id)
|
||||
)
|
||||
access = self.Product.check_product_portal_access()
|
||||
self.assertTrue(access, msg="Access must be True")
|
||||
|
||||
self._icp_sudo.set_param(self.visible_user_key, "[('id', '=', 1)]")
|
||||
access = self.Product.check_product_portal_access()
|
||||
self.assertFalse(access, msg="Access must be False")
|
||||
|
||||
def test_get_portal_products(self):
|
||||
self.test_user.write({"groups_id": [(6, 0, [self.portal_group_id])]})
|
||||
self._icp_sudo.set_param(self.visible_user_key, "[]")
|
||||
self._icp_sudo.set_param(self.visible_product_key, "")
|
||||
expected_products = self.Product.sudo().search([])
|
||||
products = self.Product.get_portal_products([])
|
||||
self.assertItemsEqual(expected_products, products, msg="Items must be Equal")
|
||||
self._icp_sudo.set_param(self.visible_product_key, "[]")
|
||||
products = self.Product.get_portal_products([])
|
||||
self.assertItemsEqual(expected_products, products, msg="Items must be Equal")
|
||||
self._icp_sudo.set_param(
|
||||
self.visible_product_key, "[('id', '=', {})]".format(self.product_test.id)
|
||||
)
|
||||
products = self.Product.get_portal_products([])
|
||||
self.assertEqual(
|
||||
products,
|
||||
self.product_test,
|
||||
msg="Recordset must be equal to 'Test Product' record",
|
||||
)
|
||||
self._icp_sudo.set_param(self.visible_product_key, "[('id', '=', -1)]")
|
||||
products = self.Product.get_portal_products([])
|
||||
self.assertFalse(products, msg="Recordset must be empty")
|
||||
self._icp_sudo.set_param(self.visible_user_key, "[('id', '=', 1)]")
|
||||
products = self.Product.get_portal_products([])
|
||||
self.assertFalse(products, msg="Recordset must be empty")
|
||||
121
stock_available_portal/views/product_portal_templates.xml
Normal file
121
stock_available_portal/views/product_portal_templates.xml
Normal file
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<template
|
||||
id="portal_my_home_menu_product"
|
||||
name="Portal layout : product menu entries"
|
||||
inherit_id="portal.portal_breadcrumbs"
|
||||
priority="30"
|
||||
>
|
||||
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
|
||||
<li
|
||||
t-if="page_name == 'product'"
|
||||
t-attf-class="breadcrumb-item #{'active ' if not product else ''}"
|
||||
>
|
||||
<a
|
||||
t-if="product"
|
||||
t-attf-href="/my/products?{{ keep_query() }}"
|
||||
>My Products
|
||||
</a>
|
||||
<t t-else="">My Products</t>
|
||||
</li>
|
||||
<li t-if="product" class="breadcrumb-item active">
|
||||
<t t-esc="product.name" t-if="product.name != '/'" />
|
||||
<t t-else="">
|
||||
<em>Draft Product</em>
|
||||
</t>
|
||||
</li>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template
|
||||
id="portal_my_home_product"
|
||||
name="My Products"
|
||||
inherit_id="portal.portal_my_home"
|
||||
customize_show="True"
|
||||
priority="30"
|
||||
>
|
||||
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
|
||||
<t t-call="portal.portal_docs_entry" t-if="access_to_products">
|
||||
<t t-set="title">My Products</t>
|
||||
<t t-set="url" t-value="'/my/products'" />
|
||||
<t t-set="placeholder_count" t-value="'product_count'" />
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_products" name="My Products">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="True" />
|
||||
|
||||
<t t-call="portal.portal_searchbar">
|
||||
<t t-set="title">Products</t>
|
||||
</t>
|
||||
<t t-if="not products or not access_to_products">
|
||||
<div class="alert alert-warning mt8" role="alert">
|
||||
There are no products.
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="products and access_to_products" t-call="portal.portal_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th>Product</th>
|
||||
<th class="text-right">Immediately Usable Qty</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="products" t-as="product">
|
||||
<td>
|
||||
<a
|
||||
t-attf-href="/my/products/#{product.id}?{{ keep_query() }}"
|
||||
t-field="product.display_name"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-esc="product.immediately_usable_qty" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_product" name="My Product">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-call="portal.portal_record_layout">
|
||||
<t t-set="card_header">
|
||||
<h5 class="mb-0">
|
||||
<span t-field="product.display_name" class="text-truncate" />
|
||||
</h5>
|
||||
</t>
|
||||
<t t-set="card_body">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>
|
||||
<strong>Immediately Usable Qty: </strong>
|
||||
<span t-field="product.immediately_usable_qty" />
|
||||
</p>
|
||||
<p>
|
||||
<strong>Sales Description:</strong>
|
||||
<br />
|
||||
<span t-field="product.description_sale" />
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
t-if="product.image_128"
|
||||
class="col-12 col-md-6 mb-2 mb-md-0"
|
||||
>
|
||||
<img
|
||||
t-if="product.image_128"
|
||||
t-att-src="image_data_uri(product.image_128)"
|
||||
class="mr-3 text-center o_image_128_max float-right"
|
||||
alt="Product"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
39
stock_available_portal/views/res_config_settings_views.xml
Normal file
39
stock_available_portal/views/res_config_settings_views.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_stock_configuration" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.stock.available.portal</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="stock_available.view_stock_configuration" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath
|
||||
expr="//field[@name='module_stock_available_immediately']/../../.."
|
||||
position="inside"
|
||||
>
|
||||
<div class="col-xs-12 col-md-12 o_setting_box">
|
||||
<div class="o_setting_left_pane" />
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="portal_visible_products_domain" />
|
||||
<field
|
||||
name="portal_visible_products_domain"
|
||||
widget="domain"
|
||||
options="{'model': 'product.product'}"
|
||||
/>
|
||||
</div>
|
||||
<div class="o_setting_left_pane" />
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="portal_visible_users" />
|
||||
<field
|
||||
name="portal_visible_users"
|
||||
widget="domain"
|
||||
options="{'model': 'res.users'}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user