Fix cost/egg window to use later of first egg or first feed event
All checks were successful
Deploy / deploy (push) Successful in 1m39s
All checks were successful
Deploy / deploy (push) Successful in 1m39s
When egg data is imported but feed data starts later, cost/egg was incorrectly using the egg window (e.g., 30 days) instead of the period where both data types exist. Now cost/egg uses max(first_egg, first_feed) to ensure accurate cost calculation. Each metric now displays its own window: "4.7 eggs/day (30d) | €0.28/egg (7d)" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -184,7 +184,8 @@ def _get_global_cost_per_egg(db: Any, now_ms: int) -> tuple[float | None, int]:
|
|||||||
"""Calculate global cost per egg over dynamic window.
|
"""Calculate global cost per egg over dynamic window.
|
||||||
|
|
||||||
Aggregates feed costs and egg counts across all locations.
|
Aggregates feed costs and egg counts across all locations.
|
||||||
Uses a dynamic window based on the first egg collection event.
|
Uses a dynamic window based on the later of first egg event or first feed event,
|
||||||
|
ensuring we only calculate cost for periods with complete data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: Database connection.
|
db: Database connection.
|
||||||
@@ -195,9 +196,22 @@ def _get_global_cost_per_egg(db: Any, now_ms: int) -> tuple[float | None, int]:
|
|||||||
"""
|
"""
|
||||||
from animaltrack.events import FEED_GIVEN
|
from animaltrack.events import FEED_GIVEN
|
||||||
|
|
||||||
# Calculate dynamic window based on first egg event
|
# Calculate dynamic window based on the later of first egg or first feed event
|
||||||
|
# This ensures we only calculate cost/egg for periods with both data types
|
||||||
first_egg_ts = _get_first_event_ts(db, "ProductCollected", product_prefix="egg.")
|
first_egg_ts = _get_first_event_ts(db, "ProductCollected", product_prefix="egg.")
|
||||||
window_start, window_end, window_days = _calculate_window(now_ms, first_egg_ts)
|
first_feed_ts = _get_first_event_ts(db, "FeedGiven")
|
||||||
|
|
||||||
|
# Use the later timestamp (max) to ensure complete data for both metrics
|
||||||
|
if first_egg_ts is None and first_feed_ts is None:
|
||||||
|
first_event_ts = None
|
||||||
|
elif first_egg_ts is None:
|
||||||
|
first_event_ts = first_feed_ts
|
||||||
|
elif first_feed_ts is None:
|
||||||
|
first_event_ts = first_egg_ts
|
||||||
|
else:
|
||||||
|
first_event_ts = max(first_egg_ts, first_feed_ts)
|
||||||
|
|
||||||
|
window_start, window_end, window_days = _calculate_window(now_ms, first_event_ts)
|
||||||
|
|
||||||
event_store = EventStore(db)
|
event_store = EventStore(db)
|
||||||
|
|
||||||
@@ -300,17 +314,18 @@ def _get_eggs_display_data(db: Any, locations: list) -> dict:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with harvest_events, sell_events, eggs_per_day, cost_per_egg,
|
Dict with harvest_events, sell_events, eggs_per_day, cost_per_egg,
|
||||||
eggs_window_days, sales_stats, location_names.
|
eggs_window_days, cost_window_days, sales_stats, location_names.
|
||||||
"""
|
"""
|
||||||
now_ms = int(time.time() * 1000)
|
now_ms = int(time.time() * 1000)
|
||||||
eggs_per_day, eggs_window_days = _get_eggs_per_day(db, now_ms)
|
eggs_per_day, eggs_window_days = _get_eggs_per_day(db, now_ms)
|
||||||
cost_per_egg, _ = _get_global_cost_per_egg(db, now_ms)
|
cost_per_egg, cost_window_days = _get_global_cost_per_egg(db, now_ms)
|
||||||
return {
|
return {
|
||||||
"harvest_events": _get_recent_events(db, PRODUCT_COLLECTED, limit=10),
|
"harvest_events": _get_recent_events(db, PRODUCT_COLLECTED, limit=10),
|
||||||
"sell_events": _get_recent_events(db, PRODUCT_SOLD, limit=10),
|
"sell_events": _get_recent_events(db, PRODUCT_SOLD, limit=10),
|
||||||
"eggs_per_day": eggs_per_day,
|
"eggs_per_day": eggs_per_day,
|
||||||
"cost_per_egg": cost_per_egg,
|
"cost_per_egg": cost_per_egg,
|
||||||
"eggs_window_days": eggs_window_days,
|
"eggs_window_days": eggs_window_days,
|
||||||
|
"cost_window_days": cost_window_days,
|
||||||
"sales_stats": _get_sales_stats(db, now_ms),
|
"sales_stats": _get_sales_stats(db, now_ms),
|
||||||
"location_names": {loc.id: loc.name for loc in locations},
|
"location_names": {loc.id: loc.name for loc in locations},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ def eggs_page(
|
|||||||
eggs_per_day: float | None = None,
|
eggs_per_day: float | None = None,
|
||||||
cost_per_egg: float | None = None,
|
cost_per_egg: float | None = None,
|
||||||
eggs_window_days: int = 30,
|
eggs_window_days: int = 30,
|
||||||
|
cost_window_days: int = 30,
|
||||||
sales_stats: dict | None = None,
|
sales_stats: dict | None = None,
|
||||||
location_names: dict[str, str] | None = None,
|
location_names: dict[str, str] | None = None,
|
||||||
# Field value preservation on errors
|
# Field value preservation on errors
|
||||||
@@ -62,7 +63,8 @@ def eggs_page(
|
|||||||
sell_events: Recent ProductSold events (most recent first).
|
sell_events: Recent ProductSold events (most recent first).
|
||||||
eggs_per_day: Average eggs per day over window.
|
eggs_per_day: Average eggs per day over window.
|
||||||
cost_per_egg: Average cost per egg in EUR over window.
|
cost_per_egg: Average cost per egg in EUR over window.
|
||||||
eggs_window_days: Actual window size in days for the metrics.
|
eggs_window_days: Actual window size in days for eggs_per_day.
|
||||||
|
cost_window_days: Actual window size in days for cost_per_egg.
|
||||||
sales_stats: Dict with 'total_qty', 'total_cents', and 'avg_price_per_egg_cents'.
|
sales_stats: Dict with 'total_qty', 'total_cents', and 'avg_price_per_egg_cents'.
|
||||||
location_names: Dict mapping location_id to location name for display.
|
location_names: Dict mapping location_id to location name for display.
|
||||||
harvest_quantity: Preserved quantity value on error.
|
harvest_quantity: Preserved quantity value on error.
|
||||||
@@ -100,6 +102,7 @@ def eggs_page(
|
|||||||
eggs_per_day=eggs_per_day,
|
eggs_per_day=eggs_per_day,
|
||||||
cost_per_egg=cost_per_egg,
|
cost_per_egg=cost_per_egg,
|
||||||
eggs_window_days=eggs_window_days,
|
eggs_window_days=eggs_window_days,
|
||||||
|
cost_window_days=cost_window_days,
|
||||||
location_names=location_names,
|
location_names=location_names,
|
||||||
default_quantity=harvest_quantity,
|
default_quantity=harvest_quantity,
|
||||||
default_notes=harvest_notes,
|
default_notes=harvest_notes,
|
||||||
@@ -135,6 +138,7 @@ def harvest_form(
|
|||||||
eggs_per_day: float | None = None,
|
eggs_per_day: float | None = None,
|
||||||
cost_per_egg: float | None = None,
|
cost_per_egg: float | None = None,
|
||||||
eggs_window_days: int = 30,
|
eggs_window_days: int = 30,
|
||||||
|
cost_window_days: int = 30,
|
||||||
location_names: dict[str, str] | None = None,
|
location_names: dict[str, str] | None = None,
|
||||||
default_quantity: str | None = None,
|
default_quantity: str | None = None,
|
||||||
default_notes: str | None = None,
|
default_notes: str | None = None,
|
||||||
@@ -149,7 +153,8 @@ def harvest_form(
|
|||||||
recent_events: Recent (Event, is_deleted) tuples, most recent first.
|
recent_events: Recent (Event, is_deleted) tuples, most recent first.
|
||||||
eggs_per_day: Average eggs per day over window.
|
eggs_per_day: Average eggs per day over window.
|
||||||
cost_per_egg: Average cost per egg in EUR over window.
|
cost_per_egg: Average cost per egg in EUR over window.
|
||||||
eggs_window_days: Actual window size in days for the metrics.
|
eggs_window_days: Actual window size in days for eggs_per_day.
|
||||||
|
cost_window_days: Actual window size in days for cost_per_egg.
|
||||||
location_names: Dict mapping location_id to location name for display.
|
location_names: Dict mapping location_id to location name for display.
|
||||||
default_quantity: Preserved quantity value on error.
|
default_quantity: Preserved quantity value on error.
|
||||||
default_notes: Preserved notes value on error.
|
default_notes: Preserved notes value on error.
|
||||||
@@ -193,13 +198,13 @@ def harvest_form(
|
|||||||
loc_name = location_names.get(loc_id, "Unknown")
|
loc_name = location_names.get(loc_id, "Unknown")
|
||||||
return f"{quantity} eggs from {loc_name}", event.id
|
return f"{quantity} eggs from {loc_name}", event.id
|
||||||
|
|
||||||
# Build stats text - combine eggs/day and cost/egg
|
# Build stats text - each metric shows its own window
|
||||||
stat_parts = []
|
stat_parts = []
|
||||||
if eggs_per_day is not None:
|
if eggs_per_day is not None:
|
||||||
stat_parts.append(f"{eggs_per_day:.1f} eggs/day")
|
stat_parts.append(f"{eggs_per_day:.1f} eggs/day ({eggs_window_days}d)")
|
||||||
if cost_per_egg is not None:
|
if cost_per_egg is not None:
|
||||||
stat_parts.append(f"€{cost_per_egg:.3f}/egg cost")
|
stat_parts.append(f"€{cost_per_egg:.3f}/egg ({cost_window_days}d)")
|
||||||
stat_text = " | ".join(stat_parts) + f" ({eggs_window_days}-day avg)" if stat_parts else None
|
stat_text = " | ".join(stat_parts) if stat_parts else None
|
||||||
|
|
||||||
form = Form(
|
form = Form(
|
||||||
H2("Harvest Eggs", cls="text-xl font-bold mb-4"),
|
H2("Harvest Eggs", cls="text-xl font-bold mb-4"),
|
||||||
|
|||||||
Reference in New Issue
Block a user