diff --git a/db_migrations/phase4_borrow_transfer.sql b/db_migrations/phase4_borrow_transfer.sql new file mode 100644 index 0000000..0f47b6d --- /dev/null +++ b/db_migrations/phase4_borrow_transfer.sql @@ -0,0 +1,233 @@ +-- ============================================================================= +-- 借库转交(Borrow Transfer)一期:底层模型改造 + 转交/归还流水表 + 权限码 +-- +-- 背景 +-- 原 trans_borrow 是**单行记录模式**,身份维度只有一个 borrower_name 字符串: +-- · 「谁借的」与「谁现在还拿着」是同一个字段,无法表达持有权变更; +-- · 归还时 return_time / return_operator / return_signature 被**逐次覆盖**, +-- 部分归还下「谁在什么时候还了多少」永久丢失(失忆症)。 +-- 本迁移为转交功能打底,并把身份锚点从「姓名」升级为「ID + 姓名快照」。 +-- +-- 内容分五段: +-- 1) trans_borrow 补三列(borrower_id / current_holder_id / current_holder_name) +-- 2) 新建 trans_borrow_transfer(转交流水) +-- 3) 新建 trans_borrow_return (归还流水,治失忆症) +-- 4) 历史数据回填(仅姓名能唯一映射到 sys_user 的行) +-- 5) 注册 borrow_transfer 权限码 +-- +-- --------------------------------------------------------------------------- +-- 1) 为什么 _id 列允许 NULL,而新写入强制带 ID +-- 存量 85 行只有 borrower_name(纯姓名),其中 17/18 个姓名能唯一映射到 +-- sys_user(username 形如「姓名/拼音」)。无法映射的(如「测试01」)保持 +-- NULL —— 不回填错误数据,也不阻断迁移。 +-- 应用层(execute_dispatch)对**新数据**强制要求 borrower_id, +-- 故 NULL 只可能出现在迁移前的历史行上,是「历史未锚定」的显式标记。 +-- +-- 2) 为什么 current_holder 只回填「未归还」的行 +-- 语义约定:current_holder_id 表示**当前实际持有人**。 +-- · 未归还的行 → 持有人 = 借用人(转交会在此之上继续推进); +-- · 已全部归还的行 → 物品已回库,无人持有,保持 NULL(借用人仍记在 +-- borrower_id,作为历史)。 +-- 这样「current_holder_id IS NOT NULL」本身就是「仍在某人手上」的有效信号, +-- 归还校验(returner_id 必须等于 current_holder_id)不会对已结清单据误触发。 +-- +-- 3) 为什么借用/归还/转交三张表都不建外键 +-- 与 trans_return / trans_defective_goods 的处理一致:库存行会被入库模块 +-- 物理删除(实测已有悬空),台账表必须能独立存活,隔离判定不依赖 join 链。 +-- 故只建索引,不建 FK 约束。 +-- +-- 幂等:全部带 IF NOT EXISTS / WHERE NOT EXISTS,可重复执行。 +-- 执行:docker exec -i inventory_db psql -U test -d inventory_system < 本文件 +-- ============================================================================= + +BEGIN; + +-- --------------------------------------------------------------------------- +-- 1) trans_borrow 补列 +-- --------------------------------------------------------------------------- +-- numeric(19,4) 与 quantity / returned_quantity 同型,保证转交与归还的 +-- 数量比较(qty = quantity - returned_quantity)不引入浮点误差。 +ALTER TABLE trans_borrow + ADD COLUMN IF NOT EXISTS borrower_id integer, + ADD COLUMN IF NOT EXISTS current_holder_id integer, + ADD COLUMN IF NOT EXISTS current_holder_name varchar(100); + +COMMENT ON COLUMN trans_borrow.borrower_id IS + '初始借用人ID(sys_user.id)。历史数据回填,无法唯一映射时为 NULL'; +COMMENT ON COLUMN trans_borrow.current_holder_id IS + '当前实际持有人ID。转交会推进此列;已全部归还时为 NULL'; +COMMENT ON COLUMN trans_borrow.current_holder_name IS + '当前持有人姓名快照。供列表展示,避免逐行回查 sys_user(N+1)'; + +CREATE INDEX IF NOT EXISTS ix_trans_borrow_borrower_id + ON trans_borrow (borrower_id); +CREATE INDEX IF NOT EXISTS ix_trans_borrow_current_holder + ON trans_borrow (current_holder_id); + +-- --------------------------------------------------------------------------- +-- 2) trans_borrow_transfer —— 转交流水 +-- +-- 一行 = 一次转交动作,**不做覆盖式更新**(与 trans_return 同一范式)。 +-- 单行模式的主表只能存「当前持有人」,A→B→C 的链路全靠本表回答。 +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS trans_borrow_transfer ( + id serial PRIMARY KEY, + borrow_id integer NOT NULL, -- 关联 trans_borrow.id + from_user_id integer, -- 转出人(转交前的 current_holder) + from_user_name varchar(100), + to_user_id integer, -- 接收人(转交后的 current_holder) + to_user_name varchar(100), + transfer_qty numeric(19,4) NOT NULL DEFAULT 0, -- 一期恒等于整单待还量 + transfer_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + operator_name varchar(100), -- 执行转交操作的库管 + remark text +); + +COMMENT ON TABLE trans_borrow_transfer IS + '借库转交流水:一行=一次转交,不做覆盖式更新,承载 A→B→C 的完整持有链'; +COMMENT ON COLUMN trans_borrow_transfer.transfer_qty IS + '转交数量。一期仅支持整单全量转交,故恒等于 quantity - returned_quantity'; + +CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_borrow + ON trans_borrow_transfer (borrow_id); +CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_time + ON trans_borrow_transfer (transfer_time); + +-- --------------------------------------------------------------------------- +-- 3) trans_borrow_return —— 归还流水(治失忆症) +-- +-- 原先部分归还时主表的 return_time / return_operator / return_signature +-- 被逐次覆盖,「谁在什么时候还了多少」只剩最后一次。本表按次记录。 +-- +-- ★ 主表字段的去留:returned_quantity / is_returned / status 仍在主表累加 +-- (它们是**累计快照**,聚合语义正确);return_time / return_operator / +-- return_signature 降级为「最近一次归还」展示快照(records.vue 依赖它们 +-- 显示归还人/归还时间),权威的逐次明细以本表为准。 +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS trans_borrow_return ( + id serial PRIMARY KEY, + borrow_id integer NOT NULL, -- 关联 trans_borrow.id + returner_id integer, -- 实际来还东西的人(校验过==current_holder) + return_qty numeric(19,4) NOT NULL DEFAULT 0, + return_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + operator_name varchar(100) -- 执行还库操作的库管 +); + +COMMENT ON TABLE trans_borrow_return IS + '借库归还流水:一行=一次归还,替代主表的覆盖式写入,保留分批归还历史'; +COMMENT ON COLUMN trans_borrow_return.returner_id IS + '实际归还人ID。写入前已强校验其等于 trans_borrow.current_holder_id'; +COMMENT ON COLUMN trans_borrow_return.operator_name IS + '执行还库操作的库管姓名(窗口经手人),与 returner_id 是两个人'; + +CREATE INDEX IF NOT EXISTS ix_trans_borrow_return_borrow + ON trans_borrow_return (borrow_id); +CREATE INDEX IF NOT EXISTS ix_trans_borrow_return_time + ON trans_borrow_return (return_time); +CREATE INDEX IF NOT EXISTS ix_trans_borrow_return_returner + ON trans_borrow_return (returner_id); + +-- --------------------------------------------------------------------------- +-- 4) 历史数据回填 +-- 仅在「姓名在 sys_user 中唯一命中」时回填,杜绝重名错绑。 +-- --------------------------------------------------------------------------- +-- 4.1 borrower_id:全部可唯一映射的行(含已归还,作历史留痕) +UPDATE trans_borrow tb + SET borrower_id = u.id + FROM sys_user u + WHERE tb.borrower_id IS NULL + AND tb.borrower_name IS NOT NULL + AND split_part(u.username, '/', 1) = tb.borrower_name + AND (SELECT count(*) FROM sys_user u2 + WHERE split_part(u2.username, '/', 1) = tb.borrower_name) = 1; + +-- 4.2 current_holder:仅未归还的行(已归还=物品已回库,无人持有) +-- 姓名快照同步写入;不信任 borrower_name 的大小写/空格,统一取 sys_user 口径。 +UPDATE trans_borrow tb + SET current_holder_id = u.id, + current_holder_name = split_part(u.username, '/', 1) + FROM sys_user u + WHERE tb.current_holder_id IS NULL + AND tb.is_returned = FALSE + AND tb.borrower_name IS NOT NULL + AND split_part(u.username, '/', 1) = tb.borrower_name + AND (SELECT count(*) FROM sys_user u2 + WHERE split_part(u2.username, '/', 1) = tb.borrower_name) = 1; + +-- --------------------------------------------------------------------------- +-- 5) 注册 borrow_transfer 权限码 +-- +-- ★ 为什么用**无冒号**形式而不是 op_borrow:transfer: +-- 见 add_outbound_return_perm.sql 的论证 —— 形如 : 的码会 +-- 命中后端 _expand_operation_perms() 的前缀桥接,使持有 op_borrow:operation +-- 的角色被自动放行;无冒号码不触发桥接,与前端 hasPermission 精确匹配一致。 +-- +-- ★ 为什么授予面与 op_borrow:operation 完全一致(含 OUTBOUND): +-- 「能执行借出」的人就是窗口前处理实物交接的人,转交是同一动作的延伸。 +-- 若业务上要求收紧为仅库管,从下方 VALUES 中移除 'OUTBOUND' 即可。 +-- --------------------------------------------------------------------------- +INSERT INTO sys_element (menu_code, name, code, element_type) +SELECT 'op_borrow', '借库转交(库管)', 'borrow_transfer', 'operation' +WHERE NOT EXISTS ( + SELECT 1 FROM sys_element WHERE code = 'borrow_transfer' +); + +INSERT INTO sys_role_permission (role_code, target_code, type, company_name) +SELECT v.role_code, 'borrow_transfer', 'element', NULL +FROM (VALUES ('SUPER_ADMIN'), ('SUPERVISOR'), ('WAREHOUSE_MGR'), ('OUTBOUND')) AS v(role_code) +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_permission r + WHERE r.role_code = v.role_code + AND r.target_code = 'borrow_transfer' + AND r.type = 'element' +); + +COMMIT; + + +-- ============================================================================= +-- 执行后核对 +-- ============================================================================= +\echo '--- 1) trans_borrow 三个新列 ---' +SELECT column_name, data_type FROM information_schema.columns + WHERE table_name = 'trans_borrow' + AND column_name IN ('borrower_id','current_holder_id','current_holder_name') + ORDER BY column_name; + +\echo '--- 2) 两张新表 ---' +SELECT table_name FROM information_schema.tables + WHERE table_name IN ('trans_borrow_transfer','trans_borrow_return') + ORDER BY table_name; + +\echo '--- 3) 回填覆盖率(应 total=85,borrower_id 回填 84,holder 回填=未归还行数)---' +SELECT count(*) AS total, + count(borrower_id) AS borrower_id_filled, + count(*) FILTER (WHERE borrower_id IS NULL) AS borrower_id_null, + count(*) FILTER (WHERE is_returned = FALSE) AS unreturned, + count(current_holder_id) AS holder_filled + FROM trans_borrow; + +\echo '--- 4) 未能锚定的历史行(预期:测试01)---' +SELECT id, borrow_no, borrower_name, is_returned + FROM trans_borrow WHERE borrower_id IS NULL ORDER BY id; + +\echo '--- 5) 权限码与授权(应 1 元素 + 4 角色)---' +SELECT e.code, e.name, e.menu_code, e.element_type + FROM sys_element e WHERE e.code = 'borrow_transfer'; +SELECT role_code, COALESCE(company_name, '') AS company_name + FROM sys_role_permission WHERE target_code = 'borrow_transfer' ORDER BY role_code; + + +-- ============================================================================= +-- 回滚段 +-- ============================================================================= +-- BEGIN; +-- DELETE FROM sys_role_permission WHERE target_code = 'borrow_transfer'; +-- DELETE FROM sys_element WHERE code = 'borrow_transfer'; +-- DROP TABLE IF EXISTS trans_borrow_return; +-- DROP TABLE IF EXISTS trans_borrow_transfer; +-- ALTER TABLE trans_borrow +-- DROP COLUMN IF EXISTS borrower_id, +-- DROP COLUMN IF EXISTS current_holder_id, +-- DROP COLUMN IF EXISTS current_holder_name; +-- COMMIT; diff --git a/db_migrations/phase4b_borrow_dispatch_operator.sql b/db_migrations/phase4b_borrow_dispatch_operator.sql new file mode 100644 index 0000000..a7d213f --- /dev/null +++ b/db_migrations/phase4b_borrow_dispatch_operator.sql @@ -0,0 +1,58 @@ +-- ============================================================================= +-- 借库转交一期收口:trans_borrow 补 dispatch_operator(发货操作人) +-- +-- 背景 +-- 责任链上「谁借的 / 谁现在拿着 / 谁还的」在此前改造中已全部锚定到 ID, +-- 唯独**执行借出的库管操作人**一直没落库 —— execute_dispatch 的 +-- operator_name 形参被接收后从未写入任何字段(首轮调研已标记该缺口)。 +-- 本迁移补齐最后一环,使一次借出在台账上能回答四个问题: +-- 借用人 borrower_id / 当前持有人 current_holder_id / +-- 发货操作人 dispatch_operator / 归还人 trans_borrow_return.returner_id +-- +-- --------------------------------------------------------------------------- +-- ★ 为什么存「姓名」而不是 ID +-- execute_dispatch 的 operator_name 由 API 层经 _current_username() 取得, +-- 本身就是展示口径的字符串(与 trans_borrow_return.operator_name 一致)。 +-- 该字段仅用于台账展示与追溯,不参与任何权限判定或持有人校验, +-- 故不引入 ID —— 需要精确到人时,审计日志(audit_listener)另有记录。 +-- +-- ★ 为什么不回填历史值 +-- 改造前的执行人信息**从未被采集**,系统中不存在任何可回填的数据源 +-- (trans_borrow 没有对应列,TransBorrow 也没有其它操作人字段)。 +-- 刻意留空而非猜测填充 —— 用申请人或借用人冒充实物交接人, +-- 比留空更危险。NULL 即表示「本次借出发生在该字段上线之前」。 +-- +-- 幂等:带 IF NOT EXISTS,可重复执行。 +-- 执行:docker exec -i inventory_db psql -U test -d inventory_system < 本文件 +-- ============================================================================= + +BEGIN; + +ALTER TABLE trans_borrow + ADD COLUMN IF NOT EXISTS dispatch_operator varchar(100); + +COMMENT ON COLUMN trans_borrow.dispatch_operator IS + '执行借出(扫码发货)的库管操作人姓名。仅展示/追溯用,不参与权限判定;' + '该列上线前的历史记录为 NULL'; + +COMMIT; + + +-- ============================================================================= +-- 执行后核对 +-- ============================================================================= +\echo '--- 1) 新列已就位 ---' +SELECT column_name, data_type, character_maximum_length + FROM information_schema.columns + WHERE table_name = 'trans_borrow' AND column_name = 'dispatch_operator'; + +\echo '--- 2) 存量行均为 NULL(预期:全部为 NULL,历史数据无从回填)---' +SELECT count(*) AS total, count(dispatch_operator) AS filled FROM trans_borrow; + + +-- ============================================================================= +-- 回滚段 +-- ============================================================================= +-- BEGIN; +-- ALTER TABLE trans_borrow DROP COLUMN IF EXISTS dispatch_operator; +-- COMMIT; diff --git a/inventory-backend/app/__init__.py b/inventory-backend/app/__init__.py index fa42c56..0674fa1 100644 --- a/inventory-backend/app/__init__.py +++ b/inventory-backend/app/__init__.py @@ -314,9 +314,12 @@ def create_app(): # 预加载:审计监听器按表名从 db.metadata 取模型,未预加载的表在 # create_app() 完成时尚未映射,会漏绑审计(虽然后续惰性补绑能兜底, # 但预加载更可靠)。 + # ★ TransBorrowTransfer / TransBorrowReturn 为一期借库转交新增, + # 同理必须预加载 —— 转交与归还都是责任链上的关键动作,审计绝不能漏。 from app.models.transaction import ( TransBorrow, TransRepair, TransScrap, TransReturn, TransDefectiveGoods, + TransBorrowTransfer, TransBorrowReturn, ) # ★ 审批单模型(原仅在函数体内延迟导入,会导致审计监听器漏绑) from app.models.outbound import OutboundApproval diff --git a/inventory-backend/app/models/transaction.py b/inventory-backend/app/models/transaction.py index 2da42f9..666c537 100644 --- a/inventory-backend/app/models/transaction.py +++ b/inventory-backend/app/models/transaction.py @@ -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'