Skip to content

Commit 5dda37c

Browse files
committed
[FIX] inverse_name without keyword + tests for OLS03021
1 parent 86b4081 commit 5dda37c

File tree

4 files changed

+46
-28
lines changed

4 files changed

+46
-28
lines changed

server/src/core/diagnostic_codes_list.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ OLS03019, DiagnosticSetting::Error, "Compute method not set to modify this field
160160
*/
161161
OLS03020, DiagnosticSetting::Warning, "Model {0} is shadowing an existing model in dependencies",
162162
/**
163-
* On a One2many field, the inverse_name should be a field on the comodel that is a Many2one to the current model.
163+
* On a One2many field, the inverse_name should be a field on the comodel.
164164
*/
165165
OLS03021, DiagnosticSetting::Error, "Inverse field {0} does not exist on comodel {1}",
166166
/**

server/src/core/python_arch_eval_hooks.rs

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -109,150 +109,150 @@ static arch_eval_file_hooks: Lazy<Vec<PythonArchEvalFileHook>> = Lazy::new(|| {v
109109
if_exist_only: true,
110110
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
111111
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("bool")]));
112-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
112+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
113113
}},
114114
PythonArchEvalFileHook {odoo_entry: true,
115115
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Integer")])),
116116
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_numeric")], vec![Sy!("Integer")]))],
117117
if_exist_only: true,
118118
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
119119
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("int")]));
120-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
120+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
121121
}},
122122
PythonArchEvalFileHook {odoo_entry: true,
123123
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Float")])),
124124
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_numeric")], vec![Sy!("Float")]))],
125125
if_exist_only: true,
126126
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
127127
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("float")]));
128-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
128+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
129129
}},
130130
PythonArchEvalFileHook {odoo_entry: true,
131131
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Monetary")])),
132132
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_numeric")], vec![Sy!("Monetary")]))],
133133
if_exist_only: true,
134134
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
135135
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("float")]));
136-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
136+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
137137
}},
138138
PythonArchEvalFileHook {odoo_entry: true,
139139
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Char")])),
140140
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_textual")], vec![Sy!("Char")]))],
141141
if_exist_only: true,
142142
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
143143
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("str")]));
144-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
144+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
145145
}},
146146
PythonArchEvalFileHook {odoo_entry: true,
147147
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Text")])),
148148
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_textual")], vec![Sy!("Text")]))],
149149
if_exist_only: true,
150150
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
151151
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("str")]));
152-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
152+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
153153
}},
154154
PythonArchEvalFileHook {odoo_entry: true,
155155
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Html")])),
156156
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_textual")], vec![Sy!("Html")]))],
157157
if_exist_only: true,
158158
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
159159
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("markupsafe")], vec![Sy!("Markup")]));
160-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
160+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
161161
}},
162162
PythonArchEvalFileHook {odoo_entry: true,
163163
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Date")])),
164164
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_temporal")], vec![Sy!("Date")]))],
165165
if_exist_only: true,
166166
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
167167
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("datetime")], vec![Sy!("date")]));
168-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
168+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
169169
}},
170170
PythonArchEvalFileHook {odoo_entry: true,
171171
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Datetime")])),
172172
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_temporal")], vec![Sy!("Datetime")]))],
173173
if_exist_only: true,
174174
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
175175
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("datetime")], vec![Sy!("datetime")]));
176-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
176+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
177177
}},
178178
PythonArchEvalFileHook {odoo_entry: true,
179179
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Binary")])),
180180
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_binary")], vec![Sy!("Binary")]))],
181181
if_exist_only: true,
182182
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
183183
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("bytes")]));
184-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
184+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
185185
}},
186186
PythonArchEvalFileHook {odoo_entry: true,
187187
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Image")])),
188188
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_binary")], vec![Sy!("Image")]))],
189189
if_exist_only: true,
190190
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
191191
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("bytes")]));
192-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
192+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
193193
}},
194194
PythonArchEvalFileHook {odoo_entry: true,
195195
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Selection")])),
196196
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_selection")], vec![Sy!("Selection")]))],
197197
if_exist_only: true,
198198
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
199199
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("str")]));
200-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
200+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
201201
}},
202202
PythonArchEvalFileHook {odoo_entry: true,
203203
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Reference")])),
204204
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_reference")], vec![Sy!("Reference")]))],
205205
if_exist_only: true,
206206
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
207207
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("str")]));
208-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
208+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
209209
}},
210210
PythonArchEvalFileHook {odoo_entry: true,
211211
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Json")])),
212212
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_misc")], vec![Sy!("Json")]))],
213213
if_exist_only: true,
214214
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
215215
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("object")]));
216-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
216+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
217217
}},
218218
PythonArchEvalFileHook {odoo_entry: true,
219219
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Properties")])),
220220
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_properties")], vec![Sy!("Properties")]))],
221221
if_exist_only: true,
222222
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
223223
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("object")]));
224-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
224+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
225225
}},
226226
PythonArchEvalFileHook {odoo_entry: true,
227227
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("PropertiesDefinition")])),
228228
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_properties")], vec![Sy!("PropertiesDefinition")]))],
229229
if_exist_only: true,
230230
func: |odoo: &mut SessionInfo, entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
231231
PythonArchEvalHooks::_update_get_eval(odoo.sync_odoo, entry, symbol.clone(), (vec![Sy!("builtins")], vec![Sy!("object")]));
232-
PythonArchEvalHooks::_update_field_init(symbol.clone(), false);
232+
PythonArchEvalHooks::_update_field_init(symbol.clone(), None);
233233
}},
234234
PythonArchEvalFileHook {odoo_entry: true,
235235
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Many2one")])),
236236
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_relational")], vec![Sy!("Many2one")]))],
237237
if_exist_only: true,
238238
func: |_odoo: &mut SessionInfo, _entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
239239
PythonArchEvalHooks::_update_get_eval_relational(symbol.clone());
240-
PythonArchEvalHooks::_update_field_init(symbol.clone(), true);
240+
PythonArchEvalHooks::_update_field_init(symbol.clone(), Some(oyarn!("Many2one")));
241241
}},
242242
PythonArchEvalFileHook {odoo_entry: true,
243243
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("One2many")])),
244244
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_relational")], vec![Sy!("One2many")]))],
245245
if_exist_only: true,
246246
func: |_odoo: &mut SessionInfo, _entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
247-
PythonArchEvalHooks::_update_field_init(symbol.clone(), true);
247+
PythonArchEvalHooks::_update_field_init(symbol.clone(), Some(oyarn!("One2many")));
248248
}},
249249
PythonArchEvalFileHook {odoo_entry: true,
250250
trees: vec![(Sy!("0.0"), Sy!("18.1"), (vec![Sy!("odoo"), Sy!("fields")], vec![Sy!("Many2many")])),
251251
(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("orm"), Sy!("fields_relational")], vec![Sy!("Many2many")]))],
252252
if_exist_only: true,
253253
func: |_odoo: &mut SessionInfo, _entry: &Rc<RefCell<EntryPoint>>, _file_symbol: Rc<RefCell<Symbol>>, symbol: Rc<RefCell<Symbol>>| {
254254
PythonArchEvalHooks::_update_get_eval_relational(symbol.clone());
255-
PythonArchEvalHooks::_update_field_init(symbol.clone(), true);
255+
PythonArchEvalHooks::_update_field_init(symbol.clone(), Some(oyarn!("Many2many")));
256256
}},
257257
PythonArchEvalFileHook {odoo_entry: true,
258258
trees: vec![(Sy!("18.1"), Sy!("999.0"), (vec![Sy!("odoo"), Sy!("init")], vec![Sy!("_")]))],
@@ -1006,7 +1006,7 @@ impl PythonArchEvalHooks {
10061006
})
10071007
}
10081008

1009-
fn eval_init_common(session: &mut SessionInfo, evaluation_sym: &EvaluationSymbol, maybe_context: &mut Option<Context>, _diagnostics: &mut Vec<Diagnostic>, file_symbol: Option<Rc<RefCell<Symbol>>>, relational: bool) -> Option<EvaluationSymbolPtr>
1009+
fn eval_init_common(session: &mut SessionInfo, evaluation_sym: &EvaluationSymbol, maybe_context: &mut Option<Context>, _diagnostics: &mut Vec<Diagnostic>, file_symbol: Option<Rc<RefCell<Symbol>>>, relational: bool, one2many: bool) -> Option<EvaluationSymbolPtr>
10101010
{
10111011
let Some(context) = maybe_context else {return None};
10121012

@@ -1024,6 +1024,11 @@ impl PythonArchEvalHooks {
10241024
if let Some(first_param) = parameters.args.get(0) {
10251025
contexts_to_add.insert("comodel_name", (first_param, first_param.range(), "str"));
10261026
}
1027+
if one2many {
1028+
if let Some(second_param) = parameters.args.get(1) {
1029+
contexts_to_add.insert("inverse_name", (second_param, second_param.range(), "str"));
1030+
}
1031+
}
10271032
}
10281033

10291034
// Keyword Arguments for fields that we would like to keep in the context
@@ -1081,14 +1086,18 @@ impl PythonArchEvalHooks {
10811086
}
10821087

10831088
fn eval_init(session: &mut SessionInfo, evaluation_sym: &EvaluationSymbol, maybe_context: &mut Option<Context>, diagnostics: &mut Vec<Diagnostic>, file_symbol: Option<Rc<RefCell<Symbol>>>) -> Option<EvaluationSymbolPtr> {
1084-
return PythonArchEvalHooks::eval_init_common(session, evaluation_sym, maybe_context, diagnostics, file_symbol, false)
1089+
return PythonArchEvalHooks::eval_init_common(session, evaluation_sym, maybe_context, diagnostics, file_symbol, false, false)
10851090
}
10861091

10871092
fn eval_init_relational(session: &mut SessionInfo, evaluation_sym: &EvaluationSymbol, maybe_context: &mut Option<Context>, diagnostics: &mut Vec<Diagnostic>, file_symbol: Option<Rc<RefCell<Symbol>>>) -> Option<EvaluationSymbolPtr> {
1088-
return PythonArchEvalHooks::eval_init_common(session, evaluation_sym, maybe_context, diagnostics, file_symbol, true)
1093+
return PythonArchEvalHooks::eval_init_common(session, evaluation_sym, maybe_context, diagnostics, file_symbol, true, false)
1094+
}
1095+
1096+
fn eval_init_relational_one2many(session: &mut SessionInfo, evaluation_sym: &EvaluationSymbol, maybe_context: &mut Option<Context>, diagnostics: &mut Vec<Diagnostic>, file_symbol: Option<Rc<RefCell<Symbol>>>) -> Option<EvaluationSymbolPtr> {
1097+
return PythonArchEvalHooks::eval_init_common(session, evaluation_sym, maybe_context, diagnostics, file_symbol, true, true)
10891098
}
10901099

1091-
fn _update_field_init(symbol: Rc<RefCell<Symbol>>, relational: bool) {
1100+
fn _update_field_init(symbol: Rc<RefCell<Symbol>>, relational: Option<OYarn>) {
10921101
let init_sym = symbol.borrow().get_symbol(&(vec![], vec![Sy!("__init__")]), u32::MAX);
10931102
if init_sym.is_empty() {
10941103
return;
@@ -1098,7 +1107,11 @@ impl PythonArchEvalHooks {
10981107
Rc::downgrade(&symbol), //use the weak to keep reference to the class for the hook.
10991108
Some(true),
11001109
HashMap::new(),
1101-
Some(if relational {PythonArchEvalHooks::eval_init_relational} else {PythonArchEvalHooks::eval_init})
1110+
Some(match relational {
1111+
Some(oyarn) if oyarn == oyarn!("One2many") => PythonArchEvalHooks::eval_init_relational_one2many,
1112+
Some(_) => PythonArchEvalHooks::eval_init_relational,
1113+
None => PythonArchEvalHooks::eval_init,
1114+
})
11021115
),
11031116
value: None,
11041117
range: None,

server/tests/data/addons/module_1/models/base_test_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class BaseTestModel(models.Model):
99
test_int = fields.Integer(compute="_compute_something")
1010
partner_id = fields.Many2one("res.partner")
1111
partner_country_phone_code = fields.Integer(related="partner_id.country_id.phone_code", store=True)
12-
diagnostics_ids = fields.Many2one("module_1.diagnostics_model")
12+
diagnostics_id = fields.Many2one("module_1.diagnostics_model")
1313

1414
def get_test_int(self):
1515
self.ensure_one()

0 commit comments

Comments
 (0)