@@ -25,9 +25,16 @@ def test_count_lines(self, tmp_path: Path) -> None:
2525 def test_infer_model_from_filename (self ) -> None :
2626 """Test model name inference from various filename formats."""
2727 assert _infer_model_from_filename ("res_partner.csv" ) == "res.partner"
28- assert _infer_model_from_filename ("sale_order_line.csv" ) == "sale.order.line"
29- assert _infer_model_from_filename ("x_custom_model.csv" ) == "x.custom.model"
30- assert _infer_model_from_filename ("res_partner_fail.csv" ) == "res.partner"
28+ assert (
29+ _infer_model_from_filename ("sale_order_line.csv" )
30+ == "sale.order.line"
31+ )
32+ assert (
33+ _infer_model_from_filename ("x_custom_model.csv" ) == "x.custom.model"
34+ )
35+ assert (
36+ _infer_model_from_filename ("res_partner_fail.csv" ) == "res.partner"
37+ )
3138 assert _infer_model_from_filename ("res_users_123.csv" ) == "res.users"
3239
3340 def test_get_fail_filename_recovery_mode (self ) -> None :
@@ -357,7 +364,10 @@ def preflight_side_effect(*_args: Any, **kwargs: Any) -> bool:
357364 return True
358365
359366 mock_preflight .side_effect = preflight_side_effect
360- mock_import_data .return_value = (True , {"total_records" : 1 , "id_map" : {"1" : 1 }})
367+ mock_import_data .return_value = (
368+ True ,
369+ {"total_records" : 1 , "id_map" : {"1" : 1 }},
370+ )
361371
362372 run_import (
363373 config = "dummy.conf" ,
@@ -380,3 +390,209 @@ def preflight_side_effect(*_args: Any, **kwargs: Any) -> bool:
380390 )
381391 mock_import_data .assert_called_once ()
382392 mock_relational_import .assert_not_called ()
393+
394+
395+ @patch ("odoo_data_flow.importer.import_threaded.import_data" )
396+ @patch ("odoo_data_flow.importer.Console" )
397+ def test_run_import_fail_mode_no_records (
398+ mock_console : MagicMock , mock_import_data : MagicMock , tmp_path : Path
399+ ) -> None :
400+ """Test fail mode when the fail file has no records to retry."""
401+ source_file = tmp_path / "source.csv"
402+ source_file .touch ()
403+ fail_file = tmp_path / "res_partner_fail.csv"
404+ fail_file .write_text ("id,name\n " ) # Only a header
405+
406+ run_import (
407+ config = "dummy.conf" ,
408+ filename = str (source_file ),
409+ model = "res.partner" ,
410+ fail = True ,
411+ deferred_fields = None ,
412+ unique_id_field = None ,
413+ no_preflight_checks = True ,
414+ headless = True ,
415+ worker = 1 ,
416+ batch_size = 100 ,
417+ skip = 0 ,
418+ separator = ";" ,
419+ ignore = None ,
420+ context = {},
421+ encoding = "utf-8" ,
422+ o2m = False ,
423+ groupby = None ,
424+ )
425+ mock_import_data .assert_not_called ()
426+ mock_console .return_value .print .assert_called_once ()
427+ assert (
428+ "No records to retry"
429+ in mock_console .return_value .print .call_args [0 ][0 ].renderable
430+ )
431+
432+
433+ @patch ("odoo_data_flow.importer.sort.sort_for_self_referencing" )
434+ @patch ("odoo_data_flow.importer.import_threaded.import_data" )
435+ @patch ("odoo_data_flow.importer._run_preflight_checks" )
436+ def test_run_import_sort_strategy_already_sorted (
437+ mock_preflight : MagicMock ,
438+ mock_import_data : MagicMock ,
439+ mock_sort : MagicMock ,
440+ tmp_path : Path ,
441+ ) -> None :
442+ """Test the sort strategy when the file is already sorted."""
443+ source_file = tmp_path / "source.csv"
444+ source_file .touch ()
445+ mock_sort .return_value = True # Indicates file is already sorted
446+
447+ def preflight_side_effect (* args : Any , ** kwargs : Any ) -> bool :
448+ kwargs ["import_plan" ]["strategy" ] = "sort_and_one_pass_load"
449+ kwargs ["import_plan" ]["id_column" ] = "id"
450+ kwargs ["import_plan" ]["parent_column" ] = "parent_id"
451+ return True
452+
453+ mock_preflight .side_effect = preflight_side_effect
454+ mock_import_data .return_value = (True , {"total_records" : 1 })
455+
456+ run_import (
457+ config = "dummy.conf" ,
458+ filename = str (source_file ),
459+ model = "res.partner" ,
460+ deferred_fields = None ,
461+ unique_id_field = None ,
462+ no_preflight_checks = False ,
463+ headless = True ,
464+ worker = 1 ,
465+ batch_size = 100 ,
466+ skip = 0 ,
467+ fail = False ,
468+ separator = ";" ,
469+ ignore = None ,
470+ context = {},
471+ encoding = "utf-8" ,
472+ o2m = False ,
473+ groupby = None ,
474+ )
475+ mock_sort .assert_called_once ()
476+ assert mock_import_data .call_args .kwargs ["file_csv" ] == str (source_file )
477+
478+
479+ @patch ("odoo_data_flow.importer._show_error_panel" )
480+ def test_run_import_invalid_json_type_context (
481+ mock_show_error : MagicMock ,
482+ ) -> None :
483+ """Test that run_import handles context that is not a JSON dict."""
484+ run_import (
485+ config = "dummy.conf" ,
486+ filename = "dummy.csv" ,
487+ model = "res.partner" ,
488+ context = '["not", "a", "dict"]' , # Valid JSON, but not a dict
489+ deferred_fields = None ,
490+ unique_id_field = None ,
491+ no_preflight_checks = True ,
492+ headless = True ,
493+ worker = 1 ,
494+ batch_size = 100 ,
495+ skip = 0 ,
496+ fail = False ,
497+ separator = ";" ,
498+ ignore = None ,
499+ encoding = "utf-8" ,
500+ o2m = False ,
501+ groupby = None ,
502+ )
503+ mock_show_error .assert_called_once ()
504+ assert "must be a valid JSON dictionary" in mock_show_error .call_args [0 ][1 ]
505+
506+
507+ @patch ("odoo_data_flow.importer.cache.save_id_map" )
508+ @patch ("odoo_data_flow.importer.relational_import.run_direct_relational_import" )
509+ @patch ("odoo_data_flow.importer.import_threaded.import_data" )
510+ @patch ("odoo_data_flow.importer._run_preflight_checks" )
511+ def test_run_import_with_relational_strategy (
512+ mock_preflight : MagicMock ,
513+ mock_import_data : MagicMock ,
514+ mock_run_direct_relational : MagicMock ,
515+ mock_save_cache : MagicMock ,
516+ tmp_path : Path ,
517+ ) -> None :
518+ """Test that relational import strategies are called in Pass 2."""
519+ source_file = tmp_path / "source.csv"
520+ source_file .write_text ("id,name,tags\n p1,Partner 1,tag1,tag2" )
521+
522+ def preflight_side_effect (* args : Any , ** kwargs : Any ) -> bool :
523+ kwargs ["import_plan" ]["strategies" ] = {
524+ "tags" : {"strategy" : "direct_relational_import" }
525+ }
526+ return True
527+
528+ mock_preflight .side_effect = preflight_side_effect
529+ # Pass 1 successful, returns an id_map
530+ mock_import_data .return_value = (True , {"id_map" : {"p1" : 1 }})
531+ # Pass 2 (from relational) returns None, so no third import call
532+ mock_run_direct_relational .return_value = None
533+
534+ run_import (
535+ config = str (tmp_path / "dummy.conf" ),
536+ filename = str (source_file ),
537+ model = "res.partner" ,
538+ deferred_fields = None ,
539+ unique_id_field = None ,
540+ no_preflight_checks = False ,
541+ headless = True ,
542+ worker = 1 ,
543+ batch_size = 100 ,
544+ skip = 0 ,
545+ fail = False ,
546+ separator = "," ,
547+ ignore = None ,
548+ context = {},
549+ encoding = "utf-8" ,
550+ o2m = False ,
551+ groupby = None ,
552+ )
553+
554+ assert mock_import_data .call_count == 1 # Only the first pass
555+ mock_run_direct_relational .assert_called_once ()
556+ mock_save_cache .assert_called_once ()
557+
558+
559+ @patch ("odoo_data_flow.importer._show_error_panel" )
560+ @patch ("odoo_data_flow.importer._count_lines" , return_value = 0 )
561+ @patch ("odoo_data_flow.importer.import_threaded.import_data" )
562+ @patch ("odoo_data_flow.importer._run_preflight_checks" , return_value = True )
563+ def test_run_import_fails_without_creating_fail_file (
564+ mock_preflight : MagicMock ,
565+ mock_import_data : MagicMock ,
566+ mock_count_lines : MagicMock ,
567+ mock_show_error : MagicMock ,
568+ tmp_path : Path ,
569+ ) -> None :
570+ """Test the failure path where import fails but no fail file is created."""
571+ source_file = tmp_path / "source.csv"
572+ source_file .touch ()
573+ # Simulate import_data returning success=False
574+ mock_import_data .return_value = (False , {})
575+
576+ run_import (
577+ config = "dummy.conf" ,
578+ filename = str (source_file ),
579+ model = "res.partner" ,
580+ deferred_fields = None ,
581+ unique_id_field = None ,
582+ no_preflight_checks = False ,
583+ headless = True ,
584+ worker = 1 ,
585+ batch_size = 100 ,
586+ skip = 0 ,
587+ fail = False ,
588+ separator = ";" ,
589+ ignore = None ,
590+ context = {},
591+ encoding = "utf-8" ,
592+ o2m = False ,
593+ groupby = None ,
594+ )
595+
596+ mock_import_data .assert_called_once ()
597+ mock_show_error .assert_called_once ()
598+ assert "Import Failed" in mock_show_error .call_args [0 ]
0 commit comments