Repository: fossasia/open-event-server

Issue:
Add the unique ticket code into the downlad CSV file
The CSV download file of the attendee list does not include the numbers on the QR Code. Please add this field "Ticket-ID".

The ticket ID has the following format: 135ccbd7-9b23-4a52-a7fd-326fec1b2c1c
Whereas the order has a format like this: #O1691408152-34896

![Screenshot from 2024-03-18 11-40-16](https://github.com/fossasia/open-event-server/assets/1583873/97568f6a-bd5a-4706-899f-64400c2f4da6)

Expected: The exported CSV should have a table column "Ticket ID" with the ticket ID number that is encoded in the QR code as well.

![Screenshot from 2024-03-18 11-48-30](https://github.com/fossasia/open-event-server/assets/1583873/be8e84dd-7088-4a71-8d03-a89d4414904e)



Discussion:
Hi @mariobehling

I have a query:
When will the ticket ID column data be generate? when the ticket is created by events or when the attendee purchasing the ticket?

Please help me confirm it. Thanks
> I have a query: When will the ticket ID column data be generate? when the ticket is created by events or when the attendee purchasing the ticket?
> 
> Please help me confirm it. Thanks

The CSV is generated when the user (organiser) clicks "Export as CSV". Into this CSV generation process one column ticket ID is missing.

As you can see in the screenshot below. The web UI already shows the ticket ID in the table column "Order". Every order has an order number (it can be several tickets in one order) and every ticket has a ticket ID (the long string that we see also in the table).

![Screenshot from 2024-03-19 10-17-44](https://github.com/fossasia/open-event-server/assets/1583873/27d1f7d8-3583-4a81-841a-29474653c697)




Resolving patch:
diff --git a/app/api/helpers/csv_jobs_util.py b/app/api/helpers/csv_jobs_util.py
--- a/app/api/helpers/csv_jobs_util.py
+++ b/app/api/helpers/csv_jobs_util.py
@@ -82,75 +82,127 @@ def export_orders_csv(orders):
     return rows
 
 
-def export_attendees_csv(attendees, custom_forms, attendee_form_dict):
-    return_dict_list = []
+def get_order_ticket_data(order, ticket):
+    """Get order ticket data"""
+    data = {}
+    if not order:
+        return {
+            'Order#': '',
+            'Order Date': '',
+            'Status': '',
+            'Payment Type': '',
+            'Payment Mode': '',
+            'Ticket ID': '',
+            'Ticket Name': '',
+            'Ticket Price': '0',
+            'Ticket Type': '',
+            'Tax ID': '',
+            'Address': '',
+            'Company': '',
+            'Country': '',
+            'State': '',
+            'City': '',
+            'Zipcode': '',
+        }
 
-    for attendee in attendees:
-        data = {
-            'Order#': str(attendee.order.get_invoice_number()) if attendee.order else '-',
-            'Order Date': str(attendee.order.created_at.strftime('%B %-d, %Y %H:%M %z'))
-            if attendee.order and attendee.order.created_at
-            else '-',
-            'Status': str(attendee.order.status)
-            if attendee.order and attendee.order.status
-            else '-',
-            'Payment Type': str(attendee.order.paid_via)
-            if attendee.order and attendee.order.paid_via
-            else '',
-            'Payment Mode': str(attendee.order.payment_mode)
-            if attendee.order and attendee.order.payment_mode
-            else '',
-            'Ticket Name': str(attendee.ticket.name)
-            if attendee.ticket and attendee.ticket.name
-            else '',
-            'Ticket Price': str(attendee.ticket.price)
-            if attendee.ticket and attendee.ticket.price
-            else '0',
-            'Ticket Type': str(attendee.ticket.type)
-            if attendee.ticket and attendee.ticket.type
-            else '',
-            'Tax ID': str(attendee.order.tax_business_info)
-            if attendee.order.tax_business_info
-            else '',
-            'Address': str(attendee.order.address) if attendee.order.address else '',
-            'Company': str(attendee.order.company) if attendee.order.company else '',
-            'Country': str(attendee.order.country) if attendee.order.country else '',
-            'State': str(attendee.order.state) if attendee.order.state else '',
-            'City': str(attendee.order.city) if attendee.order.city else '',
-            'Zipcode': str(attendee.order.zipcode) if attendee.order.zipcode else '',
-            'Email': '',
+    data = {
+        'Order#': str(order.get_invoice_number()),
+        'Order Date': str(order.created_at.strftime('%B %-d, %Y %H:%M %z'))
+        if order.created_at
+        else '-',
+        'Status': str(order.status) if order.status else '-',
+        'Payment Type': str(order.paid_via) if order.paid_via else '',
+        'Payment Mode': str(order.payment_mode) if order.payment_mode else '',
+        'Ticket ID': str(order.identifier) if order.identifier else '',
+    }
+
+    if ticket:
+        data.update(get_ticket_data(ticket))
+
+    data.update(get_order_data(order))
+
+    return data
+
+
+def get_order_data(order):
+    """Get order data from order object"""
+    if not order:
+        return {
+            'Tax ID': '',
+            'Address': '',
+            'Company': '',
+            'Country': '',
+            'State': '',
+            'City': '',
+            'Zipcode': '',
         }
 
-        for field in custom_forms:
-            # keys don't match up, for keys like
-            # acceptVideoRecording vs accept_video_recording ..
-            key_mapping = {}
-
-            for k in attendee_form_dict.keys():
-                key_mapping[k.replace("_", "").lower()] = k
-
-            field_raw = field.identifier.replace("_", "").lower()
-            key = key_mapping.get(field_raw)
-            converted_header = attendee_form_dict.get(key)
-            if field.is_complex:
-                fields_dict = attendee.complex_field_values
-                converted_header = field.name
-                data[converted_header] = (
-                    fields_dict.get(field.identifier, '') if fields_dict else ''
-                )
-            else:
-                dict_value = getattr(attendee, field.identifier, '')
-                dict_value = (
-                    "Yes"
-                    if str(dict_value) == "True"
-                    else "No"
-                    if str(dict_value) == "False"
-                    else dict_value
-                )
-                converted_header = field.name
-                data[converted_header] = dict_value
-        data['virtual_event_checkin_times'] = get_virtual_checkin_times(attendee.id)
-        return_dict_list.append(data)
+    return {
+        'Tax ID': str(order.tax_business_info) if order.tax_business_info else '',
+        'Address': str(order.address) if order.address else '',
+        'Company': str(order.company) if order.company else '',
+        'Country': str(order.country) if order.country else '',
+        'State': str(order.state) if order.state else '',
+        'City': str(order.city) if order.city else '',
+        'Zipcode': str(order.zipcode) if order.zipcode else '',
+    }
+
+
+def get_ticket_data(ticket):
+    """Get ticket data from ticket object"""
+    if not ticket:
+        return {'Ticket Name': '', 'Ticket Price': '0', 'Ticket Type': ''}
+
+    return {
+        'Ticket Name': str(ticket.name) if ticket.name else '',
+        'Ticket Price': str(ticket.price) if ticket.price else '0',
+        'Ticket Type': str(ticket.type) if ticket.type else '',
+    }
+
+
+def get_attendee_data(attendee, custom_forms, attendee_form_dict):
+    """Get attendee data from attendee object"""
+    order_ticket_data = get_order_ticket_data(attendee.order, attendee.ticket)
+    data = {
+        **order_ticket_data,
+        'Email': '',
+    }
+
+    for field in custom_forms:
+        key_mapping = {k.replace("_", "").lower(): k for k in attendee_form_dict.keys()}
+        field_raw = field.identifier.replace("_", "").lower()
+        key = key_mapping.get(field_raw)
+        converted_header = attendee_form_dict.get(key)
+
+        if field.is_complex:
+            fields_dict = attendee.complex_field_values
+            converted_header = field.name
+            data[converted_header] = (
+                fields_dict.get(field.identifier, '') if fields_dict else ''
+            )
+        else:
+            dict_value = getattr(attendee, field.identifier, '')
+            dict_value = (
+                "Yes"
+                if str(dict_value) == "True"
+                else "No"
+                if str(dict_value) == "False"
+                else dict_value
+            )
+            converted_header = field.name
+            data[converted_header] = dict_value
+
+    data['virtual_event_checkin_times'] = get_virtual_checkin_times(attendee.id)
+    return data
+
+
+def export_attendees_csv(attendees, custom_forms, attendee_form_dict):
+    """Export attendees csv"""
+    return_dict_list = []
+    for attendee in attendees:
+        attendee_data = get_attendee_data(attendee, custom_forms, attendee_form_dict)
+        if attendee_data:
+            return_dict_list.append(attendee_data)
 
     return return_dict_list
 
diff --git a/migrations/versions/rev-2023-08-17-15:38:43-bce7acfe5a4f_.py b/migrations/versions/rev-2023-08-17-15:38:43-bce7acfe5a4f_.py
--- a/migrations/versions/rev-2023-08-17-15:38:43-bce7acfe5a4f_.py
+++ b/migrations/versions/rev-2023-08-17-15:38:43-bce7acfe5a4f_.py
@@ -1,7 +1,7 @@
 """empty message
 
 Revision ID: bce7acfe5a4f
-Revises: 24271525a263
+Revises: 1af4cc4f7cd5
 Create Date: 2023-08-17 15:38:43.387065
 
 """
@@ -12,7 +12,7 @@
 
 # revision identifiers, used by Alembic.
 revision = 'bce7acfe5a4f'
-down_revision = '24271525a263'
+down_revision = '1af4cc4f7cd5'
 
 
 def upgrade():


Test patch:
diff --git a/tests/all/integration/api/helpers/test_csv_jobs_util.py b/tests/all/integration/api/helpers/test_csv_jobs_util.py
--- a/tests/all/integration/api/helpers/test_csv_jobs_util.py
+++ b/tests/all/integration/api/helpers/test_csv_jobs_util.py
@@ -1,17 +1,25 @@
 import unittest
 from datetime import datetime
 
-from app.api.helpers.csv_jobs_util import *
+import pytz
+
+from app.api.helpers.csv_jobs_util import (
+    export_attendees_csv,
+    export_orders_csv,
+    export_sessions_csv,
+    export_speakers_csv,
+)
 from app.models import db
+from app.models.custom_form import ATTENDEE_CUSTOM_FORM
 from tests.all.integration.auth_helper import create_user
 from tests.all.integration.utils import OpenEventTestCase
 from tests.factories import common
 from tests.factories.attendee import AttendeeFactory
 from tests.factories.custom_form import CustomFormFactory
+from tests.factories.event import EventFactoryBasic
 from tests.factories.order import OrderFactory
 from tests.factories.session import SessionSubFactory
 from tests.factories.speaker import SpeakerFactory
-from app.models.custom_form import ATTENDEE_CUSTOM_FORM
 
 
 class TestExportCSV(OpenEventTestCase):
@@ -19,7 +27,9 @@ def test_export_orders_csv(self):
         """Method to check the orders data export"""
 
         with self.app.test_request_context():
+            test_event = EventFactoryBasic()
             test_order = OrderFactory(created_at=datetime.now())
+            test_order.event = test_event
             test_order.amount = 2
             field_data = export_orders_csv([test_order])
             assert field_data[1][2] == 'initializing'
@@ -34,7 +44,8 @@ def test_export_attendees_csv(self):
             test_attendee.order = test_order
             custom_forms = CustomFormFactory()
             field_data = export_attendees_csv(
-                [test_attendee], [custom_forms], ATTENDEE_CUSTOM_FORM)
+                [test_attendee], [custom_forms], ATTENDEE_CUSTOM_FORM
+            )
             # new export_attendees_csv will return list of dictionary for csv_writer
             assert field_data[0].get("Tax ID") == "tax id"