Files
django-vue3-admin/backend/apps/pisadmin/dashboard/views.py
T

514 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
# 须声明,否则校验时会被丢弃,SerializerMethodField 无法读到库里的采购方式
buying_method = serializers.IntegerField(required=False, allow_null=True)
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):
bm = obj.get('buying_method')
if bm == 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()
buying_method = serializers.IntegerField(required=False, allow_null=True)
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):
bm = obj.get('buying_method')
if bm == 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):
"""采购方待办任务(仅询价状态为发布/报价中/报价结束/比议价中的单,见 Inquiry.BUYER_DASHBOARD_TASK_STATUSES)。"""
inquiries = Inquiry.objects.filter(
create_user=user.username,
status__in=Inquiry.BUYER_DASHBOARD_TASK_STATUSES,
).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=QuotationMaster.DASHBOARD_PENDING_STATUSES
).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=QuotationMaster.DASHBOARD_PENDING_STATUSES
).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 _supplier_quote_remaining_sort_key(self, q, inquiry, now):
"""用于待报价清单排序:剩余时间由少到多(越早越靠前);无截止时间排最后。"""
bm = (inquiry.buying_method if inquiry is not None else None) or q.buying_method
if bm == 2:
start, end = q.bid_start_time, q.bid_end_time
if not start:
return None
if now < start:
return (start - now).total_seconds()
if end and now < end:
return (end - now).total_seconds()
return float('inf')
if q.quote_deadline:
return (q.quote_deadline - now).total_seconds()
return None
def get_supplier_pending_quotes(self, user):
"""供应商待报价清单:仅未报价(待报价)/报价中;按剩余时间由少到多取前 10 条。"""
base = QuotationMaster.objects.filter(status__in=QuotationMaster.DASHBOARD_PENDING_STATUSES)
if not user.is_superuser:
base = base.filter(supplier_code=user.username)
quotes = list(base)
if not quotes:
return []
now = timezone.now()
inquiry_nos = {q.inquiry_no for q in quotes}
inquiries = {i.inquiry_no: i for i in Inquiry.objects.filter(inquiry_no__in=inquiry_nos)}
def sort_key(q):
inq = inquiries.get(q.inquiry_no)
k = self._supplier_quote_remaining_sort_key(q, inq, now)
if k is None:
return float('inf')
return k
quotes.sort(key=sort_key)
quotes = quotes[:10]
from apps.pisadmin.miscprocurement.models import InquiryRfqItem
result = []
for q in quotes:
inquiry = inquiries.get(q.inquiry_no)
quantity = ''
unit = ''
if inquiry:
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': (inquiry.buying_method if inquiry is not None else 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)
# 须用 .data:validated_data 不含 SerializerMethodField(如 tasks[].method)
return SuccessResponse(data=serializer.data, msg="获取成功")