diff --git a/backend/application/urls.py b/backend/application/urls.py index 021bb90..6122ee2 100644 --- a/backend/application/urls.py +++ b/backend/application/urls.py @@ -25,6 +25,8 @@ from rest_framework_simplejwt.views import ( from application import dispatch from application import settings +from apps.pisadmin.miscprocurement.views import MiscInquiryComparisonShareBundleView +from apps.pissupplier.views import PublicQuotationShareDetailView from sync.views import MiscMaterialSyncView from application.sse_views import sse_view from dvadmin.system.views.dictionary import InitDictionaryViewSet @@ -108,6 +110,16 @@ urlpatterns = ( MiscMaterialSyncView.as_view(), name="sync-misc-material", ), + path( + "api/public/pissupplier/quotation/", + PublicQuotationShareDetailView.as_view(), + name="public-pissupplier-quotation-share", + ), + path( + "api/public/miscprocurement/comparison/", + MiscInquiryComparisonShareBundleView.as_view(), + name="public-misc-comparison-bundle", + ), path("api/pisadmin/miscprocurement/", include("apps.pisadmin.miscprocurement.urls")), path("api/pisadmin/basicinfo/", include("apps.pisadmin.basicinfo.urls")), path("api/pisadmin/dashboard/", include("apps.pisadmin.dashboard.urls")), diff --git a/backend/apps/pisadmin/miscprocurement/views.py b/backend/apps/pisadmin/miscprocurement/views.py index 8917c2e..c01f831 100644 --- a/backend/apps/pisadmin/miscprocurement/views.py +++ b/backend/apps/pisadmin/miscprocurement/views.py @@ -9,6 +9,8 @@ from django.db.models import Q from django.utils import timezone from rest_framework.decorators import action from rest_framework import serializers +from rest_framework.permissions import AllowAny +from rest_framework.views import APIView from dvadmin.utils.json_response import DetailResponse, ErrorResponse, SuccessResponse from dvadmin.utils.viewset import CustomModelViewSet @@ -28,7 +30,7 @@ from apps.pissupplier.models import ( QuotationProfit, QuotationItem, ) - +from apps.pissupplier.serializers import QuotationMasterSerializer logger = logging.getLogger(__name__) @@ -143,6 +145,63 @@ from .serializers import ( MiscProcProcessingMinPricesSerializer, ) +def build_comparison_share_bundle(inquiry: Inquiry) -> dict: + """与前端比价页 loadComparisonPage 所需数据一致:询价单 + 报价明细 + 议价 + 材料/工站主数据。""" + inquiry_data = InquirySerializer(inquiry).data + qm_qs = ( + QuotationMaster.objects.filter(inquiry_no=inquiry.inquiry_no) + .order_by("supplier_code", "autoid") + .prefetch_related("rfq_items", "material_costs", "process_costs", "other_costs", "profit_costs") + ) + quotations = [] + for qm in qm_qs: + quotations.append(QuotationMasterSerializer(qm).data) + part_id = _resolve_inquiry_primary_part_id(inquiry) + neg_qs = MiscNegotiationRecords.objects.filter(inquiry_no=inquiry.inquiry_no) + if part_id: + neg_qs = neg_qs.filter(part_id=part_id) + negotiation_records = MiscNegotiationRecordsSerializer(neg_qs.order_by("id"), many=True).data + materials = MiscMaterialSerializer( + MiscProcurementMaterialInfo.objects.filter(status=1).order_by("id")[:8000], + many=True, + ).data + stations = MiscStationSerializer( + MiscProcurementStationInfo.objects.all().order_by("id")[:8000], + many=True, + ).data + return { + "inquiry": inquiry_data, + "quotations": quotations, + "negotiation_records": negotiation_records, + "materials": materials, + "stations": stations, + } + + +class MiscInquiryComparisonShareBundleView(APIView): + """ + GET /api/public/miscprocurement/comparison/ + + 免登录,仅 ``?inquiry_id=<主键>``。 + """ + + permission_classes = [AllowAny] + authentication_classes = [] + + def get(self, request, *args, **kwargs): + inquiry_id_raw = (request.query_params.get("inquiry_id") or "").strip() + if not inquiry_id_raw: + return ErrorResponse(msg="缺少 inquiry_id", code=4000) + try: + pk = int(inquiry_id_raw) + except (TypeError, ValueError): + return ErrorResponse(msg="inquiry_id 无效", code=4000) + inquiry = Inquiry.objects.filter(pk=pk).first() + if not inquiry: + return ErrorResponse(msg="询价单不存在", code=4000) + bundle = build_comparison_share_bundle(inquiry) + return DetailResponse(data=bundle, msg="success") + def _negotiation_totals_from_quotation_item(quotation_no: str, part_id: str): """从杂采报价单上阶物料明细取议价前含税/不含税总价(与 part_id 匹配行)。""" @@ -1431,6 +1490,17 @@ class InquiryViewSet(CustomModelViewSet): operation_desc="议价审核完成(核价通过)", ) + @action(methods=["get"], detail=True, url_path="comparison_share") + def comparison_share(self, request, pk=None): + """ + 比价对外分享使用 ``#/public/misc-compare?inquiry_id=<主键>``(免登录、免签名)。 + """ + self.get_object() # 保留权限校验语义;不返回链接 + return ErrorResponse( + msg="采购端「复制分享链接」已关闭,请使用带 inquiry_id 的公开比价地址(由业务系统拼链)", + code=4000, + ) + @action(methods=["get"], detail=True, url_path="negotiation_records") def negotiation_records(self, request, pk=None): """按询价单号查询杂采议价记录(可选 part_id)。""" diff --git a/backend/apps/pissupplier/views.py b/backend/apps/pissupplier/views.py index 07d3727..95ba6a9 100644 --- a/backend/apps/pissupplier/views.py +++ b/backend/apps/pissupplier/views.py @@ -5,6 +5,8 @@ from django.db import transaction from django.db.models import Q from django.utils import timezone from rest_framework.decorators import action +from rest_framework.permissions import AllowAny +from rest_framework.views import APIView from apps.pisadmin.basicinfo.models import SupplierUser from dvadmin.utils.json_response import DetailResponse, ErrorResponse, SuccessResponse @@ -453,3 +455,48 @@ class QuotationItemViewSet(SupplierQuotationScopeMixin, CustomModelViewSet): def perform_update(self, serializer): serializer.save() + + +class PublicQuotationShareDetailView(APIView): + """ + GET /api/public/pissupplier/quotation/ + + 免登录,仅 ``?id=`` 或 ``?autoid=``(报价主表主键 autoid)。 + 响应形态与 ``GET /api/pissupplier/quotation_master/{autoid}/`` 详情相同。 + """ + + permission_classes = [AllowAny] + authentication_classes = [] + + def get(self, request, *args, **kwargs): + id_raw = ( + request.query_params.get("id") + or request.query_params.get("autoid") + or "" + ).strip() + if not id_raw: + return ErrorResponse(msg="缺少 id(或 autoid)", code=4000) + try: + autoid = int(id_raw) + except (TypeError, ValueError): + return ErrorResponse(msg="id 无效", code=4000) + qm = ( + QuotationMaster.objects.prefetch_related( + "rfq_items", + "material_costs", + "process_costs", + "other_costs", + "profit_costs", + ) + .filter(autoid=autoid) + .first() + ) + if not qm: + return ErrorResponse(msg="报价单不存在", code=4000) + # 与 ViewSet.retrieve 一致:`QuotationMasterSerializer` 中 template_sections / inquiry_attachments + # 仅在 context["view_action"] == "retrieve" 时展开,否则为空导致前端成本结构与合计无法渲染。 + data = QuotationMasterSerializer( + qm, + context={"request": request, "view_action": "retrieve"}, + ).data + return DetailResponse(data=data, msg="success") diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 129c0e1..00ec373 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -121,7 +121,7 @@ router.beforeEach(async (to, from, next) => { NProgress.configure({showSpinner: false}); if (to.meta.title) NProgress.start(); const token = Session.get('token'); - if ((to.path === '/login' || to.path === '/login/supplier') && !token) { + if ((to.path === '/login' || to.path === '/login/supplier' || to.meta.public) && !token) { next(); NProgress.done(); } else { diff --git a/web/src/router/route.ts b/web/src/router/route.ts index ed39d77..3dc8b85 100644 --- a/web/src/router/route.ts +++ b/web/src/router/route.ts @@ -125,5 +125,25 @@ export const staticRoutes: Array = [ meta: { title: 'message.router.personal' }, - } + }, + /** 杂采比价对外分享(免登录,`?inquiry_id=`;与 comparePrice 共用组件) */ + { + path: '/public/misc-compare', + name: 'PublicMiscComparePrice', + component: () => import('/@/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue'), + meta: { + title: '比价信息', + public: true, + }, + }, + /** 比价分享内跳转报价单详情(免登录,`?id=` 报价 autoid;与供应商 quotation detail 共用组件) */ + { + path: '/public/pissupplier/quotation', + name: 'PublicPissupplierQuotationShare', + component: () => import('/@/views/pissupplier/quotation/detail.vue'), + meta: { + title: '查看报价', + public: true, + }, + }, ]; diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts index a39f5f4..c1654de 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts @@ -1,9 +1,17 @@ +import axios from 'axios' import { request } from '/@/utils/service' +import { getBaseURL } from '/@/utils/baseUrl' const baseUrl = '/api/pisadmin/miscprocurement/inquiry/' /** 列表/增删改 API;完整表单在隐藏路由 `PisadminRfqMiscInquiryDetail`(`miscInquiryDetail/:id`)。议价相关接口由隐藏路由 `PisadminRfqMiscComparePrice`(`comparePrice/:id`)使用。 */ +/** 免登录:比价数据包,``inquiry_id`` 为询价单主键 */ +export const fetchPublicComparisonBundle = async (params: { inquiry_id: string | number }) => { + const url = `${getBaseURL().replace(/\/$/, '')}/api/public/miscprocurement/comparison/` + return axios.get(url, { params: { inquiry_id: String(params.inquiry_id) } }) +} + export const GetList = (params: any) => request({ url: baseUrl, method: 'get', params }) export const GetObj = (id: string | number) => request({ url: baseUrl + id + '/', method: 'get' }) export const AddObj = (data: any) => request({ url: baseUrl, method: 'post', data }) diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue index 782fa06..468386b 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue @@ -1,7 +1,7 @@