Commit Graph

66 Commits

Author SHA1 Message Date
e9468673c0 feat(stocktake): 抽盘范围改为「系统推荐 + 人工在库位树上勾选」
【后端】
- 新增 GET /stocktake/recommend-locations?days=30&top_n=50
  调 get_active_locations 返回推荐库位 full_path 列表 + moves/sku_count/
  last_move 明细。只推荐不落库,days/top_n 做范围钳制(1~365 / 1~500)。
- /draft/start-new 在 scope_type=active 时不再自行计算活跃库位,改为读取
  payload 的 scope_config.locations 并校验归一化(去空白、去重、保序、
  空列表 400、上限 2000)。范围决定权交还前端 UI —— 否则用户手改的勾选
  会被后端覆盖。

【前端】欢迎页选中「活跃库位抽盘」时展开配置区:
- 「近 30 天最活跃的前 [N] 个库位」+【获取推荐】
- el-tree(show-checkbox)数据取自 /v1/warehouse/tree,按公司前缀过滤
  (IRIS 只留 Y*,LICA 只留 C*/L*,复用 getAllowedLocPrefixes)
- 获取推荐后 setCheckedKeys 自动勾选;用户可自由增删
- 提交时 getCheckedNodes().map(n => n.full_path) 打包进 scope_config.locations

两个实现细节:
- 推荐里有、但树上勾不到的库位(不在当前公司前缀内等)会明确告警并打印,
  不让它们静默落选 —— 否则工人以为盘到了、实际没进范围。
- 已选库位数实时显示,因为 el-tree 默认级联:勾一个父节点会连带勾中整棵
  子树,规模可能远超推荐数量,需要让用户看得见。

实测:
  recommend-locations(top_n=10) → 10 个库位 + 明细
  start-new 传 3 个库位 → 落库正是这 3 个,总品项 36(全仓 1126)
  不传 locations → 400;勾选为空 → 400
  公司前缀过滤: IRIS→8 个(Y1~Y8),LICA→25 个,无越界
2026-09-11 14:13:10 +08:00
fc95357662 feat(stocktake): 跨公司扫码给精准报错,0 库存盘盈在明细中可见
【跨公司拦截】
get_stock_info 带 company_name 过滤后,扫到别家公司的货会直接查不到,
返回 404「未找到物料」—— 与「条码根本不存在」完全无法区分,现场人员
既不知道是扫错了还是扫了别人的货。

新增 find_stock_owner_company():不施加公司过滤地全局查条码归属;
get_stock_info 未命中时由 _classify_missing_stock() 复核,区分两种情形:
  · 条码存在但属于别家公司 → 403「条码 [X] 属于【LICA】,请勿跨公司盘点」
  · 条码不存在             → 404「未找到该物料库存: X」

该方案的前提「SKU/条码全系统唯一」已核验成立:三张库存表的表内重复、
跨公司重复、跨表重复六项检查均为 0,故归属至多命中一条,无歧义。
/scan 与 /draft/add 两个入口均已接入。

【0 库存盘盈可见性】
merged-list 的 union_sql 原本严格要求 stock_quantity > 0,导致账面为 0、
但已被扫入的盘盈物料不出现在明细抽屉里,而 total_scanned 却把它计入
「已盘」—— 工人看到已盘 +1 却在明细里找不到,以为系统丢了数据。

现改为 stock_quantity > 0 OR id IN (本会话该表的草稿 stock_id)。
条件严格限定在**本会话**的草稿,不会把全库 0 库存物料都放出来。

实测:
  IRIS 扫 LICA 条码 → 403「条码 [0000002097] 属于【LICA】,请勿跨公司盘点」
  扫不存在条码      → 404「未找到该物料库存: NOPE-99999」
  已扫的 0 库存物料 → 明细可见(账面 0 / 实盘 5 / 差异 +5 / 库位 Y3/3/3/4)
  未扫的 0 库存物料 → 明细 0 行(范围正确)
2026-09-11 14:05:59 +08:00
eabe70b00a feat(stocktake): 落地动盘(活跃库位抽盘)与导出盲盘保护
【导出收尾】/export-stocktake
会话 active 且 mode=blind 时,汇总表/差异明细/账实相符/未盘点四张 Sheet 的
账面数与差异数一律输出 '—',实盘数照常输出。会话 finished 后自动解锁 ——
盘点结束后核对差异是正常流程,否则报告本身没法用。
批量替换时误伤了 update_stocktake_quantity 里的差异计算,被静态检查脚本
抓出(会拿占位符 '—' 做减法),已改回按真实账面值计算。

