Commit Graph

37 Commits

Author SHA1 Message Date
d7f7548cee fix(outbound): 库存分配按 base_id 合并需求,修复重复行导致的假性库存不足
_allocate_bom_requirements 在整轮 for req 循环中复用同一份可用量快照
(rows_by_base 建好后不再更新),而 for 循环本身不按 base_id 去重。同一
物料以多行进入时,每行都从同一份快照重新分配一遍,把同一个 stock_id
重复分配 N 次,累计分配量可超过该行真实可用量。

超配在分配阶段不会暴露,直到 reserve_for_items 的二次校验
(take > avail)才抛错,报「可用库存不足(需 X,实剩 Y)」,而实际库存
充足。实剩为 0.0 是当最大候选批次的可用量恰好等于单行需求时的收尾形态。

重复行来自正常业务,非脏数据:
  - 购物车里同一物料的多批次就是多行(Selection.vue 提交时只带
    base_id + quantity,stock_id/source_table 被丢弃);
  - BOM 明细里同一子件被多处引用(前端 requirements 按 child_id 不去重);
  - 调拨 / 补发等拆行场景。

修复:在函数入口按 base_id 合并 reqs、required_qty 求和。分配语义不变
—— 分配器本就按 base_id 拉全量批次行、降序分配,输入里的 stock_id 从来
就被忽略。

选在此处收口而非合并调用方的 items:_allocate_bom_requirements 是全系统
库存分配的唯一权威入口(出库与借库都经 reserve_for_items 走到这里),
一处修改同时覆盖两条链路。

实测(base_id=711,四个批次可用量合计 124):
  三行各 30  修复前 ERROR(需30/实剩10) → 修复后 OK,2117 出 70 + 1182 出 20
  两行各 30  修复前静默超配(两行都绑 2117) → 修复后 OK,合并为 2117 出 60
  十行各 30  修复后报真实缺口「需 300,可用 124」(走正常缺料分支)
2026-09-20 15:09:14 +08:00
9eb4792d4a feat(return): 退回流水看板接口与权限收口
新增只读台账接口:
- GET /api/v1/outbound/returns  退回流水(分页 + 关键词 + 类型 + 时间过滤)
  返回 原出库单号 / 物料名称 / 规格 / SKU / 退回类型 / 退回数量 / 原因 /
  操作人 / 退回时间 / 公司。出库单号经 trans_outbound 批量补齐,物料名按
  多态来源批量解析,均为批量查询无 N+1。

权限收口(配合 db_migrations 里的三个权限码):
- return-from-outbound   inventory_stocktake:operation -> outbound_return
- GET /stock/defective   inventory_stocktake           -> defective_list
- restock                inventory_stocktake:operation -> defective_restock
- scrap                  inventory_stocktake:operation -> defective_scrap
- change-status          inventory_stocktake:operation -> stock_change_status

  原先这四个接口搭的是「盲盘作业」权限的便车,职责错配、审计不合规。
  实测 SALES(销售)角色持有 inventory_stocktake,意味着销售人员能读整份
  不良品台账——与业务对台账可见性的要求不符。全部改用无冒号专用码后,
  实测「只授予 inventory_stocktake:operation」对四个接口均返回 403,便车已封。

trans_return 补 company_name 快照:
  退回流水的隔离判定原先只能靠 join 链推,而库存行会被入库模块物理删除
  (实测 1077 条出库记录中已有 7 条悬空),链路一断记录就会对普通用户
  静默消失。改由退回时落快照,隔离不再依赖任何 join。
2026-09-16 16:45:52 +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
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
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
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
a910a6ea72 feat(bom): 后端承接 BOM 库存分配,消除多批次物料只能加 1 件的缺陷
问题现象
--------
BOM 选单中物料显示需求 10、聚合可用 839,加入购物车却只剩 1 件,
并提示库存不足。

根因
----
两个接口口径不一致:
  · GET  /bom/stock/<bom_no>      按 base_id 聚合 → current_stock=839
  · POST /outbound/bom-match-stock 不聚合,每批次一行 → 某行只有 1
