不少人第一次看到 project_stock,会本能地把它理解成一句很重的话:
项目模块终于和库存模块打通了,项目里可以自动跑领料和收发货流程了。
但真正看 /home/ubuntu/odoo-temp/addons/project_stock,你会发现官方这次做得很克制。
它做的不是“自动制造一条库存主链路”,而是先做三件更基础、也更稳的事:
- 给
stock.picking增加一个project_id; - 给项目页增加几个打开相关 picking 的动作入口;
- 用上下文把这些入口打开后的默认行为限制在“当前项目”语义里。
所以 project_stock 的核心价值不是自动化,而是:
把项目和库存之间原本很散的关系,变成可追溯、可导航、可过滤的显式关联。
一、最底层只有一个字段:stock.picking.project_id
在 models/stock_picking.py 里,这个模块对 stock.picking 的扩展非常轻:
- 增加一个
Many2one('project.project')字段project_id; - domain 限制
is_template = False。
这说明它不是去重写库存业务规则,而是先把“这张调拨单属于哪个项目”这件事建成正式关系。
这个动作看起来简单,但意义很大。
因为只要有了显式关系,后面你才能自然地做这些事:
- 在项目里看相关发货和收货;
- 在 picking 上回看属于哪个项目;
- 基于项目维度过滤库存动作;
- 做后续追溯、报表或二次开发。
很多联动模块最容易犯的错,是一上来就想“自动生成一切”。
project_stock 反而先把关系建清楚,这个顺序是对的。
二、项目页之所以能看到库存动作,靠的是嵌入动作,不是写死按钮
views/project_project_views.xml 里注册了多条 ir.embedded.actions:
From WHTo WHStock Moves
而且它们同时挂在:
- 项目任务入口动作;
- 项目更新/dashboard 动作。
这背后说明两件事:
1)它不是改一个视图按钮那么简单
官方用的是项目模块自己的 embedded actions 机制,把库存入口作为“可嵌入项目页面的动作卡片”来接入。
2)它天然继承项目页的可配置性
在 project/models/project_project.py 里,项目本身就有一套复制、读取、配置 embedded actions 的逻辑。
所以 project_stock 不是另开山头,而是顺着项目模块现成的交互骨架往里插库存入口。
这也是为什么这个模块代码量不大,但接入方式很“原生”。
三、真正的核心在 _get_picking_action():它决定你看到的是哪批单,以及新建时默认长什么样
models/project_project.py 里三个公开动作:
action_open_deliveries()action_open_receipts()action_open_all_pickings()
最后都汇总到 _get_picking_action()。
这个方法做了几件特别关键的事。
1)domain 锁定当前项目
它先构造:
Domain('project_id', '=', self.id)
这保证打开的列表天然只看当前项目相关的 picking。
2)根据入口再补 picking type 语义
如果是 deliveries / receipts,它会继续加:
picking_type_id.code = outgoing- 或
picking_type_id.code = incoming
这样“From WH”和“To WH”不是文字标签,而是真的把结果集分成发货与收货两类。
3)context 里塞默认值和限制值
它会给 action context 塞:
default_project_idrestricted_picking_type_code- outgoing 时还会加
default_partner_id
这三个键特别重要,因为它们会直接影响:
- 新建 picking 时默认项目是谁;
- 新建 picking 时默认 picking type 怎么选;
- 发货场景下 partner 默认带哪个客户。
也就是说,这个模块不只是“帮你打开一个过滤后的列表”,它还在悄悄把后续新建单据的默认语义一起带过去。
四、restricted_picking_type_code 不是随便塞的,它会被 stock 核心逻辑真正消费
如果只看 project_stock 自己,很容易低估这个 context 键。
但在 stock/models/stock_picking.py 里,核心模型明确会读它:
_default_picking_type_id()会根据restricted_picking_type_code选默认 picking type;get_empty_list_help()和部分 action/help 渲染也会用这个值来显示对应的说明文案。
这说明 project_stock 并不是自己发明了一套无用上下文,而是在复用 stock 核心已经支持的语义开关。
这就是官方模块常见的好味道:
不重复造轮子,只把项目语义准确地接进库存原有机制。
五、为什么 outgoing 还要额外塞 default_partner_id
在 _get_picking_action() 里,只有 outgoing 会补 default_partner_id = self.partner_id.id。
这非常符合业务语义。
因为项目的对外发货,往往天然更接近“给这个项目关联客户送东西”;而 incoming 收货不一定对应项目客户,所以不强行带 partner。
这类小差异看起来不起眼,其实反映了一个很成熟的实现习惯:
- 同样是项目相关调拨;
- 发货和收货的默认业务上下文并不对称;
- 所以默认值也不该机械复制。
六、视图层只做了最小曝光:在 picking 表单的 Other Info 里展示 project_id
views/stock_picking_views.xml 的继承也很克制:
- 不是重做 picking 视图;
- 只是把
project_id插进other_infos分组; - 还加了
project.group_project_user权限控制。
这意味着官方并不想把库存单据界面彻底“项目化”,而是:
- 让懂项目的人能看到这个关联;
- 让库存单据仍然以库存本身为中心。
这正是边界感比较好的做法。
七、它解决的是追溯和入口问题,不是自动生成库存业务
这是最重要的理解点。
从当前源码看,project_stock 没有 去做这些事情:
- 不自动根据任务生成 picking;
- 不自动根据项目预算生成领料;
- 不改库存估值逻辑;
- 不改 move / move line 的预留、过账、完成链路;
- 不把任务和调拨做强绑定状态机。
它解决的是更基础的问题:
- 让项目可以“看到”库存动作;
- 让库存动作可以“挂回”项目;
- 让新建入口默认保持项目语义。
所以如果你期待它一装上就自动跑项目物料履约,那大概率会失望;但如果你想要的是 项目维度的库存可见性与可追溯性,它就很对路。
八、实战里最容易误解的 4 件事
1)项目页能打开 picking,所以 picking 一定是项目自动生成的
不对。当前实现重点是关联与导航,不是自动生成。
2)有 project_id 就说明库存主流程改了
不对。库存核心过账、预留、完成逻辑仍然主要在 stock 模块自身。
3)From WH / To WH 只是界面名字不同
不对。它们带了不同 domain 和 context,后续新建默认值也会不同。
4)这是一个“大而全”的项目物料模块
不对。它是一个边界清晰、非常克制的“项目 ↔ picking 关系模块”。
总结
project_stock 的设计很值得学,因为它没有一上来就把项目和库存强行捏成一个大流程。
它先做的是更稳的三步:
- 在
stock.picking上建立project_id显式关系; - 用
ir.embedded.actions把收货、发货、全部调拨接进项目页; - 用
default_project_id、restricted_picking_type_code、default_partner_id把打开后的行为约束在项目语义里。
如果只记一句,可以记这句:
project_stock不是“项目自动库存流程”模块,而是“项目与调拨之间的追溯、导航和默认上下文”模块。
DISCUSSION
评论区