@@ -88,6 +88,29 @@ def _resolve_related_ids(
8888 return None
8989
9090
91+ def _derive_relation_info (
92+ model : str , field : str , related_model_fk : str
93+ ) -> tuple [str , str ]:
94+ """Derive relation table and field names based on Odoo conventions.
95+
96+ Args:
97+ model: The owning model name
98+ field: The field name
99+ related_model_fk: The related model name
100+
101+ Returns:
102+ A tuple of (relation_table, relation_field)
103+ """
104+ # Derive relation table name (typically follows pattern: model1_model2_rel)
105+ models = sorted ([model , related_model_fk ])
106+ derived_table = f"{ models [0 ].replace ('.' , '_' )} _{ models [1 ].replace ('.' , '_' )} _rel"
107+
108+ # Derive the owning model field name (typically model_name_id)
109+ derived_field = f"{ model .replace ('.' , '_' )} _id"
110+
111+ return derived_table , derived_field
112+
113+
91114def run_direct_relational_import (
92115 config : Union [str , dict [str , Any ]],
93116 model : str ,
@@ -113,6 +136,21 @@ def run_direct_relational_import(
113136 owning_model_fk = strategy_details .get ("relation_field" )
114137 related_model_fk = strategy_details .get ("relation" )
115138
139+ # Try to derive missing information if possible
140+ if (not relational_table or not owning_model_fk ) and related_model_fk :
141+ # Try to derive the relation table and field names
142+ derived_table , derived_field = _derive_relation_info (
143+ model , field , related_model_fk
144+ )
145+
146+ # Only use derived values if we were missing them
147+ if not relational_table :
148+ log .info (f"Deriving relation_table for field '{ field } ': { derived_table } " )
149+ relational_table = derived_table
150+ if not owning_model_fk :
151+ log .info (f"Deriving relation_field for field '{ field } ': { derived_field } " )
152+ owning_model_fk = derived_field
153+
116154 # If we don't have the required information, we can't proceed with this strategy
117155 if not relational_table or not owning_model_fk :
118156 log .error (
@@ -167,6 +205,44 @@ def run_direct_relational_import(
167205 }
168206
169207
208+ def _prepare_link_dataframe (
209+ source_df : pl .DataFrame ,
210+ field : str ,
211+ owning_df : pl .DataFrame ,
212+ related_model_df : pl .DataFrame ,
213+ owning_model_fk : str ,
214+ related_model_fk : str ,
215+ ) -> pl .DataFrame :
216+ """Prepare the link table DataFrame for relational imports.
217+
218+ Args:
219+ source_df: The source DataFrame
220+ field: The field name
221+ owning_df: DataFrame with owning model IDs
222+ related_model_df: DataFrame with related model IDs
223+ owning_model_fk: The owning model foreign key field name
224+ related_model_fk: The related model name
225+
226+ Returns:
227+ The prepared link DataFrame
228+ """
229+ # Create the link table DataFrame
230+ link_df = source_df .select (["id" , field ]).rename ({"id" : "external_id" })
231+ link_df = link_df .with_columns (pl .col (field ).str .split ("," )).explode (field )
232+
233+ # Join to get DB IDs for the owning model
234+ link_df = link_df .join (owning_df , on = "external_id" , how = "inner" ).rename (
235+ {"db_id" : owning_model_fk }
236+ )
237+
238+ # Join to get DB IDs for the related model
239+ link_df = link_df .join (
240+ related_model_df .rename ({"external_id" : field }), on = field , how = "inner"
241+ ).rename ({"db_id" : f"{ related_model_fk } /id" })
242+
243+ return link_df
244+
245+
170246def run_write_tuple_import (
171247 config : Union [str , dict [str , Any ]],
172248 model : str ,
@@ -192,7 +268,23 @@ def run_write_tuple_import(
192268 owning_model_fk = strategy_details .get ("relation_field" )
193269 related_model_fk = strategy_details .get ("relation" )
194270
195- # If we don't have the required information, we can't proceed with this strategy
271+ # Try to derive missing information if possible
272+ if (not relational_table or not owning_model_fk ) and related_model_fk :
273+ # Try to derive the relation table and field names
274+ derived_table , derived_field = _derive_relation_info (
275+ model , field , related_model_fk
276+ )
277+
278+ # Only use derived values if we were missing them
279+ if not relational_table :
280+ log .info (f"Deriving relation_table for field '{ field } ': { derived_table } " )
281+ relational_table = derived_table
282+ if not owning_model_fk :
283+ log .info (f"Deriving relation_field for field '{ field } ': { derived_field } " )
284+ owning_model_fk = derived_field
285+
286+ # If we still don't have the required information, we can't proceed
287+ # with this strategy
196288 if not relational_table or not owning_model_fk :
197289 log .error (
198290 f"Cannot run write tuple import for field '{ field } ': "
@@ -219,20 +311,57 @@ def run_write_tuple_import(
219311 return False
220312
221313 # 3. Create the link table DataFrame
222- link_df = source_df .select (["id" , field ]).rename ({"id" : "external_id" })
223- link_df = link_df .with_columns (pl .col (field ).str .split ("," )).explode (field )
314+ link_df = _prepare_link_dataframe (
315+ source_df , field , owning_df , related_model_df , owning_model_fk , related_model_fk
316+ )
224317
225- # Join to get DB IDs for the owning model
226- link_df = link_df .join (owning_df , on = "external_id" , how = "inner" ).rename (
227- {"db_id" : owning_model_fk }
318+ # 4. Create records in the relational table
319+ return _create_relational_records (
320+ config ,
321+ model ,
322+ field ,
323+ relational_table ,
324+ owning_model_fk ,
325+ related_model_fk ,
326+ link_df ,
327+ owning_df ,
328+ related_model_df ,
329+ original_filename ,
330+ batch_size ,
228331 )
229332
230- # Join to get DB IDs for the related model
231- link_df = link_df .join (
232- related_model_df .rename ({"external_id" : field }), on = field , how = "inner"
233- ).rename ({"db_id" : f"{ related_model_fk } /id" })
234333
235- # 4. Create records in the relational table
334+ def _create_relational_records (
335+ config : Union [str , dict [str , Any ]],
336+ model : str ,
337+ field : str ,
338+ relational_table : str ,
339+ owning_model_fk : str ,
340+ related_model_fk : str ,
341+ link_df : pl .DataFrame ,
342+ owning_df : pl .DataFrame ,
343+ related_model_df : pl .DataFrame ,
344+ original_filename : str ,
345+ batch_size : int ,
346+ ) -> bool :
347+ """Create records in the relational table.
348+
349+ Args:
350+ config: Configuration for the connection
351+ model: The model name
352+ field: The field name
353+ relational_table: The relational table name
354+ owning_model_fk: The owning model foreign key field name
355+ related_model_fk: The related model name
356+ link_df: The link DataFrame
357+ owning_df: DataFrame with owning model IDs
358+ related_model_df: DataFrame with related model IDs
359+ original_filename: The original filename
360+ batch_size: The batch size for processing
361+
362+ Returns:
363+ True if successful, False otherwise
364+ """
236365 if isinstance (config , dict ):
237366 connection = conf_lib .get_connection_from_dict (config )
238367 else :
@@ -241,8 +370,8 @@ def run_write_tuple_import(
241370
242371 # We need to map back to the original external IDs for failure reporting
243372 # This is a bit heavy, but necessary for accurate error logs.
244- original_links_df = source_df .select (["id " , field ]).rename (
245- {"id " : "parent_external_id" }
373+ original_links_df = link_df .select (["external_id " , field ]).rename (
374+ {"external_id " : "parent_external_id" }
246375 )
247376 original_links_df = original_links_df .with_columns (
248377 pl .col (field ).str .split ("," )
0 commit comments