This commit is contained in:
Home Assistant Version Control
2026-09-25 04:10:30 +00:00
parent 56c80f896e
commit ad634c1b84
60 changed files with 2895 additions and 794 deletions
+60 -4
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from calendar import monthrange
from datetime import date, datetime
from datetime import date, datetime, timedelta
from typing import TYPE_CHECKING
from homeassistant.util import dt as dt_util
@@ -833,6 +833,18 @@ class ChoresMixin:
)
return None
# Weekly target (#883): the same cap one level up. Parents completing on
# behalf are bound by it too — the quota is the point of the chore, and
# the daily limit above already treats them the same way.
if self.weekly_target_met(chore, child_id):
_LOGGER.debug(
"complete_chore no-op: weekly target reached for '%s' (%d/%d this week)",
chore.name,
self.weekly_completion_count(chore_id, child_id),
getattr(chore, "weekly_target", 0),
)
return None
# A photo-required chore always goes through parent approval (unless a
# parent is completing on behalf), so the evidence gets reviewed.
requires_photo = bool(getattr(chore, "require_photo", False))
@@ -869,6 +881,7 @@ class ChoresMixin:
completed_at=now,
approved=auto_approve,
points_awarded=effective_points if auto_approve else 0,
submitted_points=effective_points,
photo_url=photo_url or "",
note=note,
suggested_points=suggested_points,
@@ -972,6 +985,7 @@ class ChoresMixin:
approved=True,
approved_at=now,
points_awarded=0,
submitted_points=0,
)
self.storage.add_completion(completion)
@@ -1058,6 +1072,7 @@ class ChoresMixin:
completed_at=now,
approved=not chore.requires_approval,
points_awarded=subtask.points if not chore.requires_approval else 0,
submitted_points=subtask.points,
bonus_subtask_id=bonus_subtask_id,
)
@@ -1093,8 +1108,9 @@ class ChoresMixin:
worth (#832) — needed for open-ended submissions, useful for any chore.
It replaces the chore's *base* points, so the streak and level
multipliers ``_award_points`` applies still ride on top exactly as they
would for an ordinary approval. ``None`` means "use the chore's value"
and leaves the existing behaviour untouched.
would for an ordinary approval. ``None`` means "pay what the submission
was worth when it was made", falling back to the chore's value for
completions recorded before that was stored.
"""
completions = self.storage.get_completions()
for completion in completions:
@@ -1111,7 +1127,15 @@ class ChoresMixin:
if chore and child:
comp_date = dt_util.as_local(completion.completed_at).date()
is_bonus = bool(completion.bonus_subtask_id)
if is_bonus:
if completion.submitted_points is not None:
# Pay what the child was promised when they submitted.
# Recalculating here reads the chore's *current* points
# — so an edit made while the work sat in the queue
# changed the price after the fact — and silently drops
# the speed bonus and roulette multiplier that were in
# the figure they saw.
pts = completion.submitted_points
elif is_bonus:
subtask = next((b for b in chore.bonus_subtasks if b.id == completion.bonus_subtask_id), None)
pts = subtask.points if subtask else 0
elif completion.timed_duration_seconds > 0 and chore.task_type == "timed":
@@ -1601,6 +1625,34 @@ class ChoresMixin:
days_since = (today - last_dt).days
return days_since >= window_days
def weekly_completion_count(self, chore_id: str, child_id: str) -> int:
"""How many times ``child_id`` has completed ``chore_id`` this week.
Monday-anchored, matching the Challenges period convention. Bonus
sub-tasks don't count; pending completions do, so a chore can't be
submitted a fourth time just because the parent hasn't got round to
approving the third yet.
"""
today = dt_util.as_local(dt_util.now()).date()
week_start = today - timedelta(days=today.weekday())
count = 0
for comp in self._cached_completions_for_chore(chore_id):
if comp.child_id != child_id or getattr(comp, "bonus_subtask_id", ""):
continue
try:
if dt_util.as_local(comp.completed_at).date() >= week_start:
count += 1
except (AttributeError, TypeError, ValueError):
continue
return count
def weekly_target_met(self, chore, child_id: str) -> bool:
"""True when a weekly-target chore has had its quota filled this week."""
target = int(getattr(chore, "weekly_target", 0) or 0)
if target <= 0:
return False
return self.weekly_completion_count(chore.id, child_id) >= target
def _is_chore_completable_by_child(self, chore, child_id: str) -> bool:
"""Whether ``child_id`` may complete ``chore`` right now (card parity).
@@ -1657,6 +1709,10 @@ class ChoresMixin:
continue
if done >= limit:
continue
# Weekly target (#883): filled the week's quota, so nothing is
# owed until Monday even on a day the chore is scheduled.
if self.weekly_target_met(chore, child_id):
continue
out.append(chore)
return out