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())
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):
"""
Export selected signups to CSV, including flattened JSON fields
@@ -101,30 +49,26 @@ def export_signups_csv(modeladmin, request, queryset):
writer = csv.writer(response)
# Fixed headers
fixed_headers = [
'ID', '活动标题', '状态', '报名时间',
'用户ID', '用户昵称', '用户OpenID', '用户绑定手机', '用户地区', '用户注册时间',
'关联订单ID', '订单状态', '收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号', '订单总价', '商户订单号'
]
# Base fields to export
base_headers = ['ID', '活动标题', '用户昵称', '用户ID', '报名时间', '状态', '关联订单ID']
# Get dynamic JSON keys
json_keys = get_signup_info_keys(queryset)
# Write header
writer.writerow(fixed_headers + json_keys)
writer.writerow(base_headers + json_keys)
# Write data
for obj in queryset:
row_data = get_row_data(obj)
# Build the row based on fixed_headers order
row = []
for header in fixed_headers:
val = row_data.get(header, '')
if isinstance(val, (datetime.datetime, datetime.date)):
val = val.strftime('%Y-%m-%d %H:%M:%S')
row.append(str(val))
row = [
str(obj.id),
obj.activity.title,
obj.user.nickname if obj.user else 'Unknown',
str(obj.user.id) if obj.user else '',
obj.signup_time.strftime('%Y-%m-%d %H:%M:%S'),
obj.get_status_display(),
str(obj.order.id) if obj.order else ''
]
# Add JSON data
flat_info = {}
@@ -141,7 +85,7 @@ def export_signups_csv(modeladmin, request, queryset):
return response
export_signups_csv.short_description = "导出选中报名记录为 CSV (含发货/详细信息)"
export_signups_csv.short_description = "导出选中报名记录为 CSV (含详细信息)"
def export_signups_excel(modeladmin, request, queryset):
"""
@@ -165,37 +109,26 @@ def export_signups_excel(modeladmin, request, queryset):
ws = wb.active
ws.title = str(opts.verbose_name)[:31] # Sheet name limit is 31 chars
# Fixed headers
fixed_headers = [
'ID', '活动标题', '状态', '报名时间',
'用户ID', '用户昵称', '用户OpenID', '用户绑定手机', '用户地区', '用户注册时间',
'关联订单ID', '订单状态', '收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号', '订单总价', '商户订单号'
]
# Base fields to export
base_headers = ['ID', '活动标题', '用户昵称', '用户ID', '报名时间', '状态', '关联订单ID']
# Get dynamic JSON keys
json_keys = get_signup_info_keys(queryset)
# Write header
ws.append(fixed_headers + json_keys)
ws.append(base_headers + json_keys)
# Write data
for obj in queryset:
row_data = get_row_data(obj)
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)
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 ''
]
# Add JSON data
flat_info = {}
@@ -206,11 +139,11 @@ def export_signups_excel(modeladmin, request, queryset):
val = flat_info.get(key, '')
if val is None:
val = ''
row.append(str(val)) # JSON values as strings
row.append(str(val)) # Ensure string for simplicity, or handle types
ws.append(row)
wb.save(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.decorators import display
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
from io import BytesIO
import base64
@@ -403,7 +403,7 @@ class OrderAdmin(ModelAdmin):
list_filter = ('status', 'salesperson', 'distributor', 'created_at')
search_fields = ('id', 'customer_name', 'phone_number', '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):
if obj.config:
@@ -472,6 +472,7 @@ class WeChatUserAdmin(OrderableAdminMixin, ModelAdmin):
search_fields = ('nickname', 'openid', 'phone_number')
list_filter = ('is_star', GenderFilter, UserSourceFilter, 'province', 'city', 'created_at')
readonly_fields = ('openid', 'unionid', 'session_key', 'created_at', 'updated_at')
actions = [export_to_csv, export_to_excel]
def avatar_display(self, obj):
if obj.avatar_url:

View File

@@ -1,124 +1,71 @@
import csv
import json
import datetime
from django.http import HttpResponse
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
filename = f"{opts.verbose_name}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
def flatten(x, name=''):
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
response = HttpResponse(content_type='text/csv; charset=utf-8-sig')
response['Content-Disposition'] = f'attachment; filename={escape_uri_path(filename)}'
flatten(y)
return out
writer = csv.writer(response)
def get_order_signup_keys(queryset):
# 获取所有非多对多字段和非反向关联字段
fields = [field for field in opts.get_fields() if not field.many_to_many and not field.one_to_many]
# 写入表头 (使用字段的 verbose_name)
writer.writerow([field.verbose_name for field in fields])
# 写入数据
for obj in queryset:
data_row = []
for field in fields:
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)
# 处理日期时间
if isinstance(value, datetime.datetime):
value = value.strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(value, datetime.date):
value = value.strftime('%Y-%m-%d')
# 处理 None
if value is None:
value = ""
data_row.append(str(value))
writer.writerow(data_row)
return response
export_to_csv.short_description = "导出选中项为 CSV"
def export_to_excel(modeladmin, request, 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. 订单基础信息
data['订单ID'] = str(order.id)
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')
# 商品信息
if order.config:
data['商品类型'] = '硬件'
data['商品名称'] = order.config.name
elif order.course:
data['商品类型'] = '课程'
data['商品名称'] = order.course.title
elif order.activity:
data['商品类型'] = '活动'
data['商品名称'] = order.activity.title
else:
data['商品类型'] = '未知'
data['商品名称'] = '未知'
# 2. 发货/收货信息
data['收货人姓名'] = order.customer_name
data['收货电话'] = order.phone_number
data['收货地址'] = order.shipping_address
data['快递公司'] = order.courier_name or ''
data['快递单号'] = order.tracking_number or ''
# 3. 下单用户信息
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. 销售/分销信息
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.
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
导出为 Excel (需要安装 openpyxl)
"""
try:
from openpyxl import Workbook
except ImportError:
modeladmin.message_user(request, "请先安装 openpyxl 库以使用 Excel 导出功能", level='error')
modeladmin.message_user(request, "请先安装 openpyxl 库以使用 Excel 导出功能: pip install openpyxl", level='error')
return
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(
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
@@ -127,44 +74,37 @@ def export_orders_excel(modeladmin, request, queryset):
wb = Workbook()
ws = wb.active
ws.title = "订单导出"
# Sheet name limit is 31 chars
ws.title = str(opts.verbose_name)[:31]
# Fixed headers
fixed_headers = [
'订单ID', '商户订单号', '微信支付单号', '订单状态', '订单总价', '购买数量', '创建时间',
'商品类型', '商品名称',
'收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号',
'用户ID', '用户昵称', '用户OpenID', '用户绑定手机',
'销售员', '分销员'
]
fields = [field for field in opts.get_fields() if not field.many_to_many and not field.one_to_many]
# Dynamic headers from ActivitySignup (only if orders contain activity orders)
json_keys = get_order_signup_keys(queryset)
# Write header
# Add a prefix to json keys to distinguish them? Or keep as is.
# 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)
# 写入表头
ws.append([str(field.verbose_name) for field in fields])
# 写入数据
for obj in queryset:
row = []
for header in fixed_headers:
val = row_data.get(header, '')
row.append(str(val)) # Convert everything to string for safety
for field in fields:
value = getattr(obj, field.name)
for key in json_keys:
val = signup_data.get(key, '')
if val is None:
val = ''
row.append(str(val))
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)
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)
wb.save(response)
return response
export_orders_excel.short_description = "导出选中订单为 Excel (含报名信息)"
export_to_excel.short_description = "导出选中为 Excel"