Company OS

架构评审

docs/adr/0005-odoo-style-modules.md

借 Odoo 的模块契约,不借它的运行时补丁

已启用 7 个模块,33 个对象类型,20 条命令,4 张扩展表。注册表在启动时由 manifest 聚合并校验;本页所有数据来自注册表,不是手写。

校验:0 问题加载顺序:party → crm → hr → eng → scm → fin → decision
模块依赖图 · 由注册表生成mermaid
正在绘制…

只能依赖箭头指向的模块;反向要说话只能发事件。CI 用 dependency-cruiser 强制:模块之间零源码 import。

扩展点使用图 · 谁扩展谁、谁钩谁、谁订阅谁mermaid
正在绘制…

虚线 = 1:1 扩展表,箭头指向被扩展对象的 owner 模块;实线 = 命令钩子,箭头指向命令 owner 模块;粗线 = 事件订阅,箭头从发事件的模块指向订阅者。

持久层 · 阶段 2

下面的数据直接来自 Postgres:每模块一个 schema,迁移按依赖拓扑顺序执行并记进 kernel.module_record;禁用模块只改状态不删数据。 事件与审计表在数据库层拒绝 UPDATE / DELETE。

正在读取持久层…

模块清单

party参与方与房屋v0.1.0

唯一 Party 户籍与房屋主数据。禁止任何其他模块另建人表。

depends
—(只依赖内核)
objects
Party, Person, Organization, PartyRole, Property
commands
RegisterParty, RegisterProperty, UpdateParty, GrantPartyRole, MergeParty
extends
hooks
subscribes
policies
can_command(*), can_approve(*), can_read(*), can_export(*)
menu
/party
slots
menu.page.party, object.Party.summary, object.Property.summary, object.Party.tabs
projections
crm获客与签约v0.1.0

线索 → 方案 → 报价 → 合同。客户永远是 party.Party 的 ObjectId。

depends
party
objects
Lead, DesignScheme, Quote, Contract
commands
CaptureLead, ProposeScheme, IssueQuote, DraftContract, SignContract
extends
hooks
subscribes
PaymentRecorded
policies
menu
/crm
slots
menu.page.crm, object.Lead.summary, object.Lead.tabs, object.DesignScheme.summary, object.Quote.summary, object.Quote.actions, object.Contract.summary, object.Contract.actions, object.Party.tabs, object.Property.tabs
projections
hr人事与 OAv0.1.0

入转调离、文档指针、审批待办箱。不做流程引擎,不自研 IM。

depends
party
objects
Employee, OfficeDoc
commands
extends
hooks
subscribes
policies
menu
/hr
slots
approval.inbox.filters, object.Party.tabs
projections
eng工程与交付v0.1.0

合同签署后开项目、要货、排班、施工节点、验收。DMS 是种子,不是并行系统。

depends
crm, party
objects
Project, ConstructionNode, WorkInstruction, Acceptance, Crew, Requisition
commands
OpenProject, RequestMaterials, AssignCrew, StartNode, CompleteNode, AcceptHandover
extends
Property
hooks
subscribes
ContractSigned
policies
menu
/eng
slots
menu.page.eng, object.Contract.tabs, object.Contract.actions, object.Property.summary, object.Project.summary, object.Project.tabs, object.Project.tabs, object.Project.actions, object.Acceptance.summary, object.Requisition.summary, object.ConstructionNode.summary, object.Crew.summary
projections
scm材料与供应v0.1.0

材料目录、承接要货、下采购单、到货。

depends
eng, party
objects
Material, PurchaseOrder, GoodsReceipt
commands
RegisterMaterial, PlacePurchaseOrder
extends
Party
hooks
validate:RequestMaterials
subscribes
MaterialsRequested, NodeCompleted
policies
menu
/scm
slots
menu.page.scm, object.Project.tabs, object.Party.tabs, object.Material.summary, object.PurchaseOrder.summary
projections
fin应收与成本v0.1.0

合同 → 应收 → 回款;采购 → 成本。不做总账、税、工资。

depends
crm, scm
objects
Receivable, Payment, CostEntry, FinanceReview
commands
ReviewContractFinance, RecordPayment
extends
Party, Contract
hooks
after:DraftContract, after:UpdateParty, validate:SignContract, after:SignContract
subscribes
PurchaseOrderPlaced, HandoverAccepted
policies
can_command(SignContract)
menu
/fin
slots
object.Contract.tabs, object.Party.tabs, object.PurchaseOrder.tabs
projections
decision决策支持v0.1.0

