[REF] Code quality improvements and structure fixes

- Add mypy.ini configuration to exclude migration scripts
- Rename migration files to proper snake_case (post-migration.py → post_migration.py)
- Add __init__.py to migration directories for proper Python package structure
- Add new portal access tests for website_sale_aplicoop
- Code formatting improvements (black, isort)
- Update copilot instructions and project configuration

Related to previous code quality refactoring work.
This commit is contained in:
snt 2026-02-21 13:47:16 +01:00
parent 380d05785f
commit cf9ea887c1
30 changed files with 1129 additions and 1102 deletions

View file

@ -1,3 +1,16 @@
# ⚠️ Addons OCA Originales y OCB (Odoo)
No modificar el directorio de fuentes de OCB (`ocb/`) ni los siguientes addons OCA originales:
- `product_main_seller`
- `product_origin`
- `account_invoice_triple_discount`
- `product_get_price_helper`
- `product_price_category`
- `purchase_triple_discount`
Estos módulos y el core de Odoo (OCB) solo están para referencia y herencia de nuestros addons custom. Cualquier cambio debe hacerse en los addons propios, nunca en los OCA originales ni en el core OCB.
# AI Agent Skills & Prompt Guidance # AI Agent Skills & Prompt Guidance
Para máxima productividad y calidad, los agentes AI deben seguir estas pautas y consultar los archivos de skills detallados: Para máxima productividad y calidad, los agentes AI deben seguir estas pautas y consultar los archivos de skills detallados:

5
mypy.ini Normal file
View file

@ -0,0 +1,5 @@
[mypy]
# Exclude migration scripts (post-migrate.py etc.) from mypy checks to avoid
# duplicate module name errors when multiple addons include scripts with the
# same filename.
exclude = .*/migrations/.*

View file

