diff --git a/website_search_products/README.rst b/website_search_products/README.rst new file mode 100644 index 0000000..1a8aa72 --- /dev/null +++ b/website_search_products/README.rst @@ -0,0 +1,84 @@ +Website Search Products +======================= + +This module allows portal users to search and browse products in a structured table +with filtering and sorting capabilities using DataTables. + +Features +======== + +* Display products available in the store in a searchable, sortable table +* Filter by product name, category, and price +* Show real-time stock availability per location +* Restrict access to portal and internal users only +* Automatic redirection for portal users after login +* Integration with Odoo's standard product and stock management + +Installation +============ + +Install the module through the Odoo Apps interface or via: + +.. code-block:: bash + + odoo-bin -d -c -i website_search_products + +Configuration +============= + +The module requires no additional configuration. It automatically: + +1. Creates a menu item "Products in Shop" under the main website menu +2. Sets up routes for inventory search and login redirection +3. Filters products to show only those marked as "Available in POS" + +Usage +===== + +**For Portal Users:** + +1. Navigate to the website +2. Click on "Products in Shop" in the menu +3. Browse the product table with real-time stock information +4. Use the search box to filter products by name +5. Click table headers to sort by different columns + +**For Internal Users:** + +1. After login, you will be redirected to the standard Odoo web interface +2. You can still access the product search page manually at ``/inventory/search`` + +**API Usage:** + +The module extends ``stock.move`` with a method to retrieve movements by product: + +.. code-block:: python + + stock_move_env = self.env['stock.move'] + result = stock_move_env.get_stock_moves_by_product( + date_begin='2024-01-01', + date_end='2024-01-31', + location=5 # Location ID + ) + # Returns list of dicts with product data and movements + +Known issues / Roadmap +====================== + +* Consider adding product image thumbnails in the future +* Could benefit from advanced filtering by product attributes +* May need performance optimization for stores with very large product catalogs + +Credits +======= + +Authors: + +* Criptomart +* Odoo Community Association (OCA) + +Contributors: + +* Your Company Name + +The module uses DataTables.net library for enhanced table functionality. diff --git a/website_search_products/__init__.py b/website_search_products/__init__.py new file mode 100644 index 0000000..c55325e --- /dev/null +++ b/website_search_products/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import controllers +from . import models diff --git a/website_search_products/__manifest__.py b/website_search_products/__manifest__.py new file mode 100644 index 0000000..e02798d --- /dev/null +++ b/website_search_products/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2022 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Website Search Products", + "summary": "Allows portal users to search products in a table with filters.", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Criptomart, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/website", + "depends": ["stock", "website"], + "data": [ + "data/menu.xml", + "views/web_templates.xml", + ], + "demo": [], + "installable": True, + "application": False, +} diff --git a/website_search_products/controllers/__init__.py b/website_search_products/controllers/__init__.py new file mode 100644 index 0000000..2f34e6e --- /dev/null +++ b/website_search_products/controllers/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import controller diff --git a/website_search_products/controllers/controller.py b/website_search_products/controllers/controller.py new file mode 100644 index 0000000..3df776c --- /dev/null +++ b/website_search_products/controllers/controller.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import http +from odoo.http import request +from odoo.addons.website.controllers.main import Website + +import logging + +_logger = logging.getLogger(__name__) + + +class InventoryForm(http.Controller): + + @http.route(["/inventory/search"], type="http", auth="user", website=True) + def inventory_form(self, **post): + """Display inventory search form for authorized users.""" + user = request.env["res.users"].browse(request.uid) + + # Check if user has portal or internal access + has_portal = user.has_group("base.group_portal") + has_internal = user.has_group("base.group_user") + + if not has_portal and not has_internal: + return http.redirect("/web") + + # Fetch active products available in POS + products = ( + request.env["product.template"] + .sudo() + .search([("active", "=", True), ("available_in_pos", "=", True)]) + ) + vals = {"products": products} + return request.render("website_search_products.web_list_products", vals) + + +class WebsiteController(Website): + """Extend Website controller to redirect portal users after login.""" + + @http.route(website=True, auth="public") + def web_login(self, redirect=None, *args, **kw): + """Handle login redirection based on user group.""" + response = super().web_login(redirect=redirect, *args, **kw) + + # Check if login was successful + if not redirect and request.params.get("login_success"): + user = request.env["res.users"].browse(request.uid) + + # Redirect internal users to web, others to inventory search + if user.has_group("base.group_user"): + # Keep original redirect for internal users + redirect = "/web" + else: + # Redirect portal users to inventory search + redirect = "/inventory/search" + + return http.redirect(redirect) + + return response diff --git a/website_search_products/data/menu.xml b/website_search_products/data/menu.xml new file mode 100644 index 0000000..138ffcd --- /dev/null +++ b/website_search_products/data/menu.xml @@ -0,0 +1,12 @@ + + + + + + Products in Shop + /inventory/search + + 22 + + + diff --git a/website_search_products/i18n/es.po b/website_search_products/i18n/es.po new file mode 100644 index 0000000..6f0d55c --- /dev/null +++ b/website_search_products/i18n/es.po @@ -0,0 +1,78 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_search_products +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-03 17:38+0000\n" +"PO-Revision-Date: 2026-02-03 17:38+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Category" +msgstr "Categoría" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Displayed products" +msgstr "Productos mostrados" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "In this table you can consult all the products available in the store" +msgstr "En esta tabla puedes consultar todos los productos disponibles en la tienda" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Name" +msgstr "Nombre" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "" +"Please note that there may be minor discrepancies, which are updated as we " +"perform the recurrent inventories." +msgstr "Ten en cuenta que puede haber algunos pequeños desajustes que se van actualizando a medida que realizamos los inventarios de los lunes. " + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Price" +msgstr "Precio" + +#. module: website_search_products +#: model:website.menu,name:website_search_products.menu_partner_form +msgid "Products in Shop" +msgstr "Productos en tienda" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Products not found" +msgstr "Productos no encontrados" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Quantity" +msgstr "Cantidad" + +#. module: website_search_products +#: model:ir.model.fields,field_description:website_search_products.field_stock_move__smart_search +msgid "Smart Search" +msgstr "" + +#. module: website_search_products +#: model:ir.model,name:website_search_products.model_stock_move +msgid "Stock Move" +msgstr "Movimiento de stock" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "UoM" +msgstr "UdM" diff --git a/website_search_products/i18n/website_search_products.pot b/website_search_products/i18n/website_search_products.pot new file mode 100644 index 0000000..7a7d145 --- /dev/null +++ b/website_search_products/i18n/website_search_products.pot @@ -0,0 +1,78 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_search_products +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-03 17:38+0000\n" +"PO-Revision-Date: 2026-02-03 17:38+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Category" +msgstr "" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Displayed products" +msgstr "" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "In this table you can consult all the products available in the store" +msgstr "" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Name" +msgstr "" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "" +"Please note that there may be minor discrepancies, which are updated as we " +"perform the recurrent inventories." +msgstr "" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Price" +msgstr "" + +#. module: website_search_products +#: model:website.menu,name:website_search_products.menu_partner_form +msgid "Products in Shop" +msgstr "" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Products not found" +msgstr "" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "Quantity" +msgstr "" + +#. module: website_search_products +#: model:ir.model.fields,field_description:website_search_products.field_stock_move__smart_search +msgid "Smart Search" +msgstr "" + +#. module: website_search_products +#: model:ir.model,name:website_search_products.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: website_search_products +#: model_terms:ir.ui.view,arch_db:website_search_products.web_list_products +msgid "UoM" +msgstr "" diff --git a/website_search_products/models/__init__.py b/website_search_products/models/__init__.py new file mode 100644 index 0000000..296e859 --- /dev/null +++ b/website_search_products/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import stock_move diff --git a/website_search_products/models/stock_move.py b/website_search_products/models/stock_move.py new file mode 100644 index 0000000..c12c0e0 --- /dev/null +++ b/website_search_products/models/stock_move.py @@ -0,0 +1,90 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models +from datetime import datetime +from dateutil.relativedelta import relativedelta + +import logging + +_logger = logging.getLogger(__name__) + + +class StockMove(models.Model): + _inherit = "stock.move" + + def get_stock_moves_by_product(self, date_begin, date_end, location): + """ + Get stock movements grouped by product for a given date range and location. + + Args: + date_begin (str): Start date in format 'YYYY-MM-DD' + date_end (str): End date in format 'YYYY-MM-DD' + location (int): Location ID to filter movements + + Returns: + list: List of dictionaries with product and movement data + """ + if date_begin and date_end: + moves = self.env["stock.move"].search( + [ + "|", + ("location_id", "=", int(location)), + ("location_dest_id", "=", int(location)), + ("state", "=", "done"), + ("date", ">=", date_begin), + ("date", "<=", date_end), + ] + ) + else: + return [] + + moves_grouped = {} + data = [] + + if moves: + for move in moves: + if move.product_id.id and move.product_id.type == "product": + if not moves_grouped.get(move.product_id.id, False): + date_end_obj = datetime.strptime( + date_end, "%Y-%m-%d" + ).date() + relativedelta(days=1) + moves_grouped[move.product_id.id] = dict([]) + moves_grouped[move.product_id.id]["name"] = move.product_id.name + moves_grouped[move.product_id.id][ + "category" + ] = move.product_id.categ_id.name + moves_grouped[move.product_id.id][ + "list_price" + ] = move.product_id.list_price + # Get quantity at start of period + moves_grouped[move.product_id.id]["qty_init"] = ( + move.product_id.with_context( + { + "to_date": date_begin, + "location_ids": [int(location)], + } + ).qty_available + ) + # Get quantity at end of period + moves_grouped[move.product_id.id]["qty_end"] = ( + move.product_id.with_context( + { + "to_date": date_end_obj, + "location_ids": [int(location)], + } + ).qty_available + ) + + if not moves_grouped[move.product_id.id].get("in"): + moves_grouped[move.product_id.id]["in"] = 0.0 + if not moves_grouped[move.product_id.id].get("out"): + moves_grouped[move.product_id.id]["out"] = 0.0 + + if int(move.location_id.id) == int(location): + moves_grouped[move.product_id.id]["out"] += move.product_qty + elif int(move.location_dest_id.id) == int(location): + moves_grouped[move.product_id.id]["in"] += move.product_qty + + data = list(moves_grouped.values()) + + return data diff --git a/website_search_products/views/web_templates.xml b/website_search_products/views/web_templates.xml new file mode 100644 index 0000000..6480702 --- /dev/null +++ b/website_search_products/views/web_templates.xml @@ -0,0 +1,95 @@ + + + + + + +