"""地磅 ORM → API Read 模型映射。""" from __future__ import annotations import json from datetime import date, datetime, time from typing import Iterable, Optional from sqlalchemy.orm import Session from app.core.utils import format_datetime from app.models.models import SysUser # 三方预生成出入库单「日期未填」占位(原系统 date="",DDL 为 NOT NULL DATE) EMPTY_INVENTORY_DATE = date(1970, 1, 1) from app.models.db_models import DbInboundOutbound, DbPurchasePlan, DbWeighingSlip from app.schemas.db.inventory import InboundOutboundRead from app.schemas.db.plans import PurchasePlanRead, PurchasePlanWithFillLink from app.schemas.db.weighing import WeighingSlipRead def fmt_date(value: date | datetime | str | None) -> str | None: if value is None: return None if isinstance(value, datetime): return value.strftime("%Y-%m-%d") if isinstance(value, date): return value.strftime("%Y-%m-%d") return str(value) def fmt_datetime(value: datetime | str | None) -> str | None: if value is None: return None if isinstance(value, datetime): return format_datetime(value) return str(value) def load_user_display_name_map( db: Session, user_ids: Iterable[Optional[str]] ) -> dict[str, str]: """批量解析用户展示名(昵称优先,否则账号)。""" ids = {(uid or "").strip() for uid in user_ids if (uid or "").strip()} if not ids: return {} rows = ( db.query(SysUser.id, SysUser.nick_name, SysUser.user_no) .filter(SysUser.id.in_(ids), SysUser.del_flag == 0) .all() ) return {u.id: (u.nick_name or u.user_no or "").strip() for u in rows} def _create_by_name( create_by: Optional[str], user_name_map: dict[str, str] | None ) -> Optional[str]: uid = (create_by or "").strip() if not uid or user_name_map is None: return None name = (user_name_map.get(uid) or "").strip() return name or None def plan_to_read( row: DbPurchasePlan, *, executed_quantity: float | None = None, user_name_map: dict[str, str] | None = None, db: Session | None = None, ) -> PurchasePlanRead: name_map = user_name_map if name_map is None and db is not None: name_map = load_user_display_name_map(db, [row.create_by]) return PurchasePlanRead( id=row.id, planNo=row.plan_no, planDate=fmt_date(row.plan_date) or "", startDate=fmt_date(row.start_date), endDate=fmt_date(row.end_date), businessType=row.business_type, productId=row.product_id, product=row.product, specification=row.specification, plannedQuantity=row.planned_quantity, vehicleNo=row.vehicle_no or "", customerName=row.customer_name, remarks=row.remark, executionStatus=row.execution_status, executedAt=fmt_datetime(row.executed_at), lastWeighingSlipId=row.last_weighing_slip_id, executedQuantity=( executed_quantity if executed_quantity is not None else (row.executed_quantity or 0) ), autoGenerateEnabled=row.auto_generate_enabled or 0, autoGenerateEndMonth=row.auto_generate_end_month, autoGenerateLastMonth=row.auto_generate_last_month, isDeleted=row.del_flag, createdAt=format_datetime(row.create_time) or "", createBy=row.create_by, createByName=_create_by_name(row.create_by, name_map), ) def plan_to_read_with_fill_link( row: DbPurchasePlan, *, executed_quantity: float | None = None, fill_link_inventory_id: str | None = None, user_name_map: dict[str, str] | None = None, db: Session | None = None, ) -> PurchasePlanWithFillLink: base = plan_to_read( row, executed_quantity=executed_quantity, user_name_map=user_name_map, db=db ) return PurchasePlanWithFillLink( **base.model_dump(by_alias=False), fill_link_inventory_id=fill_link_inventory_id, ) def is_empty_inventory_date(value: date | None) -> bool: return value is None or value == EMPTY_INVENTORY_DATE def inventory_date_to_api(value: date | None) -> str: if is_empty_inventory_date(value): return "" return fmt_date(value) or "" def _parse_site_feedback_images(raw: str | None) -> list[str] | None: if not raw: return None try: parsed = json.loads(raw) if isinstance(parsed, list): items = [str(x) for x in parsed if x] return items or None except (TypeError, json.JSONDecodeError): pass items = [part.strip() for part in raw.split(",") if part.strip()] return items or None def inventory_to_read( row: DbInboundOutbound, *, weighing: DbWeighingSlip | None = None, user_name_map: dict[str, str] | None = None, db: Session | None = None, ) -> InboundOutboundRead: gross_weight = None tare_weight = None deduction = None net_weight = None gross_weight_time = None tare_weight_time = None has_weighing_slip = False if weighing: gross_weight = weighing.gross_weight tare_weight = weighing.tare_weight deduction = weighing.deduction net_weight = weighing.net_weight gross_weight_time = fmt_datetime(weighing.gross_weight_time) tare_time = fmt_datetime(weighing.tare_weight_time) tare_weight_time = tare_time if tare_time and tare_time.strip() else None has_weighing_slip = True name_map = user_name_map if name_map is None and db is not None: name_map = load_user_display_name_map(db, [row.create_by]) return InboundOutboundRead( id=row.id, date=inventory_date_to_api(row.date), partnerCompany=row.partner_company, businessType=row.business_type, productId=row.product_id, product=row.product, specification=row.specification, quantity=row.quantity, planNo=row.plan_no, batchNo=row.batch_no, vehicleNo=row.vehicle_no, status=row.status, remarks=row.remark, purchasePlanId=row.purchase_plan_id, isDeleted=row.del_flag, createdAt=format_datetime(row.create_time) or "", createBy=row.create_by, createByName=_create_by_name(row.create_by, name_map), grossWeight=gross_weight, tareWeight=tare_weight, deduction=deduction, netWeight=net_weight, grossWeightTime=gross_weight_time, tareWeightTime=tare_weight_time, hasWeighingSlip=has_weighing_slip, siteFeedbackRemark=row.site_feedback_remark, siteFeedbackImages=_parse_site_feedback_images(row.site_feedback_images), siteFeedbackTime=format_datetime(row.site_feedback_time) or None, ) def weighing_to_read( row: DbWeighingSlip, *, inventory_date: str | None = None, user_name_map: dict[str, str] | None = None, db: Session | None = None, ) -> WeighingSlipRead: name_map = user_name_map if name_map is None and db is not None: name_map = load_user_display_name_map(db, [row.create_by]) return WeighingSlipRead( id=row.id, vehicleNo=row.vehicle_no, planNo=row.plan_no, productId=row.product_id, cargoName=row.cargo_name, specification=row.specification, businessType=row.business_type, batchNo=row.batch_no, supplier=row.supplier, consignee=row.consignee, grossWeight=row.gross_weight, grossWeightTime=fmt_datetime(row.gross_weight_time) or "", tareWeight=row.tare_weight, tareWeightTime=fmt_datetime(row.tare_weight_time) or "", deduction=row.deduction, netWeight=row.net_weight, plannedQuantity=row.planned_quantity, executedQuantity=row.executed_quantity, purchasePlanId=row.purchase_plan_id, inboundOutboundId=row.inbound_outbound_id, weighingPoint=row.weighing_point, remarks=row.remark, currentQuantity=row.current_quantity, isVoided=row.is_voided, createdAt=format_datetime(row.create_time) or "", createBy=row.create_by, createByName=_create_by_name(row.create_by, name_map), inventoryDate=inventory_date, ) def datetime_end_of_day(value: date) -> datetime: return datetime.combine(value, time(23, 59, 59)) def parse_date_str(value: str | None) -> date | None: if not value: return None try: return datetime.strptime(value[:10], "%Y-%m-%d").date() except ValueError: return None def parse_datetime_str(value: str | None) -> datetime | None: if not value: return None for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"): try: return datetime.strptime(value[:19] if fmt != "%Y-%m-%d" else value[:10], fmt) except ValueError: continue return None