结论先行
在 Odoo 里,Fiscal Year Lock 并不是一个“这个日期前统统不准动”的粗暴开关。
更准确地说,它是一套分层保护体系:
fiscalyear_lock_date:通用会计期间保护tax_lock_date:税务申报保护sale_lock_date:销售类锁定purchase_lock_date:采购类锁定hard_lock_date:更强、不可回退的硬锁定account.lock_exception:带审计痕迹的临时例外机制
所以官方设计的重点不是“关不关”,而是:
谁在什么边界内还能做什么动作。
为什么 Odoo 不做成一个总锁
真实业务里,“不能改”其实有很多原因:
- 财务期间已结账
- 税报已经申报
- 销售或采购模块希望先局部锁定
- 某些历史期间甚至要求永久不可回退
如果只放一个 lock date,系统就会有两个问题:
- 太粗:不同责任边界被一刀切
- 不够准:用户不知道自己到底踩了哪条线
/home/ubuntu/odoo-temp/addons/account/models/company.py 里 _get_lock_date_violations() 和 _get_user_fiscal_lock_date() 就是在做这件事:
- 按不同 field 分别判断是否违规
- 再结合 journal 类型决定 sale / purchase lock 要不要参与
- 最后把 hard lock 一起纳入
这说明“锁定”在 Odoo 里是组合判断,不是单字段判断。
Hard Lock 为什么比普通 Lock 更重
源码里对 hard_lock_date 的态度很强硬:
- 不能删除
- 新的 hard lock 不能比旧的更早
- 如果锁定期间内还有 draft entries,会直接阻止
这意味着 hard lock 的语义不是“方便管理”,而是更接近:
一旦走到这一步,系统默认你是在建立不可逆的法定边界。
所以 hard lock 不是给“试试看”的。
Lock Exception 真正解决了什么问题
很多系统一旦锁账,就只能“放开锁再改”。
Odoo 没这么做。
在 /home/ubuntu/odoo-temp/addons/account/models/account_lock_exception.py 里,account.lock_exception 被设计成一个独立模型,它会记录:
- 针对哪个公司
- 给谁开的例外(甚至可对 everyone)
- 改的是哪种 lock date
- 原始公司 lock date 是多少
- 例外有效到什么时候
- 理由是什么
更重要的是,它会把变更记到 chatter,并且状态分成:
activerevokedexpired
这说明官方不是鼓励“偷偷开锁”,而是在提供:
可控、可追踪、可过期的关账例外。
这套设计很成熟。
为什么“用户看到的 lock date”不一定等于公司字段值
company.py 里的 _get_user_lock_date() 很关键。
它不是直接返回公司上的 lock date,而是:
- 先看公司及父公司
- 再看当前用户是否命中了 exception
- 最后算出“这个用户实际有效的 lock date”
所以两个用户面对同一家公司、同一期间,可能看到的是不同可操作边界。
这也是很多人排查 lock 问题时会忽略的一层:
- 配置没错
- 日期也没错
- 但当前用户正好被 exception 放行或没被放行
新手最容易误解的 4 件事
1. 以为 fiscal lock date 就是唯一锁定
不是。tax、sale、purchase、hard lock 都可能一起参与。
2. 以为 lock exception 就是偷偷绕过控制
不是。它是带审计信息的正式例外机制。
3. 以为 hard lock 只是更晚一个日期
不对。它在规则上更硬,也更不可逆。
4. 以为查公司字段就等于查当前用户实际边界
不对。用户级 exception 会改变“有效锁定日期”。
实战排查顺序
如果用户说“这个日期为什么不能改 / 别人能改我不能改”,建议这样查:
- 公司上的各类 lock date 分别是多少
- 当前 journal 属于 sale / purchase / general 哪一类
- 是否存在 hard lock
- 当前用户是否命中了
account.lock_exception - 例外是否已经 revoked 或 expired
一句话记忆法
Odoo 的关账不是一个锁,而是一组分层边界;lock exception 也不是偷开后门,而是带审计痕迹的临时通行证。
DISCUSSION
评论区