物资管理 · 使用说明
适用版本:1.6.0(Phase 4 · 安全与交付) 这份文档同时是页面
/materials/system/help的内容来源,可下载 Markdown 存档。
一、这是什么,管什么
一个只管非食材物资的库存台账:办公用品、清洁用品、纸品、电池、耗材……
它和另外两个子系统的边界是刻意的:
| 系统 | 管什么 | 与本系统的关系 |
|---|---|---|
| 物资管理(本系统) | 非食材物资的入库、领用、盘点、成本 | —— |
| diet(饮食) | 食材、餐次、营养 | 不共表、不共库;食材不进这里 |
| finance(财务) | 真实支出流水 | 只对账,不自动记账;本系统不会往 finance 写一笔 |
一句话:这里记"东西怎么变少的",finance 记"钱怎么花出去的",两者每月对一次账。
二、五分钟上手
第一次用,按这个顺序走,不要跳步:
- 建分类 → 「物资档案」页 → 新建分类(如"办公耗材""生活日用品")。
- 建库位 → 同一页 → 新建库位(如"储物柜""阳台柜")。库位可以不只一层。
- 建物资 → 填名称 + 基本单位(个/张/包/瓶)+ 分类 + 最低库存。 基本单位一旦产生流水就锁定,改不了(改了历史全部作废)。
- 录入现有库存 → 「存量建账」页。 系统上线前就有的东西走这里,它不参与财务对账,见第五节。
- 以后每次买东西 → 「采购入库」建单、收货;每次拿东西 → 「快速消耗」。
底部导航一共 11 个入口,常用的就三个:快速消耗、采购入库、库存查询。
三、七个日常动作
3.1 新建物资档案
「物资档案」→ 新建。要点:
- 基本单位是最小计量单位(张、个、毫升),库存与成本都按它算;
- 包装规格 + 换算(1 包 = 500 张)是可选的,但强烈建议填 —— 否则你买"5 包"时只能自己换算成 2500 张再录;
- 最低库存只影响"低库存"提示,不影响任何计算;
- 建档页底部可以直接填「现有库存」,保存时顺带做一笔存量建账。
3.2 存量建账(上线前就有的东西)
「存量建账」→ 一屏 6 行,填「物资 / 数量 / 单位 / 总金额」即可。
- 金额可以留空 —— 表示成本未知,不是 0;
- 也可以按行导入 CSV(见第七节);
- 这些流水不进财务对账,因为它们本来就没花"本期的钱"。
3.3 采购入库
「采购入库」→ 新建:选物资、填数量、单位、单价,保存即生成采购单。
- 采购单本身不动库存,要再点「收货入账」才进库存;
- 单价是「每采购单位」的价:按"包"买就填每包的价,系统按换算系数折成每张的成本;
- 不填单价 = 成本未知(不要填 0,0 会让平均单价失真);
- 可以分多次收货(部分入库)。
3.4 快速消耗
「快速消耗」→ 选物资(可搜)、填数量、选场景,提交。
- 库存不够会直接拒绝,绝不会把库存扣成负数;
- 成本按当前移动加权平均自动计算;成本不完整时会标「成本不完整」,数量照常扣。
3.5 盘点
「盘点调整」→ 新建盘点单 → 录入实盘数 → 完成。
- 系统按"开始盘点那一刻"的库存做快照,盘点期间的新流水会被提示;
- 差异自动生成盘盈 / 盘亏流水,不直接改库存;
- 盘点单一旦完成就不能再改,错了只能再盘一次或用流水冲销。
3.6 查库存 / 查流水
- 「库存查询」:按物资看数量、价值、成本状态、分库位分布,可只看低库存;
- 「库存流水」:全部流水,可按物资 / 类型 / 日期筛。这是唯一的账本。
3.7 改错
所有改错都遵循同一条规则:原行不动,加一笔红冲。
| 想改什么 | 怎么做 | 结果 |
|---|---|---|
| 某笔流水数量/金额错了 | 流水页 → 编辑(或删除) → 填原因 | 原行保留,新增一条冲销 + 一条新流水 |
| 采购单单价写错了(已入库) | 采购单 → 修改这张采购单 | 红冲该行有效收货 + 按新价重开 |
| 整张采购单是误录 / 退货了 | 采购单 → 删除这张采购单 | 红冲全部有效收货,单据置「已取消」 |
| 盘点单错了 | 盘点详情 → 取消整单 | 红冲该单产生的全部调整流水 |
为什么不能直接改?
一旦允许原地改,"这笔数为什么和上个月对不上"就永远查不出来了。 冲销 + 重开让每一步都留下痕迹,且账本上任何一行都可以回溯到它为什么存在。
删除是软删除:单据留在库里(状态改为已取消),列表默认隐藏, 勾选「显示已删除」还能翻出来看原因。
四、口径(这部分决定数字对不对)
4.1 库存唯一真源
库里没有任何"当前库存"字段。当前库存永远等于:
库存 = SUM(quantity_delta) WHERE status = 'posted'
这样不存在"流水和库存对不上"的可能 —— 因为它们是同一个东西。 系统页的 C1–C6 自检就是拿来验证这一点的(见第八节)。
4.2 数量与金额都是整数
- 数量按 0.001 个基本单位存整数(毫单位),避免 0.1+0.2≠0.3;
- 金额按 分 存整数,四舍五入只在唯一一个函数里做。
4.3 成本:移动加权平均
每次入库把新成本并进平均价,出库按当前平均价计价。 所以恒等式永远成立:
Σ入库金额 = Σ出库金额 + 期末存货价值
4.4 成本"未知"不等于 0
- 没填单价 → 金额存 NULL,成本状态
unknown/incomplete; - 统计里这些数量照算,金额标「不完整」,不会假装是 0;
- 想让成本完整,就补一张有价的采购单,或用"成本重估"(本版未做,见第十一节)。
4.5 冲销怎么计数
冲销行会以原类型出现在统计里(冲一笔消耗就是 usage_out 的负数),
净额天然正确;页面显示"已冲销"由 reversal_of_id 推导,原行状态永远是 posted。
4.6 时间口径
occurred_at是业务时间,所有统计、对账按它分桶,存本地时间;created_at是录入时间,审计用,存 UTC;- 表单留空 = 现在(本地时间)。
五、对账:和 finance 怎么对
「对账」页只读,不改任何数据。核心是 A 式:
期末存货价值 + 本期消耗成本 + 报损
− 存量建账 − 退回入库 − 盘盈 + 盘亏
= finance 本期「物资」类支出
几个关键点:
- 存量建账要减掉:那是上线前就有的东西,本期没花这笔钱;
- 退回入库要减掉:钱已经在上期花了,本期东西又回来了;
- 盘盈要减掉、盘亏要加上:盘盈是"没花钱多出来的",盘亏是"钱花了东西没了";
- 页面同时给 A(累计)、B(本月)、C(账内平衡)、D(完整性)四种口径, 并逐项列出"折算明细" —— 每一分钱都能指到具体流水。
差额不为 0 时先看两件事:采购单价是不是填错了(比如把"每包价"填成了"每张价"), 以及有没有支出被记到别的分类。
六、统计与补货
「统计」页(只读):
- 粒度:日 / 周 / 月 / 年;区间:本月(默认)/ 近 30 / 近 90 / 今年 / 近 12 月 / 全部 / 自定义;
- 双色柱:采购支出(现金)vs 消耗成本(用掉的),差额必须能被存货变化解释;
- 分类消耗表 + 单品消耗榜 + 期末存货价值折线;
- 可支撑天数 = 现有库存 ÷ 日均消耗(日均取近 90 天,与所选区间解耦);
- 补货建议 = max(覆盖天数 × 日均, 最低库存) − 现有 − 在途,按整单位向上取整。
补货建议只读:它不会自动下单,也不会改账本。
七、导入与导出
7.1 导出(「系统」→ 导出)
| 文件 | 内容 |
|---|---|
| 库存.csv | 每个物资的数量、价值、成本状态、库位分布 |
| 库存流水.csv | 全量流水(上限 5 万行),含冲销关系 |
| 采购单.csv | 采购明细,整单金额重复在每行(便于透视) |
| 操作审计.csv | 最近 2000 条审计 |
| 全量导出.zip | 以上四份 + README |
编码是 UTF-8 带 BOM,Excel 双击即开;用脚本读请用 utf-8-sig。
导出是只读快照,不保证能直接回灌。
7.2 导入(「系统」→ 导入)
只支持两类,且都是只增不改:
| 类型 | 必填列 | 行为 |
|---|---|---|
| 物资档案 | 名称、基本单位 | 同名物资一律跳过,绝不覆盖 |
| 存量建账 | 物资(编号或名称)、数量 | 逐行独立成败,单位/金额/日期可选 |
流程是两段式:
- 预览:逐行校验,显示"可导入几行 / 跳过几行 / 哪几行错、错在哪";
- 确认:服务端重新校验一遍再落库,写库前自动做
pre-import备份。
为什么没有"流水导入":账本是逐笔生成的,批量灌 Excel 会绕过红冲、幂等、 库存非负三条防线。要批量补历史,走「存量建账」——它本来就不参与对账。
八、数据自检、备份、恢复
8.1 系统页(/materials/system/)
一屏看完:库文件大小、append-only 触发器是否齐全、C1–C6 自检结果、 最近备份、审计摘要、版本信息。
C1–C6 的含义:
| 项 | 检查 |
|---|---|
| C1 | 全局库存 = 各库位库存之和 |
| C2 | 没有负库存 |
| C3 | 没有非 posted 状态的流水混入 |
| C4 | 冲销都指向真实流水,且同一笔不会被冲两次 |
| C5 | 消耗记录与流水一一对应 |
| C6 | 成本金额与成本状态不矛盾 |
任何一项红了,先别导入、别恢复,先去流水页核对。
8.2 备份
- 页面「系统」→「立即备份」,或备份列表页操作;
- 文件形如
materials-20261020-173000-manual.db,旁边有一份同名.json记录 sha256、行数、结构版本; - 复制用 SQLite 官方 backup API,复制后立刻做完整性检查,坏了会当场删除并报错;
- 备份文件是 rollback journal 模式,单个文件即可独立打开。
8.3 恢复
「备份列表」→ 某一份 → 恢复,需要输入确认词 恢复。
执行顺序(任何一步失败,线上库一行没动):
- 校验备份(完整性 / 关键表 / 版本);
- 给当前库自动做一份
pre-restore备份; - 复制到临时文件 → 再校验一次 →
os.replace原子替换; - 需要时补跑迁移;
- 写审计
system.restore,并跑一次一致性自检。
⚠️ 恢复是覆盖,不是合并。备份之后产生的数据会消失。 唯一例外是恢复动作本身会留下
pre-restore备份。
8.4 保留策略
本版不做自动清理 —— 删备份是破坏性操作,交给人在文件管理器里做更安全。
建议:月底归档留 12 个月;pre-import / pre-restore 这类临时备份确认无用后可删。
九、权限
| 页面 | 游客 | 普通账户 | 拥有者 |
|---|---|---|---|
| 概览 / 档案 / 流水 / 库存 / 对账 / 统计 | 读 | 读 | 读写 |
使用说明(/system/help) |
读 | 读 | 读 |
| 系统页 / 备份恢复 / 导入 / 审计 | ✗ | ✗ | ✓ |
- 判定走整站统一账户(
paios.core.auth),本模块不建用户表、不做登录页; - 非拥有者访问
/system/*会看到 403 说明页,并给出登录入口; - 全新环境里还没有 owner 账户时,系统页临时放行,避免把自己锁在门外;
PAIOS_AUTH=0可整站关闭账户机制(自用离线场景)。
十、常见问题
Q:为什么改了单价,库存金额变化很大? A:多半是把"每包价"当成了"每张价"。先到档案页登记换算(1 包 = 500 张), 再改采购单;系统会按新单位重算整行金额。
Q:库存不够,但确实要领用,怎么办? A:先补入库(采购或盘盈),再领用。不要为了领用去改历史流水 —— 那会让成本链断掉。真需要调账,用盘点做盘亏,原因写清楚。
Q:删除采购单后还能找回吗?
A:单据还在(状态「已取消」),列表勾选「显示已删除」可见;
它红冲掉的入库流水也都还在账本上,带 reversal_of_id 指回原行。
Q:对账差额一直不为 0? A:按顺序查:① 采购单价是否填错;② 有没有支出记到了别的分类; ③ finance 那边有没有一笔"物资"支出漏记;④ 存量建账是否被重复计入。
Q:能不能把食材也放进来? A:不建议。食材保质期与餐次关联在 diet 里,放进本系统会两边都不准。
Q:能不能直接改数据库?
A:触发器会拒绝 UPDATE / DELETE(流水与审计表)。要修复数据,
走应用层的冲销 / 恢复;直接改文件只应作为最后手段,且必须先备份。
十一、已知限制与后续
| 项 | 现状 |
|---|---|
| 成本重估流水 | 未做。数量不变、只调金额的场景(如运费补记)暂无入口 |
| 采购单 ↔ finance 支出字段级关联 | 未做。目前靠金额 + 月份对账,不做逐单勾稽 |
| FIFO / 批次成本 | 不支持,固定移动加权平均 |
| 多用户并发编辑同一单据 | 无锁;后提交者覆盖(审计能查到) |
| 定时自动备份 | 未内置。建议用系统计划任务调用备份脚本 |
| 备份自动清理 | 刻意不做 |
十二、数据边界(给维护者看)
- 数据目录:
paios/materials/data/(database/库、backup/备份、export/导出); - 迁移:Alembic,当前
migration_count = 1,Phase 4 未新增迁移; - 表前缀统一
mm_; - 流水与审计表由 9 个触发器强制 append-only;
- 测试:
pytest paios/materials/tests,不触碰真实库(每个用例一个临时库)。