310 lines
11 KiB
Python
310 lines
11 KiB
Python
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
|