【动盘算法】get_active_locations(company_name, days=30, top_n=50)
近 N 天异动最频繁的库位。出库经 (source_table, stock_id) 回查库存表取库位
(trans_outbound.warehouse_location 历史上 900 条里 829 条为空,实测回查
覆盖率 298/299);入库以库存表自身的 in_date / production_date 计时;
借出用 trans_borrow.location。报废/维修表无库位字段,未纳入。

【范围冻结】/draft/start-new
移除 scope_type=active 的 400 拦截,改为在开单时算出活跃库位并**冻结**进
scope_config。必须开单时算好 —— 若各接口实时计算,两次调用之间活跃度变化
会导致范围漂移,结束盘点时把已掉出范围的库存误判成盘亏。

【同源过滤】all-items / merged-list / generate-missing
三者都读同一份冻结的 scope_config。generate-missing 是重中之重:漏盘比对
只针对范围内库存,绝不把范围外未盘库存标成盘亏。
merged-list 一并加了过滤(原指令未提),否则抽屉仍会列出范围外物资,
与「总品项」对不上。

【前端】移除「活跃库位抽盘」的 disabled。

实测(IRIS,全仓 1126 → 抽盘 537):
  生成盘亏 537 条 | 落在抽盘范围内 537 | 落在范围外 0
  导出: 盲盘 active → 账面数/差异数 = '—';盲盘 finished → 76 / -76(解锁)
2026-09-11 13:41:56 +08:00
97c1a9131c feat(stocktake): 明/盲盘由后端强制驱动,前端解除屏蔽
原实现是前端把「账面数」「差异」两列用 HTML 注释藏掉 —— 数据仍在
HTTP 响应里明文下发,抓包或用改过的客户端即可看到,等于没有屏蔽。

【后端】/draft/merged-list 先查该 session_id 的 StocktakeSession.mode:
- blind:stock_qty 与 diff_qty 一律置 None,真实账面数不出现在响应中
- open :正常下发
差异必须一并置空 —— diff_qty 由账面数算出,下发差异等于变相泄漏账面数。
响应新增 data.mode 供前端提示。

Fail-Safe:查不到会话行时按盲盘处理。宁可界面少显示两列,也不能因为
会话记录缺失就把账面数泄漏出去。

【前端】删除屏蔽用的 HTML 注释,恢复两列正常渲染,不再承担数据屏蔽职责。
但 null 必须渲染为「—」而非 0:原 差异 模板用 `row.diff_qty || 0`,
会把「未下发」显示成「无差异」,工人会误以为账实相符。
另在抽屉表头加模式标签,便于核对。

实测:
  盲盘 → 账面数=None 差异=None   mode=blind
  明盘 → 账面数=1.0  差异=-1.0   mode=open
  未知 session_id → 同盲盘(Fail-Safe 生效)
2026-09-11 13:34:36 +08:00
3f964614e3 feat(stocktake): start-new 落库会话配置,active-session 改查会话表
- /draft/start-new 接收 mode / scope_type / scope_config,插入一条
  StocktakeSession(status='active')。同一公司原有活跃会话先置为 finished
  (同一事务),否则会撞 uq_stocktake_session_one_active。
  公司取值:普通用户强制本公司;跨域角色必须显式指定,否则 400。
  scope_type 目前只放行 'full' —— 抽盘的范围过滤尚未实现,此时放行会导致
  结束盘点时 generate-missing 把抽盘范围外的库存批量标成盘亏。

- /draft/active-session 改为直接查 stocktake_session
  (company_name + status='active'),并返回 mode / scope_type 供前端展示。
  旧实现靠 max(scan_time) 从草稿行猜,空会话识别不了。

- /stocktake/generate-missing 漏盘比对完成后把会话置为 finished,
  否则 active-session 会继续把它当活跃会话,其他 PDA 会加入一个已结束的盘点。

实测:
  ① 超管+IRIS 创建 blind/full 会话 → 200
  ② SALES/IRIS 查活跃会话 → 拿到同一 session_id 与 mode=blind
     (scanned=0 的空会话也能查到,旧实现此处返回 null)
  ③ 再开一轮 → superseded_count=1,旧的转 finished
  ④ scope_type=active → 400「活跃库位抽盘尚未实现」
  ⑤ DB 层验证唯一索引拒绝重复活跃会话
