LaOsaCoop/Odoo16#109 migrated website_search_products

This commit is contained in:
Luis 2026-02-03 18:40:48 +01:00
parent 26dbe222dd
commit f1a3a75988
11 changed files with 525 additions and 0 deletions

View file

@ -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 <database> -c <config_file> -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.

View file

@ -0,0 +1,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import controllers
from . import models

View file

@ -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,
}

View file

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import controller

View file

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

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<odoo>
<data noupdate="1">
<record id="menu_partner_form" model="website.menu">
<field name="name">Products in Shop</field>
<field name="url">/inventory/search</field>
<field name="parent_id" ref="website.main_menu" />
<field name="sequence" type="int">22</field>
</record>
</data>
</odoo>

View file

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

View file

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

View file

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import stock_move

View file

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

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 Criptomart
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<odoo>
<template id="web_list_products" name="Product List">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
<div class="col-md-12">
<t t-if="products">
<div class="alert alert-success">
Displayed products
</div>
</t>
<t t-if="not products">
<div class="alert alert-danger">
Products not found
</div>
</t>
<div class="col-md-12" style="text-align: center;">
<h2>In this table you can consult all the products available in the store</h2>
<h5>Please note that there may be minor discrepancies, which are updated as we perform the recurrent inventories.</h5>
</div>
<div class="col-md-12" style="margin-top: 50px;">
<table id="productTable" class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Quantity</th>
<th>UoM</th>
</tr>
</thead>
<tbody id="stockList">
<t t-foreach="products" t-as="product">
<tr>
<td>
<span t-esc="product.name" />
</td>
<td>
<span t-esc="product.categ_id.name" />
</td>
<td>
<span t-esc="product.list_price"
t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}" />
</td>
<td>
<span t-esc="product.sudo().qty_available"
t-options="{'widget': 'float', 'precision': 2}" />
</td>
<td>
<span t-esc="product.sudo().uom_id.name" />
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
</div>
</div>
<link rel="stylesheet"
href="https://cdn.datatables.net/2.0.0/css/dataTables.dataTables.css" />
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.datatables.net/2.0.0/js/dataTables.js"></script>
<script>
// Esperar a que jQuery esté disponible
(function waitForJQuery() {
if (typeof jQuery !== 'undefined') {
jQuery(document).ready(function($) {
$('#productTable').DataTable({
"pageLength": 100,
"order": [[1, "asc"], [0, "asc"], [2, "desc"]],
"layout": {
"topStart": "pageLength",
"topEnd": "search",
"bottomStart": "info",
"bottomEnd": "paging"
},
"language": {
"url": "https://cdn.datatables.net/plug-ins/1.13.1/i18n/es-ES.json"
}
});
});
} else {
setTimeout(waitForJQuery, 100);
}
})();
</script>
</t>
</template>
</odoo>