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
|