update add feed initial balance on cycle and fix its feature

This commit is contained in:
Alberto-Audrix committed 2026-09-01 14:02:41 +07:00
1 parent 91444faa31
commit 7a9d600aa6
16 files changed
+439 -88

No files matched your search

+70 -4
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import re
from datetime import date
from typing import Any
from urllib.parse import urljoin
@@ -18,17 +19,34 @@ class KarungWebError(Exception):
self.status_code = status_code
def masuk_source_for_kandang(kandang_name: str) -> str:
"""Map a kandang label like ``Kandang 1`` to the karung-web-admin source ``K1 - In``."""
match = re.search(r"(\d+)", kandang_name or "")
if not match:
raise ValidationError(
f"Cannot derive IoT masuk source from kandang name: {kandang_name!r}"
)
return f"K{match.group(1)} - In"
def _normalize_source(value: str | None) -> str:
return (value or "").strip().casefold()
class KarungWebClient:
"""Outbound client for karung-web-admin — base URL from settings only."""
PATH = "/api/v1/cycles/active/sections"
SECTIONS_PATH = "/api/v1/cycles/active/sections"
COMBINED_PATH = "/api/combined"
def __init__(self, base_url: str | None = None, timeout: float | None = None):
self.base_url = (base_url or settings.KARUNG_WEB_ADMIN_BASE_URL).rstrip("/")
self.timeout = timeout or settings.KARUNG_WEB_ADMIN_TIMEOUT_SECONDS
def fetch_active_sections(self) -> dict[str, Any]:
url = urljoin(self.base_url + "/", self.PATH.lstrip("/"))
def _get_json(self, path: str) -> dict[str, Any]:
if not self.base_url:
raise KarungWebError("KARUNG_WEB_ADMIN_BASE_URL is not configured")
url = urljoin(self.base_url + "/", path.lstrip("/"))
try:
with httpx.Client(timeout=self.timeout) as client:
response = client.get(url)
@@ -40,11 +58,59 @@ class KarungWebClient:
status_code=response.status_code,
)
payload = response.json()
if not payload.get("success", True):
if not payload.get("success", True) and payload.get("error"):
error = payload.get("error") or {}
raise KarungWebError(error.get("message") or "karung-web-admin returned failure")
return payload.get("data") or payload
def fetch_active_sections(self) -> dict[str, Any]:
return self._get_json(self.SECTIONS_PATH)
def fetch_combined(self) -> dict[str, Any]:
return self._get_json(self.COMBINED_PATH)
def fetch_iot_masuk_for_date(target_date: date, *, kandang_name: str) -> int | None:
"""Return IoT karung-masuk for ``target_date`` and kandang from karung-web-admin /api/combined."""
expected_source = _normalize_source(masuk_source_for_kandang(kandang_name))
target = target_date.isoformat()
client = KarungWebClient()
data = client.fetch_combined()
masuk = data.get("masuk") or {}
for row in masuk.get("history") or []:
if row.get("date") == target and _normalize_source(row.get("source")) == expected_source:
return int(row.get("counter_value") or 0)
if target_date == date.today():
for row in masuk.get("today") or []:
if _normalize_source(row.get("source")) == expected_source:
return int(row.get("karung") or 0)
return None
def initial_balance_accuracy(manual: int, iot_in: int | None) -> float | None:
if manual > 0 and iot_in is not None and iot_in > 0:
return round(min(manual, iot_in) / max(manual, iot_in) * 1000) / 10
if manual == 0 and iot_in == 0:
return 100.0
return None
def pull_initial_balance_iot(cycle: Cycle, balance_date: date) -> tuple[int | None, str | None]:
sync_error = None
iot_in = None
try:
iot_in = fetch_iot_masuk_for_date(
balance_date,
kandang_name=cycle.kandang.kandang_name,
)
except KarungWebError as exc:
sync_error = str(exc)
return iot_in, sync_error
def request_karung(
cycle: Cycle,