很多仓库第一次启用 Odoo 循环盘点,都会把它想成一句很简单的话:
- A 库位 7 天盘一次;
- B 库位 30 天盘一次;
- 系统到时间提醒一下,就结束了。
但源码里真正的设计并不是“单一频率字段驱动一切”,而是:
盘点计划先挂在 location 上,再落到 quant 上,并且还要和公司级年度盘点日期一起竞争。
这就是为什么同样写了库存频率,不同库位、不同公司、不同上次盘点时间,最后看到的 next_inventory_date 和 quant 的 inventory_date 可能完全不同。
一句话先讲透
Odoo 的循环盘点排程不是单点配置,而是三层合成:
stock.location.cyclic_inventory_frequency决定这个库位有没有周期性盘点节奏;last_inventory_date决定下一次日期从哪一天开始往后推;stock.location._get_next_inventory_date()还会把公司级年度盘点日一起纳入比较,最终把结果写到 quant 的inventory_date。
所以“什么时候该数这批货”,不是 quant 自己拍脑袋决定,而是 location 规则向下投影后的结果。
为什么 frequency 只写在 location,不直接写在 quant
因为 Odoo 在这里优先表达的是仓位管理策略,不是单件商品偏好。
仓库管理者真正想说的通常是:
- 高价值小件区每周盘;
- 大宗托盘区每月盘;
- 中转区也要定期盘,但节奏不同。
这类规则天然属于 location,而不是单个 quant。
因此 stock.location 上既有:
cyclic_inventory_frequencylast_inventory_datenext_inventory_date
又有 _get_next_inventory_date() 这种“把库位规则向量化到 quant”的方法。Odoo 先让仓位说话,再让库存记录继承节奏,这很符合现场管理。
next_inventory_date 为什么不是简单的“last + frequency”
_compute_next_inventory_date() 的逻辑里有一个很关键的细节:
- 如果还没盘过,就用今天加频率;
- 如果盘过,就用
last_inventory_date + frequency; - 但如果已经逾期,不会把下一次仍然留在过去,而是直接推到“明天”。
这层“逾期后推明天”的处理很重要。
它表达的不是数学精确,而是执行友好:
系统宁可把过期盘点重新排成一个可执行的近期任务,也不会继续保留一个永远落后的历史日期。
所以如果你看到某些库位的 next date 不是理论上的历史日期,不是算错,而是 Odoo 在主动把过期任务重新拉回执行窗口。
quant 的 inventory_date 为什么又是一层
很多人以为 location 已经有 next_inventory_date,quant 就不需要再存 inventory_date。
其实 quant 这层非常必要,因为最后真正参与盘点动作的是“某个产品在某个位置、lot、package、owner 维度下的一条库存记录”。
stock.quant 在 _compute_inventory_date() 里会读取 location 的 _get_next_inventory_date() 结果,把日期投到每一条 quant 上。这样后面做:
- 物理盘点列表筛选;
- 谁该先数;
- 哪些记录已经过期;
- 条码盘点端任务分发;
才有稳定抓手。
换句话说:
- location 负责定义策略;
- quant 负责承接待执行任务。
为什么公司级年度盘点日会“插手”循环盘点
_get_next_inventory_date() 里另一个很容易被忽略的点,是它会看 company 的:
annual_inventory_monthannual_inventory_day
如果公司配置了年度盘点日,location 自己算出来的 next_inventory_date 还要和公司盘点日取更早的那个。
这说明 Odoo 在设计上明确承认两类盘点制度并存:
- 平时的循环盘点;
- 财年/审计驱动的统一盘点。
而且统一盘点拥有很强的话语权。因为对财务与内控来说,全公司统一盘点窗口往往不能被局部仓位节奏覆盖。
last_count_date 解决的不是“谁最后改过数量”
很多团队看盘点历史时,只盯 quant 当前数量有没有变,却忽略了 last_count_date 的来源。
源码里它不是简单读 quant 自身更新时间,而是从 stock.move.line 里筛:
state = doneis_inventory = True
再按 product / lot / package / owner / location 这些维度去找最近一次盘点相关 move line。
也就是说,Odoo 认定“最近一次盘点时间”的依据,不是有人打开过页面,而是确实发生过盘点型库存调整。
这让循环盘点节奏能和真实执行闭环,而不是和表单编辑时间绑死。
实战里最容易踩的 4 个坑
1. 只配了 frequency,没理解 annual inventory date 会截胡
结果以为某库位应该 30 天后再盘,系统却更早把 quant 拉进年度盘点窗口。
2. 把 last_inventory_date 当成人工备注
它其实会直接影响下一轮排程,改错一天,整条节奏都会偏。
3. 以为 transit location 不参与循环盘点
源码明确允许 internal 和 transit 库位进入这套逻辑。中转位如果属于公司并配置了频率,也能被纳入计划。
4. 只看 quant,忽略 location 策略
quant 上看到的是结果,不是源头。排查“为什么今天该盘这批货”时,要先回到 location。
推荐的排查顺序
如果现场有人问:“这条库存为什么今天出现在盘点列表里?”
建议按这个顺序看:
- quant 的
location_id是哪个库位; - 该库位有没有
cyclic_inventory_frequency; last_inventory_date是哪天;- 公司有没有年度盘点月 / 日;
- quant 的
inventory_date与last_count_date是否吻合实际。
这个顺序比直接盯界面日期稳定得多。
最后的结论
Odoo 的循环盘点设计,核心不是“按天提醒”,而是把盘点节奏拆成:
- 仓位级制度;
- 公司级统一窗口;
- quant 级待执行日期;
- move line 级执行回写。
所以真正稳定的盘点体系,不是频率字段本身,而是你有没有把这四层边界看清。
DISCUSSION
评论区