csv
All checks were successful
Deploy to Server / deploy (push) Successful in 26s

This commit is contained in:
jeremygan2021
2026-02-28 11:39:40 +08:00
parent 1b5751a065
commit a5a7e1e03a
3 changed files with 101 additions and 227 deletions

View File

@@ -37,58 +37,6 @@ def get_signup_info_keys(queryset):
keys.update(flat_info.keys()) keys.update(flat_info.keys())
return sorted(list(keys)) return sorted(list(keys))
def get_row_data(obj):
"""
Build a dictionary of all exportable data for a single ActivitySignup object
"""
data = {}
# 1. 基础信息
data['ID'] = str(obj.id)
data['活动标题'] = obj.activity.title
data['报名时间'] = obj.signup_time
data['状态'] = obj.get_status_display()
# 2. 用户信息
if obj.user:
data['用户昵称'] = obj.user.nickname
data['用户ID'] = str(obj.user.id)
data['用户OpenID'] = obj.user.openid
data['用户绑定手机'] = obj.user.phone_number or ''
data['用户地区'] = f"{obj.user.country} {obj.user.province} {obj.user.city}".strip()
data['用户注册时间'] = obj.user.created_at
else:
data['用户昵称'] = 'Unknown'
data['用户ID'] = ''
data['用户OpenID'] = ''
data['用户绑定手机'] = ''
data['用户地区'] = ''
data['用户注册时间'] = ''
# 3. 订单/发货信息
if obj.order:
data['关联订单ID'] = str(obj.order.id)
data['订单状态'] = obj.order.get_status_display()
data['收货人姓名'] = obj.order.customer_name
data['收货电话'] = obj.order.phone_number
data['收货地址'] = obj.order.shipping_address
data['快递公司'] = obj.order.courier_name or ''
data['快递单号'] = obj.order.tracking_number or ''
data['订单总价'] = str(obj.order.total_price)
data['商户订单号'] = obj.order.out_trade_no or ''
else:
data['关联订单ID'] = ''
data['订单状态'] = ''
data['收货人姓名'] = ''
data['收货电话'] = ''
data['收货地址'] = ''
data['快递公司'] = ''
data['快递单号'] = ''
data['订单总价'] = ''
data['商户订单号'] = ''
return data
def export_signups_csv(modeladmin, request, queryset): def export_signups_csv(modeladmin, request, queryset):
""" """
Export selected signups to CSV, including flattened JSON fields Export selected signups to CSV, including flattened JSON fields
@@ -101,30 +49,26 @@ def export_signups_csv(modeladmin, request, queryset):
writer = csv.writer(response) writer = csv.writer(response)
# Fixed headers # Base fields to export
fixed_headers = [ base_headers = ['ID', '活动标题', '用户昵称', '用户ID', '报名时间', '状态', '关联订单ID']
'ID', '活动标题', '状态', '报名时间',
'用户ID', '用户昵称', '用户OpenID', '用户绑定手机', '用户地区', '用户注册时间',
'关联订单ID', '订单状态', '收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号', '订单总价', '商户订单号'
]
# Get dynamic JSON keys # Get dynamic JSON keys
json_keys = get_signup_info_keys(queryset) json_keys = get_signup_info_keys(queryset)
# Write header # Write header
writer.writerow(fixed_headers + json_keys) writer.writerow(base_headers + json_keys)
# Write data # Write data
for obj in queryset: for obj in queryset:
row_data = get_row_data(obj) row = [
str(obj.id),
# Build the row based on fixed_headers order obj.activity.title,
row = [] obj.user.nickname if obj.user else 'Unknown',
for header in fixed_headers: str(obj.user.id) if obj.user else '',
val = row_data.get(header, '') obj.signup_time.strftime('%Y-%m-%d %H:%M:%S'),
if isinstance(val, (datetime.datetime, datetime.date)): obj.get_status_display(),
val = val.strftime('%Y-%m-%d %H:%M:%S') str(obj.order.id) if obj.order else ''
row.append(str(val)) ]
# Add JSON data # Add JSON data
flat_info = {} flat_info = {}
@@ -141,7 +85,7 @@ def export_signups_csv(modeladmin, request, queryset):
return response return response
export_signups_csv.short_description = "导出选中报名记录为 CSV (含发货/详细信息)" export_signups_csv.short_description = "导出选中报名记录为 CSV (含详细信息)"
def export_signups_excel(modeladmin, request, queryset): def export_signups_excel(modeladmin, request, queryset):
""" """
@@ -165,38 +109,27 @@ def export_signups_excel(modeladmin, request, queryset):
ws = wb.active ws = wb.active
ws.title = str(opts.verbose_name)[:31] # Sheet name limit is 31 chars ws.title = str(opts.verbose_name)[:31] # Sheet name limit is 31 chars
# Fixed headers # Base fields to export
fixed_headers = [ base_headers = ['ID', '活动标题', '用户昵称', '用户ID', '报名时间', '状态', '关联订单ID']
'ID', '活动标题', '状态', '报名时间',
'用户ID', '用户昵称', '用户OpenID', '用户绑定手机', '用户地区', '用户注册时间',
'关联订单ID', '订单状态', '收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号', '订单总价', '商户订单号'
]
# Get dynamic JSON keys # Get dynamic JSON keys
json_keys = get_signup_info_keys(queryset) json_keys = get_signup_info_keys(queryset)
# Write header # Write header
ws.append(fixed_headers + json_keys) ws.append(base_headers + json_keys)
# Write data # Write data
for obj in queryset: for obj in queryset:
row_data = get_row_data(obj) row = [
obj.id,
obj.activity.title,
obj.user.nickname if obj.user else 'Unknown',
obj.user.id if obj.user else '',
obj.signup_time.replace(tzinfo=None) if obj.signup_time else '', # Remove tz for Excel
obj.get_status_display(),
obj.order.id if obj.order else ''
]
row = []
for header in fixed_headers:
val = row_data.get(header, '')
# Excel handles datetime natively, but we need to remove timezone info if present to avoid Excel errors or warnings depending on version
# Here we convert to naive datetime for simplicity or keep as is if library handles it.
# openpyxl supports datetime, but usually it's safer to remove tzinfo for compatibility.
if isinstance(val, (datetime.datetime, datetime.date)):
if hasattr(val, 'replace'):
val = val.replace(tzinfo=None)
# Ensure None becomes empty string
if val is None:
val = ""
row.append(val)
# Add JSON data # Add JSON data
flat_info = {} flat_info = {}
if obj.signup_info and isinstance(obj.signup_info, dict): if obj.signup_info and isinstance(obj.signup_info, dict):
@@ -206,11 +139,11 @@ def export_signups_excel(modeladmin, request, queryset):
val = flat_info.get(key, '') val = flat_info.get(key, '')
if val is None: if val is None:
val = '' val = ''
row.append(str(val)) # JSON values as strings row.append(str(val)) # Ensure string for simplicity, or handle types
ws.append(row) ws.append(row)
wb.save(response) wb.save(response)
return response return response
export_signups_excel.short_description = "导出选中报名记录为 Excel (含发货/详细信息)" export_signups_excel.short_description = "导出选中报名记录为 Excel (含详细信息)"

