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