LaOsaCoop/Odoo16#109 migrated website_search_products
This commit is contained in:
parent
26dbe222dd
commit
f1a3a75988
11 changed files with 525 additions and 0 deletions
84
website_search_products/README.rst
Normal file
84
website_search_products/README.rst
Normal 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.
|
||||
4
website_search_products/__init__.py
Normal file
4
website_search_products/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
19
website_search_products/__manifest__.py
Normal file
19
website_search_products/__manifest__.py
Normal 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,
|
||||
}
|
||||
3
website_search_products/controllers/__init__.py
Normal file
3
website_search_products/controllers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import controller
|
||||
59
website_search_products/controllers/controller.py
Normal file
59
website_search_products/controllers/controller.py
Normal 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
|
||||
12
website_search_products/data/menu.xml
Normal file
12
website_search_products/data/menu.xml
Normal 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>
|
||||
78
website_search_products/i18n/es.po
Normal file
78
website_search_products/i18n/es.po
Normal 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"
|
||||
78
website_search_products/i18n/website_search_products.pot
Normal file
78
website_search_products/i18n/website_search_products.pot
Normal 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 ""
|
||||
3
website_search_products/models/__init__.py
Normal file
3
website_search_products/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import stock_move
|
||||
90
website_search_products/models/stock_move.py
Normal file
90
website_search_products/models/stock_move.py
Normal 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
|
||||
95
website_search_products/views/web_templates.xml
Normal file
95
website_search_products/views/web_templates.xml
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue