|
|
dda6e4c787
|
feat(return): 退回、回库、报废与在管台账接口
打通逆向物流的全部后端入口。
新增接口(app/api/v1/inbound/stock.py):
- POST /stock/<id>/change-status 库存状态变更(在库/冻结/不良品)
- POST /stock/return-from-outbound 通用原单退回
- POST /stock/defective/<id>/restock 不良品修好回库(支持部分回库)
- POST /stock/defective/<id>/scrap 不良品报废销毁
- GET /stock/defective 在管台账分页查询
设计要点:
- 良品退回加回原库存行;不良品退回则库存表分毫不动,只写独立在管台账。
这样坏件从根上不会混进可分配池
- 全链路 Fail-Closed 守卫:良品退回到非「在库」行会被拒(status 是行级
属性,加回已冻结/不良品的行会让良品被连带隔离);原库存行已不存在会被
拒(入库模块会物理删除库存行,实测 1077 条出库记录中已有 7 条悬空)
- 三个写接口均加 with_for_update 行锁 + prevent_double_submit 幂等锁。
装饰器顺序为 permission_required → prevent_double_submit,顺序颠倒会因
JWT 未验证而抛错、被自身 except 捕获后 fail-open 降级
- 报废同时写 trans_scrap 台账(source_table 用 trans_defective_goods 并
存台账自身主键,与 trans_borrow/trans_repair 作来源时的约定一致),
成本按原库存行 best-effort 取价,取不到记 0 而不中断报废
报废报表集成(app/api/v1/scrap.py):
- _resolve_materials 补 trans_defective_goods 分支。物料名已在台账冗余存储,
不联表——坏件的原库存行可能已被删除,联表取名称会得到空值
- 公司隔离补 defective_subq 分支。原先按 source_table 逐个构造子查询,
未知来源会被整体过滤,导致这类记录对普通用户静默消失
- 无审批单号的分组前缀按来源分流:不良品直报不再套用 LEGACY-(它是新业务
记录,不是历史脏数据)。分组键同步带上来源标记,且 _order_key_pred 的
SQL 谓词改为同口径,否则页面分组与「按单筛选」结果会对不上
|
2026-09-16 15:45:31 +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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
2556b77530
|
perf: 系统级性能优化与并发安全修复
## 并发安全修复 (4处)
- scrap.py: 报废执行添加 SELECT FOR UPDATE 悲观锁,消除 TOCTOU 竞态
- stock.py (adjust_stock): 盘点调整添加 for_update=True 行锁
- outbound_service.py: 低库存预警 SMTP 调用移到 commit 之后,避免长事务
- trans_service.py: execute_dispatch 按 (source_table, id) 排序 items,消除死锁风险
## N+1 查询优化 (2处)
- inventory_task.py: _prefetch_inventory_map 单条 UNION ALL+GROUP BY 替代循环内逐条查询(N*4次→2次)
- stock.py (export_stocktake): get_borrowed_qty 批量 GROUP BY 替代逐条 TransBorrow 查询(~18000次→1次)
## BOM 列表性能重构
- bom_service.py: get_bom_list 单条 GROUP BY+string_agg+分页,消除 N+1 循环查询
- bom_service.py: 新增 get_bom_summary (轻量 GROUP BY category+COUNT)
- bom.py: 新增 /api/v1/bom/summary 路由,/list 支持 category 过滤
## Odoo 基础信息懒加载
- base_service.py: 新增 get_odoo_summary (GROUP BY category+COUNT)
- base.py: 新增 /api/v1/inbound/base/odoo-summary 路由
- buyOdoo.vue: 懒加载分组架构 (fetchOdooSummary + loadGroupItems)
- material_base.ts: 新增 getOdooSummary API
## 前端 Bug 修复
- BomManage.vue: 懒加载分组 (fetchBomSummary + loadGroupItems + collapse)
- BomManage.vue: 适配新 API 格式 (res.data.items 替代 res.data)
- buyOdoo.vue: 移除 "点击展开加载" 文字
- Selection.vue + borrow/apply/index.vue: openBomSelect 适配新 API 格式
|
2026-07-15 17:37:57 +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 |
|
|
|
e23e8c6a9e
|
fix(scrap): resolve material names, specs and operator names in list query
|
2026-04-09 17:49:51 +08:00 |
|
|
|
454f9b1184
|
feat(scrap): integrate repair items into physical scrap scanning flow and lock manual status
|
2026-04-09 09:53:20 +08:00 |
|
|
|
db0444cc13
|
fix(scrap): completely refactor scan API return dict with safe getattr to prevent attribute errors
|
2026-03-26 17:20:43 +08:00 |
|
|
|
c8810891d8
|
fix(api): globally replace invalid material_base/material_name attributes with correct base relationship
|
2026-03-26 17:14:26 +08:00 |
|
|
|
d58b002340
|
chore(api): expose real 500 error stack trace for scrap scan API
|
2026-03-26 16:59:14 +08:00 |
|
|
|
ebb7969807
|
fix: correct targeted search logic for material/stock list to prevent unrelated results
|
2026-03-19 09:49:21 +08:00 |
|