View File

@@ -7,7 +7,7 @@ from django.shortcuts import redirect
from unfold.admin import ModelAdmin, TabularInline from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display from unfold.decorators import display
from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VCCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder, CourseEnrollment, AdminPhoneNumber from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VCCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder, CourseEnrollment, AdminPhoneNumber
from .admin_actions import export_orders_excel from .admin_actions import export_to_csv, export_to_excel
import qrcode import qrcode
from io import BytesIO from io import BytesIO
import base64 import base64
@@ -403,7 +403,7 @@ class OrderAdmin(ModelAdmin):
list_filter = ('status', 'salesperson', 'distributor', 'created_at') list_filter = ('status', 'salesperson', 'distributor', 'created_at')
search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no') search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no')
readonly_fields = ('total_price', 'created_at', 'wechat_trade_no') readonly_fields = ('total_price', 'created_at', 'wechat_trade_no')
actions = [export_orders_excel] actions = [export_to_csv, export_to_excel]
def get_item_name(self, obj): def get_item_name(self, obj):
if obj.config: if obj.config:
@@ -472,6 +472,7 @@ class WeChatUserAdmin(OrderableAdminMixin, ModelAdmin):
search_fields = ('nickname', 'openid', 'phone_number') search_fields = ('nickname', 'openid', 'phone_number')
list_filter = ('is_star', GenderFilter, UserSourceFilter, 'province', 'city', 'created_at') list_filter = ('is_star', GenderFilter, UserSourceFilter, 'province', 'city', 'created_at')
readonly_fields = ('openid', 'unionid', 'session_key', 'created_at', 'updated_at') readonly_fields = ('openid', 'unionid', 'session_key', 'created_at', 'updated_at')
actions = [export_to_csv, export_to_excel]
def avatar_display(self, obj): def avatar_display(self, obj):
if obj.avatar_url: if obj.avatar_url:

