Files
django-vue3-admin/backend/apps/pisadmin/dashboard/views.py
T
Lewis_yan 4c681805cd fix(dashboard): replace .extra() with TruncDate to fix SQL Server column name issues
.extra(select={'day': 'DAY(create_time)'}) uses Django field name but
Inquiry.create_time has db_column='CreateTime' causing SQL Server error.
TruncDate is the proper ORM approach for date truncation.
2026-04-07 11:02:51 +08:00

477 lines
18 KiB
Python

from datetime import timedelta
from django.utils import timezone
from django.db.models import Count, F
from django.db.models.functions import TruncMonth, TruncDate
from rest_framework import serializers, views
from rest_framework.response import Response
from dvadmin.utils.json_response import SuccessResponse
from apps.pisadmin.miscprocurement.models import Inquiry
from apps.pissupplier.models import QuotationMaster
from apps.pisadmin.basicinfo.models import Supplier
class BuyerKPISerializer(serializers.Serializer):
total_inquiries = serializers.IntegerField()
pending_inquiries = serializers.IntegerField()
completed_quotes = serializers.IntegerField()
total_suppliers = serializers.IntegerField()
quote_timely_rate = serializers.FloatField()
class BuyerTaskSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField()
inquiry_no = serializers.CharField()
status = serializers.CharField()
created_at = serializers.DateTimeField()
method = serializers.SerializerMethodField()
quote_deadline = serializers.DateTimeField(allow_null=True, required=False)
bid_start_time = serializers.DateTimeField(allow_null=True, required=False)
bid_end_time = serializers.DateTimeField(allow_null=True, required=False)
def get_method(self, obj):
if obj.get('buying_method') == 2:
return '招标'
return '询价'
class SupplierKPISerializer(serializers.Serializer):
total_quotes = serializers.IntegerField()
pending_quotes = serializers.IntegerField()
won_quotes = serializers.IntegerField()
conversion_rate = serializers.FloatField()
quote_timely_rate = serializers.FloatField()
on_time_quotes = serializers.IntegerField()
overdue_quotes = serializers.IntegerField()
class SupplierQuoteSerializer(serializers.Serializer):
id = serializers.IntegerField()
inquiry_no = serializers.CharField()
item_name = serializers.CharField()
unit = serializers.CharField()
quantity = serializers.CharField(allow_blank=True, required=False)
status = serializers.IntegerField()
method = serializers.SerializerMethodField()
quote_deadline = serializers.DateTimeField(allow_null=True, required=False)
bid_start_time = serializers.DateTimeField(allow_null=True, required=False)
bid_end_time = serializers.DateTimeField(allow_null=True, required=False)
def get_method(self, obj):
if obj.get('buying_method') == 2:
return '招标'
return '询价'
class MessageSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField()
content = serializers.CharField()
is_read = serializers.BooleanField()
created_at = serializers.DateTimeField()
class TrendSerializer(serializers.Serializer):
month = serializers.CharField(required=False)
day = serializers.IntegerField(required=False)
count = serializers.IntegerField(required=False)
quotes = serializers.IntegerField(required=False)
won = serializers.IntegerField(required=False)
class BuyerDashboardSerializer(serializers.Serializer):
kpi = BuyerKPISerializer()
tasks = BuyerTaskSerializer(many=True)
messages = MessageSerializer(many=True)
trend = TrendSerializer(many=True)
class SupplierDashboardSerializer(serializers.Serializer):
kpi = SupplierKPISerializer()
pending_quotes = SupplierQuoteSerializer(many=True)
messages = MessageSerializer(many=True)
trend = TrendSerializer(many=True)
class DashboardResponseSerializer(serializers.Serializer):
buyer = BuyerDashboardSerializer(allow_null=True)
supplier = SupplierDashboardSerializer(allow_null=True)
class DashboardView(views.APIView):
"""看板数据视图"""
def get_buyer_kpi(self, user):
"""采购方 KPI 计算"""
total_inquiries = Inquiry.objects.filter(create_user=user.username).count()
pending_inquiries = Inquiry.objects.filter(
create_user=user.username, status__in=[1, 2, 3]
).count()
completed_quotes = QuotationMaster.objects.filter(
inquiry_no__in=Inquiry.objects.filter(
create_user=user.username
).values('inquiry_no')
).count()
total_suppliers = Supplier.objects.count()
# 供应商报价及时率 = 在截止时间前提交的报价数 / 总报价数
buyer_quotations = QuotationMaster.objects.filter(
inquiry_no__in=Inquiry.objects.filter(
create_user=user.username
).values('inquiry_no')
)
total_quotes = buyer_quotations.count()
timely_quotes = buyer_quotations.filter(
quotetime__isnull=False,
quote_deadline__isnull=False,
quotetime__lte=F('quote_deadline')
).count()
quote_timely_rate = round(timely_quotes / total_quotes * 100, 1) if total_quotes > 0 else 0.0
return {
'total_inquiries': total_inquiries,
'pending_inquiries': pending_inquiries,
'completed_quotes': completed_quotes,
'total_suppliers': total_suppliers,
'quote_timely_rate': quote_timely_rate,
}
def get_buyer_tasks(self, user):
"""采购方待办任务"""
inquiries = Inquiry.objects.filter(
create_user=user.username,
status__in=[1, 2, 3]
).order_by('-create_time')[:10]
return [
{
'id': i.id,
'title': i.title,
'inquiry_no': i.inquiry_no,
'status': dict(Inquiry.STATUS_CHOICES).get(i.status, str(i.status)),
'created_at': i.create_time,
'buying_method': i.buying_method,
'quote_deadline': i.quote_deadline,
'bid_start_time': i.bid_start_time,
'bid_end_time': i.bid_end_time,
}
for i in inquiries
]
def get_supplier_kpi(self, user):
"""供应商 KPI 计算"""
# 超级管理员看到所有供应商汇总数据
if user.is_superuser:
total_quotes = QuotationMaster.objects.count()
pending_quotes = QuotationMaster.objects.filter(status__in=[1, 2]).count()
won_quotes = QuotationMaster.objects.filter(is_awarded=1).count()
else:
total_quotes = QuotationMaster.objects.filter(supplier_code=user.username).count()
pending_quotes = QuotationMaster.objects.filter(
supplier_code=user.username, status__in=[1, 2]
).count()
won_quotes = QuotationMaster.objects.filter(
supplier_code=user.username, is_awarded=1
).count()
conversion_rate = (won_quotes / total_quotes * 100) if total_quotes > 0 else 0.0
# 及时率计算
if user.is_superuser:
all_quotes = QuotationMaster.objects.all()
else:
all_quotes = QuotationMaster.objects.filter(supplier_code=user.username)
total_with_deadline = all_quotes.filter(quote_deadline__isnull=False).count()
on_time_quotes = all_quotes.filter(
quotetime__isnull=False,
quote_deadline__isnull=False,
quotetime__lte=F('quote_deadline')
).count()
overdue_quotes = total_with_deadline - on_time_quotes
quote_timely_rate = round(on_time_quotes / total_with_deadline * 100, 1) if total_with_deadline > 0 else 0.0
return {
'total_quotes': total_quotes,
'pending_quotes': pending_quotes,
'won_quotes': won_quotes,
'conversion_rate': round(conversion_rate, 2),
'quote_timely_rate': quote_timely_rate,
'on_time_quotes': on_time_quotes,
'overdue_quotes': overdue_quotes,
}
def get_supplier_pending_quotes(self, user):
"""供应商待报价清单"""
# 超级管理员看到所有待报价,供应商只看自己的
if user.is_superuser:
quotes = QuotationMaster.objects.filter(status__in=[1, 2]).order_by('-creattime')[:10]
else:
quotes = QuotationMaster.objects.filter(
supplier_code=user.username,
status__in=[1, 2]
).order_by('-creattime')[:10]
result = []
for q in quotes:
inquiry = Inquiry.objects.filter(inquiry_no=q.inquiry_no).first()
# 获取询价单中的数量
quantity = ''
unit = ''
if inquiry:
from apps.pisadmin.miscprocurement.models import InquiryRfqItem
item = InquiryRfqItem.objects.filter(inquiry_no=q.inquiry_no).first()
if item:
quantity = item.qty
unit = item.unit or ''
result.append({
'id': q.autoid,
'inquiry_no': q.inquiry_no,
'item_name': inquiry.title if inquiry else '',
'quantity': quantity,
'unit': unit,
'status': q.status,
'buying_method': q.buying_method,
'quote_deadline': q.quote_deadline,
'bid_start_time': q.bid_start_time,
'bid_end_time': q.bid_end_time,
})
return result
def get_trend_data(self, user, is_buyer=True):
"""近6个月趋势数据"""
six_months_ago = timezone.now() - timedelta(days=180)
if is_buyer:
data = (
Inquiry.objects.filter(
create_user=user.username,
create_time__gte=six_months_ago
)
.annotate(month=TruncMonth('create_time'))
.values('month')
.annotate(count=Count('id'))
.order_by('month')
)
return [
{'month': d['month'].strftime('%Y-%m'), 'count': d['count']}
for d in data
]
else:
# 超级管理员看到所有供应商汇总趋势
if user.is_superuser:
data = (
QuotationMaster.objects.filter(creattime__gte=six_months_ago)
.annotate(month=TruncMonth('creattime'))
.values('month')
.annotate(quotes=Count('autoid'))
.order_by('month')
)
result = []
for d in data:
won = QuotationMaster.objects.filter(
creattime__month=d['month'].month,
creattime__year=d['month'].year,
is_awarded=1
).count()
result.append({
'month': d['month'].strftime('%Y-%m'),
'quotes': d['quotes'],
'won': won,
})
return result
else:
data = (
QuotationMaster.objects.filter(
supplier_code=user.username,
creattime__gte=six_months_ago
)
.annotate(month=TruncMonth('creattime'))
.values('month')
.annotate(quotes=Count('autoid'))
.order_by('month')
)
result = []
for d in data:
won = QuotationMaster.objects.filter(
supplier_code=user.username,
creattime__month=d['month'].month,
creattime__year=d['month'].year,
is_awarded=1
).count()
result.append({
'month': d['month'].strftime('%Y-%m'),
'quotes': d['quotes'],
'won': won,
})
return result
def get_buyer_trend_daily(self, user):
"""获取采购方当月每日趋势数据"""
now = timezone.now()
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
data = (
Inquiry.objects.filter(
create_user=user.username,
create_time__gte=start_of_month,
create_time__lte=now
)
.annotate(day=TruncDate('create_time'))
.values('day')
.annotate(count=Count('id'))
.order_by('day')
)
return [
{'day': d['day'].day, 'count': d['count']}
for d in data
]
def get_supplier_trend_daily(self, user):
"""获取供应商当月每日趋势数据"""
now = timezone.now()
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
if user.is_superuser:
data = (
QuotationMaster.objects.filter(
creattime__gte=start_of_month,
creattime__lte=now
)
.annotate(day=TruncDate('creattime'))
.values('day')
.annotate(quotes=Count('autoid'))
.order_by('day')
)
result = []
for d in data:
won = QuotationMaster.objects.filter(
creattime__day=d['day'].day,
creattime__month=now.month,
creattime__year=now.year,
is_awarded=1
).count()
result.append({
'day': d['day'].day,
'quotes': d['quotes'],
'won': won,
})
return result
else:
data = (
QuotationMaster.objects.filter(
supplier_code=user.username,
creattime__gte=start_of_month,
creattime__lte=now
)
.annotate(day=TruncDate('creattime'))
.values('day')
.annotate(quotes=Count('autoid'))
.order_by('day')
)
result = []
for d in data:
won = QuotationMaster.objects.filter(
supplier_code=user.username,
creattime__day=d['day'].day,
creattime__month=now.month,
creattime__year=now.year,
is_awarded=1
).count()
result.append({
'day': d['day'].day,
'quotes': d['quotes'],
'won': won,
})
return result
def get_messages(self, user):
"""消息通知 - 占位实现,后续接入通知系统"""
return []
def get_buyer_data(self, user):
"""获取采购方看板数据"""
return {
'kpi': self.get_buyer_kpi(user),
'tasks': self.get_buyer_tasks(user),
'messages': self.get_messages(user),
'trend': self.get_buyer_trend_daily(user),
}
def get_supplier_data(self, user):
"""获取供应商看板数据"""
return {
'kpi': self.get_supplier_kpi(user),
'pending_quotes': self.get_supplier_pending_quotes(user),
'messages': self.get_messages(user),
'trend': self.get_supplier_trend_daily(user),
}
def get(self, request):
"""获取看板数据
Returns role-based dashboard data based on user's permissions.
- Buyer role: returns buyer dashboard data
- Supplier role: returns supplier dashboard data
- Both roles: returns both
- Superadmin or no business data: returns empty structure
"""
user = request.user
if not user or not user.is_authenticated:
return Response({'buyer': None, 'supplier': None})
# 分别判断用户角色,避免一个表不存在影响另一个
has_supplier_role = False
has_buyer_role = False
try:
has_supplier_role = QuotationMaster.objects.filter(
supplier_code=user.username
).exists()
except Exception:
pass
try:
has_buyer_role = Inquiry.objects.filter(
create_user=user.username
).exists()
except Exception:
pass
# 超级管理员或有任何角色,始终返回对应看板数据结构
buyer_data = None
supplier_data = None
# 采购方看板:超级管理员或有任何采购记录的用户
if user.is_superuser or has_buyer_role:
try:
buyer_data = self.get_buyer_data(user)
print(f"[DEBUG] get_buyer_data success, tasks count: {len(buyer_data.get('tasks', []))}")
except Exception as e:
print(f"[DEBUG] get_buyer_data exception: {e}")
import traceback
traceback.print_exc()
buyer_data = {
'kpi': {'total_inquiries': 0, 'pending_inquiries': 0, 'completed_quotes': 0, 'total_suppliers': 0, 'quote_timely_rate': 0},
'tasks': [],
'messages': [],
'trend': [],
}
# 供应商看板:超级管理员或有任何供应商记录的用户
if user.is_superuser or has_supplier_role:
try:
supplier_data = self.get_supplier_data(user)
except Exception:
supplier_data = {
'kpi': {'total_quotes': 0, 'pending_quotes': 0, 'won_quotes': 0, 'conversion_rate': 0},
'pending_quotes': [],
'messages': [],
'trend': [],
}
response_data = {'buyer': buyer_data, 'supplier': supplier_data}
# 使用序列化器验证响应数据
serializer = DashboardResponseSerializer(data=response_data)
serializer.is_valid(raise_exception=True)
return SuccessResponse(data=serializer.validated_data, msg="获取成功")