Files
KCGL/inventory-backend/app/models/scrap_approval.py
yueli 8755837fe8 fix(timezone): 审批单据时间统一为 naive 北京时间,消除 8 小时偏差
成因:approved_at / executed_at 列是 timestamp without time zone,而赋值用了
带时区的 datetime.now(timezone(timedelta(hours=8)))。psycopg2 对 naive 列不会
剥掉 tzinfo,而是转成 naive UTC 再写入,结果比同为北京时间的 created_at 早 8 小时。
(代码里并无 datetime.utcnow(),真实成因是 aware 值被驱动的隐式 UTC 转换。)

改动:
  · outbound_service / borrow_service 的审批分支
    datetime.now(beijing_tz).replace(tzinfo=None) → beijing_time()
    (免审批分支已在上一轮改为 beijing_time,此处补齐审批分支);
  · purchase_service 审批/完结分支同源缺陷一并修复;
  · scrap_approval / scrap_approval_service 的 _beijing()、_beijing_now() 由
    tz-aware 改为 naive —— 配合上述迁移把列类型改为 naive,
    若仍返回 aware 值,timestamptz→timestamp 后会反向早 8 小时。

实测四表(scrap/outbound/borrow/purchase)created_at 与 approved_at 差值
均在同一秒内(-1ms ~ -12ms)。
2026-09-10 10:14:31 +08:00

97 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from app.extensions import db, beijing_time
import json
def _beijing_now():
"""
统一时间口径:naive 北京时间,与 created_at / OutboundApproval / BorrowApproval 一致。
★ 原先返回 tz-aware 时间,配合 timestamptz 列才正确;现已将该表列类型统一为
timestamp without time zone(见 db_migrations/unify_approval_timezone.sql),
故此处必须返回 naive 值,否则存库会被当 UTC 处理而早 8 小时。
"""
return beijing_time()
class ScrapApproval(db.Model):
"""
报废申请/审批单(镜像 BorrowApproval / OutboundApproval)
与出/借库的关键差异:items_json 里每条明细必须携带
source_table + stock_id(精准定位到某条实物库存记录),
因为报废从“申请那一刻”就要求指明具体要报废的东西。
"""
__tablename__ = 'scrap_approval'
id = db.Column(db.Integer, primary_key=True)
request_no = db.Column(db.String(100), unique=True, nullable=False, index=True)
applicant_id = db.Column(db.Integer, nullable=False, index=True)
remark = db.Column(db.Text) # 报废原因
# 状态: 0-待审批, 1-已通过(待执行), 2-已驳回, 3-已执行(已报废)
status = db.Column(db.Integer, default=0, nullable=False, index=True)
allowed_approvers = db.Column(db.Text) # JSON: [{"type":"user","value":123}, ...]
actual_approver_id = db.Column(db.Integer, index=True)
approved_at = db.Column(db.DateTime)
reject_reason = db.Column(db.Text)
# 明细快照: [{"source_table","stock_id","sku","name","spec_model","location","batch_number","scrap_qty"}]
items_json = db.Column(db.Text, nullable=False)
# 执行信息
executed_at = db.Column(db.DateTime)
executor_name = db.Column(db.String(100))
created_at = db.Column(db.DateTime, default=_beijing_now, nullable=False)
updated_at = db.Column(db.DateTime, default=_beijing_now, onupdate=_beijing_now, nullable=False)
def set_items(self, items):
self.items_json = json.dumps(items or [], ensure_ascii=False)
def get_items(self):
try:
return json.loads(self.items_json) if self.items_json else []
except Exception:
return []
def set_allowed_approvers(self, approvers):
self.allowed_approvers = json.dumps(approvers or [], ensure_ascii=False)
def get_allowed_approvers(self):
try:
return json.loads(self.allowed_approvers) if self.allowed_approvers else []
except Exception:
return []
@staticmethod
def _user_name(user_id):
if not user_id:
return ''
try:
from app.models.system import SysUser
u = SysUser.query.get(user_id)
return u.username if u else ''
except Exception:
return ''
def to_dict(self):
return {
'id': self.id,
'request_no': self.request_no,
'applicant_id': self.applicant_id,
'applicant_name': self._user_name(self.applicant_id),
'remark': self.remark or '',
'status': self.status,
'allowed_approvers': self.get_allowed_approvers(),
'actual_approver_id': self.actual_approver_id,
'actual_approver_name': self._user_name(self.actual_approver_id),
'approved_at': self.approved_at.strftime('%Y-%m-%d %H:%M:%S') if self.approved_at else None,
'reject_reason': self.reject_reason or '',
'items': self.get_items(),
'executed_at': self.executed_at.strftime('%Y-%m-%d %H:%M:%S') if self.executed_at else None,
'executor_name': self.executor_name or '',
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
}