@ -1,8 +1,6 @@
# Copyright 2026 Your Company # Copyright 2026 Your Company
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _
from odoo import api
from odoo import fields from odoo import fields
from odoo import models from odoo import models
@ -41,7 +39,7 @@ class ResPartner(models.Model):
# Return action to open wizard modal # Return action to open wizard modal
return { return {
"type": "ir.actions.act_window", "type": "ir.actions.act_window",
"name": _("Update Product Price Category"), "name": self.env._("Update Product Price Category"),
"res_model": "wizard.update.product.category", "res_model": "wizard.update.product.category",
"res_id": wizard.id, "res_id": wizard.id,
"view_mode": "form", "view_mode": "form",

View file

@ -1,8 +1,6 @@
# Copyright 2026 Your Company # Copyright 2026 Your Company
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _
from odoo import api
from odoo import fields from odoo import fields
from odoo import models from odoo import models
@ -53,8 +51,8 @@ class WizardUpdateProductCategory(models.TransientModel):
"type": "ir.actions.client", "type": "ir.actions.client",
"tag": "display_notification", "tag": "display_notification",
"params": { "params": {
"title": _("No Products"), "title": self.env._("No Products"),
"message": _("No products found with this supplier."), "message": self.env._("No products found with this supplier."),
"type": "warning", "type": "warning",
"sticky": False, "sticky": False,
}, },
@ -67,9 +65,12 @@ class WizardUpdateProductCategory(models.TransientModel):
"type": "ir.actions.client", "type": "ir.actions.client",
"tag": "display_notification", "tag": "display_notification",
"params": { "params": {
"title": _("Success"), "title": self.env._("Success"),
"message": _('%d products updated with category "%s".') "message": self.env._(
% (len(products), self.price_category_id.display_name), "%(count)d products updated with category %(category)s",
count=len(products),
category=self.price_category_id.display_name,
),
"type": "success", "type": "success",
"sticky": False, "sticky": False,
}, },

View file

@ -0,0 +1,4 @@
"""Make migrations folder a package so mypy maps module names correctly.
Empty on purpose.
"""

View file

@ -1,4 +1,3 @@
from odoo import api
from odoo import fields from odoo import fields
from odoo import models from odoo import models

View file

@ -31,3 +31,10 @@ known_odoo = ["odoo"]
known_odoo_addons = ["odoo.addons"] known_odoo_addons = ["odoo.addons"]
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "ODOO", "ODOO_ADDONS", "FIRSTPARTY", "LOCALFOLDER"] sections = ["FUTURE", "STDLIB", "THIRDPARTY", "ODOO", "ODOO_ADDONS", "FIRSTPARTY", "LOCALFOLDER"]
default_section = "THIRDPARTY" default_section = "THIRDPARTY"
[tool.mypy]
# Excluir carpetas de migraciones y archivos de post-migrate.py que usan guiones
# (evita errores de "Duplicate module" en mypy cuando múltiples addons contienen
# archivos con el mismo nombre como `post-migrate.py`). Usamos una expresión
# regular que coincide con cualquier ruta que contenga `/migrations/`.
exclude = "(?i).*/migrations/.*"

View file

@ -4,15 +4,18 @@ Script de prueba para verificar que los precios incluyen impuestos.
Se ejecuta dentro del contenedor de Odoo. Se ejecuta dentro del contenedor de Odoo.
""" """
import logging
import os import os
import sys import sys
# Agregar path de Odoo # Agregar path de Odoo
sys.path.insert(0, "/usr/lib/python3/dist-packages") sys.path.insert(0, "/usr/lib/python3/dist-packages")
import odoo import odoo # noqa: E402
from odoo import SUPERUSER_ID from odoo import SUPERUSER_ID # noqa: E402
from odoo import api from odoo import api # noqa: E402
logger = logging.getLogger(__name__)
# Configurar Odoo # Configurar Odoo
odoo.tools.config["db_host"] = os.environ.get("HOST", "db") odoo.tools.config["db_host"] = os.environ.get("HOST", "db")
@ -20,9 +23,9 @@ odoo.tools.config["db_port"] = int(os.environ.get("PORT", 5432))
odoo.tools.config["db_user"] = os.environ.get("USER", "odoo") odoo.tools.config["db_user"] = os.environ.get("USER", "odoo")
odoo.tools.config["db_password"] = os.environ.get("PASSWORD", "odoo") odoo.tools.config["db_password"] = os.environ.get("PASSWORD", "odoo")
print("\n" + "=" * 60) logger.info("\n" + "=" * 60)
print("TEST: Precios con impuestos incluidos") logger.info("TEST: Precios con impuestos incluidos")
print("=" * 60 + "\n") logger.info("=" * 60 + "\n")
try: try:
db_name = "odoo" db_name = "odoo"
@ -31,26 +34,26 @@ try:
with registry.cursor() as cr: with registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {}) env = api.Environment(cr, SUPERUSER_ID, {})
print(f"✓ Conectado a BD: {db_name}") logger.info(f"✓ Conectado a BD: {db_name}")
print(f" Usuario: {env.user.name}") logger.info(f" Usuario: {env.user.name}")
print(f" Compañía: {env.company.name}\n") logger.info(f" Compañía: {env.company.name}\n")
# Test 1: Verificar módulo # Test 1: Verificar módulo
print("TEST 1: Verificar módulo instalado") logger.info("TEST 1: Verificar módulo instalado")
print("-" * 60) logger.info("-" * 60)
module = env["ir.module.module"].search( module = env["ir.module.module"].search(
[("name", "=", "website_sale_aplicoop")], limit=1 [("name", "=", "website_sale_aplicoop")], limit=1
) )
if module and module.state == "installed": if module and module.state == "installed":
print(f"✓ Módulo website_sale_aplicoop instalado") logger.info("✓ Módulo website_sale_aplicoop instalado")
else: else:
print(f"✗ Módulo NO instalado") logger.error("✗ Módulo NO instalado")
sys.exit(1) sys.exit(1)
# Test 2: Verificar método nuevo # Test 2: Verificar método nuevo
print("\nTEST 2: Verificar método _compute_price_with_taxes") logger.info("\nTEST 2: Verificar método _compute_price_with_taxes")
print("-" * 60) logger.info("-" * 60)
try: try:
from odoo.addons.website_sale_aplicoop.controllers.website_sale import ( from odoo.addons.website_sale_aplicoop.controllers.website_sale import (
AplicoopWebsiteSale, AplicoopWebsiteSale,
@ -59,20 +62,20 @@ try:
controller = AplicoopWebsiteSale() controller = AplicoopWebsiteSale()
if hasattr(controller, "_compute_price_with_taxes"): if hasattr(controller, "_compute_price_with_taxes"):
print("✓ Método _compute_price_with_taxes existe") logger.info("✓ Método _compute_price_with_taxes existe")
import inspect import inspect
sig = inspect.signature(controller._compute_price_with_taxes) sig = inspect.signature(controller._compute_price_with_taxes)
print(f" Firma: {sig}") logger.info(f" Firma: {sig}")
else: else:
print("✗ Método NO encontrado") logger.error("✗ Método NO encontrado")
except Exception as e: except Exception as e:
print(f"✗ Error: {e}") logger.exception("✗ Error verificando método: %s", e)
# Test 3: Probar cálculo de impuestos # Test 3: Probar cálculo de impuestos
print("\nTEST 3: Calcular precio con impuestos") logger.info("\nTEST 3: Calcular precio con impuestos")
print("-" * 60) logger.info("-" * 60)
# Buscar un producto con impuestos # Buscar un producto con impuestos
product = env["product.product"].search( product = env["product.product"].search(
@ -80,7 +83,7 @@ try:
) )
if not product: if not product:
print(" Creando producto de prueba...") logger.info(" Creando producto de prueba...")
# Buscar impuesto existente # Buscar impuesto existente
tax = env["account.tax"].search( tax = env["account.tax"].search(
@ -97,19 +100,22 @@ try:
"sale_ok": True, "sale_ok": True,
} }
) )
print(f" Producto creado: {product.name}") logger.info(f" Producto creado: {product.name}")
else: else:
print(" ✗ No hay impuestos de venta configurados") logger.error(" ✗ No hay impuestos de venta configurados")
sys.exit(1) sys.exit(1)
else: else:
print(f" Producto encontrado: {product.name}") logger.info(f" Producto encontrado: {product.name}")
print(f" Precio de lista: {product.list_price:.2f}") logger.info(f" Precio de lista: {product.list_price:.2f}")
taxes = product.taxes_id.filtered(lambda t: t.company_id == env.company) taxes = product.taxes_id.filtered(lambda t: t.company_id == env.company)
if taxes: if taxes:
print(f" Impuestos: {', '.join(f'{t.name} ({t.amount}%)' for t in taxes)}") logger.info(
" Impuestos: %s",
", ".join(f"{t.name} ({t.amount}%)" for t in taxes),
)
# Calcular precio con impuestos # Calcular precio con impuestos
base_price = product.list_price base_price = product.list_price
@ -124,24 +130,26 @@ try:
price_with_tax = tax_result["total_included"] price_with_tax = tax_result["total_included"]
tax_amount = price_with_tax - price_without_tax tax_amount = price_with_tax - price_without_tax
print(f"\n Cálculo:") logger.info("\n Cálculo:")
print(f" Base: {base_price:.2f}") logger.info(f" Base: {base_price:.2f}")
print(f" Sin IVA: {price_without_tax:.2f}") logger.info(f" Sin IVA: {price_without_tax:.2f}")
print(f" IVA: {tax_amount:.2f}") logger.info(f" IVA: {tax_amount:.2f}")
print(f" CON IVA: {price_with_tax:.2f}") logger.info(f" CON IVA: {price_with_tax:.2f}")
if price_with_tax > price_without_tax: if price_with_tax > price_without_tax:
print( logger.info(
f"\n ✓ PASADO: Precio con IVA ({price_with_tax:.2f}) > sin IVA ({price_without_tax:.2f})" "\n ✓ PASADO: Precio con IVA (%.2f) > sin IVA (%.2f)",
price_with_tax,
price_without_tax,
) )
else: else:
print(f"\n ✗ FALLADO: Impuestos no se calculan correctamente") logger.error("\n ✗ FALLADO: Impuestos no se calculan correctamente")
else: else:
print(" ⚠ Producto sin impuestos") logger.warning(" ⚠ Producto sin impuestos")
# Test 4: Verificar OCA _get_price # Test 4: Verificar OCA _get_price
print("\nTEST 4: Verificar OCA _get_price") logger.info("\nTEST 4: Verificar OCA _get_price")
print("-" * 60) logger.info("-" * 60)
pricelist = env["product.pricelist"].search( pricelist = env["product.pricelist"].search(
[("company_id", "=", env.company.id)], limit=1 [("company_id", "=", env.company.id)], limit=1
@ -154,33 +162,35 @@ try:
fposition=False, fposition=False,
) )
print(f" OCA _get_price:") logger.info(" OCA _get_price:")
print(f" value: {price_info.get('value', 0):.2f}") logger.info(" value: %.2f", price_info.get("value", 0))
print(f" tax_included: {price_info.get('tax_included', False)}") logger.info(
" tax_included: %s", str(price_info.get("tax_included", False))
)
if not price_info.get("tax_included", False): if not price_info.get("tax_included", False):
print(f" ✓ PASADO: OCA retorna precio SIN IVA (esperado)") logger.info(" ✓ PASADO: OCA retorna precio SIN IVA (esperado)")
else: else:
print(f" ⚠ OCA indica IVA incluido") logger.warning(" ⚠ OCA indica IVA incluido")
print("\n" + "=" * 60) logger.info("\n" + "=" * 60)
print("RESUMEN") logger.info("RESUMEN")
print("=" * 60) logger.info("=" * 60)
print(""" logger.info("""
Corrección implementada: Corrección implementada:
1. Método _compute_price_with_taxes añadido 1. Método _compute_price_with_taxes añadido
2. Calcula precio CON IVA usando taxes.compute_all() 2. Calcula precio CON IVA usando taxes.compute_all()
3. Usado en eskaera_shop y add_to_eskaera_cart 3. Usado en eskaera_shop y add_to_eskaera_cart
4. Soluciona problema de precios sin IVA en la tienda 4. Soluciona problema de precios sin IVA en la tienda
El método OCA _get_price retorna precios SIN IVA. El método OCA _get_price retorna precios SIN IVA.
Nuestra función _compute_price_with_taxes añade el IVA. Nuestra función _compute_price_with_taxes añade el IVA.
""") """)
print("✓ Todos los tests completados exitosamente\n") logger.info("✓ Todos los tests completados exitosamente\n")
except Exception as e: except Exception as e:
print(f"\n✗ ERROR: {e}\n") logger.exception("\n✗ ERROR: %s\n", e)
import traceback import traceback
traceback.print_exc() traceback.print_exc()

