Commit Graph

309 Commits

Author SHA1 Message Date
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
4cd3eefdf4 feat(borrow,outbound): 默认免审批改造——命中物料需审批才走原流程
- models/base.py 加 is_approval_required 列及 isApprovalRequired 序列化;field_permissions 登记
- 新增 POST /inbound/base/batch-approval(批量设需审批,仿批量质检)
- borrow_service.submit_approval / outbound_service.create_request:明细含需审批物料→须选审批人走原审批;否则创建即 status=1(待库管执行)、不发审批邮件
- 判定按 (name,spec_model) 反查启用物料
2026-09-09 09:31:18 +08:00
2b0e335790 fix(inbound/buy): 批号自增改为按物料精准取最近一条
- 新增 get_latest_batch_record_by_base_id:SQL 直接 filter(base_id)+order by in_date/id desc first(),O(1) 不分页
- 新增 GET /inbound/buy/latest-record
- 修复原“拉全表前1000条再前端过滤”导致的历史被截断→批号退回000001/重复入库被拦(如 CCAB0029)
2026-09-08 18:16:04 +08:00
66b3e4897f fix(bom): get_bom_summary 关键词与 list 对齐 + 状态更新方法健壮化
- get_bom_summary 关键词过滤改为与 get_bom_list 同款子查询(同时匹配 bom_no、父件名/规格、子件名/规格),任意关键词下 summary 计数与 list 明细严格一致,消除“标题有数、表格为空”
- update_enabled/update_archived 对 bom_no/version 做 strip 规范化,并细化“BOM不存在”报错(带上实际参数便于排查)
2026-09-08 13:58:43 +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
91fb6c1a03 feat(bom): 自制件子件选版本落库/强校验 + 独立启停 + 状态筛选
- save_bom 落库 child_bom_no/child_bom_version;自制件必选且须属于启用未归档配方集,外购件置空;跨版本查重纳入引用版本
- get_bom_detail 改为读存储列,空则回退最新启用配方;候选/校验/回退读取均排除 is_archived
- 新增 GET /bom/self-bom-versions(子件候选版本);新增 POST /bom/status 独立启停(update_enabled 清缓存)
- 草稿链路 save_draft/get_draft_detail/publish 持久化引用版本
- get_bom_list/get_bom_summary 支持 status(enabled/disabled) 过滤
2026-09-08 10:14:44 +08:00
3dd8458de0 feat: BOM 子件展示自制件 BOM 版本号——子件列表与出库/借库 BOM 清单打印均显示 2026-09-07 18:01:16 +08:00
478b90fe50 feat: 出库类型贯穿申请→扫码出库——申请单加 outbound_type,申请时选类型、扫码出库自动带出 2026-09-07 14:01:13 +08:00
73510d3a71 feat: 借还记录默认排序重构——有限期单按预计归还时间升序靠前,无限期单按借出时间升序靠后(优先关注快到期/逾期) 2026-09-04 16:17:42 +08:00
0daf50ac21 fix: 成品/半成品/维修列表第二层跨域判断补认 crossDomain(解决库管配跨域仍看不到双边) 2026-09-04 13:01:44 +08:00
1527d552d4 feat: 出库/借库记录记录并展示库位快照(DB加列+后端写入+记录页展示) 2026-09-04 11:42:44 +08:00
e5c49c5177 fix: 入库 search-base 物料选择加公司隔离,恢复半成品批号按本公司历史自动递增 2026-09-04 09:42:49 +08:00
1b760f351e fix: 出库/借库扫码查库存与 BOM 子件物料选择加行级公司隔离(超管/跨域不受限) 2026-09-04 09:40:00 +08:00
c68ffdca0f fix: 入库成品/半成品/采购编辑其它字段时不再误清空图片——仅显式携带图片字段才更新 2026-09-03 14:14:24 +08:00
7fe861b4a7 fix: 基础信息修改其它字段时不再误清空 product_image(图片消失)——仅显式携带 generalImage 才更新图片 2026-09-03 14:12:15 +08:00
bf7b2cc6d3 feat: BOM 库存接口支持按版本查询,修复同一编号多版本下拉回显错乱 2026-09-02 18:39:17 +08:00
ee22927660 fix: 解耦 Webhook 身份证与库存序列号,恢复批号入库纯净
- 前端:新增临时字段 form.track_id 存 16位身份证,Batch 时 form.serial_number 清空
- 前端:submitForm 不再把身份证补进 payload.serial_number,改为透传 payload.track_id
- 后端:StockSemi 仅存 serial_number(Batch 为空),notify_track 优先用 track_id
2026-09-01 16:44:48 +08:00
fbb4160787 feat: 采购申请新增库管'完结'功能(仿出库审批)
- 后端:approve 接口支持 action=close(已通过→已完结 status 1→4),模型 status_text 支持已完结
- 前端:采购申请状态tab加'已完结',已通过单显示'完结'按钮(库管)
2026-09-01 16:15:48 +08:00
2c092c4c01 feat: MOM 出库联动 Track 标记已出库
- 发货出库时反查库存表 serial_number(Track 身份证),commit 后通知 Track mom-outbound
- notify_track 支持指定 webhook url(TRACK_OUTBOUND_WEBHOOK_URL)
2026-09-01 13:53:00 +08:00
bee037f925 feat: MOM 扫码入库对接 Track 产品身份证自动填充
- 半成品/成品入库弹窗顶部新增'扫码入库'按钮,打开扫码面板(扫码枪/摄像头)
- 扫码 Track 身份证后自动填充物料信息与序列号,不再二次搜索
- 后端新增 track_query_service(httpx 调 Track lookup)、track-lookup 接口
- 入库成功后 notify_track 通知 Track 闭环
2026-09-01 13:52:56 +08:00
1000deda2f feat(purchase): 采购单号改为 批次-批内序号 格式,显式体现同批
按用户原始逻辑还原并扩展:
- 单号格式: PUR-日期-时间-批次-批内序号
  例: PUR-20260831-1021-0001-0001(今日第1次采购的第1条)
