addons-cm/website_sale_aplicoop/controllers/website_sale_validators.py
GitHub Copilot aba22fd230 [IMP] website_sale_aplicoop: automate non-weekly group order cycles
One-time, biweekly and monthly group orders now follow the same cron
confirmation flow as weekly ones (confirm sale orders + batch pickings
when the cycle cutoff passes):

- Biweekly/monthly keep the cutoff_day/pickup_day weekday scheme on a
  recurrence grid anchored at start_date (creation date as fallback):
  cutoffs advance +14 days / +1 month snapped to cutoff_day, with
  catch-up after cron downtime. Previously they behaved as weekly.
- One-time orders (specials/promotions) are driven by end_date
  (cutoff_date = end_date); once passed, the cron confirms, batches
  and closes the group order.
- end_date keeps its "empty = permanent" meaning for recurring orders.
- Website draft-cart lookup window is now period-aware instead of
  assuming a 6-day weekly cycle.
- New cron tests for once/biweekly/monthly cycles; i18n es/eu updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 16:26:14 +02:00

256 lines
9.2 KiB
Python

import logging
from odoo.http import request
_logger = logging.getLogger(__name__)
def _to_bool(self, value):
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return value != 0
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in {"1", "true", "t", "yes", "y", "on"}:
return True
if normalized in {"0", "false", "f", "no", "n", "off", ""}:
return False
return bool(value)
def _validate_user_group_access(self, group_order, current_user):
partner = current_user.partner_id
if not partner or not group_order:
raise ValueError("User is not a member of any consumer group in this order")
user_group_ids = set(partner.group_ids.ids)
for consumer_group in group_order.group_ids:
if consumer_group.id in user_group_ids:
return consumer_group.id
_logger.warning(
"_validate_user_group_access: user %s (%s) not member of any consumer group in order %s",
current_user.name,
current_user.id,
group_order.id,
)
raise ValueError("User is not a member of any consumer group in this order")
def _get_consumer_group_for_user(self, group_order, current_user):
partner = current_user.partner_id
if not partner or not group_order:
return False
user_group_ids = set(partner.group_ids.ids)
for consumer_group in group_order.group_ids:
if consumer_group.id in user_group_ids:
return consumer_group.id
_logger.warning(
"_get_consumer_group_for_user: User %s (%s) is not member of any consumer group in order %s",
current_user.name,
current_user.id,
group_order.id,
)
return False
def _get_salesperson_for_order(self, partner):
if partner.user_id and not partner.user_id._is_public():
return partner.user_id
commercial_partner = partner.commercial_partner_id
if commercial_partner.user_id and not commercial_partner.user_id._is_public():
return commercial_partner.user_id
return False
def _find_recent_draft_order(self, partner_id, group_order, request_obj=None):
"""Return the active-cycle draft sale.order for the partner, or empty.
The active ordering period ends at group_order.cutoff_date and its length
depends on the order period: 7 days (weekly), 14 days (biweekly), one
month (monthly). One-time orders have a single cycle starting at
start_date (or unbounded when start_date is empty). Drafts created
outside this window belong to a previous cycle and must not be reused —
otherwise stale carts come back when the user re-enters the order page.
"""
req = request_obj or request
from datetime import timedelta
from dateutil.relativedelta import relativedelta
if not group_order or not group_order.cutoff_date:
return req.env["sale.order"]
period_end = group_order.cutoff_date
if group_order.period == "weekly":
period_start = period_end - timedelta(days=6)
elif group_order.period == "biweekly":
period_start = period_end - timedelta(days=13)
elif group_order.period == "monthly":
period_start = period_end - relativedelta(months=1) + timedelta(days=1)
else: # once: single cycle, bounded by start_date when set
period_start = group_order.start_date
domain = [
("partner_id", "=", partner_id),
("group_order_id", "=", group_order.id),
("state", "=", "draft"),
("create_date", "<=", f"{period_end} 23:59:59"),
]
if period_start:
domain.append(("create_date", ">=", f"{period_start} 00:00:00"))
return (
req.env["sale.order"].sudo().search(domain, order="create_date desc", limit=1)
)
def _validate_confirm_request(self, data, request_obj=None):
req = request_obj or request
order_id = data.get("order_id")
if not order_id:
raise ValueError("order_id is required")
try:
order_id = int(order_id)
except (ValueError, TypeError) as err:
raise ValueError(f"Invalid order_id format: {order_id}") from err
group_order = req.env["group.order"].sudo().browse(order_id)
if not group_order.exists():
raise ValueError(f"Order {order_id} not found")
if group_order.state != "open":
raise ValueError("Order is not available (not in open state)")
current_user = req.env.user
if not current_user.partner_id:
raise ValueError("User has no associated partner")
_validate_user_group_access(self, group_order, current_user)
items = data.get("items", [])
if not items:
raise ValueError("No items in cart")
_logger.info(
"_validate_confirm_request: Valid request for order %d with %d items",
order_id,
len(items),
)
return order_id, group_order, current_user, items
def _validate_draft_request(self, data, request_obj=None):
req = request_obj or request
order_id = data.get("order_id")
if not order_id:
raise ValueError("order_id is required")
try:
order_id = int(order_id)
except (ValueError, TypeError) as err:
raise ValueError(f"Invalid order_id format: {order_id}") from err
group_order = req.env["group.order"].sudo().browse(order_id)
if not group_order.exists():
raise ValueError(f"Order {order_id} not found")
current_user = req.env.user
if not current_user.partner_id:
raise ValueError("User has no associated partner")
_validate_user_group_access(self, group_order, current_user)
items = data.get("items", [])
if not items:
raise ValueError("No items in cart")
merge_action = data.get("merge_action")
existing_draft_id = data.get("existing_draft_id")
_logger.info(
"_validate_draft_request: Valid request for order %d with %d items (merge_action=%s)",
order_id,
len(items),
merge_action,
)
return (order_id, group_order, current_user, items, merge_action, existing_draft_id)
def _validate_confirm_json(self, data, request_obj=None):
req = request_obj or request
order_id = data.get("order_id")
if not order_id:
raise ValueError("order_id is required")
try:
order_id = int(order_id)
except (ValueError, TypeError) as err:
raise ValueError(f"Invalid order_id format: {order_id}") from err
group_order = req.env["group.order"].sudo().browse(order_id)
if not group_order.exists():
raise ValueError(f"Order {order_id} not found")
if group_order.state != "open":
raise ValueError(f"Order is {group_order.state}")
current_user = req.env.user
if not current_user.partner_id:
raise ValueError("User has no associated partner")
_validate_user_group_access(self, group_order, current_user)
items = data.get("items", [])
if not items:
raise ValueError("No items in cart")
is_delivery = _to_bool(self, data.get("is_delivery", False))
_logger.info(
"_validate_confirm_json: Valid request for order %d with %d items (is_delivery=%s)",
order_id,
len(items),
is_delivery,
)
return order_id, group_order, current_user, items, is_delivery
def _validate_items_for_group_order(self, items, group_order, request_obj=None):
req = request_obj or request
if not items:
return {
"available_items": [],
"unavailable_items": [],
"unavailable_products": set(),
"warning_message": "",
}
try:
available_products = req.env["group.order"]._get_products_for_group_order(
group_order.id
)
available_product_ids = set(available_products.ids)
except Exception as e:
_logger.error(
"Error getting available products for group_order %d: %s", group_order.id, e
)
return {
"available_items": items,
"unavailable_items": [],
"unavailable_products": set(),
"warning_message": "",
}
available_items = []
unavailable_items = []
unavailable_product_ids = set()
for item in items:
product_id = item.get("product_id")
if product_id in available_product_ids:
available_items.append(item)
else:
unavailable_items.append(item)
unavailable_product_ids.add(product_id)
warning_message = ""
if unavailable_items:
unavailable_names = [
item.get("product_name", "Unknown") for item in unavailable_items
]
warning_message = req.env._(
"%(count)d product(s) from your saved order are no longer available in this group order: %(names)s. Only available products will be loaded.",
count=len(unavailable_items),
names=", ".join(unavailable_names),
)
_logger.warning(
"load_order_from_history: %d unavailable items in group_order %d (products: %s)",
len(unavailable_items),
group_order.id,
unavailable_product_ids,
)
return {
"available_items": available_items,
"unavailable_items": unavailable_items,
"unavailable_products": unavailable_product_ids,
"warning_message": warning_message,
}