漏斗、项目毛利、回款账龄。全部来自事件投影;不许在看板上改业务状态。

depends
crm, eng, scm, fin
objects
commands
extends
hooks
subscribes
LeadCaptured, QuoteIssued, ContractSigned, ProjectOpened, MaterialsRequested, PurchaseOrderPlaced, NodeCompleted, HandoverAccepted, PaymentRecorded
policies
menu
/decision
slots
home.widgets, menu.page.decision
projections
spine_funnel, project_margin, receivable_aging

扩展表(1:1,主键 = 被扩展对象的 ObjectId)

扩展者 → owner字段说明
eng.property_extengparty.PropertysurveyedAreaSqm: number?, surveyNote: text?, surveyedAt: date?工程可补勘测数据,落 eng.property_ext,不改 party 的表。
scm.party_extscmparty.PartysupplierRating: number?, leadTimeDays: number?落 scm.party_ext。供应商仍是同一个 Party。
fin.party_extfinparty.PartytaxNo: string?, invoiceTitle: string?, bankAccount: string?落 fin.party_ext。不是第二张客户表:主键就是 Party 的 ObjectId。
fin.contract_extfincrm.ContractpaymentTerms: text?, depositRatio: number?落 fin.contract_ext。crm 不需要懂账期。

命令与它的管线

主 handler 永远属于对象 owner 模块。别的模块只能在前面否决、在后面追加,或订阅事件。

命令owner钩子事件 → 订阅者
RegisterParty
登记参与方
partyPartyRegistered
RegisterProperty
登记房屋
partyPropertyRegistered
UpdateParty
更新参与方
partyfin:afterPartyUpdated
GrantPartyRole
授予角色
partyPartyRoleGranted
MergeParty
合并参与方
partyPartyMerged
CaptureLead
捕获线索
crmLeadCaptured → decision
ProposeScheme
提出方案
crmSchemeProposed
IssueQuote
签发报价
crmQuoteIssued → decision
DraftContract
起草合同
crmfin:afterContractDrafted
SignContract
签署合同
crmfin:validate, fin:afterContractSigned → eng, decision
OpenProject
打开项目
engProjectOpened → decision
RequestMaterials
要货
engscm:validateMaterialsRequested → scm, decision
AssignCrew
召集施工团队
engCrewAssigned
StartNode
开始施工节点
engNodeStarted
CompleteNode
完成施工节点
engNodeCompleted → scm, decision
AcceptHandover
交付验收
engHandoverAccepted → fin, decision
RegisterMaterial
登记材料
scmMaterialRegistered
PlacePurchaseOrder
下采购单
scmPurchaseOrderPlaced → fin, decision
ReviewContractFinance
财务复核合同
finContractFinanceReviewed
RecordPayment
登记回款
finPaymentRecorded → crm, decision
一条命令穿过扩展点 · SignContractmermaid
正在绘制…

Odoo 里 fin 可以 super() 改写签约;这里 fin 只能 validate 否决、after 追加。审计里每一步都能看到是哪个模块干的。

启动序列 · 静态、确定性mermaid
正在绘制…

任何一步失败进程不起。与 Odoo 的运行时装载相比,不存在「装了一半」的数据库。

Odoo 机制 → 我们的对应
__manifest__.pymanifest.ts · 类型化清单,编译期检查
base / res.partnermodules/party · 唯一户籍基础模块
_inherit 加字段extends → 1:1 扩展表 <模块>.<对象>_ext
_inherit 覆盖方法 + super()不借钩子 validate / before / after;不能替换 handler
ir.rule 权限规则policies · 只能 restrict,多模块取交集
ir.ui.menu / actionsworkbench.menu + 命名槽位
XPath 视图继承不借不借:槽位不够先改壳
每个模型自动 RPC不借不借:一律走命令网关
App Store 运行时安装os.config 声明 + 部署时应用
卸载删表不借禁用不删数据,只标 ModuleRecord
工作台壳预定义槽位

menu.page.{module}模块菜单页模块自己菜单项对应的页面;只能给自己声明。

object.{Object}.tabs对象详情页标签在某对象详情页追加一个标签页。

object.{Object}.actions对象详情页命令按钮在某对象详情页追加命令入口。

object.{Object}.summary对象摘要卡对象头部的只读摘要块。

home.widgets首页组件登录后首页的只读卡片。

approval.inbox.filters审批箱筛选审批待办箱的过滤器。

party.tabs参与方 360Party 详情页的标签(等价于 object.Party.tabs)。

模块只能往这些槽位放东西。槽位不够,改壳并写进本清单;不允许模块改别人的页面。