2026-09-11 13:28:49 +08:00
f8403d2fb2 feat(stocktake): 活跃会话接口补充已扫件数与发起人
多人协同场景下,加入者需要知道「谁开的、盘到哪了」,原接口只返回会话ID和
行数,信息不足以提示。

- /draft/active-session 新增 scanned(按 source_table+stock_id 去重,
  且排除 user_id='system' 的自动漏盘记录,避免进度虚高)
  与 initiator(该会话最早一条人工扫码记录的操作人姓名)
- 把 export_stocktake 内嵌的 get_user_name 提到模块级 _resolve_user_name
  供两处复用,并补齐它漏掉的 user_id 格式:
    盘点/流水里实际存的是 JWT 的 display_name,形如 "孙霞(sunxia)",
    而原实现只认 SysUser.username 的 "姓名/账号" 格式,导致解析不出来时
    原样返回。现兼容 "姓名(账号)" / "姓名/账号" / 纯数字ID 三种。

实测: SUPER_ADMIN 无参数 → scanned=1117, initiator='韩善龙'
      SALES + IRIS    → 会话 company_name 为 NULL(迁移前遗留),正确返回空
2026-09-11 12:42:55 +08:00
02e03e6fb6 fix(stocktake): 补上模块级 traceback 导入,修复异常分支的 NameError
stock.py 模块级从未 import traceback,却有 3 处裸调 traceback.print_exc():

- scan_stock_by_barcode (/scan) —— 本次改动之前就存在
- get_stocktake_companies (新增)
- get_active_session    (新增)

后果是真正的错误被 NameError 盖住:例如 company_name 列不存在时,
日志里只剩「name 'traceback' is not defined」,看不到根因。

其余 4 处在 except 块内写了局部 import 因而一直正常,现统一由模块级提供。
2026-09-11 11:34:23 +08:00
b8d18c71d8 fix(stocktake): 盘点链路按公司隔离,开启新盘点不再清空整表
修复三处跨公司数据污染:

1. /draft/start-new 原本执行 StocktakeDraft.query.all() 后逐条 delete,
   任一库管点「开启新盘点」就会物理删除全公司所有人的盘点进度。
   改为只签发新 session_id,历史数据原样保留;清理走 /draft/clear,
   且强制要求 session_id(不传直接 400),杜绝误清整表。

2. get_stock_info() 全局按 barcode/sku 匹配三张库存表,不同公司的同码
   物料互相串货。新增 company_name 参数,精确与模糊两段查询均经由 base
   关系按 material_base.company_name 过滤;草稿去重键同步改为
   (uuid, session_id, company_name)。

3. 盘点相关读写接口统一施加公司隔离:/draft/list、/draft/add、/draft/clear、
   /variance-report、/draft/merged-list、/stocktake/all-items、
   /stocktake/generate-missing、/stocktake/update-quantity、/export-stocktake、
   /adjust、/scan。

顺带修复 /export-stocktake:差异与相符两张 Sheet 原本不按 session 过滤,
过去依赖 start-new 清空整表才恰好等价于当前会话,现显式按 session_id 过滤,
否则历史会话会被一并导出。

新增接口:
- GET /stocktake/companies   盘点页公司下拉(普通用户只返回本公司,
  避免复用 /inbound/buy/options 时因缺 inbound_buy 权限而 403)
- GET /draft/active-session  该公司最近一次活跃会话,供多设备加入
2026-09-11 11:33:12 +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
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
b40aa3ab90 fix: 出库选单手动添加库存(/v1/inbound/stock/list)加行级公司隔离(三表按 base 公司过滤,超管/跨域不受限) 2026-09-04 15:55:24 +08:00
893e7250db fix(api): update-stocktake-quantity 按 session_id 隔离更新,防止跨会话误改
后端 update_stocktake_quantity 原只按 stock_id+source_table 查草稿,
不同盘点会话会相互覆盖实盘数。后端增加 session_id 过滤,
前端 handleQuantityChange 配套传入当前会话ID,API 类型同步补全。
2026-08-31 16:00:20 +08:00
eb017d881c perf(api): draft/list 与 stocktake/all-items 改为数据库级分页,消除 N+1
- /draft/list: 废弃 query.all() + 逐条查库存/material_base 的 N+1,
  改为原生 SQL 连表(草稿 JOIN 三张库存表 + material_base)+ LIMIT/OFFSET 分页
