Repository: django-import-export/django-import-export

Issue:
Exporting fields with changed `column_name` doesn't work in v4
**Describe the bug**
Fields like `email = Field(column_name="recipientEmail")` from my `ModelResource` are missing in the CSV.

**To Reproduce**
Steps to reproduce the behavior:
1. Add `ModelResource` like:
```python
class WisePayoutResource(ModelResource):
    note_recipient = Field(column_name="name")
    email = Field(column_name="recipientEmail")
    id = Field(column_name="paymentRefernece")
    reciever_type = Field(column_name="recieverType")
    amount_currency = Field(column_name="amountCurrency")
    amount = Field(column_name="amount")
    source_currency = Field(column_name="sourceCurrency")
    target_currency = Field(column_name="targetCurrency")
    type = Field(column_name="type")

    class Meta:
        model = models.Payout
        export_order = (
            "note_recipient",
            "email",
            "id",
            "reciever_type",
            "amount_currency",
            "amount",
            "source_currency",
            "target_currency",
            "type",
        )

    def dehydrate_source_currency(self, payout):
        return "USD"

    def dehydrate_target_currency(self, payout):
        return "USD"

    def dehydrate_amount_currency(self, payout):
        return "source"

    def dehydrate_note_recipient(self, payout):
        return NOTE_RECIPIENT

    def dehydrate_reciever_type(self, payout):
        return "PERSON"

    def dehydrate_email(self, payout):
        return payout.get_payout_data()["payout_email"]

    def dehydrate_amount(self, payout):
        return payout.amount

    def dehydrate_id(self, payout):
        return payout.id

    def dehydrate_type(self, payout):
        return "EMAIL"

```

