借项通知单

Odoo 借项通知单为什么不是简单复制发票:account_debit_note 的关联、草稿生成与编号边界讲透

结合 account_debit_note 源码,讲清 Odoo 借项通知单为什么不是普通新发票:哪些单据能发起、为什么默认草稿、何时复制行、如何保留原始单据关联,以及独立借项编号怎样生效。

会计
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 7 阅读

很多人第一次看到 Odoo 的借项通知单,会把它理解成“再开一张多收款发票”。

/home/ubuntu/odoo-temp/addons/account_debit_note 的实现说明,系统想表达的不是“复制一张新的发票”,而是:

基于原单据创建一张带追溯关系的正向调整单。

这也是为什么借项通知单和“随手新建一张发票”在会计语义上差很多。

一、入口第一关:不是所有 move 都能做借项通知单

default_get() 先对当前选中的 account.move 做了几层硬校验:

  • 必须已经 posted
  • 不能已经有 debit_origin_id
  • move_type 只能是:
  • out_invoice
  • in_invoice
  • out_refund
  • in_refund

这个判断已经说明两件事:

1)借项通知单不是草稿修正工具

原单必须先过账,说明它针对的是已经进入正式会计语义的单据

2)它不是无限嵌套链

如果一张发票已经是某次借项链的下游,再继续对它起借项通知单会让追溯关系越来越乱,所以源码直接禁止。

二、借项通知单最关键的不是金额,而是“保留原单关联”

_prepare_default_values() 里,核心字段之一是:

  • debit_origin_id = move.id

account.move 上也显式加了:

  • debit_origin_id
  • debit_note_ids
  • debit_note_count

这代表 Odoo 对借项通知单的核心建模不是“另一张 invoice”,而是:

一张能明确回指原始单据的 adjustment document。

这样做的价值很现实:

  • 表单上能追原单;
  • 原单上能看所有借项通知单;
  • 后续审计、客服和财务沟通都能沿链追溯。

三、为什么从 refund 发起借项通知单时,单据类型会翻回 invoice

_prepare_default_values() 有个很容易忽略的逻辑:

  • in_refund -> in_invoice
  • out_refund -> out_invoice
  • 其他情况保持原类型

这段逻辑其实非常有业务含义。

因为借项通知单的语义是“正向补回金额”。

如果你对一张 refund 再做 debit note,本质上不是“再来一张 refund”,而是把方向翻正,变回普通 invoice / bill 语义。

所以这里处理的不是技术映射,而是会计方向回正

四、为什么默认是草稿,而且很多时候不复制行

草稿

测试里明确验证:新建出来的 debit note 是 draft

这是合理的,因为系统只是根据原单做了一次结构化衍生,并不意味着这张调整单已经完成会计确认。

仍然要给财务一个最后检查和改写的机会。

不总是复制行

copy_lines 是个可选布尔值。

而且源码还额外规定:

  • 如果不勾选 copy_lines,就清空 line_ids
  • 即使勾选了,但原单是 in_refund/out_refund,也不会复制行

这反映了一个很重要的产品判断:

借项通知单不一定是“照原单逐行加价”,很多时候只是要保留调整关系,再让财务重新组织行项目。

所以“复制行”只是辅助,不是借项通知单的定义本身。

五、为什么 include_business_fields=True 很值得注意

create_debit() 在遍历原单时用了:

  • with_context(include_business_fields=True)

旁边注释也写得很直白:

  • copy sale/purchase links

这说明借项通知单在 Odoo 看来不只是会计凭证复制,还希望把:

  • 销售链路
  • 采购链路
  • 相关业务引用

尽量一起带过去。

这在落地里很重要,因为财务调整单如果跟业务单完全脱链,后续:

  • 对账
  • 查询来源
  • 客诉追踪
  • 销售/采购协同

都会变得很难看。

六、为什么还专门改了编号域和起始前缀

account_move.py 里还有两段很容易被忽视,但非常“财务系统”的实现。

_get_last_sequence_domain()

如果 journal 开启了 debit_sequence,系统会把:

  • debit_origin_id IS NOT NULL
  • IS NULL

纳入序列取号域。

这意味着普通发票和借项通知单可以走并行但分隔的编号序列

_get_starting_sequence()

在满足这些条件时:

  • journal 启用 debit_sequence
  • 当前单据有 debit_origin_id
  • 类型是 in_invoice/out_invoice

起始序列会在前面加一个 D

这相当于告诉财务和审计:

这不是普通 invoice 编号,它属于借项通知单序列。

这不是 UI 装饰,而是正式单据治理。

七、为什么消息内容也要专门改

_get_copy_message_content() 对 debit note 做了专门分支:

  • 会在消息里写“这张借项通知单由哪张单据创建”。

这件事看起来小,但很符合 Odoo 的产品哲学:

  • 业务链关系不只存在字段里;
  • 也应该在 chatter 这种“人能直接看懂”的界面里体现。

八、测试用例把产品预期说得很清楚

test_out_debit_note.py 至少说明两件事:

1)普通客户发票 + copy_lines=True

  • 会复制发票行;
  • 类型保持 out_invoice
  • 新单据是草稿。

2)供应商退款发起借项通知单

  • 默认不复制行;
  • 类型回正成 in_invoice
  • 新单据仍是草稿。

这两条就把“借项通知单不是普通复制发票”讲得很透了。

九、实战里最容易踩的坑

1)把借项通知单当成“重新开票”按钮

它本质是带追溯关系的调整单,不是随手重开一张发票。

2)以为勾了复制行就什么都照搬

退款类原单就是明确不照搬。

3)忽略独立借项序列

如果公司审计要求借项单据单独编号,应该关注 journal 的 debit_sequence,而不是只看表单显示。

4)只看金额,不看原单关系

借项通知单最值钱的是“为什么它存在、它补的是哪一张”。

总结

account_debit_note 的核心不是“创建一张金额更高的发票”。

它真正做的是:

  • 严格限制哪些单据能发起借项通知单;
  • 把新单据和原单据通过 debit_origin_id 牢牢连起来;
  • 在 refund 场景下把单据方向翻回 invoice / bill;
  • 保持草稿审查边界;
  • 在需要时使用独立借项编号序列。

如果只记一句,可以记这句:

Odoo 借项通知单不是“复制原发票再加钱”,而是“围绕原单据做一次可追溯、可审计的正向调整”。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。