前端用 stockList.find(s => s.base_id == child_id) 只取第一条库存行,
若首行恰好只剩 1 件,需求量又被 Math.min 压到 1,现象即如此。

为何必须放在后端
----------------
前端 stockList 由多个入口写入(手动选单/搜索/BOM),随时可能被覆盖;
且 base_id 与 stock_id 的类型差异会让匹配静默落空,表现同样是「库存不足」。
更关键的是:分配需要「该 base_id 全部可用库存行」的完整视图,
而这必须与出库扣减(create_outbound_batch 按 stock_id 逐行加锁扣减)
使用同一份数据源。

改动
----
bom-match-stock 新增分配模式:
  请求 { requirements: [{base_id, required_qty, name, spec_model}] }
  响应 { items: [...已分配行], shortages: [...缺料明细] }

_allocate_bom_requirements() 在 DB 层完成:
  1. 三张库存表按 base_id 一次性取全部 available_quantity > 0 的行
     (带公司隔离,join base 取名称规格);
  2. 可用量降序排序 —— 优先进大行,减少购物车拆分行数;
  3. 逐物料扣减 required_qty,产出真实 stock_id + source_table + allocated_qty;
  4. 分配不足记录 shortage 但不阻断其它物料。
  每行仍携带 uniqueKey,前端可直接入购物车。
  返回前剥离价格成本字段(Fail-Closed)。

旧查询模式(child_ids)保留,兼容未改造的调用方。

顺带修复一处静默失败
--------------------
查询块的 except 原为直接 continue,会把 NameError 等错误吞成「该物料无库存」。
改为 logger.error 输出,避免同类问题再次以业务结论的形式出现。

实测(base_id=2405,聚合 841,需 10):分配 1 行 stock_id=1961 分配 10,无短缺;
base_id=2963(9 行各 1),需 5 → 5 行各 1 合计 5;
需 20(聚合仅 9)→ 9 行合计 9,短缺 11。
2026-09-10 14:16:47 +08:00
4808a48594 refactor(audit): 审计架构清理——复活白名单监听器、停用噪声监听器、清除僵尸装饰器
一、统一为单一监听器实现
  原先两套 SQLAlchemy 事件监听器并存:
    · app/utils/audit_events.py   —— 全局监听 db.Model、无白名单、无请求上下文守卫(实际在跑)
    · app/core/audit_listener.py  —— 白名单制、有守卫、有模型级开关(从未生效)
  后者失效的根因:注册代码写在 extensions.py 的 init_extensions() 内,
  而该函数全仓库只有定义、没有任何调用(create_app 直接内联调用 db.init_app 等)。

  现统一由 app/core/audit_listener.py 承担,并在 create_app() 中显式注册。
  extensions.py 的死函数 init_extensions 整体删除,避免后人误以为它是有效入口。

二、修复监听器三处致命缺陷(此前注册了也写不进数据)
  1. 事件回调第二个参数是 Connection,原代码却调用 Connection.add()(不存在),
     每次写日志都抛 AttributeError 并被 except 吞掉 → 改为 connection.execute()
  2. register_audit_listeners 从 app.models 批量 import 多个未导出的模型,
     ImportError 被上层 try/except 吞掉 → 改为按表名从 db.metadata 取模型
  3. 本项目有 31 处函数体内延迟导入模型(如 scrap.py 内部才 import ScrapApproval),
     一次性注册会静默漏表 → 增加 ensure_audit_listeners() 惰性补绑,
     并在模型预加载段补全审批单/BOM/采购等模型

三、强约束
  · WHITELIST_TABLES:仅 18 张核心业务表,系统表/草稿表/向量表不再自审
  · has_request_context() 守卫:系统初始化与后台定时任务不再产生 username=system 噪声
  · IGNORE_FIELDS 增加 password/password_hash/salt/token/secret/api_key(安全红线)
  · created_at 显式写 beijing_time(),与全系统时间口径一致