View file

@ -3,7 +3,6 @@
import logging import logging
from odoo import _
from odoo.http import request from odoo.http import request
from odoo.http import route from odoo.http import route
@ -37,13 +36,13 @@ class CustomerPortal(sale_portal.CustomerPortal):
# Add translated day names for pickup_day display # Add translated day names for pickup_day display
values["day_names"] = [ values["day_names"] = [
_("Monday"), request.env._("Monday"),
_("Tuesday"), request.env._("Tuesday"),
_("Wednesday"), request.env._("Wednesday"),
_("Thursday"), request.env._("Thursday"),
_("Friday"), request.env._("Friday"),
_("Saturday"), request.env._("Saturday"),
_("Sunday"), request.env._("Sunday"),
] ]
request.session["my_orders_history"] = values["orders"].ids[:100] request.session["my_orders_history"] = values["orders"].ids[:100]
@ -60,13 +59,13 @@ class CustomerPortal(sale_portal.CustomerPortal):
# If it's a template render (not a redirect), add day_names to the context # If it's a template render (not a redirect), add day_names to the context
if hasattr(response, "qcontext"): if hasattr(response, "qcontext"):
response.qcontext["day_names"] = [ response.qcontext["day_names"] = [
_("Monday"), request.env._("Monday"),
_("Tuesday"), request.env._("Tuesday"),
_("Wednesday"), request.env._("Wednesday"),
_("Thursday"), request.env._("Thursday"),
_("Friday"), request.env._("Friday"),
_("Saturday"), request.env._("Saturday"),
_("Sunday"), request.env._("Sunday"),
] ]
return response return response

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
"""Make migrations folder a package so mypy maps module names correctly.
Empty on purpose.
"""

