diff --git a/backend/apps/pisadmin/basicinfo/views/supplier.py b/backend/apps/pisadmin/basicinfo/views/supplier.py index 7737feb..a45625d 100644 --- a/backend/apps/pisadmin/basicinfo/views/supplier.py +++ b/backend/apps/pisadmin/basicinfo/views/supplier.py @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- from rest_framework import serializers -from rest_framework.decorators import action from apps.pisadmin.basicinfo.models import Supplier, SupplierUser -from dvadmin.utils.json_response import DetailResponse, ErrorResponse, SuccessResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet class SupplierUserSerializer(CustomModelSerializer): - """供应商用户信息(子表 pis_supplier_users)""" + """供应商用户信息""" class Meta: model = SupplierUser @@ -26,25 +24,13 @@ class SupplierUserCreateUpdateSerializer(CustomModelSerializer): class SupplierSerializer(CustomModelSerializer): - """供应商信息-序列化器(含关联 supplier_user)""" - - supplier_user = serializers.SerializerMethodField(read_only=True) + """供应商信息""" class Meta: model = Supplier - fields = [f.name for f in Supplier._meta.concrete_fields] + ["supplier_user"] + fields = [f.name for f in Supplier._meta.concrete_fields] read_only_fields = ["id"] - def get_supplier_user(self, obj): - cache = self.context.get("supplier_user_by_sid") - if cache is not None: - su = cache.get(obj.supplier_id) - else: - su = SupplierUser.objects.filter(supplier_id=obj.supplier_id).first() - if su is None: - return None - return SupplierUserSerializer(su, context=self.context).data - class SupplierCreateUpdateSerializer(CustomModelSerializer): """供应商信息 创建/更新""" @@ -73,71 +59,14 @@ class SupplierViewSet(CustomModelViewSet): search_fields = ["supplier_name", "supplier_short_name", "supplier_id", "company_code"] ordering = ["-create_datetime"] - def get_serializer_context(self): - ctx = super().get_serializer_context() - m = getattr(self, "_supplier_user_map", None) - if m is not None: - ctx["supplier_user_by_sid"] = m - return ctx - - def list(self, request, *args, **kwargs): - queryset = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(queryset) - if page is not None: - rows = page - else: - rows = list(queryset) - sids = [row.supplier_id for row in rows] - self._supplier_user_map = { - u.supplier_id: u for u in SupplierUser.objects.filter(supplier_id__in=sids) - } - try: - serializer = self.get_serializer(rows, many=True, request=request) - if page is not None: - return self.get_paginated_response(serializer.data) - return SuccessResponse(data=serializer.data, msg="获取成功") - finally: - self._supplier_user_map = None - - @action(methods=["get", "put", "patch"], detail=True, url_path="supplier_user") - def supplier_user_nested(self, request, pk=None): - """按供应商主键维护关联的供应商用户信息(与 Supplier.supplier_id 对应,唯一)。""" - supplier = self.get_object() - sid = (supplier.supplier_id or "").strip() - if not sid: - return ErrorResponse(msg="供应商缺少 supplier_id") - - if request.method == "GET": - su = SupplierUser.objects.filter(supplier_id=sid).first() - if su is None: - return DetailResponse(data=None, msg="暂无供应商用户信息") - return DetailResponse(data=SupplierUserSerializer(su, context={"request": request}).data) - - su = SupplierUser.objects.filter(supplier_id=sid).first() - ser = SupplierUserCreateUpdateSerializer( - instance=su, - data=request.data, - partial=request.method == "PATCH", - context={"request": request}, - ) - ser.is_valid(raise_exception=True) - name = ser.validated_data.get("supplier_name") - if name is None or str(name).strip() == "": - name = supplier.supplier_name - obj = ser.save(supplier_id=sid, supplier_name=name) - return DetailResponse( - data=SupplierUserSerializer(obj, context={"request": request}).data, - msg="保存成功", - ) - class SupplierUserViewSet(CustomModelViewSet): - """供应商用户信息接口(子表;与 Supplier.supplier_id 关联)""" + """供应商用户信息(独立表;按 supplier_id 筛选可得到某供应商下全部联系人)""" queryset = SupplierUser.objects.all() serializer_class = SupplierUserSerializer create_serializer_class = SupplierUserCreateUpdateSerializer update_serializer_class = SupplierUserCreateUpdateSerializer - filter_fields = ["company_code", "supplier_id", "supplier_role", "status", "user_email"] + filter_fields = ["supplier_id", "supplier_role", "status", "user_email"] search_fields = ["supplier_id", "supplier_name", "user_email", "user_name", "user_phone"] ordering = ["-create_datetime", "id"] diff --git a/backend/apps/pisadmin/miscprocurement/models.py b/backend/apps/pisadmin/miscprocurement/models.py index cf3b556..8fd763c 100644 --- a/backend/apps/pisadmin/miscprocurement/models.py +++ b/backend/apps/pisadmin/miscprocurement/models.py @@ -921,67 +921,5 @@ class RFQOperationLogs(CoreModel): except Exception: logger.exception("写入询价操作日志失败") - @classmethod - def bulk_append_quote_deadline_expired( - cls, - quotation_rows: list, - inquiry_purchase_type: dict, - *, - operation_user: Optional[str] = None, - ) -> int: - """ - 供应商端批量将超期报价单置为「已过期」时,按报价单一笔一条写入操作类型「报价截止」(5)。 - - ``quotation_rows``:``values()`` 得到的列表,每项须含 ``quotation_no``、``inquiry_no``、``status``(置过期前,1/2)、``supplier_name``。 - ``inquiry_purchase_type``:``inquiry_no`` -> ``purchase_type``(杂采/策采)。 - """ - from apps.pissupplier.models import QuotationMaster - - if not quotation_rows: - return 0 - now = timezone.now() - op_user = (operation_user or "").strip()[:20] if operation_user else None - qm_status_label = dict(QuotationMaster.STATUS_CHOICES) - cur_label = (qm_status_label.get(4) or "已过期")[:100] - - batch = [] - for row in quotation_rows: - inq_no = str(row.get("inquiry_no") or "").strip()[:20] - qn = str(row.get("quotation_no") or "").strip()[:20] or "-" - supplier_name = (str(row.get("supplier_name") or "").strip() or "—")[:50] - desc = f"供应商({supplier_name})逾期未报价"[:200] - try: - old_st = int(row.get("status")) - except (TypeError, ValueError): - old_st = None - pt = inquiry_purchase_type.get(inq_no) - if pt is None: - continue - per_label = (qm_status_label.get(old_st) or "")[:50] if old_st is not None else None - batch.append( - cls( - operation_type="5", - operation_user=op_user, - operation_time=now, - operation_desc=desc, - inquiry_no=inq_no, - quotation_no=qn, - per_status=per_label or None, - cur_status=cur_label, - is_show_user=1, - purchase_type=int(pt), - create_datetime=now, - update_datetime=now, - ) - ) - if not batch: - return 0 - try: - cls.objects.bulk_create(batch, batch_size=300) - except Exception: - logger.exception("批量写入报价截止询价操作日志失败") - return 0 - return len(batch) - def __str__(self) -> str: return f"{self.inquiry_no}-{self.quotation_no}" diff --git a/backend/apps/pisadmin/miscprocurement/views.py b/backend/apps/pisadmin/miscprocurement/views.py index 77bed00..7fa320f 100644 --- a/backend/apps/pisadmin/miscprocurement/views.py +++ b/backend/apps/pisadmin/miscprocurement/views.py @@ -1,5 +1,6 @@ import logging from decimal import Decimal +from datetime import datetime from typing import Optional from django.db import transaction @@ -30,6 +31,70 @@ from apps.pissupplier.models import ( logger = logging.getLogger(__name__) +def _fmt_dt_for_inquiry_quotation(dt: Optional[datetime]) -> Optional[str]: + if not dt: + return None + if timezone.is_naive(dt): + dt = timezone.make_aware(dt, timezone.get_current_timezone()) + return dt.strftime("%Y-%m-%d %H:%M:%S") + + +def _quotation_master_status_label(status) -> str: + """与 ``QuotationMaster.STATUS_CHOICES`` 一致的可读文案。""" + labels = {1: "待报价", 2: "报价中", 3: "已报价", 4: "已过期"} + try: + st = int(status) if status is not None else 1 + except (TypeError, ValueError): + return "—" + return labels.get(st, str(st)) + + +def build_invited_supplier_quotation_rows(inquiry) -> list: + """询价单发布后各受邀供应商报价单当前状态(与 ``QuotationMaster`` 一致,供操作日志嵌套表使用)。""" + inquiry_no = (inquiry.inquiry_no or "").strip() + if not inquiry_no: + return [] + + name_map: dict[str, str] = {} + for inv in InquirySupplier.objects.filter(inquiry_no=inquiry_no).only("supplier_code", "supplier_name"): + code = (inv.supplier_code or "").strip() + if not code: + continue + n = (inv.supplier_name or "").strip() + prev = name_map.get(code, "") + if len(n) > len(prev): + name_map[code] = n + + rows_out: list[dict] = [] + qms = QuotationMaster.objects.filter(inquiry_no=inquiry_no).order_by("supplier_code", "autoid") + for qm in qms: + code = (qm.supplier_code or "").strip() + full_name = (name_map.get(code) or (qm.supplier_name or "").strip() or code or "—").strip() + + st = int(qm.status or 1) + if st == 3 and qm.quotetime: + op_time = _fmt_dt_for_inquiry_quotation(qm.quotetime) + else: + op_time = _fmt_dt_for_inquiry_quotation(qm.creattime) if qm.creattime else None + + if st == 3: + op_user = (qm.quoteuser or "").strip() or "—" + else: + op_user = "—" + + rows_out.append( + { + "operation_time": op_time or "—", + "supplier_full_name": full_name, + "operator": op_user, + "quotation_status": _quotation_master_status_label(qm.status), + "operation_desc": "", + "quotation_no": (qm.quotation_no or "").strip(), + } + ) + return rows_out + + from .models import ( MiscProcurementMaterialInfo, MiscProcurementStationInfo, @@ -1105,6 +1170,8 @@ class InquiryViewSet(CustomModelViewSet): return ErrorResponse(msg='只有“确认”状态的询价单才能发布') current_user = self._get_request_username() or None current_time = timezone.now() + supplier_groups = self._group_inquiry_suppliers(instance) + supplier_count = len(supplier_groups) try: with transaction.atomic(): self._create_supplier_quotations(instance) # 发布询价单时按供应商关联表,逐个创建对应的报价单 @@ -1117,7 +1184,7 @@ class InquiryViewSet(CustomModelViewSet): release_user=current_user, release_time=current_time, operation_type=3, - operation_desc="询价单发布", + operation_desc=f"询价单发布,共{supplier_count}个受邀供应商", ) except serializers.ValidationError as exc: detail = getattr(exc, "detail", None) @@ -1231,6 +1298,13 @@ class InquiryViewSet(CustomModelViewSet): data = RFQOperationLogsSerializer(qs, many=True).data return DetailResponse(data=data, msg="success") + @action(methods=["get"], detail=True, url_path="invited_supplier_quotations") + def invited_supplier_quotations(self, request, pk=None): + """受邀供应商报价单一览(与发布生成的 ``QuotationMaster`` 一致,供操作日志嵌套表使用)。""" + instance = self.get_object() + data = build_invited_supplier_quotation_rows(instance) + return DetailResponse(data=data, msg="success") + @action(methods=["put"], detail=True, url_path="save_negotiation_records") def save_negotiation_records(self, request, pk=None): """按报价单写入杂采议价记录:议价结果 + 该报价单议价前含税/不含税总价快照(来自上阶物料明细)。""" diff --git a/backend/apps/pissupplier/views.py b/backend/apps/pissupplier/views.py index 21d71ef..ec45a6a 100644 --- a/backend/apps/pissupplier/views.py +++ b/backend/apps/pissupplier/views.py @@ -114,12 +114,6 @@ class QuotationMasterViewSet(CustomModelViewSet): username = getattr(getattr(request, "user", None), "username", None) with transaction.atomic(): updated = qs.update(status=4) - if quotation_rows: - RFQOperationLogs.bulk_append_quote_deadline_expired( - quotation_rows, - inquiry_purchase_type, - operation_user=username, - ) inquiries_closed = Inquiry.sync_to_quote_closed_when_no_open_quotations( inquiry_nos, actor_username=username, diff --git a/web/src/views/pisadmin/basicinfo/supplier_user/api.ts b/web/src/views/pisadmin/basicinfo/supplier_user/api.ts new file mode 100644 index 0000000..35f1c77 --- /dev/null +++ b/web/src/views/pisadmin/basicinfo/supplier_user/api.ts @@ -0,0 +1,5 @@ +import { request } from '/@/utils/service' + +const apiPrefix = '/api/pisadmin/basicinfo/supplier_users/' + +export const GetList = (params: any) => request({ url: apiPrefix, method: 'get', params }) diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts index 87b3ef1..d69ecad 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts @@ -20,6 +20,9 @@ export const SubmitPriceAuditObj = (id: string | number) => request({ url: `${ba /** 询价单操作日志(按主键,返回该单 inquiry_no 下全部记录,时间倒序) */ export const GetOperationLogs = (id: string | number) => request({ url: `${baseUrl}${id}/operation_logs/`, method: 'get' }) +/** 受邀供应商报价单一览(与发布生成的报价主表一致,操作日志嵌套表用) */ +export const GetInvitedSupplierQuotations = (id: string | number) => + request({ url: `${baseUrl}${id}/invited_supplier_quotations/`, method: 'get' }) /** 查询杂采议价记录(比价议价价格存此表,非报价明细「中标价格」) */ export const GetNegotiationRecordsObj = (id: string | number, params?: { part_id?: string }) => request({ url: `${baseUrl}${id}/negotiation_records/`, method: 'get', params: params ?? {} }) diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/miscInquiryDetail.vue b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/miscInquiryDetail.vue index c30f650..7343060 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/miscInquiryDetail.vue +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/miscInquiryDetail.vue @@ -362,14 +362,55 @@ - + - + + + + @@ -502,7 +543,7 @@ - +
@@ -542,41 +583,89 @@ 操作履历明细表 - - - + + + + + - + - + @@ -600,7 +689,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' -import { ArrowLeft, Clock, Document } from '@element-plus/icons-vue' +import { ArrowDown, ArrowLeft, ArrowRight, Clock, Document } from '@element-plus/icons-vue' import { normalizeDict, formatCostTemplateVersion, formatRfqApiErrorMessage } from './crud' import * as api from './api' import * as costTemplateApi from '../cost_template/api' @@ -608,6 +697,7 @@ import { GetCompanies, GetList as GetCurrencies } from '../../basicinfo/currency import { GetList as GetParts } from '../misc_parts/api' import { GetList as GetMaterials } from '../misc_materials/api' import { GetList as GetSuppliers } from '../../basicinfo/supplier/api' +import { GetList as GetSupplierUsers } from '../../basicinfo/supplier_user/api' import { GetList as GetStations } from '../misc_stations/api' import { GetList as GetUnits } from '../../basicinfo/unit/api' import { useUserInfo } from '/@/stores/userInfo' @@ -688,10 +778,13 @@ const supplierOptions = ref<{ label: string supplier_code?: string supplier_name?: string - contact?: string - email?: string }[]>([]) const supplierLoading = ref(false) + +/** 按 supplier_id 缓存「供应商用户信息」列表(询价单联系人下拉) */ +type SupplierUserOption = { id: number; user_name: string; user_email: string; user_phone: string } +const supplierUsersBySid = ref>({}) +const supplierUsersLoading = ref>({}) const stationOptions = ref<{ value: string; label: string; rate?: number; unit?: string }[]>([]) const stationLoading = ref(false) const unitOptions = ref<{ value: string; label: string; name?: string }[]>([]) @@ -826,7 +919,7 @@ const loadCurrencyOptions = async (factory?: string) => { [] currencyOptions.value = (Array.isArray(list) ? list : []).map((c: any) => ({ value: c.currencycode || c.currency_code || c.code || c.currency, - label: c.currencyname || c.currency_name || c.currencycode || c.code || '币别', + label: c.currencycode || c.code || c.currencyname || c.currency_name || '币别', taxRate: extractTaxRate(c) })) } catch (e) { @@ -974,8 +1067,6 @@ const loadSupplierOptions = async (factory?: string) => { label: `${s.supplier_short_name || s.supplier_name || s.supplier_id || '供应商'}${s.supplier_id ? `(${s.supplier_id})` : ''}`, supplier_code: s.supplier_id || s.supplier_code, supplier_name: s.supplier_name || s.supplier_short_name, - contact: s.contact_person || s.contact || s.linkman, - email: s.contact_email || s.email, factory: supplierFactoryKeys.map((k) => s?.[k]).find((v) => v) })) } catch (e) { @@ -986,6 +1077,43 @@ const loadSupplierOptions = async (factory?: string) => { } } +const ensureSupplierUsersLoaded = async (sid: string) => { + const k = String(sid || '').trim() + if (!k) return + if (supplierUsersBySid.value[k]) return + if (supplierUsersLoading.value[k]) return + supplierUsersLoading.value = { ...supplierUsersLoading.value, [k]: true } + try { + const res = await GetSupplierUsers({ supplier_id: k, status: 1, page: 1, page_size: 500, pageSize: 500 }) + const list = + res?.data?.data?.results || + res?.data?.results || + res?.data?.list || + res?.data || + res?.results || + res?.list || + [] + supplierUsersBySid.value[k] = (Array.isArray(list) ? list : []).map((u: any) => ({ + id: Number(u.id), + user_name: String(u.user_name ?? ''), + user_email: String(u.user_email ?? ''), + user_phone: String(u.user_phone ?? '') + })) + } catch (e) { + console.warn('加载供应商用户失败', e) + supplierUsersBySid.value[k] = [] + } finally { + const next = { ...supplierUsersLoading.value } + delete next[k] + supplierUsersLoading.value = next + } +} + +const vendorContactOptionsForRow = (row: any) => { + const sid = String(row?.supplier_code || row?.supplier_id || '').trim() + return supplierUsersBySid.value[sid] || [] +} + const normalizeSections = (sections: any) => { if (typeof sections === 'string') { try { @@ -1168,6 +1296,26 @@ type RfqOperationLogRow = { const operationLogs = ref([]) const operationLogsLoading = ref(false) + +/** 受邀供应商报价一览(嵌套表,与后端 invited_supplier_quotations 一致) */ +type InvitedSupplierQuotationRow = { + operation_time?: string + supplier_full_name?: string + operator?: string + /** 报价单当前状态(与 QuotationMaster.STATUS_CHOICES 一致) */ + quotation_status?: string + operation_desc?: string + quotation_no?: string +} +type OpLogSnapshotRow = { _type: 'snapshot'; rowKey: string; anchorId?: number } +type CombinedOpLogRow = (RfqOperationLogRow & { _type?: 'log'; rowKey?: string }) | OpLogSnapshotRow + +const invitedSupplierQuotations = ref([]) +/** 受邀供应商明细:默认展开,与主表同一套列含义(内嵌表不重复表头) */ +const inviteListExpanded = ref(true) + +const isOpLogSnapshotRow = (row: unknown): row is OpLogSnapshotRow => + typeof row === 'object' && row !== null && (row as OpLogSnapshotRow)._type === 'snapshot' let handlingPlantChange = false const userStore = useUserInfo() /** 与主表 `purchase_dept` CharField(max_length=20) 一致 */ @@ -1638,8 +1786,41 @@ const timelineStepsView = computed(() => { }) }) -/** 明细表按时间正序,与参考稿「操作履历明细表」阅读习惯一致 */ -const operationLogsTableRows = computed(() => operationLogsChronological.value) +/** 明细表按时间正序;在首条「询价单发布」(3) 之后插入一行快照占位,用于嵌套受邀供应商报价表 */ +const combinedOpLogRows = computed((): CombinedOpLogRow[] => { + const list = operationLogsChronological.value + const out: CombinedOpLogRow[] = [] + let inserted = false + let idx = 0 + for (const row of list) { + const rowKey = `log-${row.id ?? idx}-${parseOperationLogTime(row.operation_time)}` + out.push({ ...row, _type: 'log', rowKey } as CombinedOpLogRow) + if (Number(row.operation_type) === 3 && !inserted) { + out.push({ + _type: 'snapshot', + rowKey: `snapshot-${row.id ?? 'pub'}-${idx}`, + anchorId: row.id + }) + inserted = true + } + idx += 1 + } + return out +}) + +const opLogSpanMethod = ({ row, columnIndex }: { row: Record; columnIndex: number }) => { + if (row._type === 'snapshot') { + if (columnIndex === 0) return [1, 6] + return [0, 0] + } + return [1, 1] +} + +const opLogRowKey = (row: CombinedOpLogRow) => { + if (isOpLogSnapshotRow(row)) return row.rowKey + const r = row as RfqOperationLogRow & { rowKey?: string } + return r.rowKey ?? `log-${r.id ?? ''}` +} const operationTypeLabel = (t: unknown) => { const n = typeof t === 'number' ? t : Number(t) @@ -1690,9 +1871,25 @@ const formatOperationStatusChange = (per: string | null | undefined, cur: string return `${a} → ${b}` } +const loadInvitedSupplierQuotations = async () => { + if (!currentId.value) { + invitedSupplierQuotations.value = [] + return + } + try { + const res = await api.GetInvitedSupplierQuotations(currentId.value) + const raw = unwrapResponseData(res) + const list = Array.isArray(raw) ? raw : (raw as { results?: unknown[] })?.results + invitedSupplierQuotations.value = Array.isArray(list) ? (list as InvitedSupplierQuotationRow[]) : [] + } catch { + invitedSupplierQuotations.value = [] + } +} + const loadOperationLogs = async () => { if (!currentId.value) { operationLogs.value = [] + invitedSupplierQuotations.value = [] return } operationLogsLoading.value = true @@ -1703,8 +1900,10 @@ const loadOperationLogs = async () => { operationLogs.value = Array.isArray(list) ? list.map((row) => normalizeOperationLogRow(row as Record)) : [] + await loadInvitedSupplierQuotations() } catch { operationLogs.value = [] + invitedSupplierQuotations.value = [] ElMessage.error('加载操作日志失败') } finally { operationLogsLoading.value = false @@ -1914,19 +2113,42 @@ const supplierOptionsForVendorRow = (row: any) => { }) } -const handleVendorSelect = (row: any, value?: string) => { +const handleVendorSelect = async (row: any, value?: string) => { row.name = value || '' const supplier = supplierOptions.value.find((s) => s.value === value) + row.supplier_user_id = undefined + row.contact = '' + row.email = '' + row.phone = '' if (supplier) { row.supplier_id = value || '' row.supplier_code = supplier.supplier_code || value || '' row.supplier_name = supplier.supplier_name || supplier.label || '' - if (supplier.contact) { - row.contact = supplier.contact - } - if (supplier.email) { - row.email = supplier.email - } + } else { + row.supplier_id = '' + row.supplier_code = '' + row.supplier_name = '' + } + const sid = String(row.supplier_code || row.supplier_id || '').trim() + if (sid) await ensureSupplierUsersLoaded(sid) +} + +const handleVendorContactSelect = (row: any, userId?: number | string | null) => { + const sid = String(row.supplier_code || row.supplier_id || '').trim() + const users = supplierUsersBySid.value[sid] || [] + const uid = + userId === null || userId === undefined || userId === '' ? undefined : Number(userId) + const u = uid != null && Number.isFinite(uid) ? users.find((x) => x.id === uid) : undefined + if (u) { + row.supplier_user_id = u.id + row.contact = u.user_name || '' + row.email = u.user_email || '' + row.phone = u.user_phone || '' + } else { + row.supplier_user_id = undefined + row.contact = '' + row.email = '' + row.phone = '' } } @@ -2120,15 +2342,31 @@ const openDetail = async (row: any, mode: 'edit' | 'view') => { form.vendors = Array.isArray(detail?.suppliers) ? detail.suppliers.map((s: any, idx: number) => ({ id: s.id || `v-${idx}-${Date.now()}`, - name: s.supplier_name || '', + name: s.supplier_code || '', supplier_id: s.supplier_code || '', supplier_code: s.supplier_code || '', supplier_name: s.supplier_name || '', + supplier_user_id: undefined as number | undefined, contact: s.contact_person || '', email: s.contact_email || '', phone: s.contact_phone || '' })) : [] + const resolveVendorContactSelections = async () => { + for (const v of form.vendors || []) { + const sid = String(v.supplier_code || v.supplier_id || '').trim() + if (!sid) continue + await ensureSupplierUsersLoaded(sid) + const users = supplierUsersBySid.value[sid] || [] + const email = String(v.email || '').trim() + const contact = String(v.contact || '').trim() + let match = users.find((u) => u.user_email === email && u.user_name === contact) + if (!match && email) match = users.find((u) => u.user_email === email) + if (!match && contact) match = users.find((u) => u.user_name === contact) + if (match) v.supplier_user_id = match.id + } + } + await resolveVendorContactSelections() const next = { drawing: [], tender: [], other: [] as any[] } if (Array.isArray(detail?.attachments)) { detail.attachments.forEach((a: any) => { @@ -2153,8 +2391,10 @@ const addVendorRow = () => { supplier_id: '', supplier_code: '', supplier_name: '', + supplier_user_id: undefined as number | undefined, contact: '', - email: '' + email: '', + phone: '' }) } @@ -2472,6 +2712,24 @@ const saveForm = async () => { } seenVendorKeys.add(k) } + for (const v of form.vendors || []) { + const k = vendorRowSelectKey(v) + if (!k) continue + const sid = String(v.supplier_code || v.supplier_id || '').trim() + if (!sid) continue + await ensureSupplierUsersLoaded(sid) + const users = supplierUsersBySid.value[sid] || [] + if (!users.length) { + ElMessage.warning( + `供应商「${v.supplier_name || sid}」下暂无有效用户信息,请先在基础资料维护「供应商用户信息」后再选择` + ) + return + } + if (v.supplier_user_id == null || Number.isNaN(Number(v.supplier_user_id))) { + ElMessage.warning(`请为供应商「${v.supplier_name || sid}」从下拉框选择联系人`) + return + } + } try { const uploadedAttachments = await prepareAttachmentsForSubmit() form.attachments = uploadedAttachments @@ -3250,4 +3508,55 @@ onMounted(() => { .op-type--default { background: #909399; } +.op-log-main-table :deep(.el-table__body td.el-table__cell) { + vertical-align: top; +} +.op-invite-wrap { + width: 100%; + margin: 0; + padding: 0; +} +.op-invite-toggle { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 2px 0 6px; + margin: 0; + border: none; + background: transparent; + cursor: pointer; + font: inherit; + color: var(--el-text-color-secondary); + text-align: left; + line-height: 1.4; +} +.op-invite-toggle:hover { + color: var(--el-color-primary); +} +.op-invite-toggle__icon { + flex-shrink: 0; + font-size: 14px; + transition: transform 0.15s ease; +} +.op-invite-toggle__text { + font-size: 13px; + font-weight: 500; +} +.op-invite-body { + margin: 0; + padding: 0; +} +.op-invite-nested-table { + width: 100%; +} +.op-invite-nested-table :deep(.el-table__inner-wrapper::before) { + display: none; +} +.op-invite-nested-table :deep(.el-table__body-wrapper) { + margin-bottom: 0; +} +.op-invite-nested-table :deep(.el-table__body tr:last-child td) { + border-bottom-color: var(--el-border-color-lighter); +} diff --git a/web/src/views/pissupplier/quotation/crud.tsx b/web/src/views/pissupplier/quotation/crud.tsx index 4e031e2..0fc2e35 100644 --- a/web/src/views/pissupplier/quotation/crud.tsx +++ b/web/src/views/pissupplier/quotation/crud.tsx @@ -16,7 +16,6 @@ const extractPagedList = (res: any): any[] => { export type QuoteStatus = 'pending' | 'quoted' | 'completed' | 'expired' type CostAttr = { key: string; label: string; value: string | number; type?: string } -type CostTemplateItem = { section: string; attrs: CostAttr[]; span?: 'wide'; allowAdd?: boolean } type CostItem = { id: string; section: string; span?: 'wide'; attrs: CostAttr[]; field?: string } type CostRow = { id: string @@ -72,6 +71,7 @@ type Quote = { /** 询价单关联附件(`pis_proc_inquiry_attachment`),详情接口 `inquiry_attachments` */ inquiryAttachments: InquiryAttachmentRow[] remark: string + inquiryRemark: string createdAt: string templateSections?: any enableCostStructure?: boolean @@ -149,7 +149,7 @@ export function getSupplierBidWindowRejectReason(row: any, nowMs: number = Date. if (!Number.isFinite(start) || !Number.isFinite(end)) { return '招标项目缺少投标开始或截止时间,无法报价或提交' } - if (nowMs < start) return '投标尚未开始,请在投标开始后再报价或提交' + // if (nowMs < start) return '投标尚未开始,请在投标开始后再报价或提交' if (nowMs > end) return '已超过投标截止时间,无法报价或提交' return null } @@ -431,49 +431,14 @@ const processStationKeys = ['process_station', 'processStation'] /** * 与后端子表模型字段及 costRowsToNestedPayload 使用的 row.values 键一致。 - * 展示用列名优先来自询价 `template_sections` 各段 `fields[].label`(与 miscInquiryDetail 成本结构一致), - * 本常量仅在模板未配置字段时作列键与默认标题的 fallback。 - * QuotationMaterial / QuotationProcess / QuotationOther / QuotationProfit 见 apps.pissupplier.models + * 列定义仅来自询价单绑定的成本模板 `template_sections` 各段 `fields`;无模板字段时该段无列。 */ export type QuotationCostColumn = { key: string label: string } -export const FIXED_QUOTATION_SECTION_COLUMNS: Record = { - 材料成本: [ - { key: 'material', label: '材质' }, - { key: 'len', label: '长(mm)' }, - { key: 'width', label: '宽(mm)' }, - { key: 'height', label: '高(mm)' }, - { key: 'specificgravity', label: '比重(kg/cm³)' }, - { key: 'qty', label: '数量' }, - { key: 'weight', label: '重量(kg)' }, - { key: 'unitPrice', label: '单价' }, - { key: 'material_fee', label: '材料费用' }, - { key: 'remark', label: '备注' } - ], - 加工成本: [ - { key: 'processStation', label: '加工工站' }, - { key: 'processUnit', label: '单位' }, - { key: 'processRate', label: '费率' }, - { key: 'processMeasure', label: '加工计量' }, - { key: 'processFee', label: '加工费' }, - { key: 'remark', label: '备注' } - ], - 其它成本: [ - { key: 'packageFee', label: '包装费' }, - { key: 'transportFee', label: '运输费' } - ], - 利润: [ - { key: 'profitRate', label: '利润率(%)' } - ], - 税金: [ - { key: 'taxRate', label: '税率(%)' } - ] -} - -/** 模板 CostEstimateTemplateBody.item_no 归一化后 → 上表 UI 字段 key,用于只读规则(不用于列) */ +/** 模板 item_no 归一化后 → 报价单 UI `row.values` 键(与询价端模板字段对齐) */ const TEMPLATE_KEY_TO_UI_KEYS: Record> = { 材料成本: { material: ['material'], @@ -533,164 +498,9 @@ const TEMPLATE_KEY_TO_UI_KEYS: Record> = { const normTplKey = (k: string) => String(k || '').toLowerCase().replace(/[^a-z0-9]/g, '') -/** 模板未声明某固定列时的列标题兜底(与 merge 补列一致) */ -const labelFallbacks: Record = { - material: '材质', - material_cost: '材料费用', - material_fee: '材料费用', - materialFee: '材料费用', - material_amount: '材料费用', - materialAmount: '材料费用', - specificgravity: '比重', - density: '比重', - weight: '重量', - length: '长', - len: '长', - width: '宽', - height: '高', - qty: '数量', - quantity: '数量', - unitPrice: '单价', - unitprice: '单价', - price: '单价', - unit_price: '单价', - process_station: '加工工站', - processStation: '加工工站', - process_unit: '单位', - unit: '单位', - unitrate: '费率', - rate: '费率', - process_rate: '费率', - fee_rate: '费率', - processqty: '加工计量', - process_qty: '加工计量', - processprice: '加工费', - process_cost: '加工费', - fee: '加工费', - process_fee: '加工费', - packageFee: '包装费', - transportFee: '运输费', - profitRate: '利润率(%)', - taxRate: '税率(%)' -} - const templateFieldDisplayLabel = (f: any, rawKey: string) => String(f?.label ?? f?.nameCn ?? f?.name_cn ?? f?.name ?? rawKey).trim() || rawKey -/** - * 成本结构列:与 miscInquiryDetail 一致,优先按 `template_sections` 各段 `fields` 顺序与中文名展示; - * 模板字段 key 经 TEMPLATE_KEY_TO_UI_KEYS 映射到报价单 UI 存储键;无模板字段时回退 FIXED_QUOTATION_SECTION_COLUMNS。 - */ -const mergeQuotationSectionColumns = (section: string, templateSections: any): QuotationCostColumn[] => { - const fixedFallback = [...(FIXED_QUOTATION_SECTION_COLUMNS[section] || [])] - const tpl = normalizeSections(templateSections).find( - (s: any) => (s.title || s.name || s.section || '') === section - ) - const fields = Array.isArray(tpl?.fields) ? tpl.fields : [] - const keyMap = TEMPLATE_KEY_TO_UI_KEYS[section] || {} - - if (!fields.length) { - return fixedFallback - } - - const out: QuotationCostColumn[] = [] - const seen = new Set() - - for (const f of fields) { - const rawKey = String(f?.key || '').trim() - if (!rawKey) continue - const nk = normTplKey(rawKey) - if (section === '加工成本' && (nk === 'processfee' || rawKey === 'process_fee')) continue - if (nk === 'partid' || rawKey === 'part_id') continue - - const uiKeys: string[] = keyMap[nk] || keyMap[String(f.key).toLowerCase()] || [] - const uiKey = uiKeys.length ? uiKeys[0] : rawKey - if (seen.has(uiKey)) continue - seen.add(uiKey) - out.push({ key: uiKey, label: templateFieldDisplayLabel(f, rawKey) }) - } - - for (const fc of fixedFallback) { - if (!seen.has(fc.key)) { - seen.add(fc.key) - out.push({ - key: fc.key, - label: labelFallbacks[fc.key] || fc.label - }) - } - } - - return out.length ? out : fixedFallback -} - -const costTemplates: Record = { - default: [ - { - section: '材料成本', - span: 'wide', - allowAdd: true, - attrs: [ - { key: 'material', label: '材质', value: '' }, - { key: 'len', label: '长', value: '' }, - { key: 'width', label: '宽', value: '' }, - { key: 'height', label: '高', value: '' }, - { key: 'density', label: '比重', value: '' }, - { key: 'qty', label: '数量', value: '' }, - { key: 'weight', label: '重量', value: '' }, - { key: 'unitPrice', label: '单价', value: '' }, - { key: 'materialTotal', label: '材料成本合计', value: '' } - ] - }, - { - section: '加工成本', - span: 'wide', - allowAdd: true, - attrs: [ - { key: 'processStation', label: '加工工站', value: '' }, - { key: 'processUnit', label: '单位', value: '' }, - { key: 'processRate', label: '费率', value: '' }, - { key: 'processMeasure', label: '加工计量', value: '' }, - { key: 'processFee', label: '加工费', value: '' }, - { key: 'processTotal', label: '加工成本合计', value: '' } - ] - }, - { - section: '管销研费用', - attrs: [ - { key: 'overheadType', label: '费用类别', value: '' }, - { key: 'overheadTotal', label: '费用', value: '' } - ] - }, - { - section: '其他费用', - attrs: [ - { key: 'packageFee', label: '包装费', value: '' }, - { key: 'transportFee', label: '运输费', value: '' }, - { key: 'otherTotal', label: '合计', value: '' } - ] - }, - { - section: '利润', - attrs: [ - { key: 'profitRate', label: '利润率(%)', value: '' }, - { key: 'profitTotal', label: '利润金额', value: '' } - ] - }, - { - section: '税金', - attrs: [ - { key: 'taxRate', label: '税率(%)', value: '' }, - { key: 'taxTotal', label: '税金金额', value: '' } - ] - } - ], - tooling: [], - equipment: [], - plastic: [] -} - -const resolveTemplate = (key: string) => (costTemplates[key]?.length ? costTemplates[key] : costTemplates.default) - const normalizeSections = (sections: any) => { if (!sections) return [] if (typeof sections === 'string') { @@ -704,15 +514,47 @@ const normalizeSections = (sections: any) => { return Array.isArray(sections) ? sections : [] } +/** 仅从询价 `template_sections` 各段 `fields` 生成列;无字段则空数组。 */ +const mergeQuotationSectionColumns = (section: string, templateSections: any): QuotationCostColumn[] => { + const tpl = normalizeSections(templateSections).find( + (s: any) => (s.title || s.name || s.section || '') === section + ) + const fields = Array.isArray(tpl?.fields) ? tpl.fields : [] + const keyMap = TEMPLATE_KEY_TO_UI_KEYS[section] || {} + + if (!fields.length) { + return [] + } + + const out: QuotationCostColumn[] = [] + const seen = new Set() + + for (const f of fields) { + const rawKey = String(f?.key || f?.item_no || '').trim() + if (!rawKey) continue + const nk = normTplKey(rawKey) + if (section === '加工成本' && (nk === 'processfee' || rawKey === 'process_fee')) continue + if (nk === 'partid' || rawKey === 'part_id') continue + + const uiKeys: string[] = keyMap[nk] || keyMap[String(rawKey).toLowerCase()] || [] + const uiKey = uiKeys.length ? uiKeys[0] : rawKey + if (seen.has(uiKey)) continue + seen.add(uiKey) + out.push({ key: uiKey, label: templateFieldDisplayLabel(f, rawKey) }) + } + + return out +} + const allowedSectionsForTemplate = (sections: any, enableCostStructure = true) => { const base = enableCostStructure ? costEnabledSections : costDisabledSections const tplSections = normalizeSections(sections) - const templateSections = tplSections + const templateTitles = tplSections .filter((s: any) => s?.enabled !== false) .map((s: any) => s.title || s.name || s.section || '') .filter(Boolean) - const allowed = templateSections.length ? base.filter((t) => templateSections.includes(t)) : base - return allowed.length ? allowed : base + if (!templateTitles.length) return [] + return base.filter((t) => templateTitles.includes(t)) } /** 与 CostEstimateTemplateHead.is_can_add_materials / is_can_add_process 对应;后端 template_sections 每段带 supplierCanAddRow */ @@ -752,17 +594,15 @@ const buildRowsFromTemplate = (sections: any, enableCostStructure = true): CostR const rows: CostRow[] = [] allowed.forEach((title: string, idx: number) => { const cols = mergeQuotationSectionColumns(title, sections) + if (!cols.length) return const values: Record = {} const labels: Record = {} - ;(cols || []).forEach((c) => { + cols.forEach((c) => { values[c.key] = '' labels[c.key] = c.label }) rows.push({ id: `tpl-${title}-${idx}-${Date.now()}`, section: title, field: `tpl-${title}-${idx}`, values, labels }) }) - if (!rows.length) { - return costDisabledSections.map((title, idx) => ({ id: `def-${idx}-${Date.now()}`, section: title, field: `def-${idx}`, values: {}, labels: {} })) - } return rows } @@ -1150,6 +990,12 @@ const formatMoney = (v: number | string) => { return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } +/** 成本结构各段标题旁「合计」,与 `sectionAmountMap` 及单元格金额小数位一致(2~4 位) */ +export function formatCostStructureSectionTotal(n: number): string { + if (!Number.isFinite(n)) return '0' + return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 4 }) +} + /** 主表无报价金额字段时,由上阶物料明细含税总价汇总 */ const totalInclTaxFromRfqItems = (rfqItems: unknown): number | '' => { if (!Array.isArray(rfqItems) || !rfqItems.length) return '' @@ -1195,6 +1041,7 @@ function blankQuote(): Quote { attachments: [], inquiryAttachments: [], remark: '', + inquiryRemark: '', createdAt: '', templateSections: [], enableCostStructure: true, @@ -1263,12 +1110,13 @@ export function useQuoteCrud(options?: { const tplSec = templateSectionMap.value.get(section) const keyMap = TEMPLATE_KEY_TO_UI_KEYS[section] || {} ;(tplSec?.fields || []).forEach((f: any) => { - if (!f?.key) return + const fk = String(f?.key || f?.item_no || '').trim() + if (!fk) return const ic = Number(f.is_computed ?? f.isComputed ?? (f.autoFill ? 1 : 0)) === 1 || f.autoFill === true const sr = Number(f.supplier_required ?? f.supplierRequired ?? 0) - const nk = normTplKey(f.key) - const uiKeys = keyMap[nk] || keyMap[String(f.key).toLowerCase()] || [f.key] + const nk = normTplKey(fk) + const uiKeys = keyMap[nk] || keyMap[String(fk).toLowerCase()] || [fk] uiKeys.forEach((uk) => { m.set(uk, { isComputed: ic, supplierRequired: sr }) }) @@ -1278,7 +1126,7 @@ export function useQuoteCrud(options?: { return out }) - /** 固定列 + 模板定义的扩展列(仅材料/加工,与 option_json 一致);锁格规则仍来自 template_sections */ + /** 列与锁格规则均来自询价 `template_sections` 各段 `fields` */ const sectionColumns = computed>(() => { const res: Record = {} const tpl = current.templateSections @@ -1324,13 +1172,15 @@ export function useQuoteCrud(options?: { if (map['其它成本']) { if (!map['其它成本'].length) { const cols = sectionColumns.value['其它成本'] || [] - const values: Record = {} - const labels: Record = {} - cols.forEach((c) => { - values[c.key] = '' - labels[c.key] = c.label - }) - map['其它成本'] = [{ id: 'other-default', section: '其它成本', field: 'other-default', values, labels }] + if (cols.length) { + const values: Record = {} + const labels: Record = {} + cols.forEach((c) => { + values[c.key] = '' + labels[c.key] = c.label + }) + map['其它成本'] = [{ id: 'other-default', section: '其它成本', field: 'other-default', values, labels }] + } } else if (map['其它成本'].length > 1) { map['其它成本'] = [map['其它成本'][0]] } @@ -1463,6 +1313,7 @@ export function useQuoteCrud(options?: { q.inquiryStatusCode = Number(invSt) } q.inquiryStatus = formatMiscInquiryStatus(q) + q.inquiryRemark = inv.remark || q.inquiryRemark || '' } } @@ -1548,6 +1399,7 @@ export function useQuoteCrud(options?: { ? item.inquiryAttachments : [], remark: item.remark || '', + inquiryRemark: item.inquiry_remark || item.inquiryRemark || '', createdAt: item.creattime || item.creat_time || item.create_time || item.createTime || item.createdAt || '', templateSections: item.template_sections || @@ -1562,10 +1414,25 @@ export function useQuoteCrud(options?: { return quote } - const loadMaterialOptions = async () => { + /** + * 杂采材质:必须按询价单交易厂区 `Inquiry.company_code` 过滤 `MiscProcurementMaterialInfo.factory`, + * 否则不同厂区同材质名会重复出现且选材质时 `find` 会命中最低价那条。 + */ + const loadMaterialOptions = async (factoryCode?: string) => { + const fc = (factoryCode || '').trim() + if (!fc) { + materialOptions.value = [] + return + } materialLoading.value = true try { - const res = await GetMaterials({ page: 1, page_size: 300, pageSize: 300 }) + const res = await GetMaterials({ + page: 1, + page_size: 500, + pageSize: 500, + factory: fc, + status: 1 + }) const list = res?.data?.data?.results || res?.data?.results || res?.data?.list || res?.data || res?.results || res?.list || [] materialOptions.value = (Array.isArray(list) ? list : []).map((m: any) => ({ value: m.materialtype || m.material || m.name, @@ -1581,10 +1448,21 @@ export function useQuoteCrud(options?: { } } - const loadStationOptions = async () => { + const loadStationOptions = async (companyCode?: string) => { + const cc = (companyCode || '').trim() + if (!cc) { + stationOptions.value = [] + return + } stationLoading.value = true try { - const res = await GetStations({ page: 1, page_size: 500, pageSize: 500 }) + const res = await GetStations({ + page: 1, + page_size: 500, + pageSize: 500, + company_code: cc, + status: 1 + }) const list = res?.data?.data?.results || res?.data?.results || res?.data?.list || res?.data || res?.results || res?.list || [] stationOptions.value = (Array.isArray(list) ? list : []).map((s: any) => ({ value: s.stationcode || s.station_code || s.stationname || s.name, @@ -1666,8 +1544,6 @@ export function useQuoteCrud(options?: { if (uiContext === 'list') { loadQuotes() } - loadMaterialOptions() - loadStationOptions() loadUnitOptions() loadMiscPartLookup() }) @@ -2040,6 +1916,8 @@ export function useQuoteCrud(options?: { quoteData.inquiryStatus = formatMiscInquiryStatus(quoteData) fillCurrent(quoteData, effectiveRows) + await loadMaterialOptions(current.inquiryCompanyCode) + await loadStationOptions(current.inquiryCompanyCode) // 让列表行立即反映「报价(2)」状态,保证表格里的「提交」按钮可用 if (quoteData?.id) { @@ -2089,17 +1967,28 @@ export function useQuoteCrud(options?: { }) const list = extractPagedList(res) const inv = list[0] - if (inv?.attachments?.length) { - merged.inquiryAttachments = mapInquiryAttachmentsFromInquiryApi(inv.attachments) + if (inv) { + const icc = + inv.company_code != null && inv.company_code !== '' ? String(inv.company_code).trim() : '' + if (icc && !(merged.inquiryCompanyCode || '').trim()) { + merged.inquiryCompanyCode = icc + } + if (inv.attachments?.length) { + merged.inquiryAttachments = mapInquiryAttachmentsFromInquiryApi(inv.attachments) + } } } } catch (e) { console.warn('加载询价附件失败', e) } fillCurrent(merged) + await loadMaterialOptions(current.inquiryCompanyCode) + await loadStationOptions(current.inquiryCompanyCode) } catch (e) { console.warn('加载报价详情失败', e) fillCurrent(row) + await loadMaterialOptions(current.inquiryCompanyCode) + await loadStationOptions(current.inquiryCompanyCode) } finally { loading.value = false } @@ -2207,6 +2096,10 @@ export function useQuoteCrud(options?: { const addCostRow = (section: string) => { const cols = mergeQuotationSectionColumns(section, current.templateSections) + if (!cols.length) { + ElMessage.warning('该段未在成本模板中配置字段,无法新增行') + return + } const values: Record = {} const labels: Record = {} cols.forEach((c) => { @@ -2510,6 +2403,10 @@ export function useQuoteCrud(options?: { quoteSummaryRows, quoteAmountPreTax, quoteTotal, + /** 成本合计 / 税前 / 税后(与报价合计、保存汇总一致) */ + quoteRollupForRfq, + sectionAmountMap, + formatCostStructureSectionTotal, addCostRow, removeCostRow, materialOptions, @@ -2533,18 +2430,9 @@ export function useQuoteCrud(options?: { } } -const buildCostItems = (templateKey: string, templateSections?: any, enableCostStructure = true): CostItem[] => { +const buildCostItems = (_templateKey: string, templateSections?: any, enableCostStructure = true): CostItem[] => { const fromTplSections = buildCostItemsFromTemplateSections(templateSections, enableCostStructure) if (fromTplSections.length) return fromTplSections if (!enableCostStructure) return buildDefaultNonBomCostItems() - const tpl = resolveTemplate(templateKey) - const allowed = new Set(enableCostStructure ? costEnabledSections : costDisabledSections) - return tpl - .filter((item) => allowed.has(item.section)) - .map((item) => ({ - id: crypto.randomUUID(), - section: item.section, - span: item.span, - attrs: item.attrs.map((a) => ({ ...a })) - })) + return [] } diff --git a/web/src/views/pissupplier/quotation/detail.vue b/web/src/views/pissupplier/quotation/detail.vue index 53b96dd..6563024 100644 --- a/web/src/views/pissupplier/quotation/detail.vue +++ b/web/src/views/pissupplier/quotation/detail.vue @@ -22,7 +22,7 @@
-
+
{{ displayText(current.inquiryCode) }} {{ displayText(current.inquiryTitle) }} @@ -30,38 +30,39 @@ {{ displayText(current.currency) }} {{ formatQuoteDeadlineDisplay(current.quoteDeadline) || '—' }} {{ displayText(current.companyShortName || current.inquiryCompanyCode) }} + {{ displayText(current.inquiryRemark) }}
询价附件
-
-
{{ group.label }}
-
-
暂无询价附件
-
-
-
+