"""MOM 报废单只读查询 — 直连 MOM 库(跨库,无 ORM) Track 侧只存**回执号**(`product_scraps.scrap_request_no`),状态与金额一律实时 回查 MOM。为什么不落一份到 Track: · 状态会变。MOM 里审批、执行之后 Track 不会收到通知(报废没有回调), 本地存的那份立刻就过期,而「到底批没批、执行没执行」正是用户要看的。 · 金额**取决于执行时的实际扫码量**。MOM 允许少扫(合法子集), 所以「受理量」≠「执行量」,金额必须在 MOM 执行那一刻才算得准。 本地另算一份就是第二份口径,迟早对不上。 写法照 mom_outbound_service(同步 psycopg2 + text() + MomSessionLocal), 调用方一律 `run_in_threadpool` 包出去,别阻塞事件循环。 """ import logging from sqlalchemy import text from app.core.mom_database import MomSessionLocal logger = logging.getLogger(__name__) # MOM scrap_approval.status → 中文。与 MOM 的 scrap_approval_service 逐字对齐, # 不要自创说法(用户在 MOM 界面看到的和 Track 上看的不一致会让人怀疑数据错了)。 MOM_SCRAP_STATUS_LABELS = { 0: '待审批', 1: '已通过(待执行)', 2: '已驳回', 3: '已执行', 4: '已撤回', } def describe_status(code) -> str: """状态码 → 中文。未知/空值返回空串(前端兜底显示 '-')。""" if code is None: return '' try: return MOM_SCRAP_STATUS_LABELS.get(int(code), '') except (TypeError, ValueError): return '' def fetch_scrap_status(request_nos: list[str]) -> dict[str, dict]: """按报废申请单号批量查 MOM 的审批状态与执行金额。 返回 `{request_no: {...}}`;查不到的**不出现在结果里**(调用方按「缺失 = MOM 侧还没有/已清理」处理,不要伪造成一个空状态)。 金额字段说明(都是 Decimal → float): · `cost_at_scrap` 报废成本(单价) · `total_loss` **报废损失 = 单价 × 实报废数量**,这是「统计生产报废金额」要的数 · `scrapped_quantity` 实际报废数量(执行时扫码量,可能小于受理量) ⚠️ 单号可能一张都没执行(还在待审批),此时 trans_scrap 里**毫无痕迹**, 金额是 0 而不是「缺失」—— 用 `executed` 标志区分这两种情况, 别让前端把「还没批」显示成「损失 0 元」。 """ nos = [str(n).strip() for n in (request_nos or []) if str(n or '').strip()] if not nos: return {} db = MomSessionLocal() try: # ---- 1. 申请单头:状态 / 审批人 / 执行人 ---- head_rows = db.execute( text(""" SELECT request_no, status, applicant_id, actual_approver_id, approved_at, executor_name, executed_at, reject_reason, remark, reason_category FROM scrap_approval WHERE request_no = ANY(:nos) """), {"nos": nos}, ).fetchall() out: dict[str, dict] = {} for r in head_rows: status = r.status out[r.request_no] = { 'request_no': r.request_no, 'status': status, 'status_label': describe_status(status), 'applicant_id': r.applicant_id, 'approver_name': _user_name(db, r.actual_approver_id), 'approved_at': _iso(r.approved_at), 'executor_name': r.executor_name or '', 'executed_at': _iso(r.executed_at), 'reject_reason': r.reject_reason or '', 'reason_category': r.reason_category or '', # 只有 status==3 才真的执行过、台账里才有金额 'executed': status == 3, 'cost_at_scrap': None, 'total_loss': None, 'scrapped_quantity': None, } # ---- 2. 报废流水:实际报废量与损失金额(只有执行过才有行) ---- if out: ledger_rows = db.execute( text(""" SELECT scrap_request_no, count(*) AS line_count, sum(quantity) AS scrapped_quantity, max(cost_at_scrap) AS cost_at_scrap, sum(total_loss) AS total_loss FROM trans_scrap WHERE scrap_request_no = ANY(:nos) GROUP BY scrap_request_no """), {"nos": list(out.keys())}, ).fetchall() for r in ledger_rows: item = out.get(r.scrap_request_no) if item is None: continue item['scrapped_quantity'] = _f(r.scrapped_quantity) item['cost_at_scrap'] = _f(r.cost_at_scrap) item['total_loss'] = _f(r.total_loss) return out finally: db.close() def _f(v): """Decimal/None → float/None(JSON 友好)""" return float(v) if v is not None else None def _iso(dt): """MOM 的时间列是 naive 北京时间,补 +08:00 再交给上层。 ⚠️ 少这一步,前端会按**本地时区**解释这个 naive 串,非北京时区的人看到的 时间就是错的。与 mom_outbound_service._as_beijing 同一处理。 """ if dt is None: return None from datetime import timedelta, timezone if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone(timedelta(hours=8))) return dt.isoformat() def _user_name(db, user_id): """MOM sys_user.id → 中文姓名('张三/zhangsan01' 取 '/' 前那段)。""" if not user_id: return '' try: row = db.execute( text("SELECT username FROM sys_user WHERE id = :uid"), {"uid": int(user_id)}, ).fetchone() except Exception: return '' if not row or not row.username: return '' return row.username.split('/')[0] if '/' in row.username else row.username