很多人第一次排查 Odoo 发票分录时,都会先看伙伴主数据:
- 客户就看
property_account_receivable_id - 供应商就看
property_account_payable_id
然后一旦分录上的应收/应付科目和自己预期不同,就会觉得系统“乱选科目”。
但如果你去看 /home/ubuntu/odoo-temp/addons/account/models/account_move_line.py 里 account.move.line._compute_account_id() 的实现,会发现 Odoo 对 payment term 行 的科目选择远比“取伙伴属性”复杂。
它真正的优先链大致是:
- 同一张 move 里已有的 payment term 行沿用原科目;
- 否则取伙伴的应收/应付属性科目;
- 再不行就退到公司自己的伙伴科目;
- 还不行再退到公司可用的 receivable/payable 兜底科目;
- 最后如果 move 上挂了
fiscal_position_id,还会再做一次map_account()重映射。
也就是说,发票上的应收应付科目不是一个“静态字段展开”,而是一条带继承、带兜底、还能被 fiscal position 改写的生成链。
一、为什么 payment term 行不是“随便找个应收科目”
Odoo 先区分:
display_type == 'payment_term'的行- 普通产品行
- tax 行 / rounding 行等其他动态行
payment term 行本质上不是商品收入或费用,它代表的是:
这张单最终落到谁的应收/应付,以及这些余额该在哪个往来科目上承载。
所以它的科目生成,不走产品收入/费用逻辑,而走往来账户逻辑。
二、第一优先级:沿用同 move 已有的 payment term 科目
源码里先查的是同一个 account.move 里,是否已经存在别的 payment term 行。
这一步很关键。
它表达的是:
如果这张凭证已经出现过 payment term 行,那新增/重算出来的 payment term 行,优先保持和现有行一致。
这能避免两个很常见的问题:
- 同一张发票拆成多期后,多个 term line 落到不同往来科目;
- 重算动态行时,因为伙伴属性或上下文变化,导致同一张单内部分裂。
所以 Odoo 先求“单据内部一致性”,再求“主数据默认值”。
三、第二优先级:伙伴属性科目
如果 move 里没有已有的 term line 可参考,才回到大家熟悉的路径:
- 销售单据用
property_account_receivable_id - 采购单据用
property_account_payable_id
这一步依然不是简单取 partner_id,而是取:
commercial_partner_id- 并且在
with_company(move.company_id)的上下文里读取
这两个细节很重要:
1)为什么要用 commercial partner
因为 Odoo 记账时希望同一商业主体的往来余额尽量归到商业伙伴层,而不是某个联系人地址层。
2)为什么要切公司上下文
因为这些属性字段是 company-dependent 的。多公司环境下,不切对公司,拿到的可能根本不是当前账套应使用的科目。
四、第三和第四优先级:公司伙伴 + 公司兜底科目
如果伙伴身上根本没配应收/应付科目,Odoo 不会立刻报错,而会继续往后找:
- 先找
company.partner_id上的应收/应付属性; - 再找该公司下活跃的 receivable / payable 兜底科目。
这说明 Odoo 的设计目标不是“主数据不全就彻底停摆”,而是:
尽量把分录生成出来,但仍保证它落在语义正确的往来账户类型里。
所以你有时会看到:
- 某个伙伴并没配往来科目;
- 发票却仍然能出分录;
- 最后落的是公司层默认或兜底科目。
这不是随机行为,而是源码里明确写死的 fallback 链。
五、最容易被忽略的一步:Fiscal Position 还能再改一次科目
很多人知道 fiscal position 会改税,但不知道它也能改科目。
在 payment term 行选出基础 account 后,源码还有一刀:
move.fiscal_position_id.map_account(...)
也就是说,最终应收/应付科目并不一定等于伙伴属性科目本身。
如果 fiscal position 里配置了 account mapping,那么 Odoo 会把“原本准备使用的往来科目”再映射成另一个科目。
这就解释了很多排查现场里的疑问:
- 伙伴属性明明是 A;
- 为什么分录最后挂到 B?
答案往往不是伙伴错了,而是:
- 伙伴给出了基础科目;
- fiscal position 又在最后一步把它改成了目标科目。
六、为什么这套链路很适合发票动态重算
发票在 Odoo 里不是“一次算完不再动”,而是会随着:
- partner 改变
- move type 改变
- fiscal position 改变
- payment term 改变
不断重算动态行。
如果科目选择没有这套层级,就很容易出现:
- 同一张单前后重算结果不一致;
- 多期付款行各自挂不同往来科目;
- 多公司环境误用别家公司科目;
- fiscal position 只能改商品收入费用,不能改往来逻辑。
Odoo 现在这套写法,本质上是在保证:
payment term 行无论怎么重算,都尽量先保持单据内部一致,再遵守公司与 fiscal position 规则。
七、排错时应该按什么顺序看
如果你发现发票上的 receivable/payable 科目“不对”,别只盯伙伴卡片,建议按这个顺序看:
- 这条行是不是
display_type = payment_term; - 同 move 里是否已有别的 payment term 行先占了科目;
- 当前公司下商业伙伴的应收/应付属性是什么;
- 公司自己的伙伴科目和兜底科目是什么;
- move 上的
fiscal_position_id是否有 account mapping。
这个顺序比“打开伙伴表单看一眼”靠谱得多。
一句话记忆
Odoo 的 payment term 行科目,不是“直接拿伙伴属性”,而是“先保单据内一致,再走伙伴/公司 fallback,最后再让 fiscal position 改写一次”。
DISCUSSION
评论区