成因: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)。
97 lines
3.8 KiB
Python
97 lines
3.8 KiB
Python
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,
|
||
}
|