|
|
3bb5fc6eb8
|
fix(outbound): 执行校验兼容无 base_id 的历史单据,修复老单必然被拒
2026-09-10 预占改造(b57c21a)之前建的单,items_json 里没有 base_id。
这类单只要在改造上线时仍处于「已通过待执行」状态,就再也执行不了:
批准侧 build_approval_index() → identity_key(base_id, name, spec)
无 base_id → 退化成 ('name', 名称, 规格)
扫码侧 stock_identity() → ('base', id)
两侧键不同构,**永远不相等**,verify_scanned() 必然抛
「扫码物料【…】不在该申请单的批准明细中,禁止出库」——批的就是这件货。
identity_key() 的文档本就把 (name, spec_model) 写作「历史数据的兜底」,
只是校验时只有批准侧降了级、扫码侧没有,两侧因此错开。
实测(APR-OUT-20260909-1313-0007,建于 09-09,改造上线前一天):
批准侧 ('name', '派里肯安全箱1600(黑色)', 'PS-9640B001-black')
扫码侧 ('base', 3012)
修复:
· 新增 stock_name_spec() —— 库存行 → 名称型身份键,刻意丢掉 base_id
· 新增 build_legacy_approval_index() —— 无 base_id 的历史明细旁路索引
· verify_scanned() —— 主索引落空时用库存行名称+规格再比一次;命中则
改用**批准侧的键**继续,使 acc 累计与 approved_idx 的批准量同口径
安全边界(关键):降级只对真正来自老单据的名称型键放行,白名单是
legacy_idx 而非 approved_idx 本身——build_legacy_approval_index() 只收
identity_key() 产出名称型键的明细,有 base_id 的一律跳过;名称与规格皆空
的明细直接丢弃,绝不退化成「任意物料都能匹配」。无老单时该索引为空集,
降级分支恒不命中,行为与改造前逐字节一致。
影响范围:当时处于 status=1 的老单全库仅 1 张(即上述单号)。其余 358 张
老单(已完成 327 / 已驳回 9 / 已完结 22)均为终态,不受影响——它们在改造
上线前就已执行完毕。
实测验证:
T1 老单 360 + 扫批准范围内物料 修复前必拒 → 修复后通过
T2 老单 360 + 扫单外物料 仍拒绝
T3 老单 360 + 数量超批准 仍拒绝
T4 现代单 + 扫批准范围内物料 通过(旁路索引 0 键,未受影响)
T5 现代单 + 扫单外物料 仍拒绝
副作用改善:降级后标签改用批准侧的键,超量报错从「物料#3012」变为
「派里肯安全箱1600(黑色)(PS-9640B001-black)」,可读性提升。
|
2026-09-20 16:43:37 +08:00 |
|
|
|
26a1857acb
|
fix(purchase): 待采购清单的参考价格改按 material_list:referencePrice 管控
修复一个权限旁路:待采购清单的「参考单价」原本后端无条件返回、前端列也没有
任何门控,而 material_list:referencePrice 只授予 SUPER_ADMIN 与 SUPERVISOR。
结果是 WAREHOUSE_MGR(库管) / INBOUND(入库员) / SALES(销售) 虽然都持有
inbound_purchase:pending_pool、能打开待采购清单,就会看到参考价格 —— 而这个
数字他们在物料列表里是被挡住的。等于本页面成了绕过该权限的后门。
参考价格来自 material_base.reference_price,与物料列表同源,因此复用同一个
权限码管控,不另造新码(同源数据用同码,口径才不会漂)。
改动:
- 后端 get_pending_purchase_pool 新增 include_reference_price 参数,
fail-closed 默认 False;无权限时**整个字段不返回**而不是给 None
- 新增 _has_material_reference_price_perm(),判定方式与既有的
_filter_purchase_prices 保持一致(超管/主管放行 + 逐个权限码比对)
- 前端「参考单价」列加 hasPermission 门控,TS 类型改为可选
★ 只挡前端等于没挡(接口仍然裸奔),前后端必须同时改。
|
2026-09-18 14:36:02 +08:00 |
|
|
|
b4445f07d9
|
feat(material): 物料列表新增「采购在途」自动标识,预警邮件改用活跃单静音
base_service.get_list:
新增 isPurchasing 字段,与待采购池共用同一份活跃单判定。放在预警权限
判断之外 —— 能看到物料列表的人都该知道这个物料已经在采购了。
inventory_task:
静音条件从人工标记 is_ordered 改为「该物料无活跃采购单」。
★ 为什么必须同时改邮件:预警邮件由出库执行流程触发(outbound_service 出库
完成后的调用),功能是活的;而它的静音入口(前端人工「标记已采购」)随本次
改造一并移除。若不改这里,用户会彻底失去静音能力,每个缺货物料在每次出库后
都会发信。自动判定在建单时静默、单据驳回或强制结案后自动恢复,比人工标记准。
两张表的判定口径都由 utils/purchase_activity.py 提供,不各写一遍。
|
2026-09-18 14:31:00 +08:00 |
|
|
|
dec33b685c
|
feat(purchase): 新增待采购池接口(防重复采购)
GET /api/v1/purchase/pending-pool:找出「有效供给仍低于预警线」的物料。
有效供给 = 物理总库存 + 在途量
过滤规则 = 有效供给 <= 红/黄阈值 才进池
建议采购量 = max(1, 目标阈值 - 有效供给)
★ 为什么不用「有活跃单就排除」:红线 10、库存 0、某采购员只建了一张
数量 5 的单时,该物料会立刻从池中消失,剩下 5 个缺口永远无人认领。
改为按量计算后它继续留池,suggested_qty 自动降到 5。
★ 用 <= 而非 < 是与业务方确认后刻意维持的口径(与物料列表预警、预警邮件
同源),勿擅自改成严格小于 —— 只改本接口会造成「列表亮黄灯、池子却排除」
的撕裂,真要改必须三处一起动。代价是恰好等于阈值时建议量落到保底的 1,
前端 tooltip 已单独说明,不展示算不成立的错误算式。
权限:新增 inbound_purchase:pending_pool(注册为 sys_element 而非新建
SysMenu)。因 ensure_default_permissions 在角色已有权限时整段跳过,另写了
存量角色自动迁移,并按源行镜像 company_name 避免跨公司作用域泄漏。
|
2026-09-18 14:30:53 +08:00 |
|
|
|
eafdf992c7
|
fix(borrow): 借还记录「归还人」列错标成了经手库管,拆成两列
问题
----
列表里那一列标着「归还人」,读的却是 trans_borrow.return_operator —— 而该字段
存的是**办理还库的库管**,不是来还东西的人。两者本就是不同的人:
· returner_id(trans_borrow_return)—— 把东西交回窗口的人,已校验 == 当时持有人
· return_operator —— 经手办理的库管
实测(借用行 120):return_operator = 杜邢宸/duxingchen(库管),
而实际归还人是 returner_id = 21(测试)—— 页面却显示成了「杜邢宸」。
改动
----
一、后端 get_records 增补 returners:从 trans_borrow_return 取 returner_id 并
反查 sys_user 得到姓名(去重按明细挂回)。批量查一次,不做 N+1。
二、前端拆成两列,各自名副其实:
「归还人」 ← returners(后端新增)
「经手库管」 ← return_operators(原列改为正确标签)
三、顺带修展示口径:return_operator 存的是**完整 username**(高闯/gaochuang),
未按全站口径截断。_display_borrow_operator 现统一归一到展示名
(数字 id 反查 / 姓名、斜杠前段 / 已是展示名 原样),并修正其 docstring
—— 它原本也把该字段称作「归还人」,是同一个误解的源头。
★ 历史数据的现实
实际归还人流水是二期才建的,**历史归还没有这个记录**。这部分行的「归还人」
显示为空并挂 tooltip 说明「该笔归还发生在实际归还人记录上线之前」——
刻意不拿库管的名字顶上,那正是本次要修的错。
验证
BOR-20260918-0001 → 归还人=['测试']、经手库管='杜邢宸' ✓ 两者分开
BOR-20260914-0001 → 归还人=[](历史)、经手库管='高闯' ✓
归一化:'高闯/gaochuang'→'高闯'、'21'→'测试' ✓
前端 vite build 通过;本次无需 DB 迁移。
|
2026-09-18 09:43:25 +08:00 |
|
|
|
e3fe1fc12a
|
feat(borrow): 借还记录「已归还」页签改为按归还时间倒序
问题
----
三个页签共用同一套排序(有限期单在前 → 最早应还时间 ASC → 最早借出时间 DESC),
这套「优先关注快到期/逾期」的逻辑对「未归还」是对的,但对「已归还」正好**
反了**:已归还列表要回答的是「最近还了哪几笔」,而按应还时间排会让最近刚还的
那几笔排到最后。
改动
----
order_subq 增加 max_return_time(单号内**最晚**一次归还时间)作为排序键,
并按页签分流:
· 已归还 → nullslast(max_return_time DESC),borrow_no DESC 兜底保证稳定
· 全部/未归还 → 原三级排序不变
★ 为什么取「最晚」而不是「最早」一次归还时间
多明细分批归还时,整单结清的那一刻才是有意义的节点;且与主行「归还时间」列
的展示口径一致(前端同样取 latest),排序依据与可见值不会打架。
验证(真实数据)
已归还页签:09-15 11:41 → 09-14 16:02 → 09-09 11:46 → 09-08 13:22 →
09-04 17:26 → 09-04 09:45 → 09-03 15:25,第 2 页续 08-27 → …,
严格递减且**跨页连续**;
未归还页签:排序与改动前完全一致(回归确认)。
|
2026-09-18 09:34:45 +08:00 |
|
|
|
191724a176
|
fix(outbound): 修复扫码出库 500 —— applicant_id 用了尚未赋值的 approval
现象
----
所有扫码出库报 500(前端 create.vue 提交即失败)。
根因
----
上一个提交把 'applicant_id': approval.applicant_id 写进了 common_data,
但 common_data 构造在第 234 行,而 approval 直到第 269 行(「强制按单出库」
那一段)才查询赋值 —— 典型的变量先用后赋,直接 UnboundLocalError。
它影响的是**每一条**出库请求,属于必然复现而非偶发。
修复
----
把赋值移到 approval 取出并校验**之后**:common_data['applicant_id'] = ...
并在原处留注释说明为什么不能写在字典字面量里,避免后人搬回去。
★ 我的测试为什么没抓到
上一轮只测了「退回 → 补发」这条路径,而 applicant_id 的写入在**扫码出库**
路径上 —— 两条路径不重合,所以漏了。本次补测了完整的
「出库申请(预占) → 扫码出库(扣减)」链路。
验证(6 项断言全通过,走真实接口链路)
申请单创建(status=1,申请人=12,预占 stock_buy#2058 两件)
→ 扫码出库不再抛 500
→ 出库明细生成且 applicant_id = 12(不再是 NULL)
→ consumer_name 照常写入
→ 审批单置为已完成
→ 库存实际扣减 2
清理后库存与数据零残留。
★ 顺带在真实数据上得到印证:生产库中已有一笔由业务方重试成功的出库
(OUT-20260917-1333-0001),其 applicant_id 正确等于所关联申请单的申请人。
|
2026-09-17 13:35:47 +08:00 |
|
|
|
0005a689dc
|
fix(outbound): 补发申请人取原申请人,绝不再回退为库管
问题
----
上一版在库管未指定「补发给谁」时,把补发单申请人回退成了**当前操作人(库管)**。
但补发是**原申请人的需求**,挂到库管名下逻辑不通 —— 那张单会出现在库管的
「我的申请」里,而真正该拿东西的人什么也看不到。
根因
----
trans_outbound 只有 consumer_name(**扫码时前端自由填写**的领用人/客户名,
既不可靠也可能是外部客户),没有任何指回原审批单的关联,所以当时只能退而求其次。
根治
----
一、trans_outbound 新增 applicant_id,**创建出库时从关联审批单带出**
(request_id 已强制必填、approval 恒非 None,故新单据必然有值)。
落这一列后,「退回 → 补发」即可自动找回真正的原申请人。
⚠ 存量行为 NULL —— 存量出库与其来源审批单之间没有任何可用关联,无从回填。
刻意留 NULL 而不是按姓名猜(consumer_name 是自由文本,会重名错绑),
与 dispatch_operator 同一取舍:宁可留空,也不猜。
二、退回接口的申请人优先级改为:
① 前端显式指定 reissue_applicant_id
② 原出库明细记录的 applicant_id(真实原申请人)
③ 都没有 → **报错要求指定**
★ 彻底移除「回退为当前操作人」—— 那正是本次要修的逻辑错误。
三、出库列表明细返回 applicant_id,供前端精确预填。
验证(9 项断言全通过)
· 新单据 → 申请人 = 原申请人(12),绝不是库管(7)
· 显式指定优先于原申请人
★ 历史单据未指定 → 接口拒绝、要求选择「补发给谁」、整笔回滚、
且**未生成任何挂在库管名下的补发单**
· 历史单据 + 显式指定 → 正常
库存与数据零残留。
|
2026-09-17 12:07:21 +08:00 |
|
|
|
6cb30a21fd
|
feat(material): 采购链接接入读写映射与服务层
· models/base.py:新增 purchase_link 列(text)并加入 to_dict(purchaseLink)
· field_permissions.py:读过滤映射 purchaseLink → material_list:purchaseLink
· inbound/base.py:POST 与 PUT 两处 field_to_perm 同时补入(漏掉任一处,
新增/修改就会各缺一半)
· base_service.py:create_material 写入、update_material 按字段存在与否更新
★ 写侧刻意**显式纳入映射**而非依赖「不在映射中→默认允许」的兜底分支 ——
那个分支正是上一轮附件备注「谁都能写」的成因,新字段不再走它。
验证(6 个角色)
SUPER_ADMIN / SUPERVISOR / WAREHOUSE_MGR / INBOUND → 读✓ 写✓
OUTBOUND / SALES → 读✗ 写✗
读写逐角色一致;超管走 material_list:* 通配分支、过滤整段跳过(已单独复核)。
|
2026-09-17 11:26:36 +08:00 |
|
|
|
681607bd43
|
feat(borrow): 拒收原因独立成列,与转交备注彻底分离
背景
----
拒收原因此前是**拼进 remark** 的:
transfer.remark = f"{remark}\n[拒绝原因] {reason}"
前端拿到的是「3333\n[拒绝原因] 5555」这样一坨,时间线上两句挤在一起,
无法分辨哪句是发起备注、哪句是对方拒收的原因。
改动
----
· trans_borrow_transfer 新增 reject_reason text 列;
reject_transfer 改为写入该列,不再拼进 remark。
· 存量按 '[拒绝原因] ' 标记切分回填(实测仅 #22:
remark 3333 / reject_reason 5555)。
· 时间线事件带出 reject_reason,前端才能分行展示。
★ 为什么拆列而不是让前端解析字符串
1) 拼接格式是隐式契约:改分隔符或加前缀,前端解析就静默失效且难排查;
2) 用户完全可能在备注里自己打出 '[拒绝原因]' 字样,按标记切分必然误判 ——
已加测试用例锁定该场景;
3) 结构化字段才能参与查询与统计(如按拒收原因归类)。
存储层能表达的东西,不该靠字符串约定去还原。
★ 一个迁移期踩到的坑:btrim 默认只去空格、不去换行。
拼接留下的是 '3333\n',只写 btrim(x) 会残留换行;必须显式给出字符集
btrim(x, E' \t\r\n')。已修正脚本并对存量做了一次清理。
验证(7 项断言全通过)
备注不被污染、原因写独立列、无原因时为 None、
用户备注含同名标记也不误判、库存零副作用、数据零残留。
|
2026-09-17 10:46:31 +08:00 |
|
|
|
f24797ff1f
|
feat(borrow): 拒收须告知发起方(责任回到他手上,不能静默)
背景
----
双向握手补上了「接收人确认」,却只做了单向告知:接收人能看到待办,发起方却
对结果一无所知。**被拒绝时物品责任仍在发起方手上** —— 他若不主动查列表,
就会误以为已经交接出去,责任链出现静默断点。
(ACCEPTED 不需要告知:东西已经交出去了,发起方无需动作。)
改动
----
· trans_borrow_transfer 新增 reject_seen_at(NULL 且 REJECTED = 尚未告知)。
★ 为什么需要持久标记而不是前端去重:换台电脑、换个浏览器就会重新提醒;
而这条信息的分量(责任归属)值得一个持久标记。
★ 存量已拒绝的流水一律标记为已告知:它们产生于本功能上线之前,
追溯提醒只会打扰(实测仅 1 条:#22,验收时的测试数据)。
· get_unseen_rejects(user_id):返回「我发起、被拒、尚未告知我」的转交,
并批量解析物料名 —— 只说「某笔转交被拒」发起方仍不知是哪件东西还在
自己手上,必须让他一眼认出来。
· ack_rejects(user_id, ids):发起方确认后写 reject_seen_at,幂等。
· GET .../transfer/pending-count 的响应并入 rejects:与待接收数量共用同一次
轮询,前端不必多打一个请求。
· POST .../transfer/reject-ack:无 permission_required,同 accept/reject。
顺带补一处同源显示缺口
----
流转时间线里,被拒绝的转交与成功的长得一模一样 —— 发起方翻记录时同样会
误判。现将转交状态一并带出时间线事件。
验证(15 项断言全通过)
----
发起方收到待告知的拒绝(含物料名/接收人/拒绝原因);接收人与无关人看不到;
ack 后不再提醒且幂等;ACCEPTED 不产生告知;None/非法 user_id 均安全返回;
库存零副作用、数据零残留。
|
2026-09-17 10:44:00 +08:00 |
|
|
|
1c58789fd9
|
feat(borrow): 新增「待我接收」转交数量接口
GET /api/v1/transactions/borrow/transfer/pending-count -> { count: X }
用途:双向握手引入后,发起方提交了转交,接收人若不来借还记录页主动查看就
完全处于盲区 —— 物品挂着「待接收」,责任悬空。该接口供前端做全局强提醒。
设计
----
· 刻意做成极轻量:一次 count,不联表、不解析物料名。轮询接口必须便宜,
否则会从「提醒」变成「后台噪音」。
· 无 permission_required:接收人可能是普通员工,待办提醒必须人人可见
(与 accept/reject 同级 —— 员工处置自己名下资产,非库管职权)。
· user_id 为 None / 非法时返回 0 而不是抛错:提醒类接口不该因边界输入 500。
路由无冲突
----
「transfer」匹配不了 <int:borrow_id>,「pending-count」也匹配不了
<int:transfer_id>,Werkzeug 按转换器精确分派。实测:
GET /borrow/transfer/pending-count -> 401(已注册且受 JWT 保护)
GET /borrow/11/transfer -> 405(路径命中但方法不符,证无冲突)
验证(9 项断言全通过)
----
发起后接收人计数 +1、非接收人不变;拒绝/接收后均回落;
None 与非法 user_id 返回 0 不报错;库存零副作用、数据零残留。
|
2026-09-17 10:30:07 +08:00 |
|
|
|
4bd6765ab4
|
feat(borrow): 转交发起收紧为「仅当前持有人本人」(责任链隔离)
背景
----
此前【转交】只要持有 borrow_transfer 权限就可见可调,与「当前持有人」无关 ——
任何库管都能把别人保管的资产转给第三方,责任链形同虚设。业务方确认改为
**只有该物品的当前持有人本人可以发起**。
改动(前后端同改,缺一不可)
----
· service.transfer_borrow 新增 caller_user_id,强校验其 == 该明细
current_holder_id;传 None 一律拒绝,不做「系统内部调用」的隐式放行。
· get_records 为每条明细附加 can_transfer(当前持有人 == 我)—— 前端
localStorage 里只有 username 没有 user_id,故与 is_mine 一样由后端判定。
· 前端明细行【转交】改判 can_transfer;主行【转交】改为「该单下存在由我持有
的未还物品」时才出现;弹窗候选也过滤为「由我持有」,不是我的不列进来
(后端会拒,列出来只会误导)。
★ 连带调整:移除 route 上的 permission_required('borrow_transfer')
责任链规则既然是「持有人本人」,而持有人是普通员工、通常不持有库管权限,
再加一道库管权限,实际能发起的人变成「持有人 ∩ 库管」,绝大多数持有人
反而发不了 —— 功能形同虚设。这与 accept/reject 同级:员工处置自己名下资产。
真正的边界是 service 层的 caller_user_id 强校验,不是界面遮挡。
⚠ 由此 borrow_transfer 权限码已无任何代码引用(sys_element 中的定义与
4 个角色的授权仍在,属无害冗余)。若后续需要「管理员代办」入口,
可在此基础上加豁免;若确定不需要,该权限码可择期下线。
验证(13 项断言全通过)
----
· 非持有人发起被拒;未传调用者被拒;持有人转给自己被拒
· 持有人本人发起成功,from_user_id 正确记为持有人
· can_transfer:持有人 True / 接收人 False;接收转移后新持有人变 True
· 接收环节不受影响;库存零副作用、数据零残留
|
2026-09-17 10:20:34 +08:00 |
|
|
|
7d9cdeb297
|
feat(borrow): 转交粒度下沉到明细行,支持部分转交
背景(业务方推翻上一轮约束)
----
上一轮按「一张单同时只能有一个持有人」实现了**整单转交**,并把「单内出现多个
持有人」当作 bug 去修。业务方验收后明确纠正:
物理现场经常只转交部分工具(借了 2 件、只把 1 件转给别人),
单内多持有人才是符合现实的正常状态。
故转交粒度从 borrow_no 下沉回 trans_borrow.id(明细行)。
改动
----
· transfer_borrow:只操作传入的那**一行**明细,不再按单号整批覆盖。
转出方 = 该行当前持有人;数量 = 该行待还量。
· accept_transfer:只转移 transfer.borrow_id 指向的那一行 ——
整批改写会把别人手上的东西一并抢过来(部分转交下同单明细分属不同人)。
· 唯一性约束从「单号至多一条 PENDING」下沉为「明细行至多一条」:
同单的其他明细可以同时各自挂着待接收,互不阻塞 —— 这正是部分转交的语义。
· get_records 的 pending_transfer 改按 borrow_id 关联(原按 borrow_no),
否则同单多项待接收会互相覆盖。
· 删除已无用的 _load_slip_for_update。
★ 数量粒度:一行只支持**整行转交**。一行只能有一个 current_holder_id,
「同一行只转一部分」需要把这行拆成两行 —— 经业务确认,现场场景中
「借 2 件转 1 件」的两件本就是两条明细行,故该限制不影响实际使用;
接口对传入的非整行数量会明确提示「应另立一条明细行」。
数据层
----
无需改表结构:borrow_id 本就是流水的关联列,borrow_no 退化为单据归属与
分组展示用。仅补 (borrow_id, status) 复合索引支撑新的查询路径。
存量撕裂数据(BOR-20260917-0001 的「测试 / 杜邢宸」)按业务方选择**保留不动**
—— 它现在不再是 bug,而是部分转交的正常形态。
验证(合成 2 明细单,21 项断言全通过)
----
· 只转工具A:工具B 完全不受影响
· 同一张单可同时挂两条待接收,互不阻塞;同一明细重复发起被拒
· accept 工具A 后:A→测试,B 仍是杜邢宸(单内两个持有人)
· 两个持有人、以及待接收人,三方各自都能在列表中看到该单
· pending_transfer 挂在正确的明细行上,is_mine 判定正确
· reject 后主表持有人不变;非整行数量被拒并提示拆行
· 全程 available_quantity 无变化,库存精确还原、零残留数据
|
2026-09-17 10:12:31 +08:00 |
|
|
|
1a8e3e3dc0
|
feat(borrow): 转交改为整单覆盖 + 双向握手,并修复接收人可见性
一、整单覆盖(修复漏行 / 单内撕裂)
----
transfer_borrow 现在按 borrow_no 定位**整张单**,覆盖全部未还明细;accept 时
整批转移持有权。原先只改传入的那一行,2 明细的单转完会出现两个持有人。
新增 _load_slip_for_update():按单号锁整单,且按 id 升序取行锁 —— 并发下所有
事务以相同顺序加锁,避免与归还/转交交叉加锁死锁。
二、双向握手
----
· transfer_borrow(发起):只落一条 PENDING 流水,**不再改主表 current_holder**。
东西还没到对方手上,责任仍归原持有人 —— 这是与旧实现最本质的区别。
同一单号已有 PENDING 时拒绝再次发起,避免两个接收人争抢同一批实物。
· accept_transfer:流水置 ACCEPTED,把该单**全部未还明细**的持有人改为接收人。
. reject_transfer:流水置 REJECTED,主表不动。
两者都强校验「当前登录人 == to_user_id 本人」。
★ accept/reject 刻意**不加 permission_required**:这不是库管职权,而是员工对
自己名下资产的确认动作,加库管权限会把接收人挡在门外。
三、接收人可见性(OR 过滤)
----
get_records 普通用户过滤原先只比对 borrower_name,接收人在自己的列表里看不到
已经接收的东西。现改为三种关系任一成立:
① 我是借用人
② 我是**当前持有人**(转交接收后)
③ 有一条**待我接收**的 PENDING 转交 —— 东西还在对方手上、主表尚未转移,
② 匹配不到,必须单独并入,否则接收人看不到待办、无从确认
ID 与姓名双口径并存,兼容只有姓名没有 ID 的历史行。
列表项附加 pending_transfer(含后端判定的 is_mine)—— 前端 localStorage 里
只有 username 没有 user_id,靠姓名比对既有歧义又不可靠,故由后端标记。
四、验证(合成 2 明细单,25 项断言全通过)
----
· 发起后两条明细持有人均未变(责任未转移)
· 非接收人无法 accept / reject;重复发起被拒
· ★ accept 后**两条明细**持有人一并转移(漏行修复的核心)
· 接收前凭 PENDING 分支可见、接收后凭 current_holder 可见
· reject 后主表持有人不变
· 全程 available_quantity 无变化,库存精确还原、零残留数据
|
2026-09-17 10:04:12 +08:00 |
|
|
|
15d0c6b97a
|
fix(borrow): 修复借还记录排序被静默丢弃,无限期改按借出时间从近到远
现象
----
借还记录列表的排序看起来毫无规律:无限期单排在有限期前面,有限期内
10-01 排在 11-01 之后。业务方反馈「不是逾期的、剩余天数最近的排前面吗?」
根因
----
三级复合排序(trans_service get_records 步骤 2)算得完全正确,但**结果被
后面一步覆盖**:
# 步骤 2:算出分页用的 page_borrow_nos(顺序正确)
# 步骤 3:再按集合把明细拉回来 ——
detail_records = TransBorrow.query.filter(borrow_no.in_(page_borrow_nos))
.order_by(TransBorrow.borrow_no.asc(), ...)
单号形如 BOR-YYYYMMDD-NNNN,**它的字母序恰好等于借出日期序**。于是这 10 条
明细被重排成「按借出日期升序」,那份精心设计的排序被整套丢弃。
实测(修复前,未归还页签第 1 页):
1 BOR-20260413-0001 无限期 04-13 ← 无限期在最前
4 BOR-20260611-0001 无限期 06-11
5 BOR-20260903-0010 逾期 09-10 ← 逾期单反而最后
8 BOR-20260904-0005 10-01 ← 10-01 排在 11-01 之后
★ 该功能自上线起从未生效:
1450e6c (06-16) 引入按 borrow_no 重排的明细拉取
73510d3 (09-04) 才加入三级复合排序 —— 加在了被覆盖的路径上,
提交信息「借还记录默认排序重构」名存实亡。
修复
----
按 page_borrow_nos 的顺序还原输出(明细内部仍按 id 升序,即扫码顺序)。
同时按业务方要求调整第二梯队方向:
① 有限期单在前(有任何明细含预计归还时间)
② 有限期内按单内最早预计归还时间**升序** —— 逾期优先,其后剩余天数由近到远
③ 无限期内按单内最早借出时间**降序**(从近到远)
★ 原为升序「借出越久越靠前,暴露呆滞借用」,业务方明确要求反转
修复后实测(未归还页签):
有限期 09-10(逾期7天) → 09-11(逾期6天) → 09-15(逾期2天) → 10-01 → 11-01 → 11-27
无限期 09-17 → 09-14 → 09-11 → 09-10 → … → 04-13(跨页连续)
验证:borrowed / returned 两个页签各 3 页顺序全部核对通过;关键词、物料名、
高级筛选、日期范围、空结果六条过滤路径冒烟通过;同单号明细未被跨单号打散。
|
2026-09-17 09:53:26 +08:00 |
|
|
|
8d83d4404e
|
fix(borrow): 恢复选中审批单后的「一键带出」,并补上领用人自动回填
现象
----
借库执行页选中一张已通过的审批单后,下方的「预计归还日期」「备注说明」
不再自动带出;「领用人」在改为用户下拉后也恒为空,库管每次都得手工补填。
根因(不在前端下拉框,而在后端持久化)
----
borrow_service.submit_approval 会把 reserve_for_items() 的返回值直接
set_items() 落库 —— 即审批单的 items_json **就是预占结果**。
而 reserve_for_items 内部是 `reserved.append({...})` **重建**字典,只保留
库存定位与数量,把申请端按明细传上来的 expected_return_time / is_indefinite
全部丢弃。执行页读的正是 firstItem.expected_return_time,读不到就落到
「清空」分支。
这是 2026-09-10 库存预占改造引入的回退:
b57c21a fix 前: approval.set_items(items) # 原始申请明细
b57c21a fix 后: approval.set_items(reserved_items) # 重建字典,字段丢失
数据完全吻合该提交的上线时刻(09-10 14:59):
最后一张「有日期」的审批单 id=40 创建于 09-10 09:30
第一张「丢失日期」的审批单 id=41 创建于 09-10 16:20
修复
----
1) inventory_reservation.reserve_for_items:透传申请「意图字段」
(expected_return_time / is_indefinite / remark)到预占结果。
· 一条申请明细会被拆到多个批次行(同物料多批次),故按 base_id 建索引,
把同一份意图回填到它拆分出的每一行;
· **只透传意图,不透传库存定位与数量** —— 实扫可能换批次,
这里写什么执行页就按什么释放,绝不能覆盖分配结果;
· 跳过 None:无限期借用提交的 expected_return_time 本就是 null。
2) borrow.vue handleApprovalChange:实现领用人自动回填
· 口径差异:审批单上的 borrower_name 是**完整 username**
(「杜邢宸/duxingchen」),人员名单返回的是展示名(「杜邢宸」)。
两边归一化到「斜杠前段」再比对,否则永远匹配不上;
· borrower_name 优先,匹配不到才回退 applicant_id —— 库管代建时申请人是
库管本人,回退到它会选错人;
· 名单加载改为可重复 await:选中单据时要拿它反查,名单没回来就比对会
误判为「找不到」而清空;
· 匹配不到时保持未选,不回退到自由文本 —— 借用人 ID 是转交/归还责任链的
唯一锚点,宁可让库管手选也不能猜。
可编辑性
----
三个字段均保持可改:领用人下拉、备注文本框无 disabled;日期选择器仅在勾选
「无限期/长期借用」时置灰(原有语义,取消勾选即可重新填写)。
影响与验证
----
· 存量:仅 1 张待执行审批单(id=46)受影响,其日期在库中已无任何留存,
无法回填,需库管手工补一次;已完结单据不受影响。
· 回归:reserve_for_items 透传 9 项断言、submit_approval 端到端 5 项断言
全部通过;库存精确还原、无残留数据。
|
2026-09-17 09:26:42 +08:00 |
|
|
|
f7c49f41a8
|
fix(borrow): 统一归还/报废时间口径为北京时间,并修正过期注释
时间口径(与归还流水同时引入,故随本轮一并修)
----
process_return 与 scrap_sources.deduct 原用 datetime.now() 写 return_time,
而 datetime.now() 取的是**容器本地时间**(Docker 下为 UTC);同一行的
borrow_time 却由 beijing_time() 写入 —— 两个字段差 8 小时,台账时间线
自相矛盾,并会让新做的流转时间线出现「先借出、后归还,却显示归还更早」
的倒序假象。
统一改用 beijing_time(),并与新增的归还流水共用同一个时间戳,保证主表快照
与流水逐笔完全对齐。
(与 db_migrations/unify_approval_timezone.sql 处理的是同一类问题:aware/naive
与本地/北京时间的口径混用。)
注释修正
----
borrow_service.submit_approval 的 docstring 写着「仅存储意向,不扣库存」,
但 Phase 1 起该函数已调用 reserve_for_items() 预占库存(扣 available_quantity)。
过期注释会误导后续开发者(尤其是做转交的人以为库存没动过),已改写为实际的
预占语义与三种释放路径(驳回 / 撤回 / 扫码执行)。
|
2026-09-17 09:18:02 +08:00 |
|
|
|
cccd6f6081
|
feat(borrow): 转交接口、身份ID锚点与流转时间线后端
责任链收口
----
· execute_dispatch:强制 borrower_id,姓名一律由 sys_user 反查,**不回退**到
申请单姓名 —— 申请单上的姓名是「申请意向」,与扫码时实际来领的人本就可能
不同(库管代建场景尤甚),回退会让 current_holder 从第一刻就记错人。
同时落库 dispatch_operator,补齐「谁经手发货」。
· process_return:新增 returner_id,记录有 current_holder_id 时强校验二者相等,
不符即整单回滚(原实现只要持有 op_return:operation 即可归还任何人借出的
物品,函数内从不读取 borrower_name,归还环节责任链是断的);
每次归还写 trans_borrow_return 流水;全量归还清空 current_holder
(borrower_id 保留作历史)。
· transfer_borrow(新):整单全量转交,写转交流水 + 推进主表 current_holder。
为什么一期只允许整单全量转交
----
trans_borrow 是单行模型,只能存一个 current_holder_id。部分转交(借 10 转 5)
会让这一行同时表示两个人,语义直接撕裂,且归还时无法判定该由谁还。
若未来需支持,必须改为按数量**拆行**(新建一条承接转出量、原行扣减),
而不是在单行上加字段打补丁。
为什么转交绝不触碰库存
----
借出期间 available 已冻结、stock 仍含借出未还量(deduct_stock=False)。
转交若动库存,会同时破坏两个既有假设:「归还只加 available」会算多,
「借库转报废在确认损失时扣 stock」(scrap_sources.BorrowScrapAdapter)
会重复扣减。
接口
----
· POST /borrow/<id>/transfer 转交(borrow_transfer 权限 + 防抖锁 + 行锁)
· GET /borrow/<id>/history 单品流转历史(供精确追溯)
· GET /borrow/slip/<no>/history 整单时间线(实测单号最多 21 条明细,
逐条调用会产生 21 个请求,故聚合返回)
· GET /borrow/users 人员名单(借出/转交/归还共用,公司隔离)
一个隐蔽缺陷(本轮发现并修复)
----
整单时间线初版按展示用的时间字符串排序,而该字符串截断到**秒** —— 同一秒内的
连续动作(如「转交 A→B 紧接着转交 B→C」)会退化为并列,顺序取决于数据库返回
顺序,时间线随机错乱。已改为按真实 datetime 排序,并加回归用例锁定。
|
2026-09-17 09:17:57 +08:00 |
|
|
|
58fa42bff3
|
feat(scrap): 报废全链路收口到审批流
系统自陈的规则是「报废一律需审批」(SCRAP_ALWAYS_REQUIRES_APPROVAL = True),
但实际有 4 条写 TransScrap 的路径,其中 3 条绕过审批。本提交把三条旁路
全部收口,只保留「申请 → 审批 → 执行」一条写入路径。
【删除】直接报废 POST /api/v1/scrap
同时移除 ScrapService.process_scrap()。该路径的一个连带影响是
「维修件报废」能力随之消失 —— process_scrap 的 trans_repair 分支是
repair_status='报废转出' 的唯一写入点。实测该能力零使用(报废转出 0 条、
trans_scrap 来源 0 条),且早已半死:审批流的扫码校验会拒绝 trans_*
来源。repair_service.py 的误导文案(原文指引操作员「前往报废管理进行
扫码操作」,而那条路根本不通)已改为「维修件报废暂未开放」。
权限元素 scrap_create:operation 保留不删 —— add_scrap_perm.sql 以它为
scrap_apply/scrap_execute 的授权来源。
【改造】借库转报废 POST /borrow/scrap → POST /borrow/scrap-request
TransService.scrap_borrow() 删除,逻辑迁入 BorrowScrapAdapter。
沿用 op_return:operation 权限(零授权变更)。已归还/已报废的记录改为
**直接报错**,不再静默 continue 返回 count=0(原缺陷:用户以为成功)。
【改造】不良品报废 POST /defective/<id>/scrap → POST /defective/<id>/scrap-request
申请时**不预占** remaining_qty(与报废模块「仅锁定意向,不扣库存」的
既有哲学一致)。副作用:同一批坏件可重复提交多张申请单,执行期由适配器
按 Fail-Closed 拒绝超额的那几张。已在该接口注释与前端提示中写明。
【服务层】ScrapApprovalService 接入来源适配层
- submit_approval 改由 get_adapter() 分派,并新增整单 scrap_mode 一致性
校验(混合「需扫码/免扫码」两类来源直接拒绝,让 execute 分流保持简单)
- _build_scanned_index 的来源校验改为 is_scan_source():语义上是**收窄**
而非放宽,扫码通道永远不接纳 trans_* 来源
- execute 按模式分流:scan 项走原有扫码匹配(索引只由 scan 项构建),
auto 项按批准量执行。签名与调用契约不变。
- _match_key 加入来源表:不良品 SKU 是从原库存行复制的,不带来源时
同一张单里的两者会**必然串键**,扫码量算到错误对象上。纯库存单两侧
同源、键仍匹配,对既有流程零行为变更。
【修复】approve() 的 fail-open
原实现 `if user_entries and str(operator_id) not in user_entries` ——
allowed_approvers 为空时条件短路为假,**任何登录用户都能审批**。
这与「报废一律需审批」直接矛盾,留这扇门等于没有审批。改为无名单即拒绝。
已实测存量「无审批人」的在途单为 0 张,不会卡死历史数据。
【扫码通道】scrap/scan 的 trans_repair 分支改为 trans_defective_goods
在管不良品按 SKU 匹配,在管量回填到既有字段形状,前端无需按来源分支。
【清理】scrap_approval_service 的 _stock_models 与 TransScrap 导入已移除
(来源差异全部收敛到适配层);trans_service / inventory_reservation 中
指向已删方法的注释已更新指向 BorrowScrapAdapter。
|
2026-09-16 17:14:17 +08:00 |
|
|
|
e4d2b2ec68
|
feat(scrap): 报废来源适配层(纯新增,零行为变更)
把「报废来源差异」从审批服务里抽出来,为后续把借出未还、在管不良品
接入审批流铺路。
背景
----
报废审批流原先只认三张库存表(ScrapApprovalService._stock_models 硬编码),
导致另外两类来源只能各走直报接口绕过审批 —— 与系统自陈的
「报废一律需审批」(SCRAP_ALWAYS_REQUIRES_APPROVAL)冲突,构成职责分离
漏洞:同一个库管可自行宣告实物销毁而无人复核。
三类来源的语义差异
------------------
维度 库存行 在管不良品 借出未还
执行模式 scan scan auto(免扫码)
可报废量 available_qty remaining_qty quantity-returned
扣减 available 与 只动在管台账, 只扣 stock_quantity
stock 同扣 不碰任何库存表 (可用量已在借出时冻结)
成本 沿用现口径 defective_unit_cost 0/0
★ 为什么在管不良品保留扫码、借出未还免扫码:
坏件实物就在仓库里、有 SKU,扫码是有效的「申请报 A、实际毁 B」防护;
借出物在借用人手上,物理上不可能扫到,且执行只改台账与总库存、
不产生任何可被挪用的可用库存,风险等级不同量级。
其他要点
--------
- defective_unit_cost() 从 api/v1/inbound/stock.py 迁入本模块:服务层不得
反向 import API 层。复用 inventory_reservation.stock_model_map(),
避免第四份库存表字典。
- 提交期新增 submit_guard 钩子,用于修复「提交只校验 available、执行却校验
both」的校验不对称(会出现申请通过、执行必失败的单据)。
- is_scan_source() 对未知来源返回 False(Fail-Closed):扫码通道只接纳明确
声明为 scan 的来源,杜绝 trans_repair 那类「扫得到、执行却拒绝」的错配。
本步不触碰任何现有路由,现网仍走老路径,可独立评审与验证。
|
2026-09-16 17:14:05 +08:00 |
|
|
|
fbc9296056
|
feat(stock): status 硬隔离与出库通道封堵
激活库存表长期「只写不读」的 status 列,使其成为分配准入的硬门槛。
- inventory_reservation: 新增状态语义单一事实来源(STOCK_STATUS_*/
allocatable_filter/is_allocatable);verify_scanned 增加状态准入校验
(覆盖出库与借库两条提交路径)
- outbound: _allocate_bom_requirements 过滤条件加 allocatable_filter,
非「在库」一律不进入候选集;扫码路由把 ValueError 转为 400
- outbound_service: create_outbound_batch 强制 request_id 必填。原先无单
时会跳到所谓「散单」分支,而该分支的扣减逻辑从未落地(注释点名的
_apply_reservation_override 全仓不存在),实测只写台账不扣库存,
同一批货可被反复出库;get_stock_by_barcode 接入扫码准入;维修单扫码
补排除「报废转出」;预占再平衡块补 rollback 兜底
- fill_missing_stock_status.sql: 把存量行刷回「在库」。该过滤是
Fail-Closed 的,不先洗刷会让全库无法出库
破坏性变更:不传 request_id 的出库请求现在会被拒绝。
|
2026-09-16 15:45:02 +08:00 |
|
|
|
97ff63523c
|
fix(export): 公司隔离移出 if filters 判定,确保无条件执行
export_excel 的行级公司隔离原本嵌套在 `if filters:` 内部:
只要调用方不传或传空筛选条件,整段隔离会被跳过且不抛错 —— 静默失效。
(实测: 修复前 export_excel({}, None) 会导出跨公司全部 1816 行。)
现把 get_current_company_filter() 及其 filter_conditions.append 提到
if filters: 之前,无论有无筛选条件都绝对执行。
同时清理路由里 filters 字典的 'company' 死键 —— export_excel 从不消费它,
真正生效的是 get_current_company_filter() 直接读 request.args 上的公司标识。
注意:前端 handleExport 的 company 参数必须保留(已在代码中加注说明)。
实测 WAREHOUSE_MGR 带 ?company=IRIS 导出 1126 行仅 IRIS,不带则 1816 行
涵盖 IRIS+LICA —— 跨域角色按公司收窄范围完全依赖这个 query 参数。
实测(修复后):
SALES/IRIS filters={} → 1126 行, 仅 IRIS
SALES/IRIS filters={keyword:''}→ 1126 行, 仅 IRIS
WAREHOUSE_MGR ?company=IRIS → 1126 行, 仅 IRIS
WAREHOUSE_MGR (无参数) → 1816 行, IRIS+LICA
|
2026-09-11 12:41:27 +08:00 |
|
|
|
d69a77cec1
|
fix(export): 补上 export_excel 缺失的表头定义
GET /api/v1/inbound/base/export(物料页「导出库存统计」)在
base_service.py 里执行 ws.append(headers),而 headers 全文件从未赋值,
每次调用必抛 NameError,导出功能 100% 不可用。
按 _write_stock_rows 实际写入的 22 列顺序补齐表头,命名对齐同仓库
inbound_summary_service 与 stock.py 的既有报表。
实测: 容器内导出成功 203282 字节,1817 行(含表头),表头与数据行
均为 22 列,未出现串列。
|
2026-09-11 11:44:35 +08:00 |
|
|
|
06cb5f1cac
|
fix(reservation): 实扫来源不可识别时整单失败,避免预占永久锁死
出库与借库两条执行路径原先写作:
_scanned = [i for i in items if i.get('source_table') != 'trans_repair']
if _scanned:
verify_scanned(...); restore_then_deduct(...)
`if _scanned:` 为假时校验与释放被整体跳过,而下方仍无条件把单据置为
status=3(已完成)。此时申请阶段 reserve_for_items() 扣掉的 available_quantity
无人归还,且 status=3 之后 /close(要求 status==1)与 /withdraw(要求
status∈(0,1))都拒绝再释放 —— 预占就此永久锁死,成为谁也领不走的幽灵库存,
且没有流水可供追溯。
改为 Fail-Closed:实扫明细的来源必须全部落在 model_map 内,否则整单失败。
事务回滚后单据保持 status=1、预占原样保留,库管可重试执行,或走驳回/撤回 ——
任一路径都能正常归还预占。
- outbound_service.create_outbound_batch:以 model_map 白名单过滤,
并要求 _scanned 非空
- trans_service.execute_dispatch:先显式校验来源合法性并列出非法值,
再要求 _scanned 非空
|
2026-09-11 10:34:55 +08:00 |
|
|
|
a8a3c82331
|
fix(stock): 补行级防穿仓与入库改量下限,杜绝可用数变负
available_quantity 一旦为负,预占/释放/盘点的全部算术都会失真。此前有两处
缺口,本次一并堵上,使 available_quantity >= 0 成为不变量。
1) 执行阶段逐行扣减无下限校验(inventory_reservation.restore_then_deduct)
物料级校验只保证「Σ实扫 ≤ Σ可用」这一总量关系,拦不住「总量守恒但单行
穿仓」:同物料下 A 批可用 2、B 批可用 8,工人把 5 件全压在 A 批上,总量
5 ≤ 10 通过,A 批却被扣成 -3。借库走 deduct_stock=False,连实物数校验都
跳过,是裸扣。
已实测复现:借库与出库路径均可把单行扣成 -3。
校验放在 release_reserved() 之后,故不会误拒合法的换批次(物理覆盖):
释放后每行 available 已含本单预占,扫自己预占过的批次时 raw >= 0 保证
必然放行;改扫其它批次时,该批次实时可用量就是它自己的上限。
2) 下调入库数量可把可用数压成负(buy/product/semi 三处 update_inbound)
按 diff 同步增减 stock/available,但无任何下限检查。该批次若已有部分被
预占/出库/借出,向下调整即产生负可用数。现在下调前校验可用数是否够扣。
正常数据下 stock >= available 恒成立,故守住 available 同时守住 stock。
配套:三个 update_* 端点此前只捕获 Exception → 500,没有 ValueError 分支
(同文件的 delete_* 早就有)。补上 400 分支,使业务校验失败不再被记成
服务端故障。
验证(事务内执行并回滚,未落库):跨批次穿仓被拦、合法换批次放行、全额执行
本单预占批次放行、下调击穿被拦(API 返回 400,三个端点一致)、上调不受影响、
真实借库单 52 行全额执行正常、全库无负可用数。
|
2026-09-11 10:34:15 +08:00 |
|
|
|
cbcedecba2
|
fix(outbound): 扫码/备选库位回加本单预占,修正可用数重复计数
出库选单提交申请时 reserve_for_items() 会立即扣减 available_quantity(预占,
防超卖),但扫码页拿到的仍是这个已被本单扣过的值,并当作「本单能扫多少」的
上限。对本单而言它自己锁掉的货当然该能扫,于是同一批货被算了两次:
· 某行被本单占满时 available=0,工人直接扫不进去,提示「库存不足或已出库」
· /alternatives 按 available_quantity > 0 过滤,被本单占满的行从列表消失
· 草稿恢复时刷新实时库存,误报「实际库存已少于你扫的数量」
改法:扫码阶段的可用量 = 实时可用量 + 本单在该行的预占量。别人单子的预占
不回加,防超卖能力不丢。该值与后端 restore_then_deduct() 释放预占后用于校验
的数字精确相等,是同一口径而非近似。
后端:
- inventory_reservation.py 新增 reserved_index()/reserved_qty() 纯读工具
- outbound.py 新增 _own_reserved_index(),改造 /scan 与 /alternatives
- outbound_service.py 的 _format_scan_result 返回归一化可用量
两道门禁:
- biz_type 区分出库/借库两张审批单(独立表、ID 空间独立,而两个端点被
出库页与借库页共用),否则借库单 ID 会命中另一张出库单
- 单据状态仅放行 status ∈ {0,1}。set_items() 只在创建时调用,执行/驳回后
items_json 里的 reserved=True 仍原样保留而库存早已归还,门禁一松就会
二次回加 → 真超卖(现有 3 张已完成单据即属此形态)
不满足门禁时静默降级为不回加(fail-closed),并回传 reservation_applied。
|
2026-09-11 10:34:00 +08:00 |
|
|
|
f702a70fe5
|
fix(borrow): 借出只冻结可用数,不再扣减实物库存
问题
----
上一轮库存预占改造(b57c21a)把借库执行改用了 restore_then_deduct(),
而该函数是按**出库语义**设计的(物品永久离开仓库),会同时扣减
available_quantity 与 stock_quantity。借库是可逆的,于是产生两个缺陷:
1) 借出再归还后,实物库存永久少一份
初始 (stock=20, avail=20)
借出5 (stock=15, avail=15)
归还5 (stock=15, avail=20) ← 实物没回来,丢了 5
2) 借出后转报废会重复扣减
scrap_borrow 的注释明确写着「扣减总库存(该物品确认损失);
可用库存已在借出时冻结,无需重复扣」—— 它假设借出**没动实物**。
借出已扣 stock 后,转报废再扣一次:
借出5 stock=15 → 转报废 stock=10(正确应为 15)
修复
----
restore_then_deduct 增加 deduct_stock 参数:
出库 deduct_stock=True (默认)—— 可用数、实物数同时扣
借库 deduct_stock=False —— 只冻结可用数,实物数不动
★ 这是**恢复原有设计**,不是新设计。git 历史显示从最初的
04ee938 借库逻辑实现 起,历经 7ef22a3 / 83b3db6 / b79b0f9 /
2556b77 / 1527d55 六次提交,一直是:
stock.available_quantity = float(stock.available_quantity) - qty
(只减可用,不动实物)。b57c21a 把它替换掉了。
盘点的差异计算也印证了该口径的正确性:
adjusted_stock_qty = stock_quantity - 借出未还
diff_qty = 实盘数 - adjusted_stock_qty
这段逻辑要求 stock_quantity **包含**借出未还的实物,否则会重复扣减。
最终语义
--------
stock_quantity = 账面实物总数(含借出未还)
available_quantity = 实际可取用数
出库申请(预占) — available ↓
出库执行 stock ↓ available ↓
借库申请(预占) — available ↓
借库执行 — available ↓
借库归还 — available ↑
借库转报废 stock ↓ —
实测(stock, available)
------------------------
借还循环:初始(20,20) → 借出5(20,15) → 归还5(20,20) 完全可逆
出库: 预占4(20,16) → 执行(16,16) 实物确实减
转报废: 借出3(20,17) → 转报废(17,17) 实物减一次,不重复
|
2026-09-11 09:39:20 +08:00 |
|
|
|
33acdc758c
|
fix(scan-draft): 撤回申请单时清理其扫码草稿
问题
----
草稿按 (user_id, biz_type, request_id) 存储,而撤回逻辑原先只做两件事:
释放预占、把状态置为 4。没有任何草稿清理 —— 撤回后草稿成为孤儿数据:
· 单据状态变为 4,不再出现在扫码页的下拉里(那里只拉 status=1),
草稿从此永远读不到;
· 若不清理会持续累积,每行是一个整单 JSON 快照。
实测(撤回前 2 份草稿):撤回后该单草稿数归 0。
★ 清的是**所有用户**在这张单上的草稿,而非仅撤回者自己的 ——
可能有多人扫过同一张单,只清自己的会留下他人的孤儿数据。
出库 outbound_service._release_and_close
借库 borrow_service._release_and_close
两处共用底层,一并补上。
|
2026-09-11 09:06:43 +08:00 |
|
|
|
e93107458a
|
feat(purchase): 采购列表支持关键词搜索与日期范围
背景
----
采购管理此前只有状态筛选,无法按单号/物料/申请人检索,也无法按采购
日期收窄范围。现对齐出库/报废记录的搜索语义,便于用户迁移使用习惯。
后端
----
PurchaseService.get_purchase_list 新增参数:
keyword / search_type / start_date / end_date
search_type 取值(与出库、报废一致):
all 单号 | 物料名称 | 规格型号 | 备注 | 申请人
no 单号
name 物料名称
spec_model 规格型号
requester 申请人
实现要点
--------
· 申请人姓名存在 SysUser.username,格式「姓名/账号」(如 韩善龙/hanshanlong),
ilike 直接匹配整串即可命中;
· 该类字段只在 SysUser 上,故**按需 join** —— 单号/名称/规格分支不联表,
避免无谓开销;
· 公司隔离分支也需 join SysUser,与关键词分支可能重复 join 同一目标,
会产生笛卡尔积导致结果翻倍,故按需补 join 并用 distinct 兜底;
· 日期范围按 purchase_date 过滤(该列是 Date,无需补时分秒)。
实测:名称'充电器'→1 单,申请人'韩善龙'→21 单,日期 05-13→2 单,
关键词不存在→0 单,状态+搜索组合→14 单。
|
2026-09-10 17:41:22 +08:00 |
|
|
|
077fd2f2cf
|
feat(borrow,scrap): 补齐申请人撤回端点,与出库对齐
背景
----
出库已有独立的申请人撤回端点(仅 @jwt_required + 服务层归属断言),
但借库/报废没有:
· 借库撤回复用 close 端点,权限是 op_borrow_approval(管理路径),
普通员工调用返回 403;
· 报废撤回带 @permission_required('scrap_apply'),同样挡住普通申请人
(scrap_apply 只授予 INBOUND/OUTBOUND/SUPERVISOR/SUPER_ADMIN)。
结果是「我的申请单」页面里,借库/报废的撤回按钮对普通员工点了报错。
借库
----
服务层拆成与管理路径并列的两条入口(与出库同构):
mark_completed —— 管理路径,需 op_borrow_approval,仅 status==1
withdraw_request —— 申请人路径,仅校验单据归属,status 0 或 1
两者共用 _release_and_close(),释放逻辑只有一份实现。
新增 POST /transactions/borrow/request/<id>/withdraw(仅 @jwt_required)。
报废
----
withdraw() 增加 require_owner 参数区分两条路径:
require_owner=True (默认,申请人路径)→ 断言 applicant_id == operator_id
require_owner=False(管理路径) → 由调用方权限装饰器鉴权
WITHDRAWABLE_STATUS 从 (1,) 放宽到 (0, 1),与出库/借库对齐。
移除端点上的 @permission_required('scrap_apply'),改走归属校验。
安全实测
--------
借库 B 撤 A 的单 → 403,库存仍是 27(未被释放)
借库 A 撤自己的单 → 200,30 完全释放
报废 B 撤 A 的单 → 403
报废 A 撤自己的单 → 200
主管代撤员工的单(特权路径)→ 200
|
2026-09-10 15:46:55 +08:00 |
|
|
|
bcbee8a194
|
feat(outbound): 申请人撤回自己的申请单 + 我的申请单端点
背景
----
出库审批页是管理视角(需 outbound_approval 权限),普通申请人提交后
**没有任何入口看回自己的单据**,更谈不上撤回。
服务层:抽出共用释放逻辑
------------------------
新增 OutboundApprovalService.withdraw_request(),与既有的 close_request()
(管理路径)形成两条独立入口:
close_request —— 管理路径,需 outbound_approval 等权限,仅 status==1
withdraw_request —— 申请人路径,仅校验「单据归属」,status 0 或 1 均可
两者各自完成权限与状态校验后,调用**同一个** _release_and_close()。
释放逻辑只有一份实现,不会因修改其中一处而漏掉另一处。
为什么单独开一条路径,而不是在 close_request 里加 if 分支:
权限模型不同(管理角色 vs 单据归属)。混在一个函数里,后续修改容易
互相影响 —— 这正是需要避免的访问控制风险。
API
---
POST /outbound/request/<id>/withdraw 申请人撤回(仅 @jwt_required)
· 归属断言:非本人且非特权 → 403「无权撤回他人的申请单」
· 状态守卫:仅 0/1 可撤回;执行成功后 status 会被置 3,故该判断
本身即执行守卫,已执行或已撤回的单都进不来
GET /outbound/my-requests 我的申请(仅 @jwt_required)
· applicant_id 硬编码为当前登录用户,不接受任何入参覆盖
两者都**不做模块权限校验** —— 普通申请人无需持有 outbound_approval
(那是管理权限)。与「给审批端点加 if 降级放行」是两条路:后者把管理
逻辑与用户逻辑混在一个端点里,一旦 is_privileged_viewer() 判定出错
即越权;本端点从设计上就没有「看别人」的分支。
安全实测
--------
普通员工查我的申请(此前 403) → 200
B 查列表看不到 A 的单 → 39 单中无 A 的单
B 撤回 A 的单 → 403,且库存未被释放
A 撤回自己的单 → 200,库存 17→20 完全释放
主管代撤他人工单 → 200(特权路径)
待审批(status=0) 撤回 → 200,库存释放
重复撤回 → 400「当前状态不可撤回」
|
2026-09-10 15:36:59 +08:00 |
|
|
|
9925bf2b99
|
fix(inventory): 撤回/作废单据时释放预占库存,消除库存泄漏
问题
----
系统原本已有「完结」功能(出库/借库),但它只改状态、不释放预占:
approval.status = 4 # 已完结
db.session.commit() # ← 库存没还回去
申请阶段 reserve_for_items() 扣掉的 available_quantity 就此永久泄漏 ——
货被一张永不执行的作废单锁死,谁也领不走。
核查存量 23 张 status=4 的单,所幸均为预占改造前提交(items_json 无
stock_id),尚未造成实际损失。但缺陷本身是真实的。
改动(三个模块统一)
--------------------
出库 outbound_service.close_request
借库 borrow_service.mark_completed
· 调用 release_reserved(approval.get_items()) 按 items_json 原样归还;
· 明确「仅 status==1(已通过待执行)可撤回」—— 执行成功后
create_outbound_batch / execute_dispatch 会把 status 置为 3,
故该状态判断本身即执行守卫,已执行或已撤回的单都进不来;
· 返回消息带上释放条数,便于操作者确认。
报废 scrap_approval_service.withdraw(新增能力)
· 报废原先只有 approve/reject,没有撤回入口,补齐;
· 复用同一个 release_reserved():报废当前尚未接入预占,调用它会安全
跳过(无 reserved 标记),但将来报废接入预占时该段代码自动生效;
· 新增端点 POST /api/v1/scrap/request/<id>/withdraw(权限 scrap_apply)。
未采用按流水表二次校验:request_no(APR-OUT-…) 与 outbound_no(OUT-…)
格式不同、无关联字段,按单号比对是无效的,状态判断已足够。
实测
----
决定性用例(证明释放真实生效,非账面功夫):
A单预占5 → available=1
B单要5 → 400 拒绝(被A占住)
撤回A → available=6
B单再要5 → 200 成功,available=1 ★ 释放的库存真的可被复用
三模块:
出库 撤回后 4→10 完全恢复,stock 未变(货没动)
借库 撤回后 6→10 完全恢复
报废 撤回成功,重复撤回被正确拒绝
状态码说明:报废复用出库/借库已有的 4=已完结 作为「已撤回」,
而非引入 -1,避免同一系统出现两套编号(其 2 已被「已驳回」占用)。
|
2026-09-10 15:08:32 +08:00 |
|
|
|
b57c21a4cd
|
feat(inventory): 库存预占生命周期,消除出库/借库超卖
问题:库存超卖
--------------
改造前出库/借库申请只记录「要什么、要多少」,不绑定具体库存行,
真正的 available_quantity 扣减发生在执行阶段。于是多张申请可以同时
claim 同一批货,等到工人拿扫码枪时才发现货已被别人领走。
生命周期(三阶段)
------------------
提交申请(预占) reserve_for_items()
用分配器把需求落到具体库存行,立即扣减 available_quantity,
并把 (stock_id, source_table, allocated_qty, reserved) 写回 items_json。
驳回(释放) release_reserved()
遍历 items_json 把预占量还回池子,避免货被永不执行的单永久占住。
扫码执行(覆盖) verify_scanned() + restore_then_deduct()
校验实扫身份/数量未超批准范围 → 释放全部预占 → 对实扫批次
同时扣减 available_quantity 与 stock_quantity。
身份键:base_id 主键 + SKU 兜底(重要设计决策)
-----------------------------------------------
本系统中 SKU 是**批次级**编号:同一 base_id 下每个入库批次各有不同的
SKU(实测 stock_buy 有 183 个物料是多批次的,如 base_id=2405 下有
0000001685 与 0000001974 两个 SKU)。
若以 SKU 作为身份主键,「申请时锁定 A 批、工人现场改扫 B 批」会被判为
身份不符而拒绝 —— 恰好否定了「物理覆盖」这个核心能力。
故改用 base_id(物料级、跨批次稳定,spec_model 由其唯一确定),
历史数据无 base_id 时降级为 (name, spec_model)。
可用量校验按物料汇总,而非按单批次
----------------------------------
开发中修正的一处缺陷:若逐行要求「该批次可用量 >= 该批次扫码量」,
工人改扫小批次时会被误拒。例如本单预占 A 批 5 件,改扫 B 批 2 件 +
C 批 3 件,B 批自身只有 2 件可用,逐行校验即失败。实际这 5 件都是本单
锁定的货,理应允许。现按物料汇总校验可用量,按行校验实物库存。
改动文件
--------
· 新增 app/services/inventory_reservation.py(通用服务层)
· outbound_service.create_request —— Phase 1 预占
· outbound_service.approve(reject) —— Phase 2 释放
· outbound_service.create_outbound_batch —— Phase 3 覆盖(移除原逐行扣减)
· borrow_service.submit_approval —— Phase 1
· borrow_service.approve(reject) —— Phase 2
· trans_service.execute_dispatch —— Phase 3,并用统一身份键替换
原有的 (name, spec_model) 字符串匹配
实测(真实 HTTP 全链路)
------------------------
初始 available=10
① 提交申请(需5) → 200,available 10→5 预占生效
② 审批通过 → available 仍为 5 预占保留
③ 扫码执行(改扫另一批次 4 件) → 200
原批次恢复满额、实扫批次扣减(0,0),available=6, stock=6
单场景验证:预占 A 批改扫 B 批放行;驳回后可用量完全恢复;
扫其他物料被拒;批准 6 扫 8 被拒。
|
2026-09-10 14:59:10 +08:00 |
|
|
|
fd0bfd3d9c
|
feat(records): 借还记录接入高级筛选
复用 app/utils/advanced_filter.py 的解析与谓词逻辑:
· 父级字段(单号 borrow_no、借用人 borrower_name)走标准 SQL 谓词
· 子级字段(SKU、物料名称)经单号子查询过滤,否定操作符走 NOT IN 整单排除
· 物料名经三表联查(buy/semi/product JOIN material_base)
借还的单号维度查询基于 order_subq 子查询分页,故此处把过滤条件施加在
order_subq.c.borrow_no 上,与既有的状态/日期/公司隔离过滤保持同一层次。
前端 records.vue 新增「高级筛选」el-popover(字段/操作符/值 + 添加条件/
应用筛选/重置),序列化为 advancedFilters JSON 字符串随查询下发。
验证:sku ne 0000000002 → 52 单(库中无单含该 SKU,正确不减);
sku contains 0000 → 52 单(52 单的 SKU 全部含 0000,数据巧合)。
|
2026-09-10 13:06:03 +08:00 |
|
|
|
e437b3cece
|
feat(records): 高级筛选引擎 + 出库记录接入
一、新增共享工具 app/utils/advanced_filter.py
系统内已有该模式(material/list.vue、stock/inbound/buy.vue),
沿用其既有约定:参数名 advancedFilters、值为 JSON 字符串、
操作符 eq/ne/contains/not_contains/ge/le。
· parse_advanced_filters() 解析并规整,坏输入退化为空列表不影响主查询
· build_predicate() 单条件 → SQLAlchemy 谓词,未登记字段返回 None 杜绝列注入
· build_material_name_select() 物料名三表联查(buy/semi/product JOIN material_base)
二、★ 父子关系处理(本次核心)
记录接口返回的是**按单号分组的订单**,而用户筛选字段多落在**明细行**上。
若直接 .filter(TransOutbound.sku.ilike(...)),会在 GROUP BY 前收窄明细范围,
展开行里的兄弟明细会凭空消失。正确做法是先求「含匹配明细的单号集合」
再让主查询按单号 IN 过滤。
实测对照(单 OUT-20260811-1519-0003,21 条明细):
按其中一条 SKU 筛选 → 子查询法保住全部 21 条;直接 filter 只剩 1 条。
三、★ 否定操作符语义(NOT IN)
子级字段的 ne / not_contains 不能直接用 SQL != / NOT LIKE —— 那表达的是
「本单存在某条不等于 X 的明细」,多明细单几乎必然成立,等于筛选失效。
用户意图是**整单排除**,故 apply_child_condition() 统一:
肯定 → order_no IN (含匹配明细的单号)
否定 → order_no NOT IN (含匹配明细的单号)
两者子查询完全一致(都用肯定形式谓词),仅外层取反。
父级字段(单号/操作人)仍走标准 SQL 谓词,语义无歧义。
四、出库记录接入(前端弹窗 + 后端接线)
验证:
eq 0000000002 → 1 单;material_name contains 白板 → 16 单
sku ne 0000000002 → 394 = 395-1,含该 SKU 的单被整体排除
material_name not_contains 白板 → 379 = 395-16
|
2026-09-10 13:05:59 +08:00 |
|
|
|
c35ec9a659
|
feat(records): 出库/借还记录统一高级搜索版式
三个记录页(出库/借还/报废)此前搜索区形态各异:出库用裸 div + 分散样式,
借还缺日期范围,报废只有 SKU 输入框。现统一为 el-form :inline="true" +
.filter-form 版式,并补齐缺失的过滤维度。
借还记录(新增日期范围能力):
· 后端 get_records 增加 start_date/end_date 参数,按「借出时间」过滤;
· 边界补全时分秒(YYYY-MM-DD → 当日 00:00:00 / 23:59:59),
与出库记录同口径,解决零点截断导致当天记录漏查的问题;
· API 层透传两个新参数。
出库记录:
· 版式统一为 inline 表单,日期选择器加 label;
· 新增「重置」按钮;
· 搜索类型切换由 500ms 防抖改为立即查询。
借还记录:
· 新增「重置」按钮;搜索类型/状态切换改为立即查询(取消防抖)。
实测(SUPER_ADMIN):
报废 全部/单号/SKU/操作人/物料名/日期 六类过滤均 200 且命中正确;
借还 无过滤 52 单 / 本月 18 单 / 空区间 0 单(日期过滤生效);
出库 无过滤 395 单 / SKU 277 / 姓名 7 / 物料名 16 / 单号 OUT-2026 命中 395。
|
2026-09-10 12:06:08 +08:00 |
|
|
|
e152f16ebc
|
feat(scrap): 报废一律需审批,不再按物料标记区分
业务规则变更:所有报废申请都必须由指定审批人审批通过后才能执行。
原逻辑走 resolve_approval_control 判定是否需审批,而 material_base 表中
仅 1/3012 个物料标记了 is_approval_required,意味着 99.97% 的报废申请会
走免审批分支——status 直接置 1、actual_approver_id 被赋为申请人自己、
审批人参数被静默丢弃。前端即便做了必填也只是摆设。
改动(规则收敛到单一来源):
· 新增 SCRAP_ALWAYS_REQUIRES_APPROVAL = True,作为唯一开关;
· submit_approval() 无审批人一律拒绝;恒置 status=0(待审批);
删除免审批自动通过分支;
· resolve_approval_control 仍调用,但仅用于生成提示文案,
不再参与是否审批的判定;
· /request/check-approval 返回 need_approval=SCRAP_ALWAYS_REQUIRES_APPROVAL,
否则该接口会继续返回 false,导致前端预检结果失真。
实测:不传审批人 → 拒绝;传审批人 → status=0 且 actual_approver_id 为空。
⚠ 注意:此前自动通过的单据今后一律进入审批队列。
|
2026-09-10 11:32:51 +08:00 |
|
|
|
ed351f96ec
|
refactor(scrap): 按单执行校验改用 SKU 主键匹配
架构要求以 SKU 为唯一校验键,原实现按 (source_table, stock_id) 匹配。
已核验数据前提:SKU 在同一库存表内唯一(0 重复),且不存在跨表重名
(stock_buy / stock_semi / stock_product 交叉 0 冲突),故 SKU 可安全
作为跨来源标识。
改动:
· 新增 _match_key():SKU 非空时以 'sku:<SKU>' 为键;个别历史库存行
SKU 为空,回退 'row:<table>#<id>',前缀区分确保绝不串键;
· _build_approved_index() 按 SKU 聚合,累加批准量并保留各行的
source_table + stock_id(rows 列表);
· 校验拆为两步并给出明确文案:SKU 是否在批准明细内、同 SKU 累计量
是否超批准量;
· ★ 扣减改为按批准单配对的 source_table + stock_id 定位库存行加锁,
不再信任前端传来的 stock_id —— 实测前端传伪造 stock_id=99999 时,
仍从批准单指定的库存行扣减,台账同样记录批准单的 stock_id;
· 同一 SKU 在批准单占多行时,按批准顺序依次分配扣减量。
实测 7 项校验用例 + 多行分配 / 批准行已删除 / 可用不足 均符合预期。
|
2026-09-10 10:21:50 +08:00 |
|
|
|
7719943779
|
feat(scrap): 报废执行改为按单扫码校验,废弃盲执行
execute() 原先只吃 request_id,按申请单快照全量扣减,用户反馈「扫码与报废脱节」。
现改为接收实扫明细 scanned_items:
· 强制按单:未提交实扫明细一律拒绝执行;
· 键为 (source_table, stock_id),与申请单 items_json 同口径,
同一物品多次扫码自动累加,兼容「逐件扫」与「输数量」两种操作;
· 校验实扫项必须落在批准明细内,且累计量不得超批准量,否则整单拒绝;
· 允许合法子集(少扫 = 本次不报废该行);
· 扣减以实扫量为准,同时扣 available_quantity 与 stock_quantity
(报废即实物销毁,上一版只扣可用库存会让总库存虚挂)。
接口 POST /scrap/request/<id>/execute 由无 body 改为接收 {items:[...]}。
|
2026-09-10 10:14:40 +08:00 |
|
|
|
b4b79c68a9
|
fix(borrow): 借库转报废实物不足时报错回滚,不再夹断到 0
原逻辑在 stock_quantity 减为负数时直接置 0。但借出时已扣减过
available_quantity,夹断会使 available_quantity > stock_quantity,
凭空多出可用库存,后续出库/借库可继续占用这批并不存在的实物。
改为不足即抛 ValueError,与同一事务内其余校验一致,整单回滚。
|
2026-09-10 10:14:36 +08:00 |
|
|
|
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 |
|
|
|
8b61cdb67d
|
fix(approval): 需审批判定改为 base_id → SKU → 名称+规格 三级降级
原判定仅有 base_id 与「名称+规格字符串」两级,出库申请明细只有 name/spec、
无 base_id,只能走脆弱的字符串匹配,同名或同规格不同料极易误判。
新增 SKU 优先级:material_base 表本身没有 sku 列,SKU 只存在于
stock_buy / stock_semi / stock_product,故经明细自带 source_table 反查
sku → base_id 再回查物料,查不到才依次降级其余两张库存表。
查到物料即定论,不再往下走字符串兜底,杜绝误杀。
|
2026-09-10 10:14:17 +08:00 |
|
|
|
1dbf74b7bc
|
feat(borrow): 借库审批补「完结」能力(对齐出库 status=4)
- mark_completed 改为 1→4 已完结(真正扫码借出完成仍为 status=3)
- 前端审批页新增「已完结」筛选项与状态映射;审批信息列对 3/4 显示审批人
|
2026-09-10 09:47:07 +08:00 |
|
|
|
ffbb2199f0
|
feat(scrap): 报废申请审批流 + 按单报废(后端)
- ScrapApproval 模型 + ScrapApprovalService:提交/列表/审批/执行(锁库存扣 available_quantity 写 TransScrap)
- 新路由 POST /scrap/request、/request/check-approval、GET /request、PATCH /request/<id>/approve、POST /request/<id>/execute;权限码 scrap_apply/scrap_approval/scrap_execute(无角色硬编码)
- 旧 /scrap 直接报废入口保持不变
- /inbound/stock/list 每项返回 source_table,供按单流程精准选库存
|
2026-09-10 09:46:57 +08:00 |
|
|
|
044e6dbd98
|
feat(borrow,outbound): 库管(WAREHOUSE_MGR)代建申请强制审批
- submit/create 新增 force_approval:库管建单无视物料是否需审批,一律走审批
- 路由按当前角色为 WAREHOUSE_MGR 传 true;未选审批人返回明确业务错误
|
2026-09-09 16:19:38 +08:00 |
|
|
|
2a9e2e4560
|
fix(bom): BOM 三态互斥 + active_only排除归档 + 详情主备注
- get_bom_list active_only 补 is_archived==False,出库/借库“按BOM套餐”不再泄漏归档版本
- get_bom_detail 返回顶层 remark,详情弹窗主备注可回显
- 状态互斥:update_enabled 切启/停即清归档;update_archived 归档→废弃(enabled=False)、取消归档→启用;停用视图排除归档
|
2026-09-09 13:00:41 +08:00 |
|
|
|
eff9b62224
|
fix(borrow,outbound): 普通用户记录只看本人——按领用人/借用人姓名(不含账号前缀)匹配
- 出库记录/借还记录:非管理者视角时按 领用人(consumer_name)/借用人(borrower_name) 过滤
- 匹配取登录名 username.split(/)[0] 的姓名;兼容库里存“姓名/xiaolongxia”全名(姓名+/前缀)
- 修复:管理员替员工创建、领用人=员工 的单,员工登录可见
|
2026-09-09 11:19:15 +08:00 |
|
|
|
aa9322bcbc
|
feat(borrow): 借库申请后端必填校验——申请原因、预计归还日期(长期借用除外)
- submit_approval 校验:申请原因非空;未勾长期借用时必须有 expected_return_time,否则 ValueError 拦截
|
2026-09-09 10:53:12 +08:00 |
|
|
|
d06ce45a72
|
feat(borrow,outbound): 需审批判定收敛——ID优先 + 名称精确AND核心规格(斜杠前段)
- approval_control 重写:带 base_id 按主键绝对判定;无 id 才名称100%一致 AND 核心规格(_code_of)一致兜底,杜绝同名不同规误杀
- 借/出库服务命中需审批但未选审批人时,报错列出需审批物料名/规格
|
2026-09-09 10:47:24 +08:00 |
|