Files

310 lines
11 KiB
Python
Raw Permalink Normal View History

2026-05-05 00:56:05 +03:30
from __future__ import annotations
from datetime import date, datetime, time, timedelta
from django.apps import apps as django_apps
from django.utils import timezone
from django.utils.dateparse import parse_date
AUTO_PLAN_SOURCE = "auto_plan_sync"
PLAN_TYPE_IRRIGATION = "irrigation"
PLAN_TYPE_FERTILIZATION = "fertilization"
def create_event_for_farm(
*,
farm,
title,
description="",
start=None,
end=None,
scheduled_date=None,
event_time=None,
priority=None,
tags=None,
zone_value="برنامه خودکار",
extended_props=None,
):
FarmerCalendarEvent = django_apps.get_model("farmer_calendar", "FarmerCalendarEvent")
FarmerCalendarZone = django_apps.get_model("farmer_calendar", "FarmerCalendarZone")
from .enums import FarmerPriority
if priority is None:
priority = FarmerPriority.MEDIUM
zone, _ = FarmerCalendarZone.objects.get_or_create(
farm=farm,
value=zone_value,
defaults={"label": zone_value},
)
if zone.label != zone_value:
zone.label = zone_value
zone.save(update_fields=["label", "updated_at"])
payload = dict(extended_props or {})
payload["tags"] = list(tags or [])
return FarmerCalendarEvent.objects.create(
farm=farm,
zone=zone,
title=title,
description=description,
deadline=int(end.timestamp()) if end else None,
scheduled_date=scheduled_date,
time=event_time,
start=start,
end=end,
priority=priority,
status=FarmerCalendarEvent.STATUS_OPEN,
extended_props=payload,
)
def delete_plan_events(*, farm, plan_type, plan_uuid):
FarmerCalendarEvent = django_apps.get_model("farmer_calendar", "FarmerCalendarEvent")
for event in FarmerCalendarEvent.objects.filter(farm=farm):
props = event.extended_props or {}
if (
props.get("source") == AUTO_PLAN_SOURCE
and props.get("plan_type") == plan_type
and str(props.get("plan_uuid")) == str(plan_uuid)
):
event.delete()
def sync_plan_events(plan, plan_type):
from .enums import FarmerPriority
delete_plan_events(farm=plan.farm, plan_type=plan_type, plan_uuid=plan.uuid)
if getattr(plan, "is_deleted", False) or not getattr(plan, "is_active", False):
return []
if plan_type == PLAN_TYPE_IRRIGATION:
items = _build_irrigation_events(plan)
elif plan_type == PLAN_TYPE_FERTILIZATION:
items = _build_fertilization_events(plan)
else:
items = []
created = []
for index, item in enumerate(items, start=1):
created.append(
create_event_for_farm(
farm=plan.farm,
title=item["title"],
description=item.get("description", ""),
start=item.get("start"),
end=item.get("end"),
scheduled_date=item.get("scheduled_date"),
event_time=item.get("time"),
priority=item.get("priority", FarmerPriority.MEDIUM),
tags=item.get("tags", []),
zone_value=item.get("zone_value", "برنامه خودکار"),
extended_props={
"source": AUTO_PLAN_SOURCE,
"plan_type": plan_type,
"plan_uuid": str(plan.uuid),
"plan_title": plan.title,
"entry_index": index,
**item.get("extended_props", {}),
},
)
)
return created
def _build_irrigation_events(plan):
from .enums import FarmerPriority, FarmerTag
payload = plan.plan_payload if isinstance(plan.plan_payload, dict) else {}
plan_data = payload.get("plan") if isinstance(payload.get("plan"), dict) else {}
water_balance = payload.get("water_balance") if isinstance(payload.get("water_balance"), dict) else {}
daily_entries = water_balance.get("daily") if isinstance(water_balance.get("daily"), list) else []
created = []
for entry in daily_entries:
if not isinstance(entry, dict):
continue
scheduled = _parse_date(entry.get("forecast_date"))
if not scheduled:
continue
start_time, end_time = _parse_time_range(entry.get("irrigation_timing") or plan_data.get("bestTimeOfDay"))
start, end = _build_datetimes(scheduled, start_time, end_time, default_duration_minutes=plan_data.get("durationMinutes"))
gross_amount = entry.get("gross_irrigation_mm")
title = f"آبیاری - {plan.crop_id or plan.title or 'مزرعه'}"
description_parts = []
if gross_amount not in (None, ""):
description_parts.append(f"مقدار آبیاری: {gross_amount} mm")
if plan_data.get("durationMinutes"):
description_parts.append(f"مدت زمان: {plan_data.get('durationMinutes')} دقیقه")
if entry.get("irrigation_timing"):
description_parts.append(f"بازه اجرا: {entry.get('irrigation_timing')}")
created.append(
{
"title": title,
"description": " | ".join(description_parts),
"scheduled_date": scheduled,
"time": start_time,
"start": start,
"end": end,
"priority": FarmerPriority.HIGH,
"tags": [FarmerTag.IRRIGATION.value],
"zone_value": "آبیاری",
"extended_props": {
"kind": "irrigation",
"gross_irrigation_mm": gross_amount,
"irrigation_timing": entry.get("irrigation_timing"),
},
}
)
if created:
return created
scheduled = timezone.localdate()
start_time, end_time = _parse_time_range(plan_data.get("bestTimeOfDay"))
start, end = _build_datetimes(scheduled, start_time, end_time, default_duration_minutes=plan_data.get("durationMinutes"))
return [
{
"title": f"آبیاری - {plan.crop_id or plan.title or 'مزرعه'}",
"description": f"برنامه فعال آبیاری: {plan.title}".strip(),
"scheduled_date": scheduled,
"time": start_time,
"start": start,
"end": end,
"priority": FarmerPriority.HIGH,
"tags": [FarmerTag.IRRIGATION.value],
"zone_value": "آبیاری",
"extended_props": {"kind": "irrigation_fallback"},
}
]
def _build_fertilization_events(plan):
from .enums import FarmerPriority, FarmerTag
payload = plan.plan_payload if isinstance(plan.plan_payload, dict) else {}
primary = payload.get("primary_recommendation") if isinstance(payload.get("primary_recommendation"), dict) else {}
guide = payload.get("application_guide") if isinstance(payload.get("application_guide"), dict) else {}
steps = guide.get("steps") if isinstance(guide.get("steps"), list) else []
interval = primary.get("application_interval") if isinstance(primary.get("application_interval"), dict) else {}
interval_days = _safe_int(interval.get("value"))
base_date = timezone.localdate()
fertilizer_name = primary.get("display_title") or primary.get("fertilizer_name") or plan.title or "برنامه کودی"
created = []
for index, step in enumerate(steps):
if not isinstance(step, dict):
continue
scheduled = _extract_step_date(step) or (base_date + timedelta(days=(index * interval_days if interval_days else index)))
start = timezone.make_aware(datetime.combine(scheduled, time(hour=8, minute=0)))
end = start + timedelta(minutes=30)
description = str(step.get("description") or guide.get("safety_warning") or "").strip()
created.append(
{
"title": f"کوددهی - {fertilizer_name}",
"description": description,
"scheduled_date": scheduled,
"time": start.time(),
"start": start,
"end": end,
"priority": FarmerPriority.MEDIUM,
"tags": [FarmerTag.FERTILIZATION.value],
"zone_value": "کوددهی",
"extended_props": {
"kind": "fertilization",
"step_number": step.get("step_number"),
"fertilizer_code": primary.get("fertilizer_code"),
},
}
)
if created:
return created
scheduled = base_date
start = timezone.make_aware(datetime.combine(scheduled, time(hour=8, minute=0)))
end = start + timedelta(minutes=30)
interval_label = interval.get("label") or ""
description = " | ".join(part for part in [str(primary.get("summary") or "").strip(), str(interval_label).strip()] if part)
return [
{
"title": f"کوددهی - {fertilizer_name}",
"description": description,
"scheduled_date": scheduled,
"time": start.time(),
"start": start,
"end": end,
"priority": FarmerPriority.MEDIUM,
"tags": [FarmerTag.FERTILIZATION.value],
"zone_value": "کوددهی",
"extended_props": {
"kind": "fertilization_fallback",
"fertilizer_code": primary.get("fertilizer_code"),
},
}
]
def _parse_date(value):
if isinstance(value, date):
return value
if not value:
return None
return parse_date(str(value))
def _parse_time_range(value):
if not value:
return None, None
raw = str(value).replace("تا", "-").replace("", "-")
parts = [part.strip() for part in raw.split("-") if part.strip()]
if not parts:
return None, None
start_time = _parse_time(parts[0])
end_time = _parse_time(parts[1]) if len(parts) > 1 else None
return start_time, end_time
def _parse_time(value):
if isinstance(value, time):
return value
if not value:
return None
cleaned = str(value).strip()
for fmt in ("%H:%M", "%H:%M:%S"):
try:
return datetime.strptime(cleaned, fmt).time()
except ValueError:
continue
return None
def _build_datetimes(scheduled, start_time, end_time, default_duration_minutes=None):
if scheduled is None:
return None, None
if start_time is None:
start_time = time(hour=6, minute=0)
start = timezone.make_aware(datetime.combine(scheduled, start_time))
if end_time is not None:
end = timezone.make_aware(datetime.combine(scheduled, end_time))
else:
end = start + timedelta(minutes=_safe_int(default_duration_minutes) or 30)
return start, end
def _extract_step_date(step):
for key in ("date", "scheduled_date", "application_date", "target_date", "forecast_date"):
parsed = _parse_date(step.get(key))
if parsed:
return parsed
return None
def _safe_int(value):
try:
return int(value)
except (TypeError, ValueError):
return None