Repository: django-import-export/django-import-export (Python)

Issue:
Referencing a column alias in the `fields` list causes a crash on import
**Describe the bug**

As title

**To Reproduce**

Update BookResource in tests/core/admin.py to the following:

```python
class BookResource(ModelResource):
    published = Field(attribute='published', column_name='published_date')

    class Meta:
        model = Book
        fields = ('id', 'name', 'price', 'published_date')
```

Note that if you use `published` instead of `published_date` in the fields list it will import ok.

Import this file:

```
id,name,price,published_date
1,Some book,15.99,2005-11-10
```

**Versions (please complete the following information):**
 - Django Import Export: 4.0.0
 - Python 3.11
 - Django 5.0.4

**Expected behavior**

An error message would be better than a crash.

This is a regression in v4.  I tested the same in v3 and it imports ok with either 'published' or 'published_date' in the fields list.





Discussion:
It is not possible to import the field when the field name is 'published'.  This makes sense, if the column_name has been declared as 'published_date' this should be respected. v3 does not support importing if the column name has been renamed.

However, it seems sensible for the field name in `fields` to be the attribute not the column name.  In v4 the import works if the attribute is used in `fields` but crashes if it is the column name.  In v3 either works ok, ('published' or 'published_date' in the `fields` declaration).

This is because v3 works with the `Field` instance and checks if 'column_name' is in the dataset.







Patch:
diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst
index b8cd6c7f5..536d8adf5 100644
--- a/docs/advanced_usage.rst
+++ b/docs/advanced_usage.rst
@@ -42,6 +42,9 @@ Or the ``exclude`` option to blacklist fields::
 If both ``fields`` and ``exclude`` are declared, the ``fields`` declaration takes precedence, and ``exclude`` is
 ignored.
 
+In cases where a :ref:`custom column name<field_declaration>` is used, declare the name of the model attribute in the
+``fields`` list., not the column alias.
+
 .. _field_ordering:
 
 Field ordering
@@ -71,8 +74,6 @@ If no ``fields``, ``import_order`` or ``export_order`` is defined then fields ar
 class.  The order of declared fields in the model instance is preserved, and any non-model fields are last in the
 ordering.
 
-.. _field_declaration:
-
 Model relations
 ---------------
 
@@ -91,6 +92,8 @@ export.
 Note that declaring the relationship using this syntax sets ``field`` as readonly, meaning this field will be skipped
 when importing data. To understand how to import model relations, see :ref:`import_model_relations`.
 
+.. _field_declaration:
+
 Explicit field declaration
 --------------------------
 
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 938fc7499..2947989d5 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -8,6 +8,8 @@ Changelog
 4.0.2 (unreleased)
 ------------------
 
+- fix export with custom column name (`1821 <https://github.com/django-import-export/django-import-export/pull/1821>`_)
+- fix allow ``column_name`` to be declared in ``fields`` list (`1815 <https://github.com/django-import-export/django-import-export/pull/1815>`_)
 - fix export with custom column name (`1821 <https://github.com/django-import-export/django-import-export/pull/1821>`_)
 
 4.0.1 (2024-05-08)
diff --git a/import_export/declarative.py b/import_export/declarative.py
index 06467a341..96c232ebc 100644
--- a/import_export/declarative.py
+++ b/import_export/declarative.py
@@ -80,7 +80,12 @@ def __new__(cls, name, bases, attrs):
             # if they appear in the 'fields' iterable.
             declared_fields = dict()
             for field_name, field in new_class.fields.items():
-                if opts.fields is not None and field_name not in opts.fields:
+                column_name = field.column_name
+                if (
+                    opts.fields is not None
+                    and field_name not in opts.fields
+                    and column_name not in opts.fields
+                ):
                     continue
                 declared_fields[field_name] = field
 
diff --git a/import_export/resources.py b/import_export/resources.py
index 15cb5baff..36634838a 100644
--- a/import_export/resources.py
+++ b/import_export/resources.py
@@ -432,9 +432,17 @@ def import_field(self, field, instance, row, is_m2m=False, **kwargs):
 
     def get_import_fields(self):
         import_fields = []