View File

@@ -1,124 +1,71 @@
import csv import csv
import json
import datetime import datetime
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.encoding import escape_uri_path from django.utils.encoding import escape_uri_path
def flatten_json(y): def export_to_csv(modeladmin, request, queryset):
""" """
Flatten a nested json object 通用导出 CSV 的 Admin Action
支持中文编码UTF-8 BOM可直接用 Excel 打开
""" """
out = {} opts = modeladmin.model._meta
# 设置文件名,使用模型的 verbose_name
def flatten(x, name=''): filename = f"{opts.verbose_name}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
if type(x) is dict:
for a in x:
flatten(x[a], name + a + '_')
elif type(x) is list:
i = 0
for a in x:
flatten(a, name + str(i) + '_')
i += 1
else:
out[name[:-1]] = x
flatten(y)
return out
def get_order_signup_keys(queryset):
"""
Collect all unique keys from the signup_info JSON of ActivitySignups related to the Order queryset
"""
keys = set()
for order in queryset:
# Check if order has related activity signups
# Assuming reverse relation name is 'activity_signups' based on ActivitySignup model
signups = order.activity_signups.all()
for signup in signups:
if signup.signup_info and isinstance(signup.signup_info, dict):
flat_info = flatten_json(signup.signup_info)
keys.update(flat_info.keys())
return sorted(list(keys))
def get_order_row_data(order):
"""
Build a dictionary of all exportable data for a single Order object
"""
data = {}
# 1. 订单基础信息 response = HttpResponse(content_type='text/csv; charset=utf-8-sig')
data['订单ID'] = str(order.id) response['Content-Disposition'] = f'attachment; filename={escape_uri_path(filename)}'
data['商户订单号'] = order.out_trade_no or ''
data['微信支付单号'] = order.wechat_trade_no or ''
data['订单状态'] = order.get_status_display()
data['订单总价'] = str(order.total_price)
data['购买数量'] = str(order.quantity)
data['创建时间'] = order.created_at.strftime('%Y-%m-%d %H:%M:%S')
# 商品信息 writer = csv.writer(response)
if order.config:
data['商品类型'] = '硬件' # 获取所有非多对多字段和非反向关联字段
data['商品名称'] = order.config.name fields = [field for field in opts.get_fields() if not field.many_to_many and not field.one_to_many]
elif order.course:
data['商品类型'] = '课程' # 写入表头 (使用字段的 verbose_name)
data['商品名称'] = order.course.title writer.writerow([field.verbose_name for field in fields])
elif order.activity:
data['商品类型'] = '活动' # 写入数据
data['商品名称'] = order.activity.title for obj in queryset:
else: data_row = []
data['商品类型'] = '未知' for field in fields:
data['商品名称'] = '未知' value = getattr(obj, field.name)
# 处理 Choice 字段,显示可读的标签
if hasattr(obj, f'get_{field.name}_display'):
value = getattr(obj, f'get_{field.name}_display')()
# 处理关联对象ForeignKey
if field.is_relation and value:
value = str(value)
# 2. 发货/收货信息 # 处理日期时间
data['收货人姓名'] = order.customer_name if isinstance(value, datetime.datetime):
data['收货电话'] = order.phone_number value = value.strftime('%Y-%m-%d %H:%M:%S')
data['收货地址'] = order.shipping_address elif isinstance(value, datetime.date):
data['快递公司'] = order.courier_name or '' value = value.strftime('%Y-%m-%d')
data['快递单号'] = order.tracking_number or ''
# 处理 None
if value is None:
value = ""
data_row.append(str(value))
writer.writerow(data_row)
return response
# 3. 下单用户信息 export_to_csv.short_description = "导出选中项为 CSV"
if order.wechat_user:
data['用户昵称'] = order.wechat_user.nickname
data['用户ID'] = str(order.wechat_user.id)
data['用户OpenID'] = order.wechat_user.openid
data['用户绑定手机'] = order.wechat_user.phone_number or ''
else:
data['用户昵称'] = 'Unknown'
data['用户ID'] = ''
data['用户OpenID'] = ''
data['用户绑定手机'] = ''
# 4. 销售/分销信息 def export_to_excel(modeladmin, request, queryset):
data['销售员'] = order.salesperson.name if order.salesperson else ''
data['分销员'] = order.distributor.user.nickname if order.distributor else ''
return data
def get_order_signup_data(order):
""" """
Get flattened signup info for the order. 导出为 Excel (需要安装 openpyxl)
If multiple signups exist, we only take the first one for simplicity in flat export,
or we could try to merge them but keys might conflict.
Given the context, usually 1 order -> 1 signup for activity.
"""
flat_info = {}
signup = order.activity_signups.first()
if signup and signup.signup_info and isinstance(signup.signup_info, dict):
flat_info = flatten_json(signup.signup_info)
return flat_info
def export_orders_excel(modeladmin, request, queryset):
"""
Export selected orders to Excel, including related Activity Signup JSON info
""" """
try: try:
from openpyxl import Workbook from openpyxl import Workbook
except ImportError: except ImportError:
modeladmin.message_user(request, "请先安装 openpyxl 库以使用 Excel 导出功能", level='error') modeladmin.message_user(request, "请先安装 openpyxl 库以使用 Excel 导出功能: pip install openpyxl", level='error')
return return
opts = modeladmin.model._meta opts = modeladmin.model._meta
filename = f"Orders_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" filename = f"{opts.verbose_name}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
response = HttpResponse( response = HttpResponse(
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
@@ -127,44 +74,37 @@ def export_orders_excel(modeladmin, request, queryset):
wb = Workbook() wb = Workbook()
ws = wb.active ws = wb.active
ws.title = "订单导出" # Sheet name limit is 31 chars
ws.title = str(opts.verbose_name)[:31]
# Fixed headers fields = [field for field in opts.get_fields() if not field.many_to_many and not field.one_to_many]
fixed_headers = [
'订单ID', '商户订单号', '微信支付单号', '订单状态', '订单总价', '购买数量', '创建时间',
'商品类型', '商品名称',
'收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号',
'用户ID', '用户昵称', '用户OpenID', '用户绑定手机',
'销售员', '分销员'
]
# Dynamic headers from ActivitySignup (only if orders contain activity orders) # 写入表头
json_keys = get_order_signup_keys(queryset) ws.append([str(field.verbose_name) for field in fields])
# Write header # 写入数据
# Add a prefix to json keys to distinguish them? Or keep as is. for obj in queryset:
# Let's keep as is but maybe add a note in header if needed.
ws.append(fixed_headers + json_keys)
# Write data
for order in queryset:
row_data = get_order_row_data(order)
signup_data = get_order_signup_data(order)
row = [] row = []
for header in fixed_headers: for field in fields:
val = row_data.get(header, '') value = getattr(obj, field.name)
row.append(str(val)) # Convert everything to string for safety
for key in json_keys: if hasattr(obj, f'get_{field.name}_display'):
val = signup_data.get(key, '') value = getattr(obj, f'get_{field.name}_display')()
if val is None:
val = ''
row.append(str(val))
# 处理关联对象ForeignKey
if field.is_relation and value:
value = str(value)
if isinstance(value, (datetime.datetime, datetime.date)):
# openpyxl 可以直接处理 datetime 格式Excel 会自动识别
# 但为了避免时区问题,通常转为无时区时间或字符串
if isinstance(value, datetime.datetime):
value = value.replace(tzinfo=None)
row.append(value)
ws.append(row) ws.append(row)
wb.save(response) wb.save(response)
return response return response
export_orders_excel.short_description = "导出选中订单为 Excel (含报名信息)" export_to_excel.short_description = "导出选中为 Excel"