四、清除僵尸装饰器
  @audit_log 早已退化为直接透传的空壳(module/action 参数全被忽略,
  数据库中零星的中文 action 即其历史遗留产物),却仍挂在 38 处路由上。
  连同 13 个文件的 import 一并移除;audit_events.register_audit_events 改为空操作。

验证:应用上下文中的写操作不产生日志;HTTP 请求产生 5 条日志,
对象为业务单号(APR-SCRAP-... / SKU),模块中文,操作人真实,时间为北京时间。
2026-09-10 14:16:27 +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
044e6dbd98 feat(borrow,outbound): 库管(WAREHOUSE_MGR)代建申请强制审批
- submit/create 新增 force_approval:库管建单无视物料是否需审批,一律走审批
- 路由按当前角色为 WAREHOUSE_MGR 传 true;未选审批人返回明确业务错误
2026-09-09 16:19:38 +08:00
eff9b62224 fix(borrow,outbound): 普通用户记录只看本人——按领用人/借用人姓名(不含账号前缀)匹配
- 出库记录/借还记录:非管理者视角时按 领用人(consumer_name)/借用人(borrower_name) 过滤
- 匹配取登录名 username.split(/)[0] 的姓名;兼容库里存“姓名/xiaolongxia”全名(姓名+/前缀)
- 修复:管理员替员工创建、领用人=员工 的单,员工登录可见
2026-09-09 11:19:15 +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
9622baf76e fix(borrow,outbound): 记录查看按角色收窄——普通只看自己,库管/主管/超管/跨域看全部
- decorators 新增 is_privileged_viewer(SUPER_ADMIN/SUPERVISOR/WAREHOUSE_MGR 或 crossDomain)
- 借库记录列表:非管理者强制 applicant_id=当前用户
- 出库记录列表+详情:非管理者强制只看自己/无权访问他人单(403)
2026-09-09 09:31:01 +08:00
478b90fe50 feat: 出库类型贯穿申请→扫码出库——申请单加 outbound_type,申请时选类型、扫码出库自动带出 2026-09-07 14:01:13 +08:00
808e491fe8 fix: BOM 匹配库存(bom_match_stock)加行级公司隔离(三表按 base 公司过滤,超管/跨域不受限) 2026-09-04 15:57:14 +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
30f336779e fix: outbound filter_item_by_permissions 只过滤敏感金额字段,基础明细字段不再剥离 2026-07-21 09:53:47 +08:00
3c0954598c perf: 综合安全加固 — RBAC严格映射+异步邮件+字段权限白名单+前端对齐+导入模板
本次提交包含本会话所有修改的最终统一提交

## 权限系统重构
- permission_service.py: 添加入库/采购操作元素 + ensure_default_permissions
- field_permissions.py: 严格1-to-1 Default Deny 字段映射(StockBuy/Semi/Product/MaterialBase)
- decorators.py: _expand_operation_perms 双向粒度桥接 + prevent_double_submit
- deploy_production.sql: 修复 sys_element 别名码(qty_inbound→in_quantity)

## 采购模块
- purchase.py: 权限驱动可见性 + inbound_purchase独立权限 + 价格字段过滤
- purchase_service.py: 异步邮件 + 三阶段批量模糊匹配防N+1
- purchase/index.vue: canApprove严格操作权限 + upload重复修复

## 导出/入
- base_service.py: export_excel 流式写入防OOM + get_latest_specs 优化
- import_service.py + import_api.py: Excel批量导入(模板+预览+执行)
- ImportDialog.vue: 三步骤导入弹窗

## 异步邮件
- email_service.py: send_email_async (守护线程)
- inventory_task.py: send_email→send_email_async

## 前端对齐
- product/semi/buy.vue: 列对齐in_quantity/stock_quantity/available_quantity + localStorage缓存V2
- buyOdoo.vue: 排序修复 + 导入按钮 + 移除点击展开加载
- BomManage.vue: 懒加载分组 + 导入按钮
- list.vue: 导入按钮
- Selection.vue + borrow/apply: BOM匹配修复 + 导入按钮
- outbound/create.vue: 出库类型必选
- AppMain.vue: 移除transition白屏修复
- material_base.ts, outbound.ts, bom.ts, stock.ts: 新增API函数
2026-07-17 13:07:12 +08:00
347f20497c feat: Redis幂等锁 + 关键端点防重复提交
## decorators.py
- 新增 @prevent_double_submit(lock_timeout=5) 装饰器
  Redis key: idem:{user_id}:{path}:{MD5(body)}
  锁存在→409, 不存在→setex→执行业务→delete
  Redis不可用时fail-open降级放行