- /stocktake/all-items: 废弃三表 .all() + item.base 懒加载 + 内存切片,
  改为 UNION ALL + LEFT JOIN + LIMIT/OFFSET,COUNT 聚合求总数
2026-08-31 16:00:13 +08:00
6ba20fa3db fix(stocktake): 修复盘库重复记录 — 去重条件去掉 user_id
根因: draft/add 用 (user_id, uuid, session_id) 匹配去重,
同一盘点单多个用户(手机/平板不同账号)操作同一物料时,
每个用户各建一条 → 重复记录。

修复:
- 去重改为 (session_id, uuid) 唯一,不按 user_id 隔离
- 已存在则更新数量并记录最后操作用户
- 清理现有重复数据(保留每组最小id)
- 新增唯一索引 uq_stocktake_draft_session_uuid 防未来再重复

数据库需执行:
DELETE FROM stocktake_draft a USING stocktake_draft b
  WHERE a.session_id=b.session_id AND a.uuid=b.uuid AND a.id>b.id;
CREATE UNIQUE INDEX IF NOT EXISTS uq_stocktake_draft_session_uuid
  ON stocktake_draft(session_id, uuid);
2026-08-31 15:18:47 +08:00
7a3ffd8ba5 feat(stocktake): 重复扫码提示并确认是否修改
问题: 同一个码扫两遍输入两个数,后端 draft/add 直接覆盖,无任何提示
可能误改已盘数量

改进:
- 后端 draft/list 支持 uuid 过滤,可精确查某物料是否已盘
- 扫码弹输入框前,先精确查询该物料是否已盘
- 已盘过 → 弹窗提示『当前实盘为 X,是否修改?』
  确认后预填旧值供修改,取消则不改变
2026-08-31 15:13:52 +08:00
c6e8c887ec perf(stocktake): 应盘清单分页 + 扫码置顶高亮
性能修复(解决打开费劲):
- get_all_stocktake_items 改为分页返回 {items(当前页), total, total_scanned}
  不再三次 .all() 全量加载 + 内存排序
- 前端 fetchAllStockItems 分页拉取(每页200),stats 用后端返回的 total
  不依赖全量数组长度

体验优化:
- 扫码成功后将物品置顶到当前表格第一行并浅绿高亮3秒
  库管无需翻页找刚扫的物品,'嘀'一下确认数量即可继续
- 主表格行高亮样式 .just-scanned-row
2026-08-31 14:59:22 +08:00
7e281cf285 fix(stocktake): 盘库扫码改为精确匹配,修复漏匹配与性能问题
问题:
1. 准确性 bug: 前端用 getStockList({pageSize:10, keyword}) 模糊搜索 + find()
   当 SKU 前缀相同、目标不在前10条时,误报'未找到该物料库存'
2. 性能: 每次扫码全表 ilike %x% 搜索 3 张表,不走索引

修复:
- 后端 get_stock_info 改为精确匹配(==)优先,未命中再回退模糊搜索
- 新增 GET /inbound/stock/scan 扫码精确匹配接口,返回唯一命中
- 前端 onScanSuccess/handleManualInput 改用 scanStockByBarcode
  一次请求直接命中,不再依赖 pageSize:10 + find()
2026-08-31 14:49:24 +08:00
2ff0f92f94 fix(stocktake): 修复盘点报告导出 500 错误
- 导出盘点报告时第1126行使用了 func.sum()/func.coalesce()
  但文件未导入 sqlalchemy 的 func
- 改为 db.func.sum()/db.func.coalesce(),与文件内其他用法一致
2026-08-28 15:53:03 +08:00
8c10bb0903 feat: 盘点合并列表 — 服务端JOIN+DB分页,消除99999全量加载
## stock.py
- 新增 GET /draft/merged-list: UNION ALL三表+LEFT JOIN draft
  数据库级 LIMIT/OFFSET 分页,不再加载全量到Python内存
- 参数: session_id, keyword, status_filter, page, pageSize
- 返回: list, total, total_scanned (已扫去重数)

## stock.ts
- 新增 getDraftMergedList() API函数

