[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:
parent
380d05785f
commit
cf9ea887c1
30 changed files with 1129 additions and 1102 deletions
13
.github/copilot-instructions.md
vendored
13
.github/copilot-instructions.md
vendored
|
|
@ -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
5
mypy.ini
Normal 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/.*
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
"""Make migrations folder a package so mypy maps module names correctly.
|
||||
|
||||
Empty on purpose.
|
||||
"""
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
from odoo import api
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
|
|
|||
|
|
@ -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/.*"
|
||||
|
|
|
|||
122
test_prices.py
122
test_prices.py
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
4
website_sale_aplicoop/migrations/18.0.1.0.0/__init__.py
Normal file
4
website_sale_aplicoop/migrations/18.0.1.0.0/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
"""Make migrations folder a package so mypy maps module names correctly.
|
||||
|
||||
Empty on purpose.
|
||||
"""
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ===
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -82,9 +82,8 @@
|
|||
/* Info value styling */
|
||||
.info-value {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.info-date {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.info-date {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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])],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])],
|
||||
|
|
|
|||
83
website_sale_aplicoop/tests/test_portal_access.py
Normal file
83
website_sale_aplicoop/tests/test_portal_access.py
Normal 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)
|
||||
85
website_sale_aplicoop/tests/test_portal_get_routes.py
Normal file
85
website_sale_aplicoop/tests/test_portal_get_routes.py
Normal 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}")
|
||||
101
website_sale_aplicoop/tests/test_portal_product_uom_access.py
Normal file
101
website_sale_aplicoop/tests/test_portal_product_uom_access.py
Normal 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)
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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])],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue