Commit Graph

721 Commits

Author SHA1 Message Date
b313fefdfa fix(web): 扫码页与备选库位带单据ID,按有效可用量限流
配合后端预占回加:扫码/借库/审批页面调用 /outbound/scan 与 /outbound/alternatives
时带上本单 id 与 biz_type,拿到的 available_quantity 即为「实时可用量 + 本单
预占」,本单锁定的行不会再因实时可用量为 0 而被拦住或从列表消失。

后端已在响应边界完成归一化,故前端模板与校验逻辑(:max、库存列展示、超量
警告、草稿存取)一律不动,只多传参数 —— 避免把同一语义散落到多个读写点。

- api/outbound.ts:getStockByBarcode / getStockAlternatives 加 requestId 与
  bizType 参数,导出 ScanBizType,ScanResult 补 reserved_quantity 等字段
- api/transaction.ts:同名的历史副本同步签名(当前无调用方,加注释指向唯一来源)
- views/outbound/create.vue、views/transaction/borrow.vue:各 3 处调用点带上
  单据 id;refreshStockFromDraft 改为显式透传 id,不依赖 computed 已解析
- views/borrow/approval/index.vue:审批页也带上本单 id,否则待审批单
  (预占已生效)锁定的行会因实时可用量为 0 而从备选列表消失

借库侧 request_id 是 BorrowApproval.id,靠 biz_type='borrow' 与出库单区分 ——
两张表 ID 空间独立,不可省。
2026-09-11 10:34:23 +08:00
b8561f69f7 V3.77,9.11推送 2026-09-11 09:39:59 +08:00
e5a7cb2736 V3.76,9.11推送 2026-09-11 09:07:14 +08:00
ce767e39ac feat(scan): 扫码时检测单据失效,避免白扫一场
场景
----
库管正在扫码,这张单被撤回/驳回/执行了。若不检测,库管会一直扫到
提交时才发现单据已作废(后端会拒绝),前面的工作白做。

实现
----
新增 checkRequestStillValid():重新拉一次「已通过(status=1)」列表,
看当前单据是否还在其中。不在 → 已被撤回/驳回/执行,立即弹窗告知并
清空界面(草稿后端也已同步清除)。复用现有列表接口,无需新增端点。

触发时机(覆盖两类场景):
  · 页面重新可见时(visibilitychange)—— PDA 熄屏唤醒、切回标签页
  · 每 60 秒一次 —— 库管一直在页面上扫、没切走

三点取舍:
  · 仅购物车非空时检测(正在作业才打扰),空清单不发请求;
  · 60 秒轮询而非实时推送 —— 引入 WebSocket 长连接成本过高,
    此处最多浪费 1 分钟扫码,相比"扫完几十分钟才发现"已足够;
  · 检测失败不阻断作业(仅 console.warn)—— 后端提交时仍会做最终校验,
    前端检测是为了早发现,不是安全防线。

出库 create.vue 与借库 borrow.vue 两页同款实现。
2026-09-11 09:06:51 +08:00
6f2f077751 feat(purchase): 采购页搜索区对齐出库记录版式
原顶部工具栏是裸 div + 状态单选,现改为 el-form :inline 版式,与出库记录 /
报废记录一致:

  状态      全部 / 待审批 / 已通过 / 已驳回 / 已完成 / 已完结
  搜索      [搜索类型下拉] + 关键词输入,500ms 防抖
  采购日期  日期范围选择
  按钮      查询 / 重置 / 新建采购申请

交互细节:
· 搜索类型切换、清空输入时立即查询(取消防抖等待);
· 「重置」恢复默认的「待审批」状态(该页默认视图),而非清成"全部"——
  与页面初始状态保持一致;
· 重置同时清空关键词、搜索类型、日期范围。

api/purchase.ts 的 getPurchaseList 补充新参数的类型定义。
2026-09-10 17:41:29 +08:00
a4a9afb6db feat(scan): 出库/借库扫码页接入草稿,切换单据不清空
交互
----
无需「暂停」按钮 —— 在下拉框切换单据这个动作本身就是暂停:
  切走 → 自动存当前单据的进度
  切回 → 自动恢复,并提示「已恢复上次的扫码进度(N 项)」
下拉框对扫到一半的单据显示橙色「已扫 N」徽标,不必逐个点开试。
提交成功后自动清除该单据的草稿。

修复的三个 bug
--------------
1) 切换时把 A 的内容存到了 B 名下
   v-model="selectedRequestId" 的 computed setter 会**先于** @change 把
   selectedRequest 改成新单,故 handleRequestChange 里读到的是新单。
   新增 activeRequestId ref 记录「界面上真正显示的是哪张单」,
   保存时显式传入离开的那张单的 ID。

2) 切回时把目标单的旧草稿删了
   原先写了「购物车为空则清除草稿」,但切换瞬间购物车必然为空,
   于是切回 A 时触发了清除。现改为空清单只跳过保存、不清除;
   清理由「提交成功」或「用户点清空列表」显式触发。

3) 恢复后名称/规格为空、出库数显示 NaN
   draftPayload 只存了 4 个字段(stock_id/source_table/sku/quantity),
   而购物车表格绑定的是 name/spec_model/available_quantity/out_quantity
   —— 全都没存。现保存完整快照,并在恢复时归一化
   (out_quantity ?? quantity)以兼容已存在的旧草稿。

补充:恢复后刷新实时库存
------------------------
草稿里的 available_quantity 是扫描那一刻的快照,跨时间恢复可能已过期
(期间别人出库/借出会消耗可用量)。恢复后复用 /alternatives 端点拉一次
实时可用量:数量超了会明确提示「N 项物料的实际库存已少于你扫的数量」,
避免工人扫满后到提交时才被后端拒绝。失败不阻断,沿用草稿快照。

另:离开页面(路由跳转)时存草稿并弹确认;beforeunload 用 sendBeacon
尽力保存(该路径无法带 Authorization 头,可能失败,但防抖保存已覆盖
绝大部分内容)。
2026-09-10 17:21:24 +08:00
a07432981e feat(borrow): 扫码借库页的清单对齐扫码出库页
问题
----
扫码借库页(views/transaction/borrow.vue)的「审批计划清单」与扫码出库页
的「计划出库清单」样式不一致:

  · 标题:审批计划清单 / 计划出库清单
  · 类型列:借库有、出库已删
  · 数量列:借库用「审批数量」且排在最右;出库是「计划数量」且前置
  · 备选库位:借库没有
  · 清单容器:借库灰底 #f5f7fa,出库是淡绿 #f0f9eb + 边框

改动(五项全部对齐)
--------------------
· 标题改为「计划借用清单」;
· 移除「类型」列;
· 「计划数量」前置到序号之后,橙色 #E6A23C 加粗 15px;
· 新增备选库位:库位格子整体可点击 + popover(与出库页逐字相同的实现);
· 清单容器改为淡绿背景 #f0f9eb + #e1f3d8 边框 + 圆角,标题绿色加粗。

备选库位复用了出库模块的 /alternatives 端点。已核实前提:借库申请单的
items_json 由预占改造写入 base_id / stock_id / source_table,故「推荐/备选」
标注可正常工作(对历史单据无这些字段时会安全回退为纯文本)。

定位说明
--------
借库有三个页面(选单 / 扫码 / 审批),上一轮按「计划清单」关键词搜索时
命中了审批页的展开行,改错了位置。本提交修正的是真正的扫码借库页。
2026-09-10 16:17:50 +08:00
f8267c6ee2 style(borrow): 审批页物料明细数量前置,清单标题对齐出库
背景
----
借库审批页展开行的「计划数量」排在表格最右(库位之后),与扫码出库页
的前置做法不一致 —— 列多时容易被容器裁掉,工人看不到数量。

改动
----
· 计划数量从最右移到序号之后,橙色加粗 + 字号 15px;
· 清单标题由朴素文字改为与出库页同款的标题栏结构
  (planned-header + planned-title + 绿色标签);
· 补上 .planned-header / .planned-title 样式定义(该页原先没有)。

注:本轮定位时曾误以为这是用户反馈的「扫码借库页」,实际扫码借库页是
views/transaction/borrow.vue(见下一提交)。此处是同类问题的另一处,
一并修正,非误改。
2026-09-10 16:17:42 +08:00
eeefaeb528 V3.74,9.10推送 2026-09-10 15:59:13 +08:00
d69ec9f5ec style(outbound,borrow): 库位格子整体可点击,图标放大
问题
----
备选库位浮层的触发区原先只有一个小图标(默认 1em),工人需要精准
点击才能弹出 —— 戴手套或在触屏上操作时命中率很低。

改动
----
· #reference 从「仅图标」改为「库位文字 + 图标」整体,
  点格子任意位置都能弹出;
· 悬停时整格高亮(#ecf5ff),给出可点击的视觉反馈;
· 图标放大到 18px;
· 库位文字着蓝色(#409EFF),并加 title 提示;
· 无 base_id 时回退为纯文本(历史单据)——原先这种情况下会渲染空白。

出库执行页与借库审批页同款实现,一并同步。

注:编辑过程中曾因残留旧的 popover 闭合标签导致模板结构损坏
(SFC PARSE ERR),已通过编译检查发现并修复。
2026-09-10 15:57:26 +08:00
ea52c78f9e feat(outbound): 计划出库清单数量前置,移除类型列
问题
----
计划出库清单的「计划数量」列排在表格最右侧(名称/规格/库位之后)。
表格共 6 列且名称规格会撑宽,最右列容易被容器裁掉,工人反馈"看不见数量"。

改动
----
· 「计划数量」从最右移到「序号」之后,橙色加粗 + 字号放大到 15px;
· 移除「类型」列(material_type);
· 库位列宽 150 → 170。

注:该列一直存在且数据正常(items_json 的 quantity 字段),
此前是可见性问题而非数据缺失。
2026-09-10 15:57:20 +08:00
851c3a9c1f feat(my-requests): 主列表展示数量,移除类型列
问题
----
主列表只有「物料数 N 种」(种类数),具体数量藏在展开行的明细表里,
不展开就看不到。

改动
----
· 移除「类型」列(type_label);
· 新增「数量」列,紧跟申请单号,橙色加粗;
· 新增 totalQty() 汇总单据总数量。

totalQty 只认 quantity 字段,无需按 type 分支 —— 因为聚合端点已做字段
归一化(报废的 scrap_qty → quantity)。这正是当初做归一化的价值。
数值经 Number(sum.toFixed(4)) 处理,去掉 1.0000 这类无意义的小数尾巴。

顺带移除已无引用的 typeTagType()。
2026-09-10 15:57:15 +08:00
dc4a0ae7a0 style: 统一申请单状态配色(四个页面一致)
配色方案
--------
  0 待审批 → 蓝(primary,待处理)
  1 已通过 → 绿(success,正向结果)
  2 已驳回 → 红(danger)
  3 已完成 → 灰(info,终态、不再需要关注)
  4 已撤回 → 黄(warning,需留意但非错误)

范围
----
「我的申请单」新页面按上述配色实现后,发现出库/借库/报废三个审批页
仍是旧配色(0:warning, 3:info, 4:info)—— 同一套状态码在两个页面
显示不同颜色,用户来回切换会困惑。故一并统一。

改动:my-requests/index.vue(新配色)+ 三个审批页的 statusTagType。

补充:el-tag 的 primary 类型已核实可用 —— Element Plus 2.13.1 的
tagProps 中 primary 为默认值,theme-chalk/el-tag.css 含 .el-tag--primary
的蓝色样式定义;vue-tsc 类型检查通过。
2026-09-10 15:47:07 +08:00
c999aac1e3 fix(scrap): 撤回按钮改用 scrap_approval 权限,修正错配
报废审批页的「撤回」按钮原先复用了执行按钮的 canExecute 条件
(scrap_execute)。但 scrap_execute 比 scrap_approval 多授予 INBOUND 角色 ——
意味着只具备执行权、无审批权的人也会看到撤回按钮。

撤回是对单据的处置动作,应与审批同权限等级。新增 canWithdraw 计算属性
(SUPER_ADMIN 或 scrap_approval),替换原 canExecute。

注:申请人在「我的申请单」页有自己的撤回入口,走的是单据归属校验,
不依赖此处权限。
2026-09-10 15:37:17 +08:00
20d49c0b0d feat(my-requests): 我的申请单独立模块(采购管理之后)
路由位置
--------
放在「采购管理」之后,作为独立一级模块:

    采购管理
    我的申请单      ← 新增(icon: Tickets)
    借库管理

★ 独立模块而非挂在某个业务页下:申请人可能提交出库/借库/报废三类单据,
  放在任一业务模块下都会让其它模块的人找不到。

页面功能
--------
  · 类型筛选:全部 / 出库 / 借库 / 报废
  · 状态筛选:全部 / 待审批 / 已通过 / 已驳回 / 已完成 / 已撤回
  · 展开行显示物料明细(数量字段已由后端归一化)
  · 撤回按钮仅对 status 0/1 可见(已预占但未执行)

撤回不硬编码 URL —— 使用后端回传的 withdraw_endpoint 分发:

    const res = await request({ url: row.withdraw_endpoint, method: 'post' })

这样将来模块端点调整(如借库从 close 换成正式 withdraw),前端无需改动。

权限说明
--------
菜单不做权限过滤(Sidebar 只过滤 meta.hidden),所有角色都会看到本页。
这正是预期 —— 申请人本就不应需要任何权限码。数据由后端强制按
applicant_id 过滤,看不到他人单据。

顺带清理
--------
移除先前加在 Selection.vue 的重复实现(约 110 行模板/脚本/样式),
改为一个跳转链接「查看我的申请单 >」;并移除随之失效的 3 个导入
(onMounted / Refresh / 两个 API 函数)。

验证:新页面 SFC 编译零告警;Selection.vue 清理后编译通过。
2026-09-10 15:37:11 +08:00
d1dd3dd404 feat(approval): 三端审批页「撤回」入口,明确告知会释放库存
后端已支持撤回时释放预占(见上一提交),前端同步:

一、文案与语义
  按钮 完结 → 撤回,颜色 danger → warning(语义从「危险操作」变为
  「可逆的库存释放」)。确认弹窗明确告知会释放多少项库存:

    确定撤回申请单【APR-OUT-...】吗?
    撤回后该单将被作废,其预占的 5 项物料库存会立即释放,
    可供其它申请使用。此操作不可恢复。

  用户看到的「1 件货」背后其实是一批被锁定的库存,不写清楚会让人
  以为撤回只是「关掉一张单」。

二、报废新增撤回入口
  报废审批页原先只有「执行报废」,没有撤回。补上按钮 + handleWithdraw(),
  并新增 API 封装 withdrawScrapRequest()。
  状态映射同步补充 4: '已撤回'(statusText / statusTagType)。

三、成功提示改为透传后端消息
  后端会返回「申请单已撤回,释放 N 项预占库存」,比前端写死的文案
  更有信息量,故改为 res?.msg 优先、前端文案兜底。

按钮可见性沿用 row.status === 1(已通过但未执行),与后端的执行守卫
(执行成功后 status 置 3)一致,故已执行或已撤回的单不会出现该按钮。
2026-09-10 15:08:43 +08:00
8c18586a94 feat(borrow): 借库审批页备选库位,与出库保持一致
借库的执行入口在审批页 —— 展开行即为工人查看/去扫码的物料明细。
申请单已把货预占在某库位,工人现场可能进不去,需要改扫同物料的其它
批次(后端执行端按 base_id 校验身份、允许换批次),但改造前系统不告诉
他还有哪些库位有货。

复用出库模块的 /api/v1/outbound/alternatives 端点(逻辑完全一致:
按 available_quantity > 0 过滤,只给真正能拿的库位,排除已被别单占满的行),
在明细表的库位列加图标 + popover:

  [推荐] L5/1/2/1  可用 840      ← 本单锁定行
  [备选] L2/1/2/1  可用 1

与出库页同款取舍:
  · trigger="click" —— 车间用扫码枪/触摸屏,hover 在触屏不可用
  · @show 时才发请求 —— 展开单据时不会打出一片并发查询
  · 附提示文案「现场取不到推荐库位时可直接扫备选库位条码借用」

注:借库申请经预占改造后 items_json 已带 base_id/stock_id/source_table,
故推荐行可正确标注;改造前的历史单无这些字段,此时全部显示为「备选」。
2026-09-10 15:02:22 +08:00
24b7fd8373 fix(audit): 模块下拉中文化,消除 image_embeddings 等英文遗留值
问题
----
模块筛选下拉仍出现 image_embeddings、purchase_request、sys_element 等
英文值。它们来自改造前的全局监听器:那时无白名单,系统表/草稿表/向量表
都会被审计,而 _infer_module_name 对未登记的类名直接回退成表名。
下拉取 DISTINCT module,这些英文值就混了进来。

处理
----
新增 moduleMap(21 项)+ moduleLabel(),并应用到**三处**渲染:
下拉选项、列表列、详情弹窗的模块标签。
value 保留原始字符串,筛选行为完全不变。

除覆盖现存 3 个英文值外,另预置了 18 个白名单表名(sys_user / stock_buy /
trans_outbound 等)。新监听器已保证 module 为中文,但若将来有数据从旧
路径写入或手工导入,这些值会再次出现,预置可避免同类问题复发。
其中 image_embeddings 标注为「图像特征(历史)」以提示其为改造前的产物。

实测:image_embeddings → 图像特征(历史),purchase_request → 采购申请,
sys_element → 系统元素;中文模块名原样保留。
2026-09-10 14:59:22 +08:00
209b29c10f feat(outbound): 备选库位可见性,让「物理覆盖」不再盲扫
问题
----
预占会把货锁定在某个库位,但工人到现场可能进不去/找不到该库位,
需要改扫同物料的其它批次。后端执行端已支持按 base_id 校验、允许换批次,
但系统从不告诉他「还有哪些库位有货」—— 工人只能凭记忆或挨个翻。

后端:新增 GET /api/v1/outbound/alternatives
--------------------------------------------
  入参 base_id(必填)、source_table/stock_id(可选,用于标注推荐行)
  返回该物料全部可用库存行 + 合计可用量,推荐行置顶、其余按可用量降序。

为什么不复用 stock/list 或 bom-match-stock 的查询模式:
那两处按 stock_quantity > 0 过滤,会把「有货但已被别单全部预占」的库位
也列出来,工人跑过去才发现拿不到。实测库中有 14 行处于该状态。
本接口按 available_quantity > 0 过滤,只给真正能拿的库位。

前端:计划清单库位列加图标 + popover
------------------------------------
  [推荐] Y1/2/1  可用 5      ← 本单锁定行(来自 items_json 的 stock_id)
  [备选] Y2/3/4  可用 10
  [备选] Z1/1/1  可用 2

三处取舍:
  · trigger="click" 而非 hover —— 车间用扫码枪/触摸屏,hover 在触屏不可用
  · @show 时才发请求 —— 计划清单可能几十行,渲染即请求会打出一片并发
  · 附提示文案「现场取不到推荐库位时可直接扫备选库位条码出库」

注:历史单据的 items_json 无 stock_id,此时所有库位显示为「备选」
(不影响可用性,仅少了推荐标记);预占改造后新提交的单可正确标注。

实测:造 3 批次 Y1/2/1(5) Y2/3/4(10) Z1/1/1(2),预占首个后其 available=0,
接口正确排除该库位,返回两个备选、合计可用 12。
2026-09-10 14:59:17 +08:00
0a70e5688a refactor(bom): 前端改为消费后端分配结果,删除本地分配与跨页数据污染
一、删除前端分配算法
  Selection.vue 与 borrow/apply/index.vue 原先各自维护一套
  rowsByBaseId 归并 + 跨行分配循环(近 80 行),现全部移除。
  改为:构造 requirements → 调用 bomMatchStock → 直接 push 返回的 items。

二、缺料提示
  后端返回 shortages 时,用 ElMessageBox.alert 逐项列出
  「物料名:需 X,实配 Y,缺 Z」,替代原先笼统的「跳过 N 种缺货物料」。
  全部满足则不弹窗,仅提示添加成功。

三、API 封装支持双签名(api/outbound.ts)
  bomMatchStock(requirements | { requirements })
  同时兼容旧的 bomMatchStock(childIds),未改造的调用方不受影响。

四、清除 loadStockForBom(两个页面)
  该方法会把 BOM 匹配结果整体写入 stockList.value,而 stockList 同时是
  「手动选单弹窗」的数据源 —— 操作过 BOM 后再打开手动选单,看到的会是
  BOM 匹配结果而非库存列表。BOM 流程不再触碰 stockList 后,该交叉污染
  一并消除,方法随之删除。

注:借库选单页存在逐字相同的 .find() + Math.min 缺陷,本次一并修复,
使两个 BOM 入口行为一致。

SFC 编译通过;vue-tsc 无新增错误(残留 6 处告警为既有问题)。
2026-09-10 14:16:52 +08:00
6068625562 feat(audit): 审计日志业务化——对象名与字段中文化、操作来源与类型筛选
一、target_name 业务化(后端 audit_listener.py)
  优先级:业务标识(request_no/outbound_no/borrow_no/sku/bom_no…)→
          名称字段 → 关联物料名 → 「中文表名 - 业务号或ID」。
  新增 TABLE_LABELS 表名到中文的映射(18 张白名单表全覆盖)。
  用户看到的从 scrap_approval ID:371 变为
  「出库申请单 - APR-OUT-20260805-1550-0005」这类可理解的对象描述。

二、前端字段中文化(AuditLog.vue)
  · fieldMap 由 11 项扩充到 110+ 项,覆盖审批单/流水/库存/主数据/系统管理五类;
  · 修复关键 bug:fieldMap 原先只作用于「变更对比」区,「删除快照」与
    「新增详情」两区直接渲染原始 key(label 直接取 String(key)),
    这正是详情里满是英文列名的直接原因。现三区统一经 fieldLabel() 取值。

三、时间显示修正
  后端已改写北京时间,前端原先补 Z 当 UTC 解析会造成二次 +8 小时,
  改为按 +08:00 理解该字符串。

四、新增两类筛选
  · 操作来源(真实用户 / 系统操作 / 全部),默认「真实用户」。
    历史存量含约 1.8 万条 username=system 的噪声日志,会把列表刷屏。
  · 操作类型别名归一:历史数据中 action 有两套写法(早期装饰器写中文
    新增/修改/删除,现行监听器写大写 CREATE/UPDATE/DELETE),导致下拉
    同时出现二者、且选中文项只能搜到 3-4 月的老数据。现 ACTION_ALIASES
    把任意写法归一化后展开匹配,下拉只暴露 3 个规范值,
    历史数据无需迁移即可被正确检索。
2026-09-10 14:16:39 +08:00
1ef9ae4ad9 feat(records): 报废记录接入高级筛选
复用 app/utils/advanced_filter.py,但报废有两处与出库/借还不同,需特殊处理:

一、scrap_request_no 可为 NULL(历史直接报废)
  出库/借还可直接对单号列做 IN,报废不行——「单号 IN」永远匹配不上 NULL 行,
  会把历史单整体漏掉。此处统一用「行级等价键」表达同单关系:
    有单号 → scrap_request_no 相等
    无单号 → 同一分钟(date_trunc) + 同一操作人(与 Python 侧分组逻辑完全一致)

二、物料名不能用单号传递结果
  物料名需三表联查,但联查结果若用「单号 IN」回接,同样会漏掉 NULL 单号行。
  改用 tuple_(source_table, stock_id).in_(pairs) 元组定位报废行。

三、否定操作符
  命中行 → 折算同单等价键谓词 → 否定时整体取反(~pred),实现整单排除。

前端 scrap/index.vue 新增「高级筛选」el-popover,与出库/借还版式一致。

验证(库中当前唯一报废单含 SKU 0000000272):
  sku contains 0000000272 → 1 单
  sku ne 0000000272       → 0 单(整单排除,符合预期)
  material_name not_contains 白板 → 0 单(该单物料名含白板,被排除)
  单号 ne(父级)          → 1 单(父级否定语义不变)
2026-09-10 13:06:07 +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
23a7fc65f1 feat(scrap): 报废记录按单号分组 + 可展开明细 + 高级搜索
一、后端按「订单」分组(原为平铺明细列表)
  有 scrap_request_no → 按单号分组;
  无单号(历史直接报废)→ 按 操作时间(精确到分钟) + 操作人 虚拟分组,
  生成 LEGACY-<yyyyMMddHHmm>-<操作人> 形式的虚拟单号,避免历史数据成为无主记录。
  每单返回 items 明细数组、损失合计、报废总数、申请人(取自 ScrapApproval)。

二、物料解析改为批量预加载
  原实现逐条记录 N+1 查询,且 5 条来源路径(stock_buy/semi/product、
  trans_borrow、trans_repair)各自 query.get()。改为按 (source_table, stock_id)
  收集 ID → 批量 joinedload 查询 → 内存拼装。

三、损失金额改为按权限可见
  原 /records 无条件剥离 total_loss,导致持有 scrap_list:loss_amount 的角色
  也看不到金额,与权限元素的存在相矛盾。改为对齐 outbound 的
  filter_item_by_permissions:无权限置 None,前端 v-if 隐藏整列。

四、新增 keyword / search_type 高级搜索参数
  (单号/SKU 在 SQL 层过滤,操作人/申请人/物料名在分组后过滤)

五、修复申请人显示
  ScrapApproval._user_name() 返回 '杜邢宸/duxingchen' 全名,统一经
  _display_name() 去掉 '/账号' 后缀。

前端 scrap/index.vue 重写为 el-table type=expand 嵌套表格,对齐出库记录版式,
搜索区改用统一 el-form inline 版式(含重置按钮)。

实测:3 单(1 张申请单 2 明细 + 1 组 legacy 合并 2 条),申请人/损失合计/
虚拟单号均正确。
2026-09-10 12:06:04 +08:00
3cadd42338 refactor(scrap): 报废管理子菜单按业务流排序
顺序调整为:报废申请 → 按单报废 → 报废记录 → 报废审批。

侧边栏由 layout/components/Sidebar/index.vue 的 v-for="child in
route.children" 直接按路由数组顺序渲染,无排序逻辑,故仅需调整数组元素。
原「按单报废执行」标题缩短为「按单报废」(四字,与同级菜单一致)。

注:父菜单 redirect 仍指向 /scrap/index(报废记录),与出库模块
redirect 指向「出库记录」的既有惯例保持一致,未改动。
2026-09-10 11:33:01 +08:00
1aa31334b1 fix(scrap): 报废申请页搜索/选择解耦,审批人常驻必填
一、搜索丢失已选(核心 bug)
主表格 :data="stockList" 同时充当搜索结果与选择容器。Element Plus 的
setData 在 reserveSelection 为假时走 clearSelection(),进而 emit
selection-change([]),故每次重新搜索都会清空选中态、「已选 N 条」归零。
(源码路径:table/src/store/index.mjs:34-52 → store/watcher.mjs:145-152)

改为「购物车 + 选择弹窗」结构,对齐出库选单页:
  · 主表格数据源改为 cart(已选清单),删除复选列,改「移除」按钮;
  · 新增「选择库存物品」弹窗,搜索框移入其中;
  · 表格 row-key + :reserve-selection="true",跨搜索/翻页保留勾选;
  · 点行勾选、改数量自动勾选、数字框 @click.stop 防冒泡反转勾选;
  · 服务端搜索(350ms 防抖)+ 分页 20/50/100/200;
  · uniqueKey 取 source_table_id(三表主键各自独立,必须带表名前缀)。

二、审批人字段不显示
原为 v-if="approvalVisible",仅库管代建或命中需审批物料时才渲染。
因全库仅 1 个物料标记 is_approval_required,该条件几乎从不成立。
现改为常驻,并用 el-form rules 声明 approver_id / remark 双必填;
打开弹窗即调用 checkScrapApproval 预检并展示提示文案。

三、顺带修复
库位列此前恒为空:三张库存表 to_dict 只输出 warehouse_loc,而模板绑定
warehouse_location。已在 loadStockList 中归一化。

SFC 编译与 vue-tsc 通过,无新增类型错误。
2026-09-10 11:32:56 +08:00
f589962e5c feat(scrap): 新增报废专用库存查询端点,解耦出库选单权限
报废申请页原复用 /inbound/stock/list,该端点权限是 outbound_selection。
持有 scrap_apply 的角色目前恰好也都持有该权限,故能跑通,但属隐性耦合:
任一角色授权脱钩就会静默 403,被前端 catch 掩盖成「加载库存失败」。

照抄借库既有先例(transactions.py 的 /borrow/stock-list),新增:
  GET /v1/scrap/stock-list  @permission_required('scrap_apply')

★ 同步把 'scrap_apply' 登记进 stock.py 的 SELECTION_PREFIXES。
  _make_price_stripper 仅当前缀为 None 或已在集合中时才剥离价格,
  漏登记会导致价格/成本字段泄露给报废申请人(Fail-Closed 失效)。

实测:OUTBOUND/WAREHOUSE_MGR 均 200,无 token 401,
响应中 unit_price/total_price/sale_price 等价格字段零泄漏。
2026-09-10 11:32:44 +08:00
f556514e72 refactor(scrap): 扫码校验改用 SKU 主键,与后端同口径
前端 validateAgainstPlan 由 (source_table, stock_id) 改为 SKU 主键:
  · normSku() 去首尾空白,与后端 _norm_sku 一致;
  · matchKey() 与后端 _match_key 完全同构(sku: 前缀 / row: 回退);
  · approvedQtyByKey() 聚合同一 SKU 的批准总量,兼容批准单同 SKU 多行;
  · 扫码不在批准明细内、或该 SKU 累计量超批准量时,红色 toast + 震动阻断,
    不入购物车;
  · 购物车去重与 maxScrapFor 上限同步改为按 SKU 计算。

已通过 SFC 编译与 vue-tsc(无新增类型错误)。
2026-09-10 10:21:53 +08:00
af2d1c10c6 feat(scrap): 报废作业页重构为「按单扫码执行」,对齐出库版式
create.vue 由「直接报废」页(492 行)重写为按单执行页:
  · 顶部选择已批准申请单(scope=executable, status=1);
  · 载入 items_json 为待执行清单,逐项显示批准数量与已扫数量;
  · 扫码头按 (source_table, stock_id) 匹配批准明细,
    不在单内或超批准量时红色 toast + 震动阻断,禁止入车;
  · 支持 ?requestId= 深链直达;提交实扫 payload 到 execute 接口。

审批页的「执行报废」由弹窗盲执行改为跳转本页(router.push + query.requestId),
移除 executeVisible/confirmExecute 等已失效代码;路由标题改为「按单报废执行」。
executeScrapByRequest 增加 items 参数。
2026-09-10 10:14:43 +08:00
1dbf74b7bc feat(borrow): 借库审批补「完结」能力(对齐出库 status=4)
- mark_completed 改为 1→4 已完结(真正扫码借出完成仍为 status=3)
- 前端审批页新增「已完结」筛选项与状态映射;审批信息列对 3/4 显示审批人
2026-09-10 09:47:07 +08:00
da5357acb3 feat(scrap): 前端报废申请 / 报废审批页 + API(对齐出库版式)
- api/scrap.ts 新增 submitScrapRequest/checkScrapApproval/getScrapApprovals/approveScrapRequest/executeScrapByRequest
- apply: 选库存(带 source_table+stock_id)/报废数量/原因/按需审批人提交
- approval: 顶部审批状态筛选+刷新、展开明细、列名对齐出库、分页含 sizes、按单执行弹窗
- 路由挂载 /scrap/apply、/scrap/approval
2026-09-10 09:47:02 +08:00
a12fcea86d fix(borrow,outbound): 库管弹窗必显审批人 + HTTP400去重toast
- 审批人 el-form-item 显隐加入 role==WAREHOUSE_MGR 判断
- 提交 catch 仅对无响应(网络)弹错,HTTP错误由全局拦截器按后端 data.msg 弹一次,避免重复
2026-09-09 16:19:45 +08:00
815ad01a5b V3.74,9.9推送 2026-09-09 13:19:49 +08:00
ffbf4689f1 fix(inbound/buy): 从采购申请导入时价格不受“是否可显示价格”权限拦截
- 价格填充不再包在 hasFormFieldPermission(unit_price) 里,库管(前端不显示价格)也能把采购单价格带入隐藏字段用于成本
- 前端价格列/输入框仍按字段权限对库管隐藏
2026-09-09 13:10:46 +08:00
e622697d7c feat(borrow): 借库申请前端必填拦截与星标——申请原因、预计归还日期(长期借用除外)
- 提交前拦截:申请原因为空或(未勾长期借用且无归还日期)提示并 return
- 表单加必填星标:申请原因恒必填;预计归还日期仅在非长期借用时 required
2026-09-09 10:53:16 +08:00
4146f35010 feat(permission): “出库/借库需审批”开关权限化——仅主管/超管可见可改
- 新增权限码 material_list:isApprovalRequired,授予仅 SUPER_ADMIN/SUPERVISOR(收回其它角色)
- 物料列表该列仅持码角色可见;后端批量端点与字段脱敏均改按新码校验,其余角色看不到值也无法改
2026-09-09 10:47:36 +08:00
6bdc8a82f2 feat(borrow,outbound): 申请预检按需显示审批人——默认不显示,命中需审批才出现并必选
- 后端新增 /borrow/request/check-approval 与 /outbound/request/check-approval 预检接口
- 申请弹窗默认隐藏审批人;提交前预检:命中需审批→显示审批人并红字列物料、未选拦截;未命中→隐藏并直接提交(approver=null)
- 申请 items 补传 base_id 供后端 ID 精确判定
2026-09-09 10:47:29 +08:00
2abac8b3f9 feat(borrow,outbound): 申请页审批人改可选(默认免审批),提示语更新
- 出库/借库申请不再强制选审批人;命中需审批且未选人时由后端提示补选
- 成功提示改为:含需审批物料将进入审批,否则直接交库管执行
2026-09-09 09:31:28 +08:00
93f149739d feat(material): 基础信息列表加“出库/借库需审批”开关列
- material_base.ts 新增 batchSetApprovalRequired
- material/list.vue:列配置/权限/表格 switch,单行拨动即保存(乐观更新)
2026-09-09 09:31:23 +08:00
6aba09880c feat(borrow): 库管借库执行自动把申请单原因带入备注说明
- 选中已审批借库申请单时 form.remark 自动填充该单 remark(申请原因),无需重复手填,可再改
2026-09-09 09:31:06 +08:00
578c15dc95 fix(bom): 另存为版本自增基准改响应式,修复 V1.1→V1.2 未升级
- originalVersion/currentBomNo 为普通 let,computed 读不到变化;且 handleSaveAs 在赋值前就算版本候选,导致从 V1.1 另存得到 V1.1 覆盖原版本
- 新增响应式 upgradeBaseVersion/upgradeBaseBomNo,versionOptions 改读 ref;handleSaveAs 先锁定基准再计算
- 修复后:V1.1→V1.2、V1.9→V1.10、主版本→V2.0,占用号自动跳过
2026-09-08 18:19:28 +08:00
00dde3a896 fix(inbound/buy): 采购入库表单批号续号改用精准接口
- checkHistoryAndSetMode 改调 /inbound/buy/latest-record 续号,不再依赖 getBuyList 前1000条
- 采购单导入“仅有 base_id”分支也自动续批号并带历史库位
2026-09-08 18:16:08 +08:00
b4971f057e fix(bom): 管理页启停/归档即时同步与界面健壮性大修
- 只读启停用后端权威 bom_no/version,修复改状态报 BOM不存在(400)
- 成功路径统一 await reloadCurrentGroups():先更新分组摘要,再静默覆盖已展开分类明细
- 本地秒移除 applyLocalRowState:按新状态+当前筛选把不符合行立即踢出缓存并强制 Map 换引用
- 接口层与界面层 try 分离:UI 刷新异常不再误报“状态更新失败”,避免红绿/黄提示同弹
- 遍历全部改为 Array.isArray/?.keys()/entries()/||[] 防御,杜绝 undefined 误报
- 兼容 el-collapse 手风琴(String)与多开(Array):新增 isCategoryActive/setActiveList,修复 activeCategories.filter is not a function
2026-09-08 13:58:48 +08:00
cb1d385e77 V3.73,9.8推送 2026-09-08 11:27:53 +08:00
9fc6ff00f9 feat(bom): 出库/借库清单头部 BOM 号后显示父件物料名
- selectedBomLabel 追加父件名(优先候选列表 parent_name,回退最近详情 parent_name)
- 主清单与欠料待补清单两处头部同时生效;取不到名称时退回原格式
2026-09-08 11:13:42 +08:00
02f74278df feat(bom): 归档业务闭环——状态筛选+归档/取消归档入口
- get_bom_list/get_bom_summary 状态过滤支持 archived,enabled 语义改为“启用且未归档”
- get_bom_list 输出补 is_archived;新增 update_archived 与 POST /bom/archive(整组切换归档、清缓存、审计)
- “未指定版本自动取最新”硬化为仅认启用且未归档(get_bom_detail / get_bom_no_by_parent)
- 前端:状态按钮组加“归档”;列表状态列区分归档;操作列加“归档/取消归档”按钮
2026-09-08 11:13:38 +08:00
1daa60590c feat(bom): BOM 管理页 UI——子件版本下拉、只读启停、状态按钮组、公司筛选、手风琴、详情 loading
- 自制件子件行内版本下拉(必选,默认最新启用,外购件不显示);保存/草稿带引用版本
- 查看/只读态“是否启用”开关解耦,切换走独立启停接口
- 状态筛选改 el-radio-group 按钮组(默认启用)
- 顶部接入 CompanySelector,父件/子件物料搜索联动公司并标注公司
- 分类手风琴(全部展开临时关手风琴)与详情弹窗 v-loading;列表默认只显示启用
2026-09-08 10:14:51 +08:00
424df231d5 feat(bom): 前端 BOM API——候选版本、独立启停、物料搜索支持公司
- bom.ts 新增 getChildBomVersions(候选版本) 与 updateBomStatus(独立启停)
- inbound/buy.ts searchMaterialBase 增加可选 company 参数(跨域按公司过滤)
2026-09-08 10:14:47 +08:00