## purchase.py
- POST /purchase (创建): +@prevent_double_submit
- PATCH /purchase/<id>/approve (审批): +@prevent_double_submit

## outbound.py
- POST /outbound (出库): +@prevent_double_submit

## transactions.py
- POST /borrow/dispatch (借库扣减): +@prevent_double_submit
2026-07-16 13:08:37 +08:00
c07f25b646 fix: Fail-Closed 字段级安全加固 — 堵住15+端点价格泄露
## stock.py
- _make_price_stripper(): 工厂函数,选单前缀自动剥离所有价格/成本字段
- _do_get_stock_list(permission_prefix): 每个item.to_dict()后剥离价格
- /all 端点: 非AI模式自动剥离价格字段
- /list 端点: permission_prefix='outbound_selection' 传递

## outbound.py
- /bom-match-stock: 返回前按stock_type剥离全部价格/成本字段
- /scan: result.pop('price', None)

## scrap.py
- /scan: result.pop('price', None)
- /records: 每条记录剥离 cost_at_scrap, total_loss

## transactions.py
- filter_item_by_permissions: 从空字典恢复完整20字段映射
- /borrow/stock-list: permission_prefix='op_borrow_apply' 传递

## buy.py / semi.py / product.py
- submit 成功响应剥离所有价格/成本字段(不泄露给前端)

## bom.py
- /base/list: 剥离 referencePrice
2026-07-16 11:26:05 +08:00
329820117f perf: 消除 outbound/borrow BOM 匹配的 while(true) 全量加载
## 后端
- outbound.py: 新增 POST /api/v1/outbound/bom-match-stock 端点
  接收 child_ids[],服务端按 base_id IN 查询三表有库存记录并返回

## 前端
- outbound.ts: 新增 bomMatchStock(childIds) API 函数
- Selection.vue: loadAllStockForBom (while(true) 全量) → loadStockForBom (单次 API)
- borrow/apply/index.vue: 同上

## 效果
- BOM 匹配从 ~90 次 HTTP 请求降为 1 次
- 浏览器内存从 ~18000 条降为 ~8-50 条
2026-07-15 17:52:08 +08:00
0651eb07fa fix: 彻底解耦出库/借库权限联动 + 路由去硬编码roles + 补API权限保护
根因: 借库选单页面14处硬编码outbound_selection:operation, 导致两模块权限联动

修复:
- borrow/apply: 14处outbound_selection:operation→op_borrow_apply:operation
- stock.py: 拆分_do_get_stock_list裸逻辑, 出库/借库各绑独立权限码
- transactions: 新增/borrow/stock-list端点(@permission(op_borrow_apply))
- transaction.ts: 新增getBorrowStockList前端API函数
- outbound.py: 出库审批4端点改用独立outbound_approval权限码
- transactions.py: 借库审批3端点补@permission(op_borrow_approval)
- purchase.py: 采购管理7端点补@permission(inbound_buy)
- audit.py: 审计日志补@permission(system_audit)
- router: 清除全部6处硬编码roles, 交由动态权限树控制
2026-07-15 14:33:11 +08:00
9988d1b7eb fix: 补全审计/出库审批/采购管理/借库审批的权限保护+公司隔离
- audit.py: 补@permission_required(system_audit)+import, 审计日志通过split_part关联用户表过滤公司
- outbound.py: 出库审批5个端点全补@permission_required(outbound_list)
- purchase.py: 采购管理7个端点全补@permission_required(inbound_buy)
- borrow_service: 借库审批列表通过applicant_id关联用户表过滤公司
- outbound_service: 出库审批列表通过applicant_id关联用户表过滤公司
- purchase_service: 采购列表通过base_id+requester_id双路过滤公司
- base_service/search_material: 去除debug日志
- decorators: 去除debug日志
2026-07-15 11:49:53 +08:00
e1417d740a feat: 全模块公司隔离 + crossDomain权限码动态跨域控制
- get_current_company_filter: 新增_has_cross_domain_permission, 权限码替代硬编码
- 补全11个Service的get_current_company_filter调用(semi/product/service/outbound/bom/trans/scrap/summary)
- base/search修复: search_material此前无隔离, 已补全
- get_current_company_filter兜底: JWT缺company_name时返回__NO_COMPANY__防止放行
- permission.py: _get_operator_company补全返回值, 修复权限页保存逻辑
- 新增crossDomain迁移脚本, element_type=element挂system_mgmt下
2026-07-15 11:11:40 +08:00
4f5965db02 feat: JWT多租户数据权限隔离 & 主管系统管理权限 & 含税单价补齐
## 多租户公司数据隔离
- 新增 get_current_company_filter() 工具函数 (decorators.py)
  SUPER_ADMIN: 可传company_name参数过滤或传ALL看全量
  其他角色: 强制隔离到JWT中的company_name
