184 lines
6.4 KiB
Python
184 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import DateTime, select
|
|
|
|
from app.core.database import session_scope
|
|
from app.modules.zootech.catalog_models import (
|
|
ZootechDailyComponentNormAdjustment,
|
|
ZootechDailyIngredientReplacement,
|
|
ZootechDailyIngredientSkip,
|
|
ZootechDailyTripSkip,
|
|
ZootechDailyUnloadingGroupSkip,
|
|
ZootechFeedDispenser,
|
|
ZootechFeedingLocation,
|
|
ZootechFeedingPeriod,
|
|
ZootechFeedingPoint,
|
|
ZootechFeedMixer,
|
|
ZootechPeriodRecipe,
|
|
ZootechTrip,
|
|
ZootechUnloadingGroup,
|
|
)
|
|
from app.modules.zootech.models import ZootechComponent, ZootechIngredient, ZootechRecipe
|
|
|
|
TABLE_MODEL_MAP = {
|
|
"component": ZootechComponent,
|
|
"recipe": ZootechRecipe,
|
|
"ingredient": ZootechIngredient,
|
|
"unloading_group": ZootechUnloadingGroup,
|
|
"feed_mixer": ZootechFeedMixer,
|
|
"feed_dispenser": ZootechFeedDispenser,
|
|
"feeding_location": ZootechFeedingLocation,
|
|
"feeding_period": ZootechFeedingPeriod,
|
|
"feeding_point": ZootechFeedingPoint,
|
|
"period_recipes": ZootechPeriodRecipe,
|
|
"daily_trip_skip": ZootechDailyTripSkip,
|
|
"daily_ingredient_skip": ZootechDailyIngredientSkip,
|
|
"daily_unloading_group_skip": ZootechDailyUnloadingGroupSkip,
|
|
"daily_ingredient_replacement": ZootechDailyIngredientReplacement,
|
|
"daily_component_norm_adjustment": ZootechDailyComponentNormAdjustment,
|
|
"trip": ZootechTrip,
|
|
}
|
|
|
|
|
|
def _coerce_for_model(model, key: str, value: Any) -> Any:
|
|
if value is None:
|
|
return value
|
|
column = model.__table__.columns.get(key)
|
|
if column is None:
|
|
return value
|
|
if isinstance(column.type, DateTime) and isinstance(value, str):
|
|
try:
|
|
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError:
|
|
return datetime.now(UTC)
|
|
return value
|
|
|
|
|
|
def _parse_period_recipe_id(record_id: str) -> tuple[str, str]:
|
|
if ":" in record_id:
|
|
period_id, recipe_id = record_id.split(":", 1)
|
|
return period_id, recipe_id
|
|
return record_id, record_id
|
|
|
|
|
|
def load_catalog_row(enterprise_id: str, table_name: str, record_id: str) -> dict[str, Any] | None:
|
|
model = TABLE_MODEL_MAP.get(table_name)
|
|
if not model:
|
|
return None
|
|
with session_scope() as db:
|
|
if model is ZootechPeriodRecipe:
|
|
period_id, recipe_id = _parse_period_recipe_id(record_id)
|
|
row = db.scalar(
|
|
select(model).where(
|
|
model.enterprise_id == enterprise_id,
|
|
model.period_id == period_id,
|
|
model.recipe_id == recipe_id,
|
|
)
|
|
)
|
|
else:
|
|
row = db.scalar(
|
|
select(model).where(model.enterprise_id == enterprise_id, model.id == record_id)
|
|
)
|
|
if not row:
|
|
return None
|
|
return _row_to_dict(row, table_name)
|
|
|
|
|
|
def apply_catalog_change(
|
|
enterprise_id: str,
|
|
table_name: str,
|
|
record_id: str,
|
|
action: str,
|
|
payload: dict[str, Any],
|
|
version: int,
|
|
content_hash: str,
|
|
) -> None:
|
|
model = TABLE_MODEL_MAP.get(table_name)
|
|
if not model:
|
|
return
|
|
with session_scope() as db:
|
|
if model is ZootechPeriodRecipe:
|
|
period_id, recipe_id = _parse_period_recipe_id(record_id)
|
|
row = db.scalar(
|
|
select(model).where(
|
|
model.enterprise_id == enterprise_id,
|
|
model.period_id == period_id,
|
|
model.recipe_id == recipe_id,
|
|
)
|
|
)
|
|
else:
|
|
row = db.scalar(
|
|
select(model).where(model.enterprise_id == enterprise_id, model.id == record_id)
|
|
)
|
|
if action == "delete":
|
|
if row:
|
|
row.is_deleted = True
|
|
row.version = version
|
|
row.content_hash = content_hash
|
|
row.updated_at = datetime.now(UTC)
|
|
return
|
|
data = dict(payload)
|
|
data["enterprise_id"] = enterprise_id
|
|
data["version"] = version
|
|
data["content_hash"] = content_hash
|
|
data["is_deleted"] = False
|
|
if model is ZootechPeriodRecipe:
|
|
data["period_id"] = data.get("period_id") or _parse_period_recipe_id(record_id)[0]
|
|
data["recipe_id"] = data.get("recipe_id") or _parse_period_recipe_id(record_id)[1]
|
|
else:
|
|
data["id"] = record_id
|
|
if row:
|
|
_apply_fields(row, data, model)
|
|
row.updated_at = datetime.now(UTC)
|
|
else:
|
|
allowed = {c.key for c in model.__table__.columns}
|
|
filtered = {
|
|
k: _coerce_for_model(model, k, v) for k, v in data.items() if k in allowed
|
|
}
|
|
extra = {k: v for k, v in data.items() if k not in allowed}
|
|
if extra and "payload_json" in allowed:
|
|
filtered["payload_json"] = json.dumps(extra, ensure_ascii=False)
|
|
db.add(model(**filtered))
|
|
|
|
|
|
def _apply_fields(row, data: dict[str, Any], model) -> None:
|
|
allowed = {c.key for c in model.__table__.columns}
|
|
extra: dict[str, Any] = {}
|
|
for key, value in data.items():
|
|
if key in allowed and key not in ("enterprise_id", "created_at"):
|
|
setattr(row, key, _coerce_for_model(model, key, value))
|
|
elif key not in ("enterprise_id", "created_at", "id"):
|
|
extra[key] = value
|
|
if model is ZootechRecipe and "ingredients" in data:
|
|
row.payload_json = json.dumps(data.get("ingredients"), ensure_ascii=False)
|
|
elif extra and "payload_json" in allowed:
|
|
row.payload_json = json.dumps(extra, ensure_ascii=False)
|
|
|
|
|
|
def _row_to_dict(row, table_name: str) -> dict[str, Any]:
|
|
result = {}
|
|
for col in row.__table__.columns:
|
|
val = getattr(row, col.key)
|
|
if isinstance(val, datetime):
|
|
val = val.isoformat()
|
|
result[col.key] = val
|
|
if isinstance(row, ZootechRecipe) and row.payload_json:
|
|
try:
|
|
parsed = json.loads(row.payload_json)
|
|
if isinstance(parsed, list):
|
|
result["ingredients"] = parsed
|
|
except json.JSONDecodeError:
|
|
pass
|
|
if table_name == "period_recipes":
|
|
result["id"] = f"{row.period_id}:{row.recipe_id}"
|
|
elif hasattr(row, "payload_json") and row.payload_json and table_name != "recipe":
|
|
try:
|
|
result.update(json.loads(row.payload_json))
|
|
except json.JSONDecodeError:
|
|
pass
|
|
return result
|