Skip to content

Commit ac4b416

Browse files
authored
Merge pull request #111 from AdaWorldAPI/claude/odoo-aliases-product-and-accounting-account-mints
feat(vocab+class-view): mint PRODUCT (0x0207) + ACCOUNTING_ACCOUNT (0x0208); extend ODOO_ALIASES
2 parents 302c284 + 316759d commit ac4b416

3 files changed

Lines changed: 149 additions & 48 deletions

File tree

crates/ogar-class-view/src/lib.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
//! # Why this crate has no `serde`, no I/O
5151
//!
5252
//! It is a *pure* in-process adapter. The registry is constructed at startup
53-
//! by calling the 32 promoted class fns; nothing reads files or parses JSON.
53+
//! by calling the promoted class fns; nothing reads files or parses JSON.
5454
//! Renderers that *do* need persistence (templating output, etc.) sit
5555
//! downstream.
5656
@@ -64,18 +64,19 @@ use lance_graph_contract::{
6464
ontology::{DisplayTemplate, FieldRef, ObjectView},
6565
};
6666
use ogar_vocab::{
67-
Class, auth_ory_keto, auth_store, auth_zanzibar, auth_zitadel, billable_work_entry,
68-
billing_party, canonical_concept_id, commercial_document, commercial_line_item,
69-
currency_policy, diagnosis, lab_value, medication, patient, payment_record, priority, project,
70-
project_actor, project_attachment, project_changeset, project_comment, project_custom_field,
71-
project_custom_value, project_enabled_module, project_forum, project_journal,
72-
project_member_role, project_membership, project_message, project_news, project_query,
73-
project_relation, project_repository, project_role, project_status, project_type,
74-
project_version, project_watcher, project_wiki_page, project_work_item, tax_policy, treatment,
75-
visit, vital_sign,
67+
accounting_account, auth_ory_keto, auth_store, auth_zanzibar, auth_zitadel,
68+
billable_work_entry, billing_party, canonical_concept_id, commercial_document,
69+
commercial_line_item, currency_policy, diagnosis, lab_value, medication, patient,
70+
payment_record, priority, product, project, project_actor, project_attachment,
71+
project_changeset, project_comment, project_custom_field, project_custom_value,
72+
project_enabled_module, project_forum, project_journal, project_member_role,
73+
project_membership, project_message, project_news, project_query, project_relation,
74+
project_repository, project_role, project_status, project_type, project_version,
75+
project_watcher, project_wiki_page, project_work_item, tax_policy, treatment, visit,
76+
vital_sign, Class,
7677
};
7778

78-
/// All 32 promoted canonical concepts: `(canonical_concept_name, Class)`.
79+
/// All promoted canonical concepts: `(canonical_concept_name, Class)`.
7980
///
8081
/// Walked at startup by [`OgarClassView::new`]. The list is exhaustive against
8182
/// [`ogar_vocab::CODEBOOK`] — a test in this crate fails if a codebook entry
@@ -117,6 +118,8 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> {
117118
("billing_party", billing_party()),
118119
("payment_record", payment_record()),
119120
("currency_policy", currency_policy()),
121+
("product", product()),
122+
("accounting_account", accounting_account()),
120123
// ── 0x09XX — health (OGIT Healthcare) ──
121124
("patient", patient()),
122125
("diagnosis", diagnosis()),
@@ -164,7 +167,7 @@ fn lift_object_view(class: &Class) -> ObjectView {
164167
view
165168
}
166169

167-
/// [`ClassView`] implementation backed by [`ogar_vocab`]'s 32 promoted
170+
/// [`ClassView`] implementation backed by [`ogar_vocab`]'s promoted
168171
/// canonical concepts.
169172
///
170173
/// Construct once at startup with [`OgarClassView::new`]; the registry is
@@ -269,7 +272,7 @@ mod tests {
269272
"{concept} ({id:#06x}) absent from OgarClassView registry"
270273
);
271274
}
272-
// The registry knows exactly the 32 promoted concepts.
275+
// The registry knows the full promoted concept set.
273276
assert_eq!(v.known_class_ids().count(), all_canonical_classes().len());
274277
}
275278

crates/ogar-vocab/src/lib.rs

Lines changed: 114 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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()

crates/ogar-vocab/src/ports.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,17 @@ pub const ODOO_ALIASES: &[(&str, u16)] = &[
476476
("res.partner", class_ids::BILLING_PARTY),
477477
("account.payment", class_ids::PAYMENT_RECORD),
478478
("res.currency", class_ids::CURRENCY_POLICY),
479+
// Product master record — both `product.template` (master) and
480+
// `product.product` (variant) converge on the same `product` id.
481+
// Same convergence pattern as `account.move ↔ sale.order →
482+
// commercial_document`. Phase-3 mint per odoo-rs PR #14 + #16.
483+
("product.template", class_ids::PRODUCT),
484+
("product.product", class_ids::PRODUCT),
485+
// General-ledger account — `account.account` (live row) and
486+
// `account.account.template` (SKR03/04 chart concept) converge on the
487+
// same `accounting_account` id. Phase-3 mint per odoo-rs PR #14 + #16.
488+
("account.account", class_ids::ACCOUNTING_ACCOUNT),
489+
("account.account.template", class_ids::ACCOUNTING_ACCOUNT),
479490
// Cross-arm bridge: the timesheet / cost line converges on the
480491
// project-arm `billable_work_entry` (0x0103) — the SAME id
481492
// OpenProject `TimeEntry` and Redmine `TimeEntry` resolve to.
@@ -508,7 +519,7 @@ mod tests {
508519

509520
#[test]
510521
fn healthcare_entities_resolve_into_the_health_domain() {
511-
use crate::{ConceptDomain, canonical_concept_domain};
522+
use crate::{canonical_concept_domain, ConceptDomain};
512523
for &(name, _) in HealthcarePort::aliases() {
513524
let id =
514525
HealthcarePort::class_id(name).unwrap_or_else(|| panic!("`{name}` must resolve"));
@@ -897,7 +908,7 @@ mod tests {
897908

898909
#[test]
899910
fn odoo_commerce_models_resolve_into_the_commerce_domain() {
900-
use crate::{ConceptDomain, canonical_concept_domain};
911+
use crate::{canonical_concept_domain, ConceptDomain};
901912
// Every commerce-arm alias lands in the Commerce (0x02XX) domain.
902913
// `account.analytic.line` is the deliberate exception — it's the
903914
// cross-arm bridge into the project domain (asserted separately).
@@ -945,12 +956,14 @@ mod tests {
945956
fn odoo_alias_count_is_stable() {
946957
// 9 Odoo model aliases = 8 commerce-arm (account.move,
947958
// sale.order, account.move.line, sale.order.line, account.tax,
948-
// res.partner, account.payment, res.currency) + 1 cross-arm
949-
// bridge (account.analytic.line → billable_work_entry).
950-
// Re-count on drift.
959+
// res.partner, account.payment, res.currency) + 4 product/accounting
960+
// master-record aliases (product.template, product.product,
961+
// account.account, account.account.template — Phase-3 mints per
962+
// odoo-rs PR #14 + #16) + 1 cross-arm bridge
963+
// (account.analytic.line → billable_work_entry). Re-count on drift.
951964
assert_eq!(
952965
OdooPort::aliases().len(),
953-
9,
966+
13,
954967
"Odoo alias count drift — re-count the ODOO_ALIASES table",
955968
);
956969
}

0 commit comments

Comments
 (0)