View file

@ -1,9 +1,13 @@
# Copyright 2025 Criptomart # Copyright 2025 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
import logging
from odoo import SUPERUSER_ID from odoo import SUPERUSER_ID
from odoo import api from odoo import api
_logger = logging.getLogger(__name__)
def migrate(cr, version): def migrate(cr, version):
"""Migración para agregar soporte multicompañía. """Migración para agregar soporte multicompañía.
@ -27,5 +31,4 @@ def migrate(cr, version):
(default_company.id,), (default_company.id,),
) )
cr.commit() _logger.info("Asignado company_id=%d a group.order", default_company.id)
print(f"✓ Asignado company_id={default_company.id} a group.order")

View file

@ -243,13 +243,11 @@ class GroupOrder(models.Model):
raise ValidationError( raise ValidationError(
self.env._( self.env._(
"Group %(group)s belongs to company %(group_company)s, " "Group %(group)s belongs to company %(group_company)s, "
"not to %(record_company)s." "not to %(record_company)s.",
group=group.name,
group_company=group.company_id.name,
record_company=record.company_id.name,
) )
% {
"group": group.name,
"group_company": group.company_id.name,
"record_company": record.company_id.name,
}
) )
@api.constrains("start_date", "end_date") @api.constrains("start_date", "end_date")
@ -545,9 +543,10 @@ class GroupOrder(models.Model):
self.env._( self.env._(
"For weekly orders, pickup day (%(pickup)s) must be after or equal to " "For weekly orders, pickup day (%(pickup)s) must be after or equal to "
"cutoff day (%(cutoff)s) in the same week. Current configuration would " "cutoff day (%(cutoff)s) in the same week. Current configuration would "
"put pickup before cutoff, which is illogical." "put pickup before cutoff, which is illogical.",
pickup=pickup_name,
cutoff=cutoff_name,
) )
% {"pickup": pickup_name, "cutoff": cutoff_name}
) )
# === Onchange Methods === # === Onchange Methods ===

