@@ -1133,6 +1133,16 @@ const CODEBOOK: &[(&str, u16)] = &[
11331133 ( "billing_party" , 0x0204 ) ,
11341134 ( "payment_record" , 0x0205 ) ,
11351135 ( "currency_policy" , 0x0206 ) ,
1136+ // Phase-3 OGAR-side mints from the cross-axis identity gap surfaced in
1137+ // odoo-rs PR #14 (`alignment_pin::seeded_classes_have_compatible_ogar_identity`):
1138+ // `OdooPort` covered the commerce arm only (9 aliases); the alignment
1139+ // table extends to 6 basins (BillingCore / SMBAccounting /
1140+ // SmbFoundryCustomer / SmbFoundryInvoice / ProductCatalog / HRFoundation).
1141+ // These two mints close the highest-impact gap (4 of 11 missing aliases).
1142+ // The remaining 7 (pricelist*, uom*, hr*) are queued for follow-up — see
1143+ // PR description for the queue.
1144+ ( "product" , 0x0207 ) ,
1145+ ( "accounting_account" , 0x0208 ) ,
11361146 // ── 0x09XX — Health domain (clinical / patient / care) ──
11371147 // medcare-rs Healthcare-namespace promotion (Northstar T9). The 7
11381148 // entities the OGIT `NTO/Healthcare/entities/` TTL ships, projected
@@ -1432,6 +1442,27 @@ pub mod class_ids {
14321442 /// `currency_policy` (`0x0206`) — currency lookup. OSB `Currency`,
14331443 /// Odoo `res.currency`.
14341444 pub const CURRENCY_POLICY : u16 = 0x0206 ;
1445+ /// `product` (`0x0207`) — saleable / billable item (catalogue master).
1446+ /// OSB `Product`, Odoo `product.template` + `product.product` (both
1447+ /// converge here; the variant relation lives outside the codebook,
1448+ /// `commercial_document.line_items.references` target).
1449+ ///
1450+ /// Promoted Phase-3 from the cross-axis identity gap surfaced in odoo-rs
1451+ /// PR #14: the alignment table seeds `product.template → schema:Product`
1452+ /// + BillingCore (0x61); this id is the OGAR-side identity that closes
1453+ /// the same axis. `OdooPort` carries `product.template` and
1454+ /// `product.product` as aliases of `PRODUCT`.
1455+ pub const PRODUCT : u16 = 0x0207 ;
1456+ /// `accounting_account` (`0x0208`) — general-ledger account (SKR-aligned
1457+ /// chart concept). OSB `Account`, Odoo `account.account` +
1458+ /// `account.account.template` (the SKR03/04 chart-of-accounts template;
1459+ /// both converge on this id).
1460+ ///
1461+ /// Promoted Phase-3 from the cross-axis identity gap surfaced in odoo-rs
1462+ /// PR #14: the alignment table seeds `account.account → fibo:Account` +
1463+ /// SMBAccounting (0x62); this id is the OGAR-side identity that closes
1464+ /// the same axis.
1465+ pub const ACCOUNTING_ACCOUNT : u16 = 0x0208 ;
14351466
14361467 // ── 0x09XX — health domain (medcare-rs Healthcare namespace) ──
14371468
@@ -1513,6 +1544,8 @@ pub mod class_ids {
15131544 ( "billing_party" , BILLING_PARTY ) ,
15141545 ( "payment_record" , PAYMENT_RECORD ) ,
15151546 ( "currency_policy" , CURRENCY_POLICY ) ,
1547+ ( "product" , PRODUCT ) ,
1548+ ( "accounting_account" , ACCOUNTING_ACCOUNT ) ,
15161549 // 0x09XX — health
15171550 ( "patient" , PATIENT ) ,
15181551 ( "diagnosis" , DIAGNOSIS ) ,
@@ -1531,7 +1564,7 @@ pub mod class_ids {
15311564 #[ cfg( test) ]
15321565 mod tests {
15331566 use super :: * ;
1534- use crate :: { CODEBOOK , canonical_concept_id } ;
1567+ use crate :: { canonical_concept_id , CODEBOOK } ;
15351568
15361569 #[ test]
15371570 fn constants_match_codebook ( ) {
@@ -2356,13 +2389,16 @@ pub fn all_promoted_classes() -> Vec<Class> {
23562389 project_member_role( ) ,
23572390 project_custom_value( ) ,
23582391 project_enabled_module( ) ,
2359- // 0x02XX — commerce arm (6 concepts).
2392+ // 0x02XX — commerce arm (8 concepts: 6 OSB-promoted + 2
2393+ // Phase-3 mints per odoo-rs PR #14 + #16).
23602394 commercial_line_item( ) ,
23612395 commercial_document( ) ,
23622396 tax_policy( ) ,
23632397 billing_party( ) ,
23642398 payment_record( ) ,
23652399 currency_policy( ) ,
2400+ product( ) ,
2401+ accounting_account( ) ,
23662402 // 0x09XX — health arm (7 OGIT Healthcare concepts), in
23672403 // class_ids::ALL order.
23682404 patient( ) ,
@@ -3175,6 +3211,58 @@ pub fn currency_policy() -> Class {
31753211 c
31763212}
31773213
3214+ /// `product` (`0x0207`) — saleable / billable item (catalogue master).
3215+ /// OSB `Product`, Odoo `product.template` + `product.product` (both
3216+ /// converge here; the variant relation is outside the codebook).
3217+ ///
3218+ /// Promoted Phase-3 from the cross-axis identity gap surfaced in odoo-rs
3219+ /// PR #14 (`alignment_pin::seeded_classes_have_compatible_ogar_identity`).
3220+ /// Attributes mirror the minimal `schema:Product`-aligned shape: `sku`
3221+ /// (stock-keeping unit / canonical identifier), `name`, `price` (decimal
3222+ /// money), `description` (free-text).
3223+ pub fn product ( ) -> Class {
3224+ let mut c = Class :: new ( "Product" ) ;
3225+ c. language = Language :: Unknown ;
3226+ c. canonical_concept = Some ( "product" . to_string ( ) ) ;
3227+ c. associations = Vec :: new ( ) ;
3228+ let mut sku = Attribute :: new ( "sku" ) ;
3229+ sku. type_name = Some ( "string" . to_string ( ) ) ;
3230+ let mut name = Attribute :: new ( "name" ) ;
3231+ name. type_name = Some ( "string" . to_string ( ) ) ;
3232+ let mut price = Attribute :: new ( "price" ) ;
3233+ price. type_name = Some ( "decimal" . to_string ( ) ) ;
3234+ let mut description = Attribute :: new ( "description" ) ;
3235+ description. type_name = Some ( "string" . to_string ( ) ) ;
3236+ c. attributes = vec ! [ sku, name, price, description] ;
3237+ c
3238+ }
3239+
3240+ /// `accounting_account` (`0x0208`) — general-ledger account (SKR-aligned
3241+ /// chart concept). OSB `Account`, Odoo `account.account` (live row) +
3242+ /// `account.account.template` (SKR03/04 chart-of-accounts template; both
3243+ /// converge here).
3244+ ///
3245+ /// Promoted Phase-3 from the cross-axis identity gap surfaced in odoo-rs
3246+ /// PR #14. Attributes mirror the minimal `fibo:Account`-aligned shape:
3247+ /// `code` (chart code, e.g. SKR `1200`), `name`, `account_type` (asset /
3248+ /// liability / equity / revenue / expense), `currency` (ISO 4217).
3249+ pub fn accounting_account ( ) -> Class {
3250+ let mut c = Class :: new ( "AccountingAccount" ) ;
3251+ c. language = Language :: Unknown ;
3252+ c. canonical_concept = Some ( "accounting_account" . to_string ( ) ) ;
3253+ c. associations = Vec :: new ( ) ;
3254+ let mut code = Attribute :: new ( "code" ) ;
3255+ code. type_name = Some ( "string" . to_string ( ) ) ;
3256+ let mut name = Attribute :: new ( "name" ) ;
3257+ name. type_name = Some ( "string" . to_string ( ) ) ;
3258+ let mut account_type = Attribute :: new ( "account_type" ) ;
3259+ account_type. type_name = Some ( "string" . to_string ( ) ) ;
3260+ let mut currency = Attribute :: new ( "currency" ) ;
3261+ currency. type_name = Some ( "string" . to_string ( ) ) ;
3262+ c. attributes = vec ! [ code, name, account_type, currency] ;
3263+ c
3264+ }
3265+
31783266// ─────────────────────────────────────────────────────────────────────
31793267// 0x09XX — Health domain (OGIT Healthcare). The reusable Active-Record
31803268// shape for the clinical concepts. `diagnosis` (0x0902) is the worked
@@ -3614,11 +3702,10 @@ mod tests {
36143702 fn tax_policy_is_an_erp_boundary_edge_not_in_project_evidence ( ) {
36153703 // TaxPolicy is a family edge on the canonical shape ...
36163704 let bwe = billable_work_entry ( ) ;
3617- assert ! (
3618- bwe. associations
3619- . iter( )
3620- . any( |e| e. class_name. as_deref( ) == Some ( "TaxPolicy" ) )
3621- ) ;
3705+ assert ! ( bwe
3706+ . associations
3707+ . iter( )
3708+ . any( |e| e. class_name. as_deref( ) == Some ( "TaxPolicy" ) ) ) ;
36223709 // ... but the project curator records work evidence with no tax.
36233710 let mut op = Class :: new ( "TimeEntry" ) ;
36243711 op. source_domain = Some ( "project" . to_string ( ) ) ;
@@ -4034,6 +4121,8 @@ mod tests {
40344121 "billing_party" ,
40354122 "payment_record" ,
40364123 "currency_policy" ,
4124+ "product" ,
4125+ "accounting_account" ,
40374126 ] {
40384127 let id = canonical_concept_id ( commerce_concept)
40394128 . unwrap_or_else ( || panic ! ( "{commerce_concept} missing from codebook" ) ) ;
@@ -4174,7 +4263,7 @@ mod tests {
41744263 }
41754264 // Counts line up with the codebook blocks.
41764265 assert_eq ! ( concepts_in_domain( ConceptDomain :: Health ) . count( ) , 7 ) ;
4177- assert_eq ! ( concepts_in_domain( ConceptDomain :: Commerce ) . count( ) , 6 ) ;
4266+ assert_eq ! ( concepts_in_domain( ConceptDomain :: Commerce ) . count( ) , 8 ) ;
41784267 assert_eq ! ( concepts_in_domain( ConceptDomain :: ProjectMgmt ) . count( ) , 26 ) ;
41794268 // An empty (reserved-but-unpopulated) domain yields nothing.
41804269 assert_eq ! ( concepts_in_domain( ConceptDomain :: Osint ) . count( ) , 0 ) ;
@@ -4556,7 +4645,7 @@ mod tests {
45564645 assert ! ( !is_cross_domain_concept( "project_role" ) ) ;
45574646 let id = canonical_concept_id ( "billable_work_entry" ) . unwrap ( ) ;
45584647 assert_eq ! ( canonical_concept_domain( id) , ProjectMgmt ) ; // home domain
4559- // Project curator (home domain) — kept.
4648+ // Project curator (home domain) — kept.
45604649 assert_eq ! (
45614650 canonical_concept_in_domain( "TimeEntry" , Some ( ProjectMgmt ) ) ,
45624651 "billable_work_entry"
@@ -4599,11 +4688,10 @@ mod tests {
45994688 . any( |a| a. name == "document"
46004689 && a. class_name. as_deref( ) == Some ( "CommercialDocument" ) )
46014690 ) ;
4602- assert ! (
4603- line. associations
4604- . iter( )
4605- . any( |a| a. name == "tax" && a. class_name. as_deref( ) == Some ( "TaxPolicy" ) )
4606- ) ;
4691+ assert ! ( line
4692+ . associations
4693+ . iter( )
4694+ . any( |a| a. name == "tax" && a. class_name. as_deref( ) == Some ( "TaxPolicy" ) ) ) ;
46074695
46084696 let doc = commercial_document ( ) ;
46094697 let line_items = doc
@@ -4613,23 +4701,20 @@ mod tests {
46134701 . unwrap ( ) ;
46144702 assert_eq ! ( line_items. kind, AssociationKind :: HasMany ) ;
46154703 assert_eq ! ( line_items. class_name. as_deref( ) , Some ( "CommercialLineItem" ) ) ;
4616- assert ! (
4617- doc. associations
4618- . iter( )
4619- . any( |a| a. name == "party" && a. class_name. as_deref( ) == Some ( "BillingParty" ) )
4620- ) ;
4621- assert ! (
4622- doc. associations
4623- . iter( )
4624- . any( |a| a. name == "currency" && a. class_name. as_deref( ) == Some ( "CurrencyPolicy" ) )
4625- ) ;
4704+ assert ! ( doc
4705+ . associations
4706+ . iter( )
4707+ . any( |a| a. name == "party" && a. class_name. as_deref( ) == Some ( "BillingParty" ) ) ) ;
4708+ assert ! ( doc
4709+ . associations
4710+ . iter( )
4711+ . any( |a| a. name == "currency" && a. class_name. as_deref( ) == Some ( "CurrencyPolicy" ) ) ) ;
46264712
46274713 let pay = payment_record ( ) ;
4628- assert ! (
4629- pay. associations
4630- . iter( )
4631- . any( |a| a. name == "party" && a. class_name. as_deref( ) == Some ( "BillingParty" ) )
4632- ) ;
4714+ assert ! ( pay
4715+ . associations
4716+ . iter( )
4717+ . any( |a| a. name == "party" && a. class_name. as_deref( ) == Some ( "BillingParty" ) ) ) ;
46334718 assert ! (
46344719 pay. associations
46354720 . iter( )
0 commit comments