-        for f in self.fields:
-            import_fields.append(f)
-        return [self.fields[f] for f in self.get_import_order()]
+        for field_name in self.get_import_order():
+            if field_name in self.fields:
+                import_fields.append(self.fields[field_name])
+                continue
+            # issue 1815
+            # allow for fields to be referenced by column_name in `fields` list
+            for field in self.fields.values():
+                if field.column_name == field_name:
+                    import_fields.append(field)
+                    continue
+        return import_fields
 
     def import_obj(self, obj, data, dry_run, **kwargs):
         warn(
@@ -1118,7 +1126,11 @@ def _get_ordered_field_names(self, order_field):
 
         order = list()
         [order.append(f) for f in defined_fields if f not in order]
-        return tuple(order) + tuple(k for k in self.fields if k not in order)
+        declared_fields = []
+        for field_name, field in self.fields.items():
+            if field_name not in order and field.column_name not in order:
+                declared_fields.append(field_name)
+        return tuple(order) + tuple(declared_fields)
 
     def _is_using_transactions(self, kwargs):
         return kwargs.get("using_transactions", False)


Test patch:
diff --git a/tests/core/tests/admin_integration/test_import.py b/tests/core/tests/admin_integration/test_import.py
index 19d459cd8..6a7b94767 100644
--- a/tests/core/tests/admin_integration/test_import.py
+++ b/tests/core/tests/admin_integration/test_import.py
@@ -4,7 +4,13 @@
 from unittest import mock
 from unittest.mock import patch
 
-from core.admin import AuthorAdmin, BookAdmin, CustomBookAdmin, ImportMixin
+from core.admin import (
+    AuthorAdmin,
+    BookAdmin,
+    CustomBookAdmin,
+    EBookResource,
+    ImportMixin,
+)
 from core.models import Author, Book, EBook, Parent
 from core.tests.admin_integration.mixins import AdminTestMixin
 from django.contrib.admin.models import DELETION, LogEntry
@@ -954,3 +960,50 @@ def test_import_preview_order(self):
             "</tr>"
         )
         self.assertRegex(str(response.content), target_row_re)
+
+
+class CustomColumnNameImportTest(AdminTestMixin, TestCase):
+    """Test preview order displayed correctly (issue 1815)."""
+
+    fixtures = ["author"]
+
+    def setUp(self):
+        super().setUp()
+        EBookResource._meta.fields = ("id", "author_email", "name", "published_date")
+
+    def tearDown(self):
+        super().tearDown()
+        EBookResource._meta.fields = ("id", "author_email", "name", "published")
+
+    def test_import_preview_order(self):
+        author_id = Author.objects.first().id
+        response = self._do_import_post(
+            self.ebook_import_url,
+            "books.csv",
+            input_format="0",
+            data={"author": author_id},
+        )
+        # test header rendered in correct order
+        target_header_re = (
+            r"<thead>[\\n\s]+"
+            r"<tr>[\\n\s]+"
+            r"<th></th>[\\n\s]+"
+            r"<th>id</th>[\\n\s]+"
+            r"<th>author_email</th>[\\n\s]+"
+            r"<th>name</th>[\\n\s]+"
+            r"<th>published_date</th>[\\n\s]+"
+            r"</tr>[\\n\s]+"
+            "</thead>"
+        )
+        self.assertRegex(str(response.content), target_header_re)
+        # test row rendered in correct order
+        target_row_re = (
+            r'<tr class="new">[\\n\s]+'
+            r'<td class="import-type">[\\n\s]+New[\\n\s]+</td>[\\n\s]+'
+            r'<td><ins style="background:#e6ffe6;">1</ins></td>[\\n\s]+'
+            r'<td><ins style="background:#e6ffe6;">test@example.com</ins></td>[\\n\s]+'
+            r'<td><ins style="background:#e6ffe6;">Some book</ins></td>[\\n\s]+'
+            r"<td><span>None</span></td>[\\n\s]+"
+            "</tr>"
+        )
+        self.assertRegex(str(response.content), target_row_re)
diff --git a/tests/core/tests/test_resources/test_import_export.py b/tests/core/tests/test_resources/test_import_export.py
index 523ff0ca0..6c9f211e8 100644
--- a/tests/core/tests/test_resources/test_import_export.py
+++ b/tests/core/tests/test_resources/test_import_export.py
@@ -2,11 +2,13 @@
 from unittest.mock import patch
 
 import tablib
-from core.models import Author, Book, Category
+from core.models import Author, Book, Category, EBook
 from core.tests.resources import AuthorResource, BookResource
 from django.test import TestCase
 
 from import_export import exceptions, fields, resources, widgets
+from import_export.fields import Field
+from import_export.resources import ModelResource
 
 
 class AfterImportComparisonTest(TestCase):
@@ -372,3 +374,32 @@ def test_import_row_with_no_defined_id_field(self):
         self.resource = AuthorResource()
         self.resource.import_data(dataset)
         self.assertEqual(1, Author.objects.count())
+
+
+class CustomColumnNameImportTest(TestCase):
+    """
+    If a custom field is declared, import should work if either the Field's
+    attribute name or column name is referenced in the ``fields`` list (issue 1815).
+    """
+
+    fixtures = ["author"]
+
+    class _EBookResource(ModelResource):
+        published = Field(attribute="published", column_name="published_date")
+
+        class Meta:
+            model = EBook
+            fields = ("id", "name", "published_date")
+
+    def setUp(self):
+        super().setUp()
+        self.resource = CustomColumnNameImportTest._EBookResource()
+
+    def test_import_with_column_alias_in_fields_list(self):
+        self.assertEqual(0, EBook.objects.count())
+        dataset = tablib.Dataset(
+            *[(1, "Moonraker", "1955-04-05")], headers=["id", "name", "published_date"]
+        )
+        self.resource.import_data(dataset, raise_errors=True)
+        self.assertEqual(1, EBook.objects.count())
+        self.assertEqual(date(1955, 4, 5), EBook.objects.first().published)