## stocktake/index.vue
- fetchInventoryList: 改用merged-list单次调用替代99999全量+find()
- resumeSession: limit 99999→1 先检查存在,再limit 500加载
2026-07-16 13:08:29 +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
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
dxc
c1092407ad V3.50 2026-06-25 15:21:43 +08:00
DXC
cf55c94826 feat: 库存接口增加 ai_mode=true 极简返回模式,键名压缩为 n/s/c 2026-05-19 09:53:06 +08:00
dxc
183b93012e 4.28 2026-04-28 16:07:11 +08:00
DXC
e08eaff40a feat(outbound): 库存列表按规格+库位聚合 + BOM明细类型修复 2026-04-28 09:23:59 +08:00
dxc
40e405becd 4.27 2026-04-27 16:33:54 +08:00
DXC
48f2011a38 fix: 盘点草稿已盘数量统计兼容字段名 quantity 2026-04-24 13:32:46 +08:00
DXC
996056d46a fix: 修复库存盘点已盘数量卡在500的问题 2026-04-24 13:19:57 +08:00
DXC
a849e14b2c refactor(orm): 将所有的批量 delete/update 重构为对象级操作,以确保触发 SQLAlchemy 审计事件 2026-04-20 15:43:48 +08:00
DXC
0d8f697df4 fix(stocktake): enforce session_id in resume, make missing generation idempotent, update UI to show SN, and fix excel time offset 2026-04-03 09:42:51 +08:00
DXC
43e1d0aa55 fix(stocktake): strictly isolate stocktake drafts by session_id in list and finish generation to prevent historical data mixing 2026-04-03 09:25:52 +08:00
DXC
6aec571775 fix(stocktake): resolve 500 error caused by missing batch or sn attributes using strict getattr fallback 2026-04-03 09:05:48 +08:00
DXC
c361d25ea0 fix(stocktake): display batch number in scanning dialog and fix empty uncounted items in Excel export 2026-04-02 19:06:19 +08:00
DXC
a52ced0375 fix(backend): resolve DetachedInstanceError in audit_log, add pessimistic locks for stock adjustments, and eliminate N+1 queries with eager loading 2026-04-02 18:44:12 +08:00
DXC
b2ce9d31f8 fix(stocktake): replace hardcoded 'admin' with actual current user in excel export 2026-03-27 09:04:13 +08:00
DXC
2a27f2e0df feat(stocktake): implement strict blind stocktake logic with hidden system qty, editable count and status filters 2026-03-26 17:42:51 +08:00
DXC
c8810891d8 fix(api): globally replace invalid material_base/material_name attributes with correct base relationship 2026-03-26 17:14:26 +08:00
DXC
93dc375ba4 perf: replace client-side pagination with server-side pagination in stock selection dialog and fix duplicate variable in semi.vue 2026-03-23 14:52:01 +08:00
DXC
c5974ff05c fix: relax permission requirement on stock list api to unblock outbound operations 2026-03-23 14:22:52 +08:00
DXC
7c9331d78a perf: implement optimistic UI for scanner, disable auto-camera, and sort excel by SKU 2026-03-23 09:51:59 +08:00
DXC
53c198f363 feat: fix stocktake deletion bug, and add pagination, search, sorting to stocktake lists 2026-03-19 16:21:09 +08:00
DXC
b37049a4d7 feat: add generate-missing logic to identify unscanned stock as inventory loss 2026-03-19 15:34:54 +08:00
DXC
7867fc5e40 fix: resolve TypeError between float and Decimal in stocktake excel export 2026-03-19 11:47:31 +08:00
DXC
a32d4f6b65 fix: resolve 500 error on excel export by aligning headers and data row lengths for remark field 2026-03-19 11:41:53 +08:00
DXC
a19167e804 fix: add missing remark header to stocktake excel export 2026-03-19 11:36:48 +08:00
DXC
0bc0908b9b fix: ensure remark field is properly parsed and saved to database during stocktake update 2026-03-19 11:27:40 +08:00
DXC
367dceef31 feat: add master summary sheet to stocktake excel export 2026-03-18 14:57:12 +08:00
DXC
f6153fc10d feat: add unscanned items sheet to stocktake export and fix user real name mapping 2026-03-18 14:48:49 +08:00
DXC
a5f4d32306 fix: map correct user real name for stocktake excel export 2026-03-18 14:43:24 +08:00