from __future__ import annotations import json from datetime import date from decimal import Decimal, InvalidOperation from typing import Any from fastapi import HTTPException from sqlalchemy import func, select from sqlalchemy.orm import Session from .models import CustomFieldDefinition, CustomFieldValue, MaterialRecord def options_for(definition: CustomFieldDefinition) -> list[str]: try: return json.loads(definition.options_json) if definition.options_json else [] except (json.JSONDecodeError, TypeError): return [] def definition_json(definition: CustomFieldDefinition, usage_count: int | None = None) -> dict[str, Any]: data = { "id": definition.id, "field_key": definition.field_key, "label": definition.label, "input_type": definition.input_type, "is_required": definition.is_required, "is_active": definition.is_active, "is_public": definition.is_public, "unit": definition.unit, "options": options_for(definition), "sort_order": definition.sort_order, "created_at": definition.created_at.isoformat(), "updated_at": definition.updated_at.isoformat(), } if usage_count is not None: data["usage_count"] = usage_count return data def active_definitions(db: Session) -> list[CustomFieldDefinition]: return list(db.scalars(select(CustomFieldDefinition).where(CustomFieldDefinition.is_active.is_(True)).order_by(CustomFieldDefinition.sort_order, CustomFieldDefinition.id)).all()) def _empty(value: Any) -> bool: return value is None or (isinstance(value, str) and not value.strip()) def validate_raw_value(definition: CustomFieldDefinition, value: Any) -> str | None: if _empty(value): if definition.is_required: raise HTTPException(422, f"{definition.label}:该字段为必填项") return None raw = str(value).strip() try: if definition.input_type == "number": if not Decimal(raw).is_finite(): raise ValueError elif definition.input_type == "integer": number = Decimal(raw) if isinstance(value, bool) or not number.is_finite() or number != number.to_integral_value(): raise ValueError elif definition.input_type == "date": date.fromisoformat(raw) elif definition.input_type == "boolean": if raw.lower() not in {"true", "false", "1", "0"}: raise ValueError raw = "true" if raw.lower() in {"true", "1"} else "false" elif definition.input_type == "select" and raw not in options_for(definition): raise ValueError except (ValueError, InvalidOperation): raise HTTPException(422, f"{definition.label}:数据格式不符合当前字段类型") if len(raw) > 10000: raise HTTPException(422, f"{definition.label}:内容不能超过 10000 个字符") return raw def validate_custom_values(db: Session, submitted: dict[str, Any], existing_record: MaterialRecord | None = None) -> dict[CustomFieldDefinition, str | None]: definitions = active_definitions(db) by_key = {item.field_key: item for item in definitions} unknown = set(submitted) - set(by_key) if unknown: raise HTTPException(422, "包含未知或已停用的自定义字段") existing_keys = {value.definition.field_key for value in existing_record.custom_values} if existing_record else set() validated: dict[CustomFieldDefinition, str | None] = {} for definition in definitions: if definition.field_key not in submitted and existing_record is not None: if definition.is_required and definition.field_key not in existing_keys: raise HTTPException(422, f"{definition.label}:该字段为必填项") continue validated[definition] = validate_raw_value(definition, submitted.get(definition.field_key)) return validated def apply_custom_values(db: Session, record: MaterialRecord, values: dict[CustomFieldDefinition, str | None]) -> None: existing = {item.field_id: item for item in record.custom_values} for definition, raw_value in values.items(): item = existing.get(definition.id) if raw_value is None: if item: db.delete(item) continue if item: item.raw_value = raw_value else: db.add(CustomFieldValue(record_id=record.id, field_id=definition.id, raw_value=raw_value)) def render_custom_value(value: CustomFieldValue) -> dict[str, Any]: definition, raw = value.definition, value.raw_value or "" compatible = True processed: Any = raw try: if definition.input_type == "number": number = Decimal(raw) if not number.is_finite(): raise ValueError processed = float(number) elif definition.input_type == "integer": number = Decimal(raw) if number != number.to_integral_value(): raise ValueError processed = int(number) elif definition.input_type == "date": processed = date.fromisoformat(raw).isoformat() elif definition.input_type == "boolean": if raw.lower() not in {"true", "false", "1", "0"}: raise ValueError processed = raw.lower() in {"true", "1"} elif definition.input_type == "select" and raw not in options_for(definition): compatible = False except (ValueError, InvalidOperation): compatible = False processed = raw display = "是" if processed is True else "否" if processed is False else str(processed) if definition.unit and display: display = f"{display} {definition.unit}" return { "field_key": definition.field_key, "label": definition.label, "input_type": definition.input_type, "unit": definition.unit, "raw_value": raw, "value": processed, "display_value": display, "compatible": compatible, "is_active": definition.is_active, "is_public": definition.is_public, } def definition_usage_counts(db: Session) -> dict[int, int]: rows = db.execute(select(CustomFieldValue.field_id, func.count(CustomFieldValue.id)).group_by(CustomFieldValue.field_id)).all() return {field_id: count for field_id, count in rows}