先说结论
/home/ubuntu/odoo-temp/enterprise/project_account_asset/ 这个模块非常小,小到很多人会低估它。
表面上它只做了三件事:
- 在
project.project上增加assets_count - 在项目右侧 stat button 里追加一个 Assets 按钮
- 点击后打开资产列表,并默认把新资产的
analytic_distribution预填到当前项目分析账户
但它真正表达的设计思路其实很明确:
Odoo 企业版并没有把“项目资产”建成一套独立关系模型,而是把项目当成“分析账户入口”,再通过
analytic_distribution去观察与承接资产及其折旧。
也就是说,项目页上的 Assets 按钮不是在说“这个项目拥有一批资产主数据”,而是在说:
- 这些资产的分析分配里包含当前项目分析账户
- 所以后续折旧、处置、账务追踪,也可能继续沿着这条分析口径落回项目
这和“项目里建个固定资产子表”是两种完全不同的产品思路。
第一层:这个模块为什么没有 Many2many,而只靠 account_id
project_account_asset/models/project_project.py 里最重要的前提,是它根本没有新增什么:
asset_idsasset_line_idsproject_idonaccount.asset
它只依赖项目已有的 account_id,也就是项目分析账户。
接着 assets_count 的计算逻辑直接做:
self.env['account.asset']._read_group(
[('analytic_distribution', 'in', self.account_id.ids)],
['analytic_distribution'],
['__count'],
)
这里非常值得停一下。
官方不是按“资产属于哪个项目”来查,而是按:
- 资产的
analytic_distribution - 是否包含当前项目的分析账户 ID
换句话说,项目和资产的连接键不是业务主键,不是外键,而是分析分配口径。
这意味着两件事。
1. 项目页看到的是“会计归属上的相关资产”
只要某个资产的分析分配中包含该项目分析账户,它就会被统计进来。
所以这个按钮表达的不是狭义“项目专属资产”,而是:
与该项目分析核算相关的资产。
2. 一项资产理论上可以同时服务多个分析维度
因为 analytic_distribution 本身允许分摊结构,所以从建模思路上说,资产并不是非得 100% 属于一个项目。
这比硬绑一个 project_id 灵活得多,也更符合财务真实场景:
- 一台设备可能同时服务多个项目
- 一笔资本化投入可能要按分析分配拆给不同项目 / 成本中心
- 项目只是其中一个分析口径,不一定是唯一口径
所以 project_account_asset 的重点不是“项目资产卡片”,而是“项目可见的资产分析视图”。
第二层:为什么它用 _read_group,而不是一条条 search_count
_compute_assets_count() 没有在每个项目上单独 search_count(),而是一次性 _read_group() 后再映射回各个项目。
这背后反映出官方对这个字段的定位:
- 它是项目页的辅助统计
- 可能在列表、看板、右侧面板里被频繁读取
- 所以必须尽量避免 N+1 查询
源码随后把结果做成:
data = {int(account_id): count for account_id, count in data}
再按项目的 account_id.id 回填 assets_count。
这说明按钮背后的统计粒度其实很单纯:
- 一个项目,对应一个分析账户
- 一个分析账户,对应若干包含它的资产
- 页面只关心这个计数,不追求在项目侧维护更复杂的桥表状态
这种实现很“轻”,但也正因为轻,它不容易把项目模块和资产模块耦得太死。
第三层:为什么点击 Assets 按钮后,重点不是过滤,而是默认值
action_open_project_assets() 做的事情比“打开一个 domain 过滤后的列表”稍微多一步。
它先找出相关资产:
assets = self.env['account.asset'].search(
[('analytic_distribution', 'in', self.account_id.ids)],
)
然后读取资产模块自己的标准动作:
action = self.env["ir.actions.actions"]._for_xml_id("account_asset.action_account_asset_form")
接着更新成:
- list / form / kanban 视图
domain = [('id', 'in', assets.ids)]context = {'default_analytic_distribution': {self.account_id.id: 100}}
很多人会把注意力只放在 domain 上,但真正有产品味道的点在 default_analytic_distribution。
这代表用户从项目进入资产页时,Odoo 默认假设:
如果你是从这个项目上下文新建资产,那最合理的默认分析分配,就是 100% 给当前项目分析账户。
这并不等于系统强制资产只能属于这个项目;它只是把“从项目入口创建资产”的默认口径先设好。
所以这个动作同时在做两件事:
- 回看历史:看哪些资产已经和当前项目分析有关
- 引导未来:新建资产时默认继续沿着当前项目分析账户走
这就是一个典型的“桥接动作”,而不是单纯导航按钮。
第四层:为什么它只给会计读权限用户显示
字段定义和 stat button 都加了同一个权限组:
groups='account.group_account_readonly'
而 _get_stat_buttons() 里还会判断:
if self.env.user.has_group('account.group_account_readonly'):
这件事特别能说明官方边界意识。
项目用户并不天然应该看到资产数据。
毕竟资产对象后面可能涉及:
- 原值
- 折旧
- 资产处置
- 会计科目
- 凭证
- 公司级账务口径
这些都不是普通项目协作者一定该看的东西。
所以官方没有把 Assets 按钮当成“项目管理增强功能”开放给所有项目成员,而是明确把它当成:
项目页里的会计观察入口。
也就是说,这个桥不是为了让项目模块吞掉资产模块,而是为了让有会计权限的人,在项目上下文里更快进入资产视角。
第五层:为什么按钮在 assets_count > 0 时才显示
_get_stat_buttons() 里的 show 条件是:
'show': self.assets_count > 0,
这意味着项目表单不会默认常驻一个空的 Assets 按钮。
这看起来像 UI 小事,其实对应的是产品定位:
- 资产不是每个项目的必备对象
- 它只是某些资本化 / 长周期项目会出现的视角
- 没有资产时,不需要把项目页变成“什么都能点”的杂货铺
换句话说,官方这里不是在宣告“项目一定要管理资产”,而是在说:
只有当分析分配已经把资产拉进项目上下文时,项目页才值得出现这个入口。
这和那种“先放个按钮,点进去永远是空列表”的实现相比,克制得多。
第六层:这个按钮为什么能和折旧归集链路接上
如果只看 project_account_asset 本身,你可能会觉得它只是个统计入口,谈不上“深度”。
但把它和 /home/ubuntu/odoo-temp/enterprise/account_asset/ 放在一起看,逻辑就完整了。
在 account_move.py 里,系统从发票行自动创建资产时,会把原始凭证行的:
'analytic_distribution': move_line.analytic_distribution,
直接带到资产上。
而后续准备折旧分录 _prepare_move_for_asset_depreciation() 时,又会把资产自身的:
analytic_distribution = asset.analytic_distribution
继续写回折旧分录的两条 move line。
这意味着什么?
资产进入项目视角,不只是因为“项目页能看到它”
更关键的是:
资产如果最初按项目分析账户建起来,它后面的折旧分录也会沿着同一分析分配继续流动。
所以项目页上的 Assets 按钮,本质上看到的是一条更长的链:
- 采购 / 发票行带分析分配
- 资产继承分析分配
- 项目通过分析账户统计资产
- 折旧分录继续继承这套分析分配
它不是独立小挂件,而是分析会计和资产会计之间的一条轻量接缝。
第七层:为什么官方选择“分析分配桥接”,而不是“项目资产专属模型”
如果要做得更“重”,当然可以设计另一条路线:
- 在
account.asset上加project_id - 项目页维护
asset_ids - 资产复制、拆分、处置时再同步项目字段
- 折旧凭证再想办法回填项目维度
但这样会立刻出现很多问题:
- 一个资产能不能跨项目?
- 多项目共同使用怎么分摊?
- 会计要按成本中心 / 部门 / 项目多维分析时怎么办?
- 资产调整、价值增加、处置凭证如何保持一致?
而 analytic_distribution 天生就是为“一个经济对象可落到多个分析维度”服务的。
所以官方这里其实做了一个很聪明的取舍:
- 不在项目模块重造资产归属模型
- 直接借分析分配当桥
- 让项目看到自己关心的资产口径即可
这就是为什么 project_account_asset 代码极少,却并不浅。
新手最容易误解的 5 件事
1. 以为项目上有 Assets 按钮,就说明 account.asset 一定有 project_id
不对。这个桥接模块压根没走外键关系,而是走 analytic_distribution。
2. 以为 assets_count 统计的是“项目独占资产”
不对。它统计的是分析分配包含当前项目分析账户的资产。
3. 以为点进列表只是为了看历史数据
不对。动作上下文还会把新建资产默认分析分配成当前项目 100%。
4. 以为所有项目成员都能看到这个按钮
不对。它明确受 account.group_account_readonly 控制。
5. 以为这个模块和折旧没关系
不对。真正的价值恰恰在于资产的分析分配还能继续进入折旧分录,项目因此能沿同一口径观察长期成本。
实战里最该注意什么
1. 想让项目看到资产,先看项目有没有分析账户
如果项目本身 account_id 没建好,这个桥接就无从谈起。
2. 资产看不到时,不要先怀疑 stat button
先查:
- 资产的
analytic_distribution有没有当前项目分析账户 - 分摊比例是不是被写到别的分析账户上
- 当前用户是否具备会计只读组权限
3. 从项目入口新建资产时,确认默认分摊是不是符合业务真实情况
源码默认给的是 {project.account_id: 100}。
这对“单项目资本化”很合理,但如果资产本来要分摊给多个项目或成本中心,就不要无脑接受默认值。
4. 不要把这个按钮误当成项目成本全景
它只覆盖资产这条线,且前提是分析分配写对了。
项目真实成本口径通常还会同时涉及:
- 工时
- 采购
- 费用
- 材料
- 订阅 / 收入
Assets 按钮只是其中一块长期资产视角,不是全部。
一句话记忆法
Odoo 企业版项目上的 Assets 按钮,不是在项目里塞进一套固定资产子表,而是让项目通过分析账户去观察“哪些资产及其后续折旧,正在沿这条分析分配为项目服务”。
DISCUSSION
评论区