[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
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
# 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 models
@ -41,7 +39,7 @@ class ResPartner(models.Model):
# Return action to open wizard modal
return {
"type": "ir.actions.act_window",
"name": _("Update Product Price Category"),
"name": self.env._("Update Product Price Category"),
"res_model": "wizard.update.product.category",
"res_id": wizard.id,
"view_mode": "form",

View file

@ -1,8 +1,6 @@
# Copyright 2026 Your Company
# 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 models
@ -53,8 +51,8 @@ class WizardUpdateProductCategory(models.TransientModel):
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("No Products"),
"message": _("No products found with this supplier."),
"title": self.env._("No Products"),
"message": self.env._("No products found with this supplier."),
"type": "warning",
"sticky": False,
},
@ -67,9 +65,12 @@ class WizardUpdateProductCategory(models.TransientModel):
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("Success"),
"message": _('%d products updated with category "%s".')
% (len(products), self.price_category_id.display_name),
"title": self.env._("Success"),
"message": self.env._(
"%(count)d products updated with category %(category)s",
count=len(products),
category=self.price_category_id.display_name,
),
"type": "success",
"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 models

View file

@ -31,3 +31,10 @@ known_odoo = ["odoo"]
known_odoo_addons = ["odoo.addons"]
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "ODOO", "ODOO_ADDONS", "FIRSTPARTY", "LOCALFOLDER"]
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.
"""
import logging
import os
import sys
# Agregar path de Odoo
sys.path.insert(0, "/usr/lib/python3/dist-packages")
import odoo
from odoo import SUPERUSER_ID
from odoo import api
import odoo # noqa: E402
from odoo import SUPERUSER_ID # noqa: E402
from odoo import api # noqa: E402
logger = logging.getLogger(__name__)
# Configurar Odoo
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_password"] = os.environ.get("PASSWORD", "odoo")
print("\n" + "=" * 60)
print("TEST: Precios con impuestos incluidos")
print("=" * 60 + "\n")
logger.info("\n" + "=" * 60)
logger.info("TEST: Precios con impuestos incluidos")
logger.info("=" * 60 + "\n")
try:
db_name = "odoo"
@ -31,26 +34,26 @@ try:
with registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
print(f"✓ Conectado a BD: {db_name}")
print(f" Usuario: {env.user.name}")
print(f" Compañía: {env.company.name}\n")
logger.info(f"✓ Conectado a BD: {db_name}")
logger.info(f" Usuario: {env.user.name}")
logger.info(f" Compañía: {env.company.name}\n")
# Test 1: Verificar módulo
print("TEST 1: Verificar módulo instalado")
print("-" * 60)
logger.info("TEST 1: Verificar módulo instalado")
logger.info("-" * 60)
module = env["ir.module.module"].search(
[("name", "=", "website_sale_aplicoop")], limit=1
)
if module and module.state == "installed":
print(f"✓ Módulo website_sale_aplicoop instalado")
logger.info("✓ Módulo website_sale_aplicoop instalado")
else:
print(f"✗ Módulo NO instalado")
logger.error("✗ Módulo NO instalado")
sys.exit(1)
# Test 2: Verificar método nuevo
print("\nTEST 2: Verificar método _compute_price_with_taxes")
print("-" * 60)
logger.info("\nTEST 2: Verificar método _compute_price_with_taxes")
logger.info("-" * 60)
try:
from odoo.addons.website_sale_aplicoop.controllers.website_sale import (
AplicoopWebsiteSale,
@ -59,20 +62,20 @@ try:
controller = AplicoopWebsiteSale()
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
sig = inspect.signature(controller._compute_price_with_taxes)
print(f" Firma: {sig}")
logger.info(f" Firma: {sig}")
else:
print("✗ Método NO encontrado")
logger.error("✗ Método NO encontrado")
except Exception as e:
print(f"✗ Error: {e}")
logger.exception("✗ Error verificando método: %s", e)
# Test 3: Probar cálculo de impuestos
print("\nTEST 3: Calcular precio con impuestos")
print("-" * 60)
logger.info("\nTEST 3: Calcular precio con impuestos")
logger.info("-" * 60)
# Buscar un producto con impuestos
product = env["product.product"].search(
@ -80,7 +83,7 @@ try:
)
if not product:
print(" Creando producto de prueba...")
logger.info(" Creando producto de prueba...")
# Buscar impuesto existente
tax = env["account.tax"].search(
@ -97,19 +100,22 @@ try:
"sale_ok": True,
}
)
print(f" Producto creado: {product.name}")
logger.info(f" Producto creado: {product.name}")
else:
print(" ✗ No hay impuestos de venta configurados")
logger.error(" ✗ No hay impuestos de venta configurados")
sys.exit(1)
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)
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
base_price = product.list_price
@ -124,24 +130,26 @@ try:
price_with_tax = tax_result["total_included"]
tax_amount = price_with_tax - price_without_tax
print(f"\n Cálculo:")
print(f" Base: {base_price:.2f}")
print(f" Sin IVA: {price_without_tax:.2f}")
print(f" IVA: {tax_amount:.2f}")
print(f" CON IVA: {price_with_tax:.2f}")
logger.info("\n Cálculo:")
logger.info(f" Base: {base_price:.2f}")
logger.info(f" Sin IVA: {price_without_tax:.2f}")
logger.info(f" IVA: {tax_amount:.2f}")
logger.info(f" CON IVA: {price_with_tax:.2f}")
if price_with_tax > price_without_tax:
print(
f"\n ✓ PASADO: Precio con IVA ({price_with_tax:.2f}) > sin IVA ({price_without_tax:.2f})"
logger.info(
"\n ✓ PASADO: Precio con IVA (%.2f) > sin IVA (%.2f)",
price_with_tax,
price_without_tax,
)
else:
print(f"\n ✗ FALLADO: Impuestos no se calculan correctamente")
logger.error("\n ✗ FALLADO: Impuestos no se calculan correctamente")
else:
print(" ⚠ Producto sin impuestos")
logger.warning(" ⚠ Producto sin impuestos")
# Test 4: Verificar OCA _get_price
print("\nTEST 4: Verificar OCA _get_price")
print("-" * 60)
logger.info("\nTEST 4: Verificar OCA _get_price")
logger.info("-" * 60)
pricelist = env["product.pricelist"].search(
[("company_id", "=", env.company.id)], limit=1
@ -154,33 +162,35 @@ try:
fposition=False,
)
print(f" OCA _get_price:")
print(f" value: {price_info.get('value', 0):.2f}")
print(f" tax_included: {price_info.get('tax_included', False)}")
logger.info(" OCA _get_price:")
logger.info(" value: %.2f", price_info.get("value", 0))
logger.info(
" tax_included: %s", str(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:
print(f" ⚠ OCA indica IVA incluido")
logger.warning(" ⚠ OCA indica IVA incluido")
print("\n" + "=" * 60)
print("RESUMEN")
print("=" * 60)
print("""
Corrección implementada:
1. Método _compute_price_with_taxes añadido
2. Calcula precio CON IVA usando taxes.compute_all()
3. Usado en eskaera_shop y add_to_eskaera_cart
4. Soluciona problema de precios sin IVA en la tienda
logger.info("\n" + "=" * 60)
logger.info("RESUMEN")
logger.info("=" * 60)
logger.info("""
Corrección implementada:
1. Método _compute_price_with_taxes añadido
2. Calcula precio CON IVA usando taxes.compute_all()
3. Usado en eskaera_shop y add_to_eskaera_cart
4. Soluciona problema de precios sin IVA en la tienda
El método OCA _get_price retorna precios SIN IVA.
Nuestra función _compute_price_with_taxes añade el IVA.
El método OCA _get_price retorna precios SIN 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:
print(f"\n✗ ERROR: {e}\n")
logger.exception("\n✗ ERROR: %s\n", e)
import traceback
traceback.print_exc()

View file

@ -3,7 +3,6 @@
import logging
from odoo import _
from odoo.http import request
from odoo.http import route
@ -37,13 +36,13 @@ class CustomerPortal(sale_portal.CustomerPortal):
# Add translated day names for pickup_day display
values["day_names"] = [
_("Monday"),
_("Tuesday"),
_("Wednesday"),
_("Thursday"),
_("Friday"),
_("Saturday"),
_("Sunday"),
request.env._("Monday"),
request.env._("Tuesday"),
request.env._("Wednesday"),
request.env._("Thursday"),
request.env._("Friday"),
request.env._("Saturday"),
request.env._("Sunday"),
]
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 hasattr(response, "qcontext"):
response.qcontext["day_names"] = [
_("Monday"),
_("Tuesday"),
_("Wednesday"),
_("Thursday"),
_("Friday"),
_("Saturday"),
_("Sunday"),
request.env._("Monday"),
request.env._("Tuesday"),
request.env._("Wednesday"),
request.env._("Thursday"),
request.env._("Friday"),
request.env._("Saturday"),
request.env._("Sunday"),
]
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
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
import logging
from odoo import SUPERUSER_ID
from odoo import api
_logger = logging.getLogger(__name__)
def migrate(cr, version):
"""Migración para agregar soporte multicompañía.
@ -27,5 +31,4 @@ def migrate(cr, version):
(default_company.id,),
)
cr.commit()
print(f"✓ Asignado company_id={default_company.id} a group.order")
_logger.info("Asignado company_id=%d a group.order", default_company.id)

