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
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue