feat(borrow): 借库转交一期数据层(迁移、转交/归还流水表、发货操作人列)
背景 ---- 原 trans_borrow 是单行记录模式,身份维度只有 borrower_name 一个字符串: 「谁借的」与「谁现在还拿着」是同一个字段,无法表达持有权变更;归还时 return_time/return_operator/return_signature 被逐次覆盖,部分归还下 「谁在什么时候还了多少」永久丢失。 本次改动(全部增量,无破坏性 DDL,可回滚) ---- 1) trans_borrow 补列 · borrower_id / current_holder_id / current_holder_name —— 身份锚点 · dispatch_operator —— 执行借出的库管(operator_name 形参此前被接收却从未落库) 2) 新建 trans_borrow_transfer —— 转交流水,一行=一次转交,不做覆盖式更新 3) 新建 trans_borrow_return —— 归还流水,逐次记录,根治部分归还失忆症 4) 历史回填(已执行):85 行中 84 行 borrower_id 唯一命中 sys_user; 未归还的 32 行全部绑定 current_holder 5) 注册 borrow_transfer 权限码(无冒号形式,避免 _expand_operation_perms 前缀桥接把权限放大给所有持有 op_borrow:operation 的角色) 设计取舍 ---- · current_holder 只回填「未归还」行:已归还=物品已回库、无人持有,保持 NULL。 这样「current_holder_id IS NOT NULL」本身就是「仍在某人手上」的有效信号, 归还校验不会对已结清单据误触发。 · 三张表都不建外键:与 trans_return / trans_defective_goods 一致 —— 库存行会被入库模块物理删除(实测已有悬空),台账必须能独立存活。 · dispatch_operator 不回填历史:执行人信息从未被采集,系统中不存在可回填的 数据源;用申请人或借用人冒充实物交接人比留空更危险。 验证:迁移已对 inventory_db 执行,核对段全部通过。
This commit is contained in:
@ -37,6 +37,11 @@ class TransBorrow(db.Model):
|
||||
borrower_name = db.Column(db.String(100))
|
||||
borrow_time = db.Column(db.DateTime, default=beijing_time)
|
||||
borrow_signature = db.Column(db.Text)
|
||||
# [一期收口] 执行借出(扫码发货)的**库管操作人**姓名。
|
||||
# 此前 execute_dispatch 的 operator_name 形参被接收后从未落库,责任链上
|
||||
# 「谁经手发货的」一直缺失。仅展示/追溯用,不参与权限判定或持有人校验。
|
||||
# ★ 该列上线前的历史记录为 NULL(执行人信息从未被采集,无从回填)。
|
||||
dispatch_operator = db.Column(db.String(100))
|
||||
expected_return_time = db.Column(db.DateTime)
|
||||
is_returned = db.Column(db.Boolean, default=False)
|
||||
return_time = db.Column(db.DateTime)
|
||||
@ -48,6 +53,29 @@ class TransBorrow(db.Model):
|
||||
status = db.Column(db.String(20), default='borrowed')
|
||||
remark = db.Column(db.Text)
|
||||
|
||||
# =========================================================================
|
||||
# 借库转交(一期)—— 身份 ID 锚点
|
||||
#
|
||||
# ★ 为什么 ID 与姓名快照并存:
|
||||
# ID 是**唯一**的身份锚点( borrower_name 是字符串,实测 85 行里 18 个姓名
|
||||
# 映射到 18 个人,一旦重名责任链即断);姓名字段保留为**展示快照**,
|
||||
# 避免列表页逐行回查 sys_user 造成 N+1,且用户行被删除后台账仍可读
|
||||
# (与 trans_return.company_name 同一处理方式)。
|
||||
#
|
||||
# ★ 为什么 _id 允许 NULL:
|
||||
# 仅迁移前的历史行可能为 NULL —— 迁移脚本按「姓名唯一命中 sys_user」回填,
|
||||
# 85 行中 84 行成功,1 行(「测试01」)姓名无法映射故留空。
|
||||
# 新数据由 execute_dispatch 强制写入 borrower_id,不存在 NULL。
|
||||
#
|
||||
# ★ borrower_id 与 current_holder_id 的区别:
|
||||
# borrower_id = 初始借用人,写一次不再变(回答「这单最初谁借的」);
|
||||
# current_holder_id = 当前实际持有人,转交会推进它(回答「东西现在在谁手上」)。
|
||||
# 归还走完后 current_holder_id 被清空,borrower_id 保留作历史。
|
||||
# =========================================================================
|
||||
borrower_id = db.Column(db.Integer, index=True) # 初始借用人ID
|
||||
current_holder_id = db.Column(db.Integer, index=True) # 当前持有人ID
|
||||
current_holder_name = db.Column(db.String(100)) # 当前持有人姓名快照
|
||||
|
||||
def to_dict(self):
|
||||
returned_qty = float(self.returned_quantity) if self.returned_quantity is not None else 0
|
||||
total_qty = float(self.quantity) if self.quantity is not None else 0
|
||||
@ -62,8 +90,14 @@ class TransBorrow(db.Model):
|
||||
'quantity': total_qty,
|
||||
'returned_quantity': returned_qty,
|
||||
'pending_quantity': pending_qty,
|
||||
'borrower_id': self.borrower_id,
|
||||
'borrower_name': self.borrower_name,
|
||||
# ★ 当前持有人:转交后前端据此展示「东西在谁手上」并预填归还人
|
||||
'current_holder_id': self.current_holder_id,
|
||||
'current_holder_name': self.current_holder_name,
|
||||
'borrow_time': self.borrow_time.strftime('%Y-%m-%d %H:%M') if self.borrow_time else None,
|
||||
# 执行借出的库管操作人(一期收口;历史行为 NULL)
|
||||
'dispatch_operator': self.dispatch_operator,
|
||||
'borrow_signature': self.borrow_signature,
|
||||
'expected_return_time': self.expected_return_time.strftime('%Y-%m-%d %H:%M') if self.expected_return_time else None,
|
||||
'is_returned': self.is_returned,
|
||||
@ -90,6 +124,112 @@ class TransBorrow(db.Model):
|
||||
return float(result) if result is not None else 0.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 借库转交流水(一期)
|
||||
# =============================================================================
|
||||
# 设计要点
|
||||
# --------
|
||||
# 1. **流水不覆盖**:一行 = 一次转交动作。trans_borrow 是单行模型,只能存
|
||||
# 「当前持有人」一个值;A→B→C 的完整持有链只能由本表回答。
|
||||
# 若改为在 trans_borrow 上覆盖 current_holder,等于重演归还失忆症。
|
||||
#
|
||||
# 2. **一期仅支持整单全量转交**:单行模型无法同时追踪两个持有人,
|
||||
# 部分转交会让 current_holder 语义撕裂(半单归 A、半单归 B)。
|
||||
# 故 transfer_qty 一期恒等于 quantity - returned_quantity,
|
||||
# 服务层对部分转交直接拒绝。未来若需部分转交,需改为按 quantity 拆行。
|
||||
#
|
||||
# 3. **不碰库存**:转交是纯持有权变更,实物不出入库,全程不得触碰
|
||||
# available_quantity / stock_quantity。
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TransBorrowTransfer(db.Model):
|
||||
"""借库转交流水:一行 = 一次转交,不做覆盖式更新。"""
|
||||
|
||||
__tablename__ = 'trans_borrow_transfer'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# 关联主表(不建 FK:与 trans_return 一致,台账必须能独立存活)
|
||||
borrow_id = db.Column(db.Integer, nullable=False, index=True)
|
||||
|
||||
# 转出方(= 转交前的 current_holder)
|
||||
from_user_id = db.Column(db.Integer)
|
||||
from_user_name = db.Column(db.String(100))
|
||||
|
||||
# 接收方(= 转交后的 current_holder)
|
||||
to_user_id = db.Column(db.Integer)
|
||||
to_user_name = db.Column(db.String(100))
|
||||
|
||||
# 一期恒等于 quantity - returned_quantity(整单全量转交)
|
||||
transfer_qty = db.Column(db.Numeric(19, 4), nullable=False, default=0)
|
||||
|
||||
transfer_time = db.Column(db.DateTime, default=beijing_time)
|
||||
operator_name = db.Column(db.String(100)) # 执行转交的库管
|
||||
remark = db.Column(db.Text)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'borrow_id': self.borrow_id,
|
||||
'from_user_id': self.from_user_id,
|
||||
'from_user_name': self.from_user_name,
|
||||
'to_user_id': self.to_user_id,
|
||||
'to_user_name': self.to_user_name,
|
||||
'transfer_qty': float(self.transfer_qty) if self.transfer_qty is not None else 0,
|
||||
'transfer_time': self.transfer_time.strftime('%Y-%m-%d %H:%M:%S') if self.transfer_time else None,
|
||||
'operator_name': self.operator_name,
|
||||
'remark': self.remark,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 借库归还流水
|
||||
# =============================================================================
|
||||
# 为什么需要它
|
||||
# ------------
|
||||
# trans_borrow 原先在部分归还时把 return_time / return_operator /
|
||||
# return_signature **逐次覆盖**,导致「谁在什么时候还了多少」只剩最后一次
|
||||
# (出库退回模块的 TransReturn 已就同一问题另建流水,见本文件上方注释)。
|
||||
# 本表按次记录归还动作,根治该失忆症。
|
||||
#
|
||||
# ★ 主表字段的定位(未废弃,但降级):
|
||||
# returned_quantity / is_returned / status —— 仍是**累计快照**,聚合语义正确,
|
||||
# 列表页的「未还/已还」tab 判定依赖它们,继续维护;
|
||||
# return_time / return_operator / return_signature —— 降级为「最近一次归还」
|
||||
# 展示快照,records.vue 的归还人/归还时间列依赖它们,继续刷新;
|
||||
# 逐次明细的**权威来源**是本表。
|
||||
#
|
||||
# ★ returner_id 与 operator_name 是两个人:
|
||||
# returner_id = 实际把东西交回窗口的人,写入前已强校验 == current_holder_id;
|
||||
# operator_name = 经手办理还库的库管。
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TransBorrowReturn(db.Model):
|
||||
"""借库归还流水:一行 = 一次归还动作。"""
|
||||
|
||||
__tablename__ = 'trans_borrow_return'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
borrow_id = db.Column(db.Integer, nullable=False, index=True) # 关联 trans_borrow.id
|
||||
returner_id = db.Column(db.Integer, index=True) # 实际归还人(==current_holder)
|
||||
return_qty = db.Column(db.Numeric(19, 4), nullable=False, default=0)
|
||||
return_time = db.Column(db.DateTime, default=beijing_time)
|
||||
operator_name = db.Column(db.String(100)) # 经手库管
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'borrow_id': self.borrow_id,
|
||||
'returner_id': self.returner_id,
|
||||
'return_qty': float(self.return_qty) if self.return_qty is not None else 0,
|
||||
'return_time': self.return_time.strftime('%Y-%m-%d %H:%M:%S') if self.return_time else None,
|
||||
'operator_name': self.operator_name,
|
||||
}
|
||||
|
||||
|
||||
class TransRepair(db.Model):
|
||||
__tablename__ = 'trans_repair'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user