View file

@ -1,11 +1,12 @@
# Copyright 2025 Criptomart # Copyright 2025 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import _
from odoo import api from odoo import api
from odoo import fields from odoo import fields
from odoo import models from odoo import models
# Note: translation function _ is not used in this module (removed to satisfy flake8)
class ProductProduct(models.Model): class ProductProduct(models.Model):
_inherit = "product.product" _inherit = "product.product"

View file

@ -1,10 +1,11 @@
# Copyright 2025-Today Criptomart # Copyright 2025-Today Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import _
from odoo import fields from odoo import fields
from odoo import models from odoo import models
# Note: translation function _ is not used in this module (removed to satisfy flake8)
class ResPartner(models.Model): class ResPartner(models.Model):
_inherit = "res.partner" _inherit = "res.partner"

View file

@ -81,10 +81,9 @@
/* Info value styling */ /* Info value styling */
.info-value { .info-value {
font-size: 1.1rem; font-size: 1.1rem;
} }
.info-date { .info-date {
font-size: 1rem; font-size: 1rem;
}
} }

View file

@ -303,7 +303,7 @@ class TestLoadDraftOrder(TransactionCase):
} }
) )
other_user = self.env["res.users"].create( self.env["res.users"].create(
{ {
"name": "Other User", "name": "Other User",
"login": "other@test.com", "login": "other@test.com",

View file

@ -18,7 +18,7 @@ from datetime import timedelta
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError # noqa: F401
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
@ -430,7 +430,7 @@ class TestOrderWithoutEndDate(TransactionCase):
"""Test order with end_date = NULL (ongoing order).""" """Test order with end_date = NULL (ongoing order)."""
start = date.today() start = date.today()
order = self.env["group.order"].create( self.env["group.order"].create(
{ {
"name": "Permanent Order", "name": "Permanent Order",
"group_ids": [(6, 0, [self.group.id])], "group_ids": [(6, 0, [self.group.id])],

View file

@ -19,9 +19,9 @@ Coverage:
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
from odoo.exceptions import AccessError from odoo.exceptions import AccessError # noqa: F401
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError # noqa: F401
from odoo.tests.common import HttpCase from odoo.tests.common import HttpCase # noqa: F401
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
@ -467,7 +467,7 @@ class TestConfirmOrderEndpoint(TransactionCase):
} }
) )
other_order = self.env["group.order"].create( self.env["group.order"].create(
{ {
"name": "Other Order", "name": "Other Order",
"group_ids": [(6, 0, [other_group.id])], "group_ids": [(6, 0, [other_group.id])],
@ -601,7 +601,7 @@ class TestLoadDraftEndpoint(TransactionCase):
expired_order.action_open() expired_order.action_open()
expired_order.action_close() expired_order.action_close()
old_sale = self.env["sale.order"].create( self.env["sale.order"].create(
{ {
"partner_id": self.member_partner.id, "partner_id": self.member_partner.id,
"group_order_id": expired_order.id, "group_order_id": expired_order.id,

View file

@ -4,7 +4,7 @@
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
from psycopg2 import IntegrityError from psycopg2 import IntegrityError # noqa: F401
from odoo import fields from odoo import fields
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError

View file

@ -152,7 +152,7 @@ class TestMultiCompanyGroupOrder(TransactionCase):
} }
) )
order2 = self.env["group.order"].create( self.env["group.order"].create(
{ {
"name": "Pedido Company 2", "name": "Pedido Company 2",
"group_ids": [(6, 0, [self.group2.id])], "group_ids": [(6, 0, [self.group2.id])],

View file

@ -0,0 +1,83 @@
# Copyright 2026
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from datetime import datetime
from datetime import timedelta
from odoo.tests import tagged
from odoo.tests.common import HttpCase
@tagged("post_install", "-at_install")
class TestPortalAccess(HttpCase):
"""Verifica que un usuario portal pueda acceder a la página de un pedido (eskaera)."""
def setUp(self):
super().setUp()
# Create a consumer group and a member partner
self.group = self.env["res.partner"].create(
{
"name": "Portal Test Group",
"is_company": True,
"email": "portal-group@test.com",
}
)
self.member_partner = self.env["res.partner"].create(
{
"name": "Portal Member",
"email": "portal-member@test.com",
}
)
# Add member to the group
self.group.member_ids = [(4, self.member_partner.id)]
# Create a portal user (password = login for HttpCase.authenticate convenience)
login = "portal.user@test.com"
self.portal_user = self.env["res.users"].create(
{
"name": "Portal User",
"login": login,
"password": login,
"partner_id": self.member_partner.id,
# Add portal group
"groups_id": [(4, self.env.ref("base.group_portal").id)],
}
)
# Create and open a group.order belonging to the same company
start_date = datetime.now().date()
self.group_order = self.env["group.order"].create(
{
"name": "Portal Access Order",
"group_ids": [(6, 0, [self.group.id])],
"type": "regular",
"start_date": start_date,
"end_date": start_date + timedelta(days=7),
"period": "weekly",
"pickup_day": "3",
"cutoff_day": "0",
}
)
self.group_order.action_open()
def test_portal_user_can_view_eskaera_page(self):
"""El endpoint /eskaera/<id> debe ser accesible por un usuario portal que pertenezca a la compañía."""
# Authenticate as portal user
self.authenticate(self.portal_user.login, self.portal_user.login)
# Request the eskaera page
response = self.url_open(
f"/eskaera/{self.group_order.id}", allow_redirects=True
)
# Should return 200 OK and not redirect to login
self.assertEqual(response.status_code, 200)
# Simple sanity: page should contain the group order name
content = (
response.get_data(as_text=True)
if hasattr(response, "get_data")
else getattr(response, "text", "")
)
self.assertIn(self.group_order.name, content)

View file

@ -0,0 +1,85 @@
# Copyright 2026
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from datetime import datetime
from datetime import timedelta
from odoo.tests import tagged
from odoo.tests.common import HttpCase
@tagged("post_install", "-at_install")
class TestPortalGetRoutes(HttpCase):
"""Comprueba que las rutas GET principales devuelvan 200 para un usuario portal."""
def setUp(self):
super().setUp()
# Create a consumer group and a member partner
self.group = self.env["res.partner"].create(
{
"name": "Portal Routes Group",
"is_company": True,
"email": "routes-group@test.com",
}
)
self.member_partner = self.env["res.partner"].create(
{"name": "Routes Member", "email": "routes-member@test.com"}
)
self.group.member_ids = [(4, self.member_partner.id)]
# Create a portal user (password = login for HttpCase.authenticate convenience)
login = "portal.routes@test.com"
self.portal_user = self.env["res.users"].create(
{
"name": "Portal Routes User",
"login": login,
"password": login,
"partner_id": self.member_partner.id,
"groups_id": [(4, self.env.ref("base.group_portal").id)],
}
)
# Create and open a minimal group.order
start_date = datetime.now().date()
self.group_order = self.env["group.order"].create(
{
"name": "Routes Test Order",
"group_ids": [(6, 0, [self.group.id])],
"type": "regular",
"start_date": start_date,
"end_date": start_date + timedelta(days=7),
"period": "weekly",
"pickup_day": "3",
"cutoff_day": "0",
}
)
self.group_order.action_open()
def test_portal_get_routes_return_200(self):
"""Verifica que las rutas principales GET devuelvan 200 para usuario portal."""
# Authenticate as portal user
self.authenticate(self.portal_user.login, self.portal_user.login)
routes = [
"/eskaera",
f"/eskaera/{self.group_order.id}",
f"/eskaera/{self.group_order.id}/checkout",
f"/eskaera/{self.group_order.id}/load-page?page=1",
"/eskaera/labels",
]
for route in routes:
response = self.url_open(route, allow_redirects=True)
status = getattr(response, "status_code", None) or getattr(
response, "status", None
)
# HttpCase returns werkzeug response-like objects; ensure we check 200
try:
code = int(status)
except Exception:
# Fallback: check content exists
code = 200 if response.get_data(as_text=True) else 500
self.assertEqual(code, 200, msg=f"Ruta {route} devolvió {code}")

View file

@ -0,0 +1,101 @@
# Copyright 2026
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from datetime import datetime
from datetime import timedelta
from odoo.tests import tagged
from odoo.tests.common import HttpCase
@tagged("post_install", "-at_install")
class TestPortalProductUoMAccess(HttpCase):
"""Verifica que un usuario portal pueda acceder a la página de tienda (eskaera)
y que la lectura de UoM para display no provoque AccessError.
"""
def setUp(self):
super().setUp()
# Grupo / partner / usuario portal (reusa patrón del otro test)
self.group = self.env["res.partner"].create(
{"name": "Portal UoM Group", "is_company": True}
)
self.member_partner = self.env["res.partner"].create(
{"name": "Portal UoM Member"}
)
self.group.member_ids = [(4, self.member_partner.id)]
login = "portal.uom@test.com"
self.portal_user = self.env["res.users"].create(
{
"name": "Portal UoM User",
"login": login,
"password": login,
"partner_id": self.member_partner.id,
"groups_id": [(4, self.env.ref("base.group_portal").id)],
}
)
# Crear una categoría de UoM y una UoM personalizada (posible restringida)
uom_cat = self.env["uom.uom.categ"].create({"name": "Test UoM Cat"})
self.uom = self.env["uom.uom"].create(
{
"name": "Test UoM",
"uom_type": "reference",
"factor_inv": 1.0,
"category_id": uom_cat.id,
}
)
# Crear producto y asignar la UoM creada
self.product = self.env["product.product"].create(
{
"name": "Producto UoM Test",
"type": "consu",
"list_price": 12.5,
"uom_id": self.uom.id,
"active": True,
}
)
# Publicar el template para que aparezca en la tienda
self.product.product_tmpl_id.write({"is_published": True, "sale_ok": True})
# Crear order y añadir producto
start_date = datetime.now().date()
self.group_order = self.env["group.order"].create(
{
"name": "Portal UoM Order",
"group_ids": [(6, 0, [self.group.id])],
"type": "regular",
"start_date": start_date,
"end_date": start_date + timedelta(days=7),
"period": "weekly",
"pickup_day": "3",
"cutoff_day": "0",
"product_ids": [(6, 0, [self.product.id])],
}
)
self.group_order.action_open()
def test_portal_user_can_view_shop_with_uom(self):
# Authenticate as portal user
self.authenticate(self.portal_user.login, self.portal_user.login)
# Request the eskaera page which renders product cards (and reads uom)
response = self.url_open(
f"/eskaera/{self.group_order.id}", allow_redirects=True
)
# Debe retornar 200 OK
self.assertEqual(response.status_code, 200)
content = (
response.get_data(as_text=True)
if hasattr(response, "get_data")
else getattr(response, "text", "")
)
# Página debe contener el nombre del producto y la categoría UoM (display-safe)
self.assertIn(self.product.name, content)
self.assertIn("Test UoM Cat", content)

View file

@ -490,6 +490,6 @@ class TestPricingWithPricelist(TransactionCase):
) )
# If it doesn't raise, check the result is valid # If it doesn't raise, check the result is valid
self.assertIsNotNone(result) self.assertIsNotNone(result)
except Exception as e: except Exception:
# If it raises, that's also acceptable behavior # If it raises, that's also acceptable behavior
self.assertTrue(True, "Negative quantity properly rejected") self.assertTrue(True, "Negative quantity properly rejected")

View file

@ -139,7 +139,8 @@ class TestProductDiscoveryUnion(TransactionCase):
"""Test discovery includes products from linked categories.""" """Test discovery includes products from linked categories."""
self.group_order.category_ids = [(4, self.category1.id)] self.group_order.category_ids = [(4, self.category1.id)]
discovered = self.group_order.product_ids # Computed # Computed placeholder to ensure discovery logic is exercised during test setup
_ = self.group_order.product_ids
# Should include cat1_product and supplier_product (both in category1) # Should include cat1_product and supplier_product (both in category1)
# Note: depends on how discovery is computed # Note: depends on how discovery is computed
@ -346,9 +347,13 @@ class TestDeepCategoryHierarchies(TransactionCase):
# Attempt to create circular ref may fail # Attempt to create circular ref may fail
try: try:
self.cat_l1.parent_id = self.cat_l5.id # Creates loop self.cat_l1.parent_id = self.cat_l5.id # Creates loop
except: except Exception as exc:
# Expected: Odoo should prevent circular refs # Expected: Odoo should prevent circular refs. Log for visibility.
pass import logging
logging.getLogger(__name__).info(
"Expected exception creating circular category: %s", str(exc)
)
class TestEmptySourcesDiscovery(TransactionCase): class TestEmptySourcesDiscovery(TransactionCase):

View file

@ -4,7 +4,6 @@
from datetime import date from datetime import date
from datetime import timedelta from datetime import timedelta
from odoo import _
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
from odoo.tests.common import tagged from odoo.tests.common import tagged
@ -82,9 +81,7 @@ class TestTemplatesRendering(TransactionCase):
def test_day_names_context_is_provided(self): def test_day_names_context_is_provided(self):
"""Test that day_names context is provided by the controller method.""" """Test that day_names context is provided by the controller method."""
# Simulate what the controller does, passing env for test context # Simulate what the controller does, passing env for test context
from odoo.addons.website_sale_aplicoop.controllers.website_sale import ( from ..controllers.website_sale import AplicoopWebsiteSale
AplicoopWebsiteSale,
)
controller = AplicoopWebsiteSale() controller = AplicoopWebsiteSale()
day_names = controller._get_day_names(env=self.env) day_names = controller._get_day_names(env=self.env)

View file

@ -349,7 +349,7 @@ class TestUserPartnerValidation(TransactionCase):
def test_user_without_partner_cannot_access_order(self): def test_user_without_partner_cannot_access_order(self):
"""Test that user without partner_id has no access to orders.""" """Test that user without partner_id has no access to orders."""
start_date = datetime.now().date() start_date = datetime.now().date()
order = self.env["group.order"].create( self.env["group.order"].create(
{ {
"name": "Test Order", "name": "Test Order",
"group_ids": [(6, 0, [self.group.id])], "group_ids": [(6, 0, [self.group.id])],