- 重构 base_service.py / buy_service.py: 用集中式函数替换内联公司过滤
- SysRolePermission 表新增 company_name 字段,支持同角色不同公司权限
- get_user_permissions() 新增 company_name 参数,查公司定制+全局模板权限
- permission.py API 新增 @permission_required 拦截 + 公司过滤
- 19个API/service文件传递 company_name 到权限查询

## 主管系统管理权限
- delete_user() 允许SUPERVISOR删除同公司用户 (原仅SUPER_ADMIN)
- get_all_users() 新增 company_name 参数过滤
- 用户列表/权限分配 API 应用 get_current_company_filter()
- 前端 UserCreate.vue: 超管可见公司下拉框,主管隐藏部门字段

## 前端多租户适配
- material/list.vue / buy.vue: 公司下拉框仅超管可见,默认ALL
- UserCreate.vue: 新增搜索栏公司筛选,部门字段按角色显隐
- auth.ts: getUserList() 支持 params 参数

## Bug修复: 含税单价字段补齐
- buy.vue: 表格列/高级筛选/排序/权限映射新增 post_tax_unit_price
- buy_service.py: allowed_fields/sort_field_map 新增 post_tax_unit_price
2026-07-13 15:12:22 +08:00
DXC
62c0e3738e fix(outbound+trans): 修复POST接口错误数据清洗导致的sku/quantity字段被清除Bug,并新增出库审批工作流全链路 2026-04-28 16:02:34 +08:00
DXC
71e5f075d2 feat: implement composite debounced search with prepended select and wipe out duplicate root permission nodes 2026-03-20 10:26:45 +08:00
DXC
ea28ee1c86 feat: 为核心业务 API 全面挂载审计日志装饰器 2026-03-10 17:16:57 +08:00
dxc
c1e4acc1d8 fix: standardize role case handling in permission logic
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 17:07:45 +08:00
dxc
a0993767fe fix: make SUPER_ADMIN role checks case-insensitive across app
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 17:04:22 +08:00
dxc
1fe00a8ba3 feat: Add field permission checks to outbound and transaction APIs
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 15:11:10 +08:00
dxc
5065410662 feat: add RBAC control for outbound list module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 13:57:59 +08:00
dxc
3714dd180b feat: apply RBAC read/write separation to outbound_create module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 13:54:06 +08:00
dxc
af41eb1803 feat: add RBAC controls for outbound selection module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 13:45:49 +08:00
dxc
c1ddb8093f 出库进行修改,确保可以进行多个样例的出库以及出库的记录展示 2026-02-05 16:54:11 +08:00
dxc
f3b60dfc54 出库操作逻辑上面实现,成功跑通 2026-02-05 10:20:52 +08:00
dxc
797b611530 出库逻辑添加,扫码识别编码成功,后续对应逻辑没有完成 2026-02-04 17:22:20 +08:00