View file

@ -243,13 +243,11 @@ class GroupOrder(models.Model):
raise ValidationError(
self.env._(
"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")
@ -545,9 +543,10 @@ class GroupOrder(models.Model):
self.env._(
"For weekly orders, pickup day (%(pickup)s) must be after or equal to "
"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 ===

View file

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

View file

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

View file

@ -82,9 +82,8 @@
/* Info value styling */
.info-value {
font-size: 1.1rem;
}
.info-date {
font-size: 1rem;
}
}
.info-date {
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",
"login": "other@test.com",

View file

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

View file

@ -19,9 +19,9 @@ Coverage:
from datetime import datetime
from datetime import timedelta
from odoo.exceptions import AccessError
from odoo.exceptions import ValidationError
from odoo.tests.common import HttpCase
from odoo.exceptions import AccessError # noqa: F401
from odoo.exceptions import ValidationError # noqa: F401
from odoo.tests.common import HttpCase # noqa: F401
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",
"group_ids": [(6, 0, [other_group.id])],
@ -601,7 +601,7 @@ class TestLoadDraftEndpoint(TransactionCase):
expired_order.action_open()
expired_order.action_close()
old_sale = self.env["sale.order"].create(
self.env["sale.order"].create(
{
"partner_id": self.member_partner.id,
"group_order_id": expired_order.id,

View file

@ -4,7 +4,7 @@
from datetime import datetime
from datetime import timedelta
from psycopg2 import IntegrityError
from psycopg2 import IntegrityError # noqa: F401
from odoo import fields
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",
"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
self.assertIsNotNone(result)
except Exception as e:
except Exception:
# If it raises, that's also acceptable behavior
self.assertTrue(True, "Negative quantity properly rejected")

View file

@ -139,7 +139,8 @@ class TestProductDiscoveryUnion(TransactionCase):
"""Test discovery includes products from linked categories."""
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)
# Note: depends on how discovery is computed
@ -346,9 +347,13 @@ class TestDeepCategoryHierarchies(TransactionCase):
# Attempt to create circular ref may fail
try:
self.cat_l1.parent_id = self.cat_l5.id # Creates loop
except:
# Expected: Odoo should prevent circular refs
pass
except Exception as exc:
# Expected: Odoo should prevent circular refs. Log for visibility.
import logging
logging.getLogger(__name__).info(
"Expected exception creating circular category: %s", str(exc)
)
class TestEmptySourcesDiscovery(TransactionCase):

View file

@ -4,7 +4,6 @@
from datetime import date
from datetime import timedelta
from odoo import _
from odoo.tests.common import TransactionCase
from odoo.tests.common import tagged
@ -82,9 +81,7 @@ class TestTemplatesRendering(TransactionCase):
def test_day_names_context_is_provided(self):
"""Test that day_names context is provided by the controller method."""
# Simulate what the controller does, passing env for test context
from odoo.addons.website_sale_aplicoop.controllers.website_sale import (
AplicoopWebsiteSale,
)
from ..controllers.website_sale import AplicoopWebsiteSale
controller = AplicoopWebsiteSale()
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):
"""Test that user without partner_id has no access to orders."""
start_date = datetime.now().date()
order = self.env["group.order"].create(
self.env["group.order"].create(
{
"name": "Test Order",
"group_ids": [(6, 0, [self.group.id])],