- generate_request_no 支持 batch_seq 参数生成批次+批内序号
- 新增 GET /purchase/next-batch-seq 返回今日下一个批次号
- 前端提交时获取批次号,同批所有行共享,循环提交生成连续批内序号
- 批次识别: 单号去掉末尾 -批内序号 即为批次前缀
- 保留旧格式兼容(无 batch_seq 时回退 PUR-日期-时间-流水)
2026-08-31 13:54:03 +08:00
f499346f74 refactor(purchase): 批次概念改为基于 request_no 前缀推导,撤销数据库字段
按用户反馈:不需要新增数据库字段,现有 request_no (PUR-20260831-1021-0001)
本身就能推导批次——去掉末尾4位流水号即为批次前缀。

- 撤销 purchase_request.batch_no 字段(模型/数据库)
- 撤销 create 接口的 batch_no 参数与 generate_batch_no
- 批次标识 = request_no 去掉末尾 -XXXX 段
  getBatchKey('PUR-20260831-1021-0001') = 'PUR-20260831-1021'
- 按批次查询/批量审批接口改用 batch_key (request_no LIKE 前缀)
- 前端列表批次号列、整批弹窗、一键审批本批均基于前缀推导
- 零数据库迁移,历史数据立即可用
2026-08-31 13:49:11 +08:00
664127ee30 feat(purchase): 采购批次号支持,同一批提交的记录可分组查看/统计
背景: 循环提交把一批物品生成多条独立申请,无法识别'哪些是同一批采购'

后端:
- purchase_request 模型新增 batch_no 字段(同一批提交共享)
- create 接口接收 batch_no,无则自动生成 BATCH-yyyyMMdd-HHmm-随机
- 新增 GET /purchase/batch/<batch_no> 按批次查询所有记录
- 批量审批接口支持按 batch_no 一键审批整批

前端:
- 提交时生成 batch_no 共享给所有行
- 列表新增批次号列(可点击)
- 新增'整批查看'弹窗:显示同批次所有记录 + 一键批量通过本批
- 操作列新增'整批'按钮

数据库需执行: ALTER TABLE purchase_request ADD COLUMN batch_no VARCHAR(100);
2026-08-31 13:46:46 +08:00
3a07f1a272 fix(bom): BOM 详情查询改为 LEFT JOIN,子件物料被删后仍返回父件结构
- get_bom_detail 使用 outerjoin 替代 join
- 子件物料被删除后 child_name 显示 [已删除物料] 而非误报 BOM 不存在
- 修复按 BOM 出库时因子件删除导致 404 BOM 不存在
2026-08-31 10:28:22 +08:00
36e9f500aa feat(borrow): 借库未归还直接报废功能(关联报废单流程)
后端:
- trans_service 新增 scrap_borrow 方法:未归还借用标记为 scrapped,
  扣减总库存 stock_quantity(可用已在借出时冻结),生成 TransScrap 报废记录
- transactions.py 新增 POST /borrow/scrap 接口(复用 op_return:operation 权限,库管/主管可操作)
- scrap.py query_records 支持 trans_borrow 来源的物料名解析 + 行级隔离

前端:
- records.vue 未归还记录新增「报废」按钮(库管/主管可见)
- 报废弹窗:勾选明细 + 填报废原因 + 二次确认
- 状态显示新增「已报废」标签;整单所有明细报废时标记为 scrapped
2026-08-31 10:21:28 +08:00
39f3f8af45 feat(outbound): 审批单手动完结/作废功能
- 后端: outbound_service 新增 close_request 方法(状态 1-已通过 → 4-已完结)
- 后端: 新增 POST /api/v1/outbound/request/<id>/close 接口
- 模型: status_text 数组增加「已完结」
- 前端: create.vue 审批单选择区新增「强制完结此单」按钮
  完结后刷新列表,该单从已通过下拉中移除
- 权限: 超级管理员/审批人/拥有 outbound_create:operation 的用户可完结(库管可操作)
2026-08-31 10:15:33 +08:00
95057dedf5 refactor(import): 导入服务核心逻辑改造
- 预览阶段增加 Excel 内部重复检测 + 空规格去重 + 字段长度校验
- 执行阶段支持 skip 模式(跳过重复行继续导入)+ update 模式(覆盖更新已有记录)
- update 模式空值保护:Excel 空单元格不覆盖已有数据
- DB commit 异常防御:字段超长等异常优雅返回而非 500
- 去重规则改为仅依据 spec_model(货号唯一键)
- BOM 导入物料查找同步改为 spec-only
2026-08-03 15:53:10 +08:00
65aba4b313 fix: token 刷新时缺少 company_name 导致多租户过滤返回空数据 2026-08-03 09:22:50 +08:00
29de22b575 fix: crossDomain 重复收集 — 真实树中的 system_mgmt/crossDomain 与虚拟 global_privileges 冲突,从真实树中移除 crossDomain 避免双重收集 2026-07-17 16:22:59 +08:00