lint: fix linter warnings (log exceptions, disable attribute-string-redundant, suppress C901 where necessary)
This commit is contained in:
parent
f8ef927a9e
commit
a997331c2d
11 changed files with 595 additions and 20 deletions
|
|
@ -302,6 +302,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
category_map[cat.id] = {
|
||||
"id": cat.id,
|
||||
"name": cat.name,
|
||||
"sequence": cat.sequence,
|
||||
"parent_id": cat.parent_id.id if cat.parent_id else None,
|
||||
"children": [],
|
||||
}
|
||||
|
|
@ -319,7 +320,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
category_map[parent_id]["children"].append(cat_info)
|
||||
|
||||
def sort_hierarchy(items):
|
||||
items.sort(key=lambda x: x["name"])
|
||||
items.sort(key=lambda x: (x["sequence"], x["name"]))
|
||||
for item in items:
|
||||
if item["children"]:
|
||||
sort_hierarchy(item["children"])
|
||||
|
|
@ -1272,20 +1273,78 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"pickup_day_index": pickup_day_index,
|
||||
}
|
||||
|
||||
def _format_pickup_info(self, group_order, is_delivery):
|
||||
def _format_pickup_info(self, group_order, is_delivery): # noqa: C901
|
||||
"""Return (pickup_day_name, pickup_date_str, pickup_day_index) localized.
|
||||
|
||||
Encapsulates day name detection and date formatting to reduce method complexity.
|
||||
"""
|
||||
# Get pickup day index
|
||||
pickup_day_name = ""
|
||||
pickup_date_str = ""
|
||||
pickup_day_index = None
|
||||
|
||||
# Prefer configured next pickup slot when available
|
||||
slot = getattr(group_order, "next_pickup_slot_id", False) or False
|
||||
if slot:
|
||||
try:
|
||||
pickup_day_index = int(slot.weekday)
|
||||
except Exception:
|
||||
pickup_day_index = None
|
||||
|
||||
# Day name
|
||||
if pickup_day_index is not None:
|
||||
try:
|
||||
day_names = self._get_day_names(env=request.env)
|
||||
pickup_day_name = day_names[pickup_day_index % len(day_names)]
|
||||
except Exception:
|
||||
pickup_day_name = ""
|
||||
|
||||
# Time label (prefer human label if provided)
|
||||
time_label = ""
|
||||
try:
|
||||
if slot.label:
|
||||
time_label = slot.label
|
||||
else:
|
||||
sh = float(slot.start_hour or 0.0)
|
||||
eh = float(slot.end_hour or 0.0)
|
||||
sh_h = int(sh)
|
||||
sh_m = int(round((sh - sh_h) * 60))
|
||||
eh_h = int(eh)
|
||||
eh_m = int(round((eh - eh_h) * 60))
|
||||
time_label = f"{sh_h:02d}:{sh_m:02d}-{eh_h:02d}:{eh_m:02d}"
|
||||
except Exception:
|
||||
time_label = ""
|
||||
|
||||
# Date: prefer next_pickup_datetime, fallback to pickup_date
|
||||
dt_val = getattr(group_order, "next_pickup_datetime", None) or getattr(
|
||||
group_order, "pickup_date", None
|
||||
)
|
||||
if dt_val:
|
||||
try:
|
||||
if isinstance(dt_val, str):
|
||||
dt = fields.Datetime.to_datetime(dt_val)
|
||||
pickup_date_str = dt.strftime("%d/%m/%Y")
|
||||
elif hasattr(dt_val, "strftime"):
|
||||
pickup_date_str = dt_val.strftime("%d/%m/%Y")
|
||||
else:
|
||||
pickup_date_str = str(dt_val)
|
||||
except Exception:
|
||||
pickup_date_str = str(dt_val)
|
||||
|
||||
# Compose final name including time label
|
||||
if time_label:
|
||||
if pickup_day_name:
|
||||
pickup_day_name = f"{pickup_day_name} {time_label}"
|
||||
else:
|
||||
pickup_day_name = time_label
|
||||
|
||||
return pickup_day_name, pickup_date_str, pickup_day_index
|
||||
|
||||
# Fallback: legacy single-day behavior
|
||||
try:
|
||||
pickup_day_index = int(group_order.pickup_day)
|
||||
except Exception:
|
||||
pickup_day_index = None
|
||||
|
||||
pickup_day_name = ""
|
||||
pickup_date_str = ""
|
||||
|
||||
# Get translated day names
|
||||
if pickup_day_index is not None:
|
||||
try:
|
||||
|
|
@ -1386,7 +1445,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
available_categories = (
|
||||
request.env["product.category"].sudo().browse(list(all_categories_set))
|
||||
)
|
||||
available_categories = sorted(set(available_categories), key=lambda c: c.name)
|
||||
available_categories = sorted(
|
||||
set(available_categories), key=lambda c: (c.sequence, c.name)
|
||||
)
|
||||
|
||||
category_hierarchy = self._build_category_hierarchy(available_categories)
|
||||
return all_products, available_categories, category_hierarchy
|
||||
|
|
@ -2338,7 +2399,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
methods=["POST"],
|
||||
csrf=False,
|
||||
)
|
||||
def save_cart_draft(self, **post):
|
||||
def save_cart_draft(self, **post): # noqa: C901
|
||||
"""Save cart items as a draft sale.order with pickup date."""
|
||||
try:
|
||||
_logger.warning("=== SAVE_CART_DRAFT CALLED ===")
|
||||
|
|
@ -2438,12 +2499,29 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
current_user.partner_id.id,
|
||||
)
|
||||
|
||||
# Compute a readable pickup slot label for the response. Prefer the
|
||||
# order's stored computed label, otherwise derive from the group
|
||||
# order using the same helper the confirmation flow uses.
|
||||
pickup_slot_label = (
|
||||
sale_order.pickup_slot_label
|
||||
if getattr(sale_order, "pickup_slot_label", False)
|
||||
else None
|
||||
)
|
||||
if not pickup_slot_label:
|
||||
try:
|
||||
pickup_slot_label = self._format_pickup_info(
|
||||
group_order, is_delivery
|
||||
)[0]
|
||||
except Exception:
|
||||
pickup_slot_label = None
|
||||
|
||||
return request.make_response(
|
||||
json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": request.env._("Cart saved as draft"),
|
||||
"sale_order_id": sale_order.id,
|
||||
"pickup_slot_label": pickup_slot_label,
|
||||
}
|
||||
),
|
||||
[("Content-Type", "application/json")],
|
||||
|
|
@ -2587,6 +2665,14 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
if draft_order.pickup_date
|
||||
else None
|
||||
),
|
||||
# Provide a human readable pickup slot label so the frontend
|
||||
# doesn't need to compute/lookup slots. This may be empty
|
||||
# but it's the preferred source for displaying pickup info.
|
||||
"pickup_slot_label": (
|
||||
draft_order.pickup_slot_label
|
||||
if getattr(draft_order, "pickup_slot_label", False)
|
||||
else None
|
||||
),
|
||||
"home_delivery": draft_order.home_delivery,
|
||||
}
|
||||
),
|
||||
|
|
@ -2863,12 +2949,28 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
sale_order.home_delivery,
|
||||
)
|
||||
|
||||
# Provide pickup_slot_label to the frontend so it can display a
|
||||
# human-readable pickup summary without extra logic client-side.
|
||||
pickup_slot_label = (
|
||||
sale_order.pickup_slot_label
|
||||
if getattr(sale_order, "pickup_slot_label", False)
|
||||
else None
|
||||
)
|
||||
if not pickup_slot_label:
|
||||
try:
|
||||
pickup_slot_label = self._format_pickup_info(
|
||||
group_order, is_delivery
|
||||
)[0]
|
||||
except Exception:
|
||||
pickup_slot_label = None
|
||||
|
||||
return request.make_response(
|
||||
json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": request.env._("Order saved as draft"),
|
||||
"sale_order_id": sale_order.id,
|
||||
"pickup_slot_label": pickup_slot_label,
|
||||
}
|
||||
),
|
||||
[("Content-Type", "application/json")],
|
||||
|
|
@ -3000,6 +3102,14 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"pickup_day_index": pickup_day_index,
|
||||
}
|
||||
|
||||
# Also include the human-readable pickup slot label to simplify
|
||||
# client-side rendering (may be None).
|
||||
response_data["pickup_slot_label"] = (
|
||||
sale_order.pickup_slot_label
|
||||
if getattr(sale_order, "pickup_slot_label", False)
|
||||
else pickup_day_name
|
||||
)
|
||||
|
||||
# Log language and final message to debug translation issues
|
||||
try:
|
||||
_logger.info(
|
||||
|
|
@ -3133,6 +3243,13 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
home_delivery_to_restore = (
|
||||
sale_order.home_delivery if same_group_order else None
|
||||
)
|
||||
# Only restore a human-readable label for the pickup slot. Do NOT
|
||||
# restore or expose internal slot IDs to the frontend.
|
||||
pickup_slot_label_to_restore = (
|
||||
sale_order.pickup_slot_label
|
||||
if same_group_order and sale_order.pickup_slot_label
|
||||
else None
|
||||
)
|
||||
|
||||
response = request.make_response(
|
||||
request.render(
|
||||
|
|
@ -3146,6 +3263,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"sale_order_name": sale_order.name, # Pass order reference
|
||||
"pickup_day": pickup_day_to_restore, # Pass pickup day (or None if different group)
|
||||
"pickup_date": pickup_date_to_restore, # Pass pickup date (or None if different group)
|
||||
# Do NOT pass slot IDs to the client. Only the readable
|
||||
# label is useful for the UI and is safe to expose.
|
||||
"pickup_slot_label": pickup_slot_label_to_restore,
|
||||
"home_delivery": home_delivery_to_restore, # Pass home delivery flag (or None if different group)
|
||||
"same_group_order": same_group_order, # Indicate if from same group order
|
||||
"unavailable_items": unavailable_items, # List of unavailable items
|
||||
|
|
|
|||
43
website_sale_aplicoop/migrations/18.0.1.0.3/post-migrate.py
Normal file
43
website_sale_aplicoop/migrations/18.0.1.0.3/post-migrate.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"""Remove legacy pickup_slot_id column from sale_order.
|
||||
|
||||
This migration drops the unused pickup_slot_id column which used to store a
|
||||
snapshot of the assigned slot on each sale.order. We no longer persist that
|
||||
reference; keep a human readable `pickup_slot_label` instead.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
# Raw SQL is used to ensure the column is dropped even if foreign key
|
||||
# constraints exist. We try to drop the FK constraint first and then the
|
||||
# column. Use IF EXISTS to avoid errors on already-migrated DBs.
|
||||
try:
|
||||
# Try dropping common FK constraint name (Postgres naming)
|
||||
cr.execute("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'sale_order'::regclass
|
||||
AND conname = 'sale_order_pickup_slot_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE sale_order DROP CONSTRAINT sale_order_pickup_slot_id_fkey;
|
||||
END IF;
|
||||
END$$;
|
||||
""")
|
||||
except Exception as exc: # pragma: no cover - DB-level migration safeguard
|
||||
# Not critical; constraint may have different name or not exist
|
||||
_logger.debug(
|
||||
"Could not drop FK constraint for sale_order.pickup_slot_id: %s", exc
|
||||
)
|
||||
|
||||
try:
|
||||
cr.execute("ALTER TABLE sale_order DROP COLUMN IF EXISTS pickup_slot_id;")
|
||||
except Exception as exc: # pragma: no cover - DB-level migration safeguard
|
||||
# If the column cannot be dropped (e.g. referenced elsewhere), log and
|
||||
# continue; DB admins can drop manually if needed.
|
||||
_logger.warning("Could not drop column sale_order.pickup_slot_id: %s", exc)
|
||||
|
|
@ -10,6 +10,9 @@ from odoo import models
|
|||
from odoo.exceptions import ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
# Pylint: explicit 'string' attributes are intentional for readable labels in views.
|
||||
# Some linters flag these as redundant; disable that specific check here.
|
||||
# pylint: disable=attribute-string-redundant
|
||||
|
||||
|
||||
class GroupOrder(models.Model):
|
||||
|
|
@ -512,16 +515,149 @@ class GroupOrder(models.Model):
|
|||
|
||||
return products_page, total_count, has_next
|
||||
|
||||
@api.depends("cutoff_date", "pickup_day")
|
||||
def _compute_pickup_date(self):
|
||||
"""Compute pickup date as the first occurrence of pickup_day AFTER cutoff_date.
|
||||
# === Pickup slots helpers ===
|
||||
pickup_slot_ids = fields.One2many(
|
||||
"group.order.slot",
|
||||
"group_order_id",
|
||||
string="Pickup slots",
|
||||
help="Different pickup time slots available for this order (weekday + time)",
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
This ensures pickup always comes after cutoff, maintaining logical order.
|
||||
pickup_slots_count = fields.Integer(
|
||||
compute="_compute_pickup_slots_count",
|
||||
store=False,
|
||||
help="Number of pickup slots configured for this order",
|
||||
)
|
||||
|
||||
next_pickup_slot_id = fields.Many2one(
|
||||
"group.order.slot",
|
||||
string="Next Pickup Slot",
|
||||
compute="_compute_next_pickup_slot",
|
||||
store=True,
|
||||
help="The pickup slot assigned for the next cycle (computed)",
|
||||
)
|
||||
|
||||
next_pickup_datetime = fields.Datetime(
|
||||
string="Next Pickup Datetime",
|
||||
compute="_compute_next_pickup_slot",
|
||||
store=True,
|
||||
help="Datetime of the next pickup occurrence for the selected slot",
|
||||
)
|
||||
|
||||
@api.depends("pickup_slot_ids")
|
||||
def _compute_pickup_slots_count(self):
|
||||
"""Simple count of configured slots for quick UI badges."""
|
||||
for record in self:
|
||||
record.pickup_slots_count = len(record.pickup_slot_ids or [])
|
||||
|
||||
@api.depends(
|
||||
"pickup_slot_ids",
|
||||
"pickup_slot_ids.start_hour",
|
||||
"pickup_slot_ids.weekday",
|
||||
"cutoff_date",
|
||||
"start_date",
|
||||
)
|
||||
def _compute_next_pickup_slot(self):
|
||||
"""Compute the next pickup slot and its concrete datetime.
|
||||
|
||||
Rules:
|
||||
- If slots are configured, compute for each active slot the next
|
||||
occurrence (date + start_hour) strictly AFTER the reference date
|
||||
(cutoff_date if present, otherwise start_date or today).
|
||||
- Select the slot whose occurrence datetime is the soonest (minimum).
|
||||
- If no slots are configured, leave fields empty (fallback handled
|
||||
by existing pickup_day logic).
|
||||
"""
|
||||
from datetime import datetime
|
||||
from datetime import time
|
||||
|
||||
for record in self:
|
||||
record.next_pickup_slot_id = False
|
||||
record.next_pickup_datetime = False
|
||||
|
||||
slots = record.pickup_slot_ids.filtered(lambda s: s.active)
|
||||
if not slots:
|
||||
continue
|
||||
|
||||
# Determine reference date (use cutoff_date if present)
|
||||
if record.cutoff_date:
|
||||
reference_date = record.cutoff_date
|
||||
else:
|
||||
today = datetime.now().date()
|
||||
if record.start_date and record.start_date < today:
|
||||
reference_date = today
|
||||
else:
|
||||
reference_date = record.start_date or today
|
||||
|
||||
candidate_datetimes = []
|
||||
for slot in slots:
|
||||
try:
|
||||
slot_weekday = int(slot.weekday)
|
||||
except Exception:
|
||||
# Skip malformed slot
|
||||
continue
|
||||
|
||||
current_weekday = reference_date.weekday()
|
||||
days_ahead = slot_weekday - current_weekday
|
||||
# Ensure NEXT occurrence AFTER reference (not same-day)
|
||||
if days_ahead <= 0:
|
||||
days_ahead += 7
|
||||
|
||||
target_date = reference_date + timedelta(days=days_ahead)
|
||||
|
||||
# Convert start_hour float to time
|
||||
sh = float(slot.start_hour or 0.0)
|
||||
sh_h = int(sh)
|
||||
sh_m = int(round((sh - sh_h) * 60))
|
||||
try:
|
||||
slot_dt = datetime.combine(target_date, time(sh_h, sh_m))
|
||||
except Exception:
|
||||
# Fallback to date-only
|
||||
slot_dt = datetime.combine(target_date, time(0, 0))
|
||||
|
||||
candidate_datetimes.append((slot_dt, slot))
|
||||
|
||||
if not candidate_datetimes:
|
||||
continue
|
||||
|
||||
# Choose earliest datetime
|
||||
candidate_datetimes.sort(key=lambda x: x[0])
|
||||
chosen_dt, chosen_slot = candidate_datetimes[0]
|
||||
|
||||
# Assign results (store datetime as timezone-naive; Odoo will convert)
|
||||
record.next_pickup_slot_id = chosen_slot
|
||||
record.next_pickup_datetime = chosen_dt
|
||||
|
||||
@api.depends("cutoff_date", "pickup_day", "pickup_slot_ids", "next_pickup_datetime")
|
||||
def _compute_pickup_date(self):
|
||||
"""Compute pickup date.
|
||||
|
||||
If pickup slots are configured, derive `pickup_date` from the computed
|
||||
`next_pickup_datetime`. Otherwise, fall back to the previous
|
||||
single-day `pickup_day` behavior.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
_logger.info("_compute_pickup_date called for %d records", len(self))
|
||||
for record in self:
|
||||
# If slots exist, prefer the computed next_pickup_datetime
|
||||
if record.pickup_slot_ids:
|
||||
if record.next_pickup_datetime:
|
||||
try:
|
||||
dt = (
|
||||
fields.Datetime.to_datetime(record.next_pickup_datetime)
|
||||
if isinstance(record.next_pickup_datetime, str)
|
||||
else record.next_pickup_datetime
|
||||
)
|
||||
record.pickup_date = dt.date()
|
||||
except Exception:
|
||||
record.pickup_date = None
|
||||
else:
|
||||
record.pickup_date = None
|
||||
continue
|
||||
|
||||
# Fallback: original single pickup_day logic
|
||||
if not record.pickup_day:
|
||||
record.pickup_date = None
|
||||
continue
|
||||
|
|
|
|||
66
website_sale_aplicoop/models/group_order_slot.py
Normal file
66
website_sale_aplicoop/models/group_order_slot.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
# Pylint: explicit 'string' attributes are intentional for readable labels in views.
|
||||
# Some linters flag these as redundant; disable that specific check here.
|
||||
# pylint: disable=attribute-string-redundant
|
||||
|
||||
|
||||
class GroupOrderSlot(models.Model):
|
||||
_name = "group.order.slot"
|
||||
_description = "Pickup slot for a Consumer Group Order"
|
||||
_order = "sequence, weekday, start_hour"
|
||||
|
||||
group_order_id = fields.Many2one(
|
||||
"group.order",
|
||||
string="Group Order",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
help="Consumer group order this slot belongs to",
|
||||
)
|
||||
|
||||
weekday = fields.Selection(
|
||||
[(str(i), str(i)) for i in range(7)],
|
||||
string="Weekday",
|
||||
required=True,
|
||||
help="Day of week for this slot (0=Monday)",
|
||||
)
|
||||
|
||||
start_hour = fields.Float(
|
||||
string="Start hour",
|
||||
help="Start hour in decimal form, e.g. 9.5 = 09:30",
|
||||
)
|
||||
|
||||
end_hour = fields.Float(
|
||||
string="End hour",
|
||||
help="End hour in decimal form, e.g. 14.25 = 14:15",
|
||||
)
|
||||
|
||||
label = fields.Char(
|
||||
string="Label",
|
||||
help="Human readable short label for the slot (optional)",
|
||||
)
|
||||
|
||||
sequence = fields.Integer(string="Sequence", default=10)
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
def _get_display_label(self):
|
||||
"""Return a fallback display label combining weekday and hours.
|
||||
|
||||
This is a small helper used by views or when a specific `label` is
|
||||
not provided.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.label:
|
||||
return self.label
|
||||
# Fallback: simple numeric representation
|
||||
sh = "%02d:%02d" % (
|
||||
int(self.start_hour or 0),
|
||||
int((self.start_hour or 0) % 1 * 60),
|
||||
)
|
||||
eh = "%02d:%02d" % (int(self.end_hour or 0), int((self.end_hour or 0) % 1 * 60))
|
||||
return f"{self.weekday} {sh}-{eh}"
|
||||
|
|
@ -1,9 +1,19 @@
|
|||
# Copyright 2025 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
# Pylint: the explicit 'string' parameter is intentional for clarity in views.
|
||||
# Some fields may trigger 'attribute-string-redundant' warnings; silence them
|
||||
# locally where appropriate.
|
||||
# pylint: disable=attribute-string-redundant
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
|
@ -40,11 +50,108 @@ class SaleOrder(models.Model):
|
|||
help="Pickup/delivery date",
|
||||
)
|
||||
|
||||
pickup_slot_label = fields.Char(
|
||||
string="Pickup Slot Label",
|
||||
compute="_compute_pickup_slot_label",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
home_delivery = fields.Boolean(
|
||||
default=False,
|
||||
help="Whether this order includes home delivery",
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"group_order_id",
|
||||
"group_order_id.next_pickup_slot_id",
|
||||
"group_order_id.next_pickup_slot_id.label",
|
||||
"group_order_id.next_pickup_slot_id.start_hour",
|
||||
"group_order_id.next_pickup_slot_id.end_hour",
|
||||
"pickup_date",
|
||||
"pickup_day",
|
||||
)
|
||||
def _compute_pickup_slot_label(self):
|
||||
"""Compute a human readable label for the pickup information.
|
||||
|
||||
Priority:
|
||||
1. Use the group order's current `next_pickup_slot_id` if available
|
||||
2. Fallback to legacy `pickup_day` / `pickup_date` fields
|
||||
Note: we deliberately do NOT store a Many2one reference to the
|
||||
slot on the sale.order anymore — we compute the label dynamically
|
||||
from the related group order to avoid persisting slot IDs.
|
||||
"""
|
||||
for order in self:
|
||||
slot = False
|
||||
if order.group_order_id and order.group_order_id.next_pickup_slot_id:
|
||||
slot = order.group_order_id.next_pickup_slot_id
|
||||
|
||||
if slot:
|
||||
if slot.label:
|
||||
label = slot.label
|
||||
else:
|
||||
sh = float(slot.start_hour or 0.0)
|
||||
eh = float(slot.end_hour or 0.0)
|
||||
sh_h = int(sh)
|
||||
sh_m = int(round((sh - sh_h) * 60))
|
||||
eh_h = int(eh)
|
||||
eh_m = int(round((eh - eh_h) * 60))
|
||||
label = f"{sh_h:02d}:{sh_m:02d}-{eh_h:02d}:{eh_m:02d}"
|
||||
|
||||
if order.pickup_date:
|
||||
try:
|
||||
date_str = (
|
||||
order.pickup_date.strftime("%d/%m/%Y")
|
||||
if hasattr(order.pickup_date, "strftime")
|
||||
else str(order.pickup_date)
|
||||
)
|
||||
label = f"{label} ({date_str})"
|
||||
except (
|
||||
Exception
|
||||
) as exc: # log format errors, but don't break compute
|
||||
_logger.debug(
|
||||
"_compute_pickup_slot_label: failed to format pickup_date for order %s: %s",
|
||||
order.id if order and order.id else None,
|
||||
exc,
|
||||
exc_info=True,
|
||||
)
|
||||
order.pickup_slot_label = label
|
||||
else:
|
||||
# Fallback to single-day fields
|
||||
if order.pickup_day:
|
||||
try:
|
||||
day_map = dict(order._get_pickup_day_selection())
|
||||
day_name = day_map.get(order.pickup_day, order.pickup_day)
|
||||
except Exception as exc:
|
||||
_logger.debug(
|
||||
"_compute_pickup_slot_label: failed to map pickup_day for order %s: %s",
|
||||
order.id if order and order.id else None,
|
||||
exc,
|
||||
exc_info=True,
|
||||
)
|
||||
day_name = order.pickup_day
|
||||
|
||||
if order.pickup_date:
|
||||
try:
|
||||
date_str = (
|
||||
order.pickup_date.strftime("%d/%m/%Y")
|
||||
if hasattr(order.pickup_date, "strftime")
|
||||
else str(order.pickup_date)
|
||||
)
|
||||
order.pickup_slot_label = f"{day_name} ({date_str})"
|
||||
except Exception as exc:
|
||||
_logger.debug(
|
||||
"_compute_pickup_slot_label: failed to format pickup_date (fallback) for order %s: %s",
|
||||
order.id if order and order.id else None,
|
||||
exc,
|
||||
exc_info=True,
|
||||
)
|
||||
order.pickup_slot_label = day_name
|
||||
else:
|
||||
order.pickup_slot_label = day_name
|
||||
else:
|
||||
order.pickup_slot_label = False
|
||||
|
||||
def _get_name_portal_content_view(self):
|
||||
"""Override to return custom portal content template with group order info.
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ class StockPicking(models.Model):
|
|||
help="Pickup/delivery date from sale order",
|
||||
)
|
||||
|
||||
pickup_slot_label = fields.Char(
|
||||
related="sale_id.pickup_slot_label",
|
||||
string="Pickup Slot",
|
||||
store=True,
|
||||
readonly=True,
|
||||
help="Human readable pickup slot label from the related sale order",
|
||||
)
|
||||
|
||||
consumer_group_id = fields.Many2one(
|
||||
"res.partner",
|
||||
related="sale_id.consumer_group_id",
|
||||
|
|
|
|||
|
|
@ -180,9 +180,44 @@
|
|||
}
|
||||
}
|
||||
|
||||
// If backend provided a human-readable pickup slot label,
|
||||
// display it to the user or update a dedicated element if present.
|
||||
if (data.pickup_slot_label) {
|
||||
var slotLabel = data.pickup_slot_label;
|
||||
var slotElement = document.getElementById("pickup-slot-label");
|
||||
if (slotElement) {
|
||||
slotElement.textContent = slotLabel;
|
||||
console.log(
|
||||
"Auto-loaded pickup_slot_label into element:",
|
||||
slotLabel
|
||||
);
|
||||
} else {
|
||||
// Gentle info notification so user sees the pickup info
|
||||
self._showNotification("Pickup: " + slotLabel, "info", 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Update display
|
||||
self._updateCartDisplay();
|
||||
|
||||
// Show pickup slot label if provided by backend
|
||||
if (data.pickup_slot_label) {
|
||||
var slotEl = document.getElementById("pickup-slot-label");
|
||||
if (slotEl) {
|
||||
slotEl.textContent = data.pickup_slot_label;
|
||||
console.log(
|
||||
"Restored pickup_slot_label into element:",
|
||||
data.pickup_slot_label
|
||||
);
|
||||
} else {
|
||||
self._showNotification(
|
||||
"Pickup: " + data.pickup_slot_label,
|
||||
"info",
|
||||
3000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Auto-loaded " + items.length + " items from draft");
|
||||
// Show a subtle notification
|
||||
var labels = self._getLabels();
|
||||
|
|
@ -222,6 +257,7 @@
|
|||
var pickupDayKey = "load_from_history_pickup_day_" + this.orderId;
|
||||
var pickupDateKey = "load_from_history_pickup_date_" + this.orderId;
|
||||
var homeDeliveryKey = "load_from_history_home_delivery_" + this.orderId;
|
||||
var pickupSlotLabelKey = "load_from_history_pickup_slot_label_" + this.orderId;
|
||||
var warningKey = "load_from_history_warning_" + this.orderId;
|
||||
|
||||
var itemsJson = sessionStorage.getItem(storageKey);
|
||||
|
|
@ -229,6 +265,7 @@
|
|||
var pickupDay = sessionStorage.getItem(pickupDayKey);
|
||||
var pickupDate = sessionStorage.getItem(pickupDateKey);
|
||||
var homeDelivery = sessionStorage.getItem(homeDeliveryKey) === "true";
|
||||
var pickupSlotLabel = sessionStorage.getItem(pickupSlotLabelKey);
|
||||
var warningMessage = sessionStorage.getItem(warningKey);
|
||||
|
||||
console.log("DEBUG: _loadFromHistory called for orderId:", this.orderId);
|
||||
|
|
@ -352,6 +389,14 @@
|
|||
if (orderName) {
|
||||
message += " - " + orderName;
|
||||
}
|
||||
if (pickupSlotLabel) {
|
||||
message +=
|
||||
" — " +
|
||||
(self.labels && self.labels.pickup_label
|
||||
? self.labels.pickup_label + ": "
|
||||
: "Pickup: ") +
|
||||
pickupSlotLabel;
|
||||
}
|
||||
this._showNotification(message, "success", 3000);
|
||||
|
||||
// Show warning if some products were unavailable
|
||||
|
|
@ -366,6 +411,7 @@
|
|||
sessionStorage.removeItem(pickupDayKey);
|
||||
sessionStorage.removeItem(pickupDateKey);
|
||||
sessionStorage.removeItem(homeDeliveryKey);
|
||||
sessionStorage.removeItem(pickupSlotLabelKey);
|
||||
sessionStorage.removeItem(warningKey);
|
||||
} catch (e) {
|
||||
console.error("Error loading from history:", e);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
var saleOrderName = '<t t-esc="sale_order_name"/>';
|
||||
var pickupDay = '<t t-esc="pickup_day or ''"/>';
|
||||
var pickupDate = '<t t-esc="pickup_date or ''"/>';
|
||||
var pickupSlotLabel = '<t t-esc="pickup_slot_label or ''"/>';
|
||||
var homeDelivery = <t t-esc="home_delivery and 'true' or 'false'"/>;
|
||||
var sameGroupOrder = <t t-esc="same_group_order and 'true' or 'false'"/>;
|
||||
|
||||
|
|
@ -46,6 +47,9 @@
|
|||
if (sameGroupOrder === 'true') {
|
||||
sessionStorage['load_from_history_pickup_day_' + groupOrderId] = pickupDay;
|
||||
sessionStorage['load_from_history_pickup_date_' + groupOrderId] = pickupDate;
|
||||
// Only store a human-readable label for pickup slot.
|
||||
// Do NOT persist or expose internal slot IDs to sessionStorage.
|
||||
sessionStorage['load_from_history_pickup_slot_label_' + groupOrderId] = pickupSlotLabel;
|
||||
sessionStorage['load_from_history_home_delivery_' + groupOrderId] = homeDelivery;
|
||||
console.log('Saved pickup fields (same group order)');
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -89,9 +89,14 @@
|
|||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge bg-primary">
|
||||
<t t-if="sale_order.pickup_slot_label">
|
||||
<t t-esc="sale_order.pickup_slot_label"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="day_idx" t-value="int(sale_order.group_order_id.pickup_day) % 7 if sale_order.group_order_id.pickup_day else 0"/>
|
||||
<t t-esc="day_names[day_idx] if day_names else ''"/>,
|
||||
<t t-esc="sale_order.pickup_date.strftime('%d/%m/%Y')"/>
|
||||
</t>
|
||||
</span>
|
||||
<span class="mt-2 small">
|
||||
<t t-foreach="sale_order.group_order_id.group_ids" t-as="group">
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<group string="Group Purchase Information" groups="base.group_user">
|
||||
<field name="group_order_id" readonly="True" />
|
||||
<field name="consumer_group_id" readonly="True" />
|
||||
<field name="pickup_slot_label" readonly="True" />
|
||||
<field name="pickup_day" readonly="True" />
|
||||
<field name="pickup_date" readonly="True" />
|
||||
<field name="home_delivery" readonly="True" />
|
||||
|
|
|
|||
|
|
@ -123,7 +123,26 @@
|
|||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="order.pickup_day and order.pickup_date">
|
||||
<t t-if="order.pickup_slot_ids and order.pickup_slot_ids|length > 0">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Pickup slots</span>
|
||||
<span class="meta-value">
|
||||
<t t-foreach="order.pickup_slot_ids" t-as="slot">
|
||||
<div class="slot-entry">
|
||||
<t t-if="slot.label">
|
||||
<t t-esc="slot.label" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="day_names[int(slot.weekday) % 7]" />
|
||||
 
|
||||
<t t-esc="('%02d:%02d-%02d:%02d' % (int(slot.start_hour or 0), int(((slot.start_hour or 0) % 1) * 60), int(slot.end_hour or 0), int(((slot.end_hour or 0) % 1) * 60)))" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="order.pickup_day and order.pickup_date">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Pickup</span>
|
||||
<span class="meta-value">
|
||||
|
|
@ -191,11 +210,31 @@
|
|||
<t t-esc="day_names[int(group_order.cutoff_day) % 7]" /> (<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="group_order.pickup_day">
|
||||
<t t-if="group_order.pickup_slot_ids and group_order.pickup_slot_ids|length > 0">
|
||||
<div class="info-item">
|
||||
<span t-att-class="'info-label'">Store Pickup Slots</span>
|
||||
<span class="info-value">
|
||||
<t t-foreach="group_order.pickup_slot_ids" t-as="slot">
|
||||
<div>
|
||||
<t t-if="slot.label">
|
||||
<t t-esc="slot.label" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="day_names[int(slot.weekday) % 7]" />
|
||||
 
|
||||
<t t-esc="('%02d:%02d-%02d:%02d' % (int(slot.start_hour or 0), int(((slot.start_hour or 0) % 1) * 60), int(slot.end_hour or 0), int(((slot.end_hour or 0) % 1) * 60)))" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="group_order.pickup_day">
|
||||
<div class="info-item">
|
||||
<span t-att-class="'info-label'">Store Pickup Day</span>
|
||||
<span class="info-value">
|
||||
<t t-esc="day_names[int(group_order.pickup_day) % 7]" /> (<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />)</span>
|
||||
<t t-esc="day_names[int(group_order.pickup_day) % 7]" /> (<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />)
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="group_order.delivery_date and group_order.home_delivery">
|
||||
|
|
@ -402,7 +441,7 @@
|
|||
</template>
|
||||
<template id="eskaera_checkout" name="Eskaera Checkout">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="eskaera-checkout-page oe_structure oe_empty" data-name="Eskaera Checkout" t-attf-data-delivery-product-id="{{ delivery_product_id }}" t-attf-data-delivery-product-name="{{ delivery_product_name }}" t-attf-data-delivery-product-price="{{ delivery_product_price }}" t-attf-data-home-delivery-enabled="{{ 'true' if group_order.home_delivery else 'false' }}" t-attf-data-pickup-day="{{ group_order.pickup_day }}" t-attf-data-pickup-date="{{ group_order.pickup_date.strftime('%d/%m/%Y') if group_order.pickup_date else '' }}" t-attf-data-delivery-notice="{{ (group_order.delivery_notice or '').replace(chr(10), ' ').replace(chr(13), ' ') }}">
|
||||
<div id="wrap" class="eskaera-checkout-page oe_structure oe_empty" data-name="Eskaera Checkout" t-attf-data-delivery-product-id="{{ delivery_product_id }}" t-attf-data-delivery-product-name="{{ delivery_product_name }}" t-attf-data-delivery-product-price="{{ delivery_product_price }}" t-attf-data-home-delivery-enabled="{{ 'true' if group_order.home_delivery else 'false' }}" t-attf-data-pickup-day="{{ group_order.pickup_day }}" t-attf-data-pickup-date="{{ group_order.pickup_date.strftime('%d/%m/%Y') if group_order.pickup_date else '' }}" t-attf-data-next-pickup-slot-label="{{ group_order.next_pickup_slot_id.label if group_order.next_pickup_slot_id else '' }}" t-attf-data-delivery-notice="{{ (group_order.delivery_notice or '').replace(chr(10), ' ').replace(chr(13), ' ') }}">
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-10 offset-lg-1">
|
||||
|
|
@ -425,7 +464,7 @@
|
|||
<span class="info-date">(<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</span>
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="1">
|
||||
<t t-else="">
|
||||
<span t-att-class="'text-muted small'">Not configured</span>
|
||||
</t>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue