diff --git a/noxfile.py b/noxfile.py index d00df751..416efc67 100644 --- a/noxfile.py +++ b/noxfile.py @@ -128,6 +128,7 @@ def precommit(session: nox.Session) -> None: "lint", external=True, ) + session.install("pydoclint") session.run("pre-commit", *args, external=True) if args and args[0] == "install": activate_virtualenv_in_precommit_hooks(session) diff --git a/src/odoo_data_flow/__main__.py b/src/odoo_data_flow/__main__.py index fdcf5a2d..b773f72b 100644 --- a/src/odoo_data_flow/__main__.py +++ b/src/odoo_data_flow/__main__.py @@ -311,6 +311,11 @@ def import_cmd(connection_file: str, **kwargs: Any) -> None: except (ValueError, SyntaxError) as e: log.error(f"Invalid --context dictionary provided: {e}") return + + groupby = kwargs.get("groupby") + if groupby: + kwargs["groupby"] = [col.strip() for col in groupby.split(",")] + run_import(**kwargs) diff --git a/src/odoo_data_flow/importer.py b/src/odoo_data_flow/importer.py index dc48e9aa..fbbb0f7a 100755 --- a/src/odoo_data_flow/importer.py +++ b/src/odoo_data_flow/importer.py @@ -196,6 +196,23 @@ def run_import( # noqa: C901 # Disable deferred fields for this strategy deferred_fields = [] + # Filter user-deferred fields to exclude required ones + if deferred_fields and model: + odoo_fields = preflight._get_odoo_fields(config, model) + if odoo_fields: + safe_deferred_fields = [] + for field_name in deferred_fields: + clean_field_name = field_name.replace("/id", "") + field_info = odoo_fields.get(clean_field_name) + if field_info and field_info.get("required", False): + log.warning( + f"Field '{field_name}' is required and cannot be deferred. " + "It will be imported in the first pass." + ) + else: + safe_deferred_fields.append(field_name) + deferred_fields = safe_deferred_fields + final_deferred = deferred_fields or import_plan.get("deferred_fields", []) final_uid_field = unique_id_field or import_plan.get("unique_id_field") or "id" fail_output_file = str(Path(filename).parent / _get_fail_filename(model, fail)) diff --git a/tests/test_import_threaded.py b/tests/test_import_threaded.py index 118582a0..71e95b14 100644 --- a/tests/test_import_threaded.py +++ b/tests/test_import_threaded.py @@ -661,3 +661,38 @@ def test_recursive_batching_group_col_not_found(self) -> None: mock_log.error.assert_called_once_with( "Grouping column 'non_existent' not found. Cannot use --groupby." ) + + def test_recursive_batching_with_special_chars_in_col_name(self) -> None: + """Test batching with special characters in column names.""" + from odoo_data_flow.import_threaded import _recursive_create_batches + + header = ["id", "name", "partner_id/id"] + data = [ + ["1", "A", "p1"], + ["2", "B", "p1"], + ["3", "C", "p2"], + ] + batches = list( + _recursive_create_batches(data, ["partner_id/id"], header, 10, False) + ) + assert len(batches) == 2 + assert batches[0][1][0][2] == "p1" + assert batches[1][1][0][2] == "p2" + + def test_recursive_batching_multiple_cols_with_special_chars(self) -> None: + """Test batching with multiple columns, one with special characters.""" + from odoo_data_flow.import_threaded import _recursive_create_batches + + header = ["id", "name", "partner_id/id", "company_id"] + data = [ + ["1", "A", "p1", "c1"], + ["2", "B", "p1", "c2"], + ["3", "C", "p2", "c1"], + ["4", "D", "p1", "c1"], + ] + batches = list( + _recursive_create_batches( + data, ["partner_id/id", "company_id"], header, 10, False + ) + ) + assert len(batches) == 3 diff --git a/tests/test_importer.py b/tests/test_importer.py index 7df90a20..c0889e4f 100644 --- a/tests/test_importer.py +++ b/tests/test_importer.py @@ -380,3 +380,54 @@ def preflight_side_effect(*_args: Any, **kwargs: Any) -> bool: ) mock_import_data.assert_called_once() mock_relational_import.assert_not_called() + + +@patch("odoo_data_flow.importer.log") +@patch("odoo_data_flow.importer.preflight._get_odoo_fields") +@patch("odoo_data_flow.importer.import_threaded.import_data") +@patch("odoo_data_flow.importer._run_preflight_checks") +def test_run_import_does_not_defer_required_fields( + mock_preflight: MagicMock, + mock_import_data: MagicMock, + mock_get_odoo_fields: MagicMock, + mock_log: MagicMock, + tmp_path: Path, +) -> None: + """Test that a required field is not deferred even if specified by the user.""" + # Arrange + source_file = tmp_path / "source.csv" + source_file.touch() + mock_preflight.return_value = True + mock_import_data.return_value = (True, {"total_records": 1}) + mock_get_odoo_fields.return_value = { + "partner_id": {"type": "many2one", "required": True} + } + + # Act + run_import( + config="dummy.conf", + filename=str(source_file), + model="res.partner.bank", + deferred_fields=["partner_id"], + unique_id_field=None, + no_preflight_checks=False, + headless=True, + worker=1, + batch_size=100, + skip=0, + fail=False, + separator=";", + ignore=None, + context={}, + encoding="utf-8", + o2m=False, + groupby=None, + ) + + # Assert + mock_import_data.assert_called_once() + assert mock_import_data.call_args.kwargs["deferred_fields"] == [] + mock_log.warning.assert_called_once_with( + "Field 'partner_id' is required and cannot be deferred. " + "It will be imported in the first pass." + )