先说结论
在 Odoo 里,落地成本并不天然等于“库存价值增加了一点”。
如果你看的只是 /home/ubuntu/odoo-temp/addons/stock_landed_costs/models/stock_landed_cost.py 主流程,那你会看到:
stock.landed.cost选中若干picking_ids- 计算
valuation_adjustment_lines - 校验分摊结果
- 生成会计分录
- 对相关 move 执行
_set_value()
但项目相关模块 project_stock_landed_costs 又往前加了一刀:
- 当 landed cost 的
target_model == 'picking' - 并且当前 adjustment line 对应到某个
move_id.picking_id.project_id _prepare_account_move_line_values()会把analytic_distribution直接设为项目的_get_analytic_distribution()结果
这意味着:落地成本不只进库存,也可能继续落到项目分析维度。
1. 主干链路:标准 landed cost 本来只关心“把附加成本分到 move 上”
stock.landed.cost 标准模块的主流程很典型:
target_model决定附加成本应用在哪类对象上,目前核心选择是picking_get_targeted_move_ids()取到这些拣货下的move_idscompute_landed_cost()按数量、成本、重量、体积等方法把成本拆给每条stock.valuation.adjustment.linesbutton_validate()为这些 adjustment line 创建会计分录- 最后把库存价值更新回 move / valuation 层
如果系统只停在这里,那 landed cost 只是库存估值和总账话题。
但项目场景里,问题并没有结束:
这笔额外运输费、关税、杂费,到底该不该算进某个项目?
Odoo 给出的答案是:如果这批拣货明确挂着项目,就不该丢失项目维度。
2. 项目扩展点:真正关键的是 stock.valuation.adjustment.lines
/home/ubuntu/odoo-temp/addons/project_stock_landed_costs/models/stock_landed_costs.py 的扩展非常短,但非常有力:
if self.cost_id.target_model == 'picking':
res['analytic_distribution'] = self.move_id.picking_id.project_id._get_analytic_distribution()
注意它不是去改 landed cost 主单,也不是去改 cost line,而是直接卡在 valuation adjustment line 准备会计分录值 的时刻。
这意味着设计意图非常明确:
- 落地成本先按库存逻辑完成分摊
- 到要落账时,再判断这条成本行是否属于某个项目 picking
- 如果属于,就把项目分析维度一并塞进分录
这样做的好处很明显:
- 不破坏标准 landed cost 的拆分算法
- 不需要重新发明成本分配模型
- 只在“真正生成会计分录”这一刻补进项目分析维度
这是一个很典型的 Odoo 扩展方式:主链路不重写,落账节点精准插桩。
3. 为什么这里要依赖 picking.project_id
别忽略另一个前提:project_stock 模块先在 stock.picking 上补了 project_id 字段。
也就是说,项目落地成本能成立,不是因为 landed cost 自己知道项目,而是因为:
- 拣货单先知道自己属于哪个项目
- landed cost 再沿着
valuation line -> move -> picking -> project这条链把项目找回来
这条路径的好处在于,它符合物流现实:
- 运费、关税、清关费,本来就是围绕某批收发货发生
- picking 是最自然的业务承载对象
- 项目只是这批物流背后的经营维度
所以 Odoo 没有把 landed cost 直接硬绑到 project 上,而是让项目成为 picking 的上游业务上下文。
4. 为什么这不是“库存 + 项目”简单相加
很多实施会把这事理解成:
- 库存模块记成本
- 项目模块看利润
- 两边顺手同步一下
但源码设计明显更严谨。
真正发生的是三层接力:
第一层:库存对象识别成本归属
stock.landed.cost 先确定成本应用在哪些 move 上。
第二层:项目维度通过 picking 传入
project_id 不直接定义在 landed cost 上,而是挂在 transfer 上。
第三层:分析分摊在分录准备阶段注入
只有到了 _prepare_account_move_line_values(),才真正把 analytic_distribution 注入到会计分录值里。
这三层拆开以后,你就能理解为什么它既不粗暴,也不容易把标准库存流程打坏。
5. 实施上最容易忽略的两个点
点一:没有项目 picking,就谈不上项目落地成本
如果 move 对应的 picking 根本没带 project_id,那扩展也无从回写项目分析维度。
点二:落地成本是否“进项目利润”取决于后续分析链路
project_stock_landed_costs 做的是把分析分摊写进分录值,不是直接在页面上神奇出现“项目运输成本”。
后续利润面板、分析报表、会计口径是否展示,还取决于更上层的 analytic 汇总逻辑。
也就是说,它解决的是 成本不丢项目维度,不是一口气包办所有展示问题。
6. 一句落地建议
如果你在做项目型采购、项目型物流或者交付制业务,最该检查的不是“landed cost 会不会过账”,而是:
- picking 有没有挂项目
- 项目的 analytic distribution 是否正确
- landed cost 分录是不是沿着 adjustment line 回写了分析维度
只有这三件事串起来,落地成本才真的进入项目经营核算,而不是只停留在库存账上。
参考源码
/home/ubuntu/odoo-temp/addons/stock_landed_costs/models/stock_landed_cost.pycompute_landed_cost()button_validate()_get_targeted_move_ids()/home/ubuntu/odoo-temp/addons/project_stock/models/stock_picking.pyproject_id/home/ubuntu/odoo-temp/addons/project_stock_landed_costs/models/stock_landed_costs.py_prepare_account_move_line_values()
DISCUSSION
评论区