服务订阅按工时收费时,最容易出现的争议不是单价,而是“这几小时到底算哪一期”。如果系统把所有 timesheet 一把抓进 delivered qty,本月开票、上月补录、退款重算很快就会彻底打架。
这篇文章主要参考了以下企业版源码入口:
enterprise/sale_subscription_timesheet/models/sale_order_line.pyenterprise/sale_subscription_timesheet/models/account_move.py
一、这篇功能真正解决什么问题
sale_subscription_timesheet 要解决的是工时订阅的账期边界。它不是让 timesheet “能开票”就结束,而是要保证 delivered qty 只反映当前可计费窗口内、尚未被正确消费的工时记录。
二、核心链路怎么走
1. 先挑出真正属于工时订阅的销售行
_get_timesheet_subscription_lines() 只取同时满足 recurring_invoice 与 qty_delivered_method == 'timesheet' 的行。这样一次性服务、手工交付服务和普通订阅行不会混进来。
2. delivered qty 的 domain 被卡在账期窗口内
_prepare_qty_delivered() 会用 last_invoice_date 与 next_invoice_date 组出 domain,再叠加“未开票 / 已取消发票 / 与退款相关的已开票工时”条件。换句话说,系统不是“按工时总量开票”,而是“按当前账期可计费工时开票”。
3. 发票日期范围也跟着订阅逻辑改写
account.move._get_range_dates(order) 在遇到含 timesheet 交付的订阅单时,会把范围改成 last_invoice_date 到 next_invoice_date - 1 day。这让发票和 delivered qty 站在同一张时间尺子上。
三、新手最容易踩的坑
- 以为 timesheet 一录入,下一张订阅发票就一定全部带上。
- 以为退款只是财务动作,不影响工时是否还能被再次计入。
- 以为 delivered qty 是简单累计字段。实际上它背后是严格 domain 运算。
四、实战落地时最该盯的点
- 月底补录工时时,要先确认它想落在哪个账期,否则业务预期和系统结果会天然不同。
- 处理退款时不要只看金额,要同步检查相关 timesheet 是否重新回到可计费集合。
- 如果 delivered qty 看起来不对,先查
last_invoice_date / next_invoice_date,它们往往比工时内容本身更关键。
五、结论
工时订阅之所以难,不是因为 timesheet 复杂,而是因为它必须同时满足账期边界、开票消费和退款回滚三层逻辑。sale_subscription_timesheet 的价值,就是把这条时间边界守得很死。
DISCUSSION
评论区