diff --git a/src/odoo_data_flow/export_threaded.py b/src/odoo_data_flow/export_threaded.py index cc499478..4f88a1e3 100755 --- a/src/odoo_data_flow/export_threaded.py +++ b/src/odoo_data_flow/export_threaded.py @@ -134,7 +134,15 @@ def _format_batch_results( if field in record: value = record[field] if isinstance(value, (list, tuple)) and value: - new_record[field] = value[1] + if len(value) > 1: + new_record[field] = value[1] + else: + log.debug( + "Malformed relational value found for field " + f"'{field}'. Got {value} instead of a " + "(id, name) tuple. Using None." + ) + new_record[field] = None else: new_record[field] = value else: @@ -213,6 +221,8 @@ def _execute_batch( ], ids_to_export for field in self.header: + if self.fields_info[field].get("type") == "non_existent": + continue base_field = field.split("/")[0].replace(".id", "id") read_fields.add(base_field) if self.is_hybrid and "/" in field and not field.endswith("/.id"): @@ -323,6 +333,8 @@ def _initialize_export( f" on model '{model_name}'. " f"An empty column will be created." ) + fields_info[original_field] = {"type": "non_existent"} + continue field_type = "char" if meta: diff --git a/tests/test_export_threaded.py b/tests/test_export_threaded.py index d403acbf..525a0f3e 100644 --- a/tests/test_export_threaded.py +++ b/tests/test_export_threaded.py @@ -983,3 +983,53 @@ def test_process_export_batches_handles_inconsistent_schemas( ) final_df = final_df.sort("id") assert_frame_equal(final_df, expected_df) + + def test_export_with_non_existent_fields( + self, mock_conf_lib: MagicMock, tmp_path: Path + ) -> None: + """Tests that exporting with non-existent fields completes and adds null columns.""" + # --- Arrange --- + header = ["id", "name", "field_does_not_exist", "another_bad_field"] + output_file = tmp_path / "output.csv" + mock_model = mock_conf_lib.return_value.get_model.return_value + mock_model.search.return_value = [1, 2] + mock_model.read.return_value = [ + {"id": 1, "name": "Test 1"}, + {"id": 2, "name": "Test 2"}, + ] + mock_model.fields_get.return_value = { + "id": {"type": "integer"}, + "name": {"type": "char"}, + } + + # --- Act --- + success, _, _, result_df = export_data( + config="dummy.conf", + model="res.partner", + domain=[], + header=header, + output=str(output_file), + technical_names=True, + ) + + # --- Assert --- + assert success is True + assert result_df is not None + + expected_df = pl.DataFrame( + { + "id": [1, 2], + "name": ["Test 1", "Test 2"], + "field_does_not_exist": [None, None], + "another_bad_field": [None, None], + } + ).with_columns( + pl.col("id").cast(pl.Int64), + pl.col("field_does_not_exist").cast(pl.String), + pl.col("another_bad_field").cast(pl.String), + ) + + # Reorder columns to match expected output + result_df = result_df[expected_df.columns] + + assert_frame_equal(result_df.sort("id"), expected_df.sort("id"))