2. Export the table (e.g. Payout) with this resource and all columns enabled.
3. In the resulting CSV are only columns `amount` and `type` (those that didn't change name of the column).

**Versions (please complete the following information):**
 - Django Import Export: 4.0.3
 - Python 3.11
 - Django 4.2

**Expected behavior**
All columns should be exported.

**Additional context**
Seems that the column name is not correctly matched in this case. @matthewhegarty I expect this would be easy for you to find the problem and fix it, but if not I can do some more digging and fixing.


Discussion:
@PetrDlouhy - I can't reproduce it exactly in the way you have it.

- What happens if you declare a `fields` list with the fields you want in the export?
- I have a test case [here](https://github.com/django-import-export/django-import-export/blob/88eb0d3761b67a03c95515ce8ab46e11b7aebf66/tests/core/tests/test_resources/test_import_export.py#L256)
  - I find that if I remove the `fields` declaration, I get all Resource fields in the export. 
  - Are you able to reproduce using this test case?

@matthewhegarty I think, that test is too specific for to test the actual problem. The test runs only `ModelResource.export()`, but the problem would probably lie in the admin form handling these field checkboxes:
![Snímek obrazovky_2024-05-27_16-02-11](https://github.com/django-import-export/django-import-export/assets/156755/83672457-cce8-488a-bfaa-ff98ec3ec190)

I expect that it behaves like if the checkbox for a field was not selected.

I will try to investigate further and make the appropriate test.

Resolving patch:
diff --git a/import_export/declarative.py b/import_export/declarative.py
--- a/import_export/declarative.py
+++ b/import_export/declarative.py
@@ -51,6 +51,8 @@ def _load_meta_options(base_, meta_):
                 field = attrs.pop(field_name)
                 if not field.column_name:
                     field.column_name = field_name
+                if not field.attribute:
+                    field.attribute = field_name
                 declared_fields.append((field_name, field))
 
         attrs["fields"] = OrderedDict(declared_fields)
diff --git a/import_export/fields.py b/import_export/fields.py
--- a/import_export/fields.py
+++ b/import_export/fields.py
@@ -100,7 +100,7 @@ def get_value(self, instance):
         Returns the value of the instance's attribute.
         """
         if self.attribute is None:
-            return None
+            raise AttributeError("Field attribute cannot be null")
 
         attrs = self.attribute.split("__")
         value = instance
diff --git a/import_export/resources.py b/import_export/resources.py
--- a/import_export/resources.py
+++ b/import_export/resources.py
@@ -419,9 +419,6 @@ def import_field(self, field, instance, row, is_m2m=False, **kwargs):
         :param \**kwargs:
             See :meth:`import_row`
         """
-        if not field.attribute:
-            logger.debug(f"skipping field '{field}' - field attribute is not defined")
-            return
         if field.column_name not in row:
             logger.debug(
                 f"skipping field '{field}' "


Test patch:
diff --git a/tests/core/admin.py b/tests/core/admin.py
--- a/tests/core/admin.py
+++ b/tests/core/admin.py
@@ -61,6 +61,7 @@ class Meta:
 
 class EBookResource(ModelResource):
     published = Field(attribute="published", column_name="published_date")
+    author_email = Field(attribute="author_email", column_name="Email of the author")
 
     def __init__(self, **kwargs):
         super().__init__()
diff --git a/tests/core/exports/ebooks.csv b/tests/core/exports/ebooks.csv
new file mode 100644
--- /dev/null
+++ b/tests/core/exports/ebooks.csv
@@ -0,0 +1,2 @@
+id,name,Email of the author
+1,Some book,test@example.com
diff --git a/tests/core/tests/admin_integration/test_export.py b/tests/core/tests/admin_integration/test_export.py
--- a/tests/core/tests/admin_integration/test_export.py
+++ b/tests/core/tests/admin_integration/test_export.py
@@ -348,7 +348,9 @@ def test_export_with_custom_field(self):
             response["Content-Disposition"],
             'attachment; filename="EBook-{}.csv"'.format(date_str),
         )
-        self.assertEqual(b"id,author_email,name,published_date\r\n", response.content)
+        self.assertEqual(
+            b"id,Email of the author,name,published_date\r\n", response.content
+        )
 
 
 class FilteredExportAdminIntegrationTest(AdminTestMixin, TestCase):
@@ -376,7 +378,7 @@ def test_export_filters_by_form_param(self):
             'attachment; filename="EBook-{}.csv"'.format(date_str),
         )
         self.assertEqual(
-            b"id,author_email,name,published_date\r\n"
+            b"id,Email of the author,name,published_date\r\n"
             b"5,ian@example.com,The Man with the Golden Gun,1965-04-01\r\n",
             response.content,
         )
@@ -521,7 +523,7 @@ def test_export_with_custom_field(self):
             'attachment; filename="EBook-{}.csv"'.format(date_str),
         )
         s = (
-            "id,author_email,name,published_date\r\n"
+            "id,Email of the author,name,published_date\r\n"
             f"{book.id},,Moonraker,1955-04-05\r\n"
         )
         self.assertEqual(str.encode(s), response.content)
diff --git a/tests/core/tests/admin_integration/test_import.py b/tests/core/tests/admin_integration/test_import.py
--- a/tests/core/tests/admin_integration/test_import.py
+++ b/tests/core/tests/admin_integration/test_import.py
@@ -932,7 +932,7 @@ def test_import_preview_order(self):
         author_id = Author.objects.first().id
         response = self._do_import_post(
             self.ebook_import_url,
-            "books.csv",
+            "ebooks.csv",
             input_format="0",
             data={"author": author_id},
         )
@@ -942,7 +942,7 @@ def test_import_preview_order(self):
             r"<tr>[\\n\s]+"
             r"<th></th>[\\n\s]+"
             r"<th>id</th>[\\n\s]+"
-            r"<th>author_email</th>[\\n\s]+"
+            r"<th>Email of the author</th>[\\n\s]+"
             r"<th>name</th>[\\n\s]+"
             r"<th>published_date</th>[\\n\s]+"
             r"</tr>[\\n\s]+"
@@ -979,7 +979,7 @@ def test_import_preview_order(self):
         author_id = Author.objects.first().id
         response = self._do_import_post(
             self.ebook_import_url,
-            "books.csv",
+            "ebooks.csv",
             input_format="0",
             data={"author": author_id},
         )
@@ -989,7 +989,7 @@ def test_import_preview_order(self):
             r"<tr>[\\n\s]+"
             r"<th></th>[\\n\s]+"
             r"<th>id</th>[\\n\s]+"
-            r"<th>author_email</th>[\\n\s]+"
+            r"<th>Email of the author</th>[\\n\s]+"
             r"<th>name</th>[\\n\s]+"
             r"<th>published_date</th>[\\n\s]+"
             r"</tr>[\\n\s]+"
@@ -1021,7 +1021,7 @@ def test_import_preview_order(self):
         # test display rendered in correct order
         target_re = (
             r"This importer will import the following fields:[\\n\s]+"
-            r"<code>id, author_email, name, published_date</code>[\\n\s]+"
+            r"<code>id, Email of the author, name, published_date</code>[\\n\s]+"
         )
         self.assertRegex(str(response.content), target_re)
 
@@ -1046,6 +1046,6 @@ def test_import_preview_order(self):
         # test display rendered in correct order
         target_re = (
             r"This importer will import the following fields:[\\n\s]+"
-            r"<code>id, name, published_date, author_email</code>[\\n\s]+"
+            r"<code>id, name, published_date, Email of the author</code>[\\n\s]+"
         )
         self.assertRegex(str(response.content), target_re)
diff --git a/tests/core/tests/test_fields.py b/tests/core/tests/test_fields.py
--- a/tests/core/tests/test_fields.py
+++ b/tests/core/tests/test_fields.py
@@ -151,3 +151,9 @@ def __call__(self):
         self.obj.name = CallableValue()
         val = self.field.get_value(self.obj)
         self.assertEqual("some val", val)
+
+    def test_get_value_with_no_attribute(self):
+        self.field.attribute = None
+        with self.assertRaises(AttributeError) as e:
+            self.field.get_value(self.obj)
+        self.assertEqual("Field attribute cannot be null", str(e.exception))
diff --git a/tests/core/tests/test_resources/test_import_export.py b/tests/core/tests/test_resources/test_import_export.py
--- a/tests/core/tests/test_resources/test_import_export.py
+++ b/tests/core/tests/test_resources/test_import_export.py
@@ -255,7 +255,7 @@ def dehydrate_author_full_name(self, obj):
         self.assertEqual(self.resource.get_export_order(), self.resource._meta.fields)
 
     def test_declared_field_export_order(self):
-        # issue 1846
+        # issue 1848
         class DeclaredModelFieldBookResource(
             ImportExportFieldOrderTest.BaseBookResource
         ):
@@ -283,6 +283,39 @@ class Meta:
         target = f"date published,id,author\r\n,{self.pk},\r\n"
         self.assertEqual(target, data.csv)
 
+    def test_export_fields_column_name(self):
+        """Test export with declared export_fields and custom column_name"""
+
+        # issue 1846
+        class DeclaredModelFieldBookResource(resources.ModelResource):
+            published = fields.Field(
+                attribute="published",
+                column_name="datePublished",
+                widget=widgets.DateWidget("%d.%m.%Y"),
+            )
+            author = fields.Field(column_name="AuthorFooName")
+
+            class Meta:
+                model = Book
+                fields = (
+                    "id",
+                    "author",
+                    "published",
+                )
+                export_order = (
+                    "published",
+                    "id",
+                    "author",
+                )
+
+            def dehydrate_author(self, obj):
+                return obj.author
+
+        self.resource = DeclaredModelFieldBookResource()
+        data = self.resource.export()
+        target = f"datePublished,id,AuthorFooName\r\n,{self.pk},\r\n"
+        self.assertEqual(target, data.csv)
+
 
 class ImportIdFieldsTestCase(TestCase):
     class BookResource(resources.ModelResource):
@@ -384,27 +417,6 @@ class Meta:
 
 class ImportWithMissingFields(TestCase):
     # issue 1517
-
-    @patch("import_export.resources.logger")
-    @patch("import_export.fields.Field.save")
-    def test_import_with_missing_instance_attribute(self, mock_field_save, mock_logger):
-        class _BookResource(resources.ModelResource):
-            name = fields.Field(column_name="name")
-
-            class Meta:
-                model = Book
-
-        dataset = tablib.Dataset(*[(1, "Some book")], headers=["id", "name"])
-        self.resource = _BookResource()
-        result = self.resource.import_data(dataset)
-        self.assertFalse(result.has_errors())
-        target = (
-            "skipping field '<import_export.fields.Field: name>' "
-            "- field attribute is not defined"
-        )
-        mock_logger.debug.assert_any_call(target)
-        self.assertEqual(1, mock_field_save.call_count)
-
     @patch("import_export.resources.logger")
     @patch("import_export.fields.Field.save")
     def test_import_with_missing_field_in_row(self, mock_field_save, mock_logger):
diff --git a/tests/core/tests/test_resources/test_modelresource/test_dynamic_customization.py b/tests/core/tests/test_resources/test_modelresource/test_dynamic_customization.py
--- a/tests/core/tests/test_resources/test_modelresource/test_dynamic_customization.py
+++ b/tests/core/tests/test_resources/test_modelresource/test_dynamic_customization.py
@@ -118,9 +118,7 @@ def before_import(self, dataset, **kwargs):
 
     def test_after_import_raises_error(self):
         class B(BookResource):
-            def after_import(
-                self, dataset, result, using_transactions, dry_run, **kwargs
-            ):
+            def after_import(self, dataset, result, **kwargs):
                 raise Exception("This is an invalid dataset")
 
         resource = B()
@@ -187,7 +185,7 @@ def field_from_django_field(self, field_name, django_field, readonly):
         self.assertEqual({"sound": "quack"}, B.fields["published"])
 
     def test_readonly_annotated_field_import_and_export(self):
-        class B(BookResource):
+        class B(resources.ModelResource):
             total_categories = fields.Field("total_categories", readonly=True)
 
             class Meta:
diff --git a/tests/core/tests/test_resources/test_modelresource/test_export.py b/tests/core/tests/test_resources/test_modelresource/test_export.py
--- a/tests/core/tests/test_resources/test_modelresource/test_export.py
+++ b/tests/core/tests/test_resources/test_modelresource/test_export.py
@@ -1,8 +1,13 @@
+from datetime import date
+
 import tablib
 from core.admin import BookResource
-from core.models import Author, Book
+from core.models import Author, Book, EBook
 from django.test import TestCase
 
+from import_export.fields import Field
+from import_export.resources import ModelResource
+
 
 class ExportFunctionalityTest(TestCase):
     def setUp(self):
@@ -75,3 +80,20 @@ def before_export(self, queryset, **kwargs):
         self.resource.export(queryset=Book.objects.all(), **{"a": 1})
         self.assertEqual(Book.objects.count(), len(self.resource.qs))
         self.assertEqual(dict(a=1), self.resource.kwargs_)
+
+    def test_export_declared_field(self):
+        # test that declared fields have the correct widget set
+        # #1860
+        class EBookResource(ModelResource):
+            published = Field(column_name="published")
+
+            class Meta:
+                model = EBook
+                fields = ("id", "published")
+
+        resource = EBookResource()
+
+        self.book.published = date(1955, 4, 5)
+        self.book.save()
+        dataset = resource.export()
+        self.assertEqual("1955-04-05", dataset.dict[0]["published"])