|
35 | 35 | //! | `scopes` | `scopes` / `default_scope` | `Scope` / `Scopes` → `Class.scopes`; `DefaultScope` → `Class.default_scope` | |
36 | 36 | //! | `acts_as` | `mixins` | rendered as `acts_as_<variant>` so they survive on the same shelf as concerns (`ogar-vocab` has no separate `acts_as` slot) | |
37 | 37 | //! | `sti.inherits_from` | `parent` | STI parent — matches `Class.parent` slot | |
| 38 | +//! | `inherits` | `mixins` (appended) | Odoo `_inherit` multi-parent mixin composition — the vocab's `mixins` doc names `_inherit`; the `inheritance` axis excludes mixins. Frontend-agnostic field, populated only by the Odoo frontend | |
38 | 39 | //! | `functions` | `Vec<ActionDef>` (DO-arm) | [`lift_actions`] — one `ActionDef` per method; standalone, not on `Class` | |
39 | 40 | //! |
40 | 41 | //! Fields NOT lifted today (no equivalent on the ruff side OR no clean |
@@ -177,6 +178,15 @@ fn lift_model_with_language(model: &Model, language: Language) -> Class { |
177 | 178 | class.canonical_concept = Some(canonical_concept(&model.name)); |
178 | 179 | class.associations = model.associations.iter().filter_map(lift_association).collect(); |
179 | 180 | class.mixins = lift_mixins(model); |
| 181 | + // Odoo `_inherit` (multi-parent mixin composition) lands on the same |
| 182 | + // mixins shelf the vocab designates for it — `Class::mixins` doc names |
| 183 | + // `_inherit = 'mixin.thread'`, and `Class::inheritance` explicitly |
| 184 | + // excludes mixins ("Mixins / concerns are a SEPARATE axis"). The ruff |
| 185 | + // frontend already normalised the names (dot→underscore→verbatim), |
| 186 | + // deduped, and excluded the bare-`_inherit` reopen self-edge. Only the |
| 187 | + // Odoo frontend populates `Model::inherits`, so this is a no-op for the |
| 188 | + // Rails (`sti`) and C++ (`bases`) producers — hence unconditional. |
| 189 | + class.mixins.extend(model.inherits.iter().cloned()); |
180 | 190 | class.attributes = model.attributes.iter().filter_map(lift_attribute).collect(); |
181 | 191 | class.enums = model.attributes.iter().filter_map(lift_enum).collect(); |
182 | 192 | class.scopes = model.scopes.iter().filter_map(lift_scope).collect(); |
@@ -885,6 +895,36 @@ mod tests { |
885 | 895 | assert!(class.computed_fields.is_empty()); |
886 | 896 | } |
887 | 897 |
|
| 898 | + #[test] |
| 899 | + fn odoo_inherit_lands_on_mixins_not_parent() { |
| 900 | + // The is_a input end of the transpile chain: `ruff_python_spo` |
| 901 | + // populates the frontend-agnostic `Model::inherits` from Odoo |
| 902 | + // `_inherit` (self-reopen already excluded upstream). The lift routes |
| 903 | + // it to `Class::mixins` — the vocab's designated multi-parent shelf — |
| 904 | + // NOT to the single `parent` / `inheritance` is_a spine (those stay |
| 905 | + // Rails-STI-shaped; the vocab excludes mixins from `inheritance`). |
| 906 | + let mut m = mk_odoo_model(); |
| 907 | + m.inherits = vec!["mail_thread".to_string(), "mail_activity_mixin".to_string()]; |
| 908 | + let class = lift_model_python(&m); |
| 909 | + |
| 910 | + // Both parents preserved on the mixins shelf, order kept. |
| 911 | + assert!(class.mixins.contains(&"mail_thread".to_string())); |
| 912 | + assert!(class.mixins.contains(&"mail_activity_mixin".to_string())); |
| 913 | + // The is_a spine is untouched — Odoo `_inherit` is NOT STI. |
| 914 | + assert_eq!(class.parent, None); |
| 915 | + assert_eq!(class.inheritance, Inheritance::Root); |
| 916 | + } |
| 917 | + |
| 918 | + #[test] |
| 919 | + fn empty_inherits_adds_no_mixins() { |
| 920 | + // Frontend-agnostic no-op: the Rails / C++ producers never populate |
| 921 | + // `Model::inherits`, so the lift must not fabricate mixins. A bare |
| 922 | + // model (no concerns, no acts_as, no `_inherit`) lifts with an empty |
| 923 | + // mixins shelf — the `inherits` extension contributes nothing. |
| 924 | + let class = lift_model(&Model::new("Bare")); |
| 925 | + assert!(class.mixins.is_empty()); |
| 926 | + } |
| 927 | + |
888 | 928 | #[test] |
889 | 929 | fn lift_inheritance_concrete_from_sti_parent() { |
890 | 930 | // mk_model's StiInfo has inherits_from = Some("Issue"). |
|
0 commit comments