diff --git a/backend/apps/pisadmin/miscprocurement/models.py b/backend/apps/pisadmin/miscprocurement/models.py
index 2df9492..fe6bc61 100644
--- a/backend/apps/pisadmin/miscprocurement/models.py
+++ b/backend/apps/pisadmin/miscprocurement/models.py
@@ -1,5 +1,5 @@
import logging
-from typing import Optional
+from typing import Iterable, Optional
from django.db import models
from django.utils import timezone
@@ -175,8 +175,13 @@ class CostEstimateTemplateBody(models.Model):
class Inquiry(CoreModel):
- """杂采询价单主表"""
-
+ """杂采询价单主表
+
+ 与供应商端报价联动:当全部报价单都不再处于待报价/报价中时,「发布」「报价中」可经
+ `Inquiry.sync_to_quote_closed_when_no_open_quotations` 收口为「报价结束」;
+ 当供应商名单中每家均有一条「已报价」报价单时,可经 `Inquiry.sync_to_quote_closed_when_all_suppliers_quoted` 收口(见 `submit`)。
+ """
+
# (1开立;2确认;3发布;4报价中;5报价结束;6比议价中;7价格审核;8核价通过(结束);9落标(结束);0作废)
STATUS_CHOICES = (
(1, "开立"),
@@ -269,6 +274,111 @@ class Inquiry(CoreModel):
def __str__(self) -> str: # pragma: no cover - simple repr
return self.inquiry_no
+ @classmethod
+ def sync_to_quote_closed_when_no_open_quotations(
+ cls,
+ inquiry_nos: Iterable[str],
+ *,
+ actor_username: Optional[str] = None,
+ ) -> int:
+ """
+ 当某询价单下已不存在「待报价」(1) 或「报价中」(2) 的报价单时,若询价单仍为「发布」(3) 或「报价中」(4),
+ 则置为「报价结束」(5),并写操作日志。
+
+ 典型调用:供应商报价单因超过截止时间被批量置为「已过期」后,判断询价维度是否可收口为报价结束。
+ """
+ from apps.pissupplier.models import QuotationMaster # 避免 apps 间循环 import
+
+ now = timezone.now()
+ username = (str(actor_username).strip()[:20] if actor_username else None) or None
+ unique = {str(x).strip() for x in inquiry_nos if x and str(x).strip()}
+ n = 0
+ for inq_no in unique:
+ if QuotationMaster.objects.filter(inquiry_no=inq_no, status__in=(1, 2)).exists():
+ continue
+ inq = cls.objects.filter(inquiry_no=inq_no, status__in=(3, 4)).first()
+ if not inq:
+ continue
+ old_status = int(inq.status if inq.status is not None else 0)
+ inq.status = 5
+ inq.update_time = now
+ inq.update_datetime = now
+ if username:
+ inq.update_user = username
+ inq.save(update_fields=["status", "update_time", "update_user", "update_datetime"])
+ RFQOperationLogs.try_append(
+ inquiry_no=inq.inquiry_no,
+ purchase_type=int(inq.purchase_type),
+ operation_type=6,
+ operation_user=username,
+ quotation_no="-",
+ per_status=old_status,
+ cur_status=5,
+ operation_desc="全部报价单已结束待报价/报价中,询价单同步为报价结束",
+ )
+ n += 1
+ return n
+
+ @classmethod
+ def sync_to_quote_closed_when_all_suppliers_quoted(
+ cls,
+ inquiry_no: str,
+ *,
+ actor_username: Optional[str] = None,
+ quotation_no: Optional[str] = None,
+ ) -> bool:
+ """
+ 若询价单供应商名单(InquirySupplier)中每个供应商在 `QuotationMaster` 上均有一条「已报价」(3) 记录,
+ 且询价单仍为「发布」(3) 或「报价中」(4),则将询价单置为「报价结束」(5)。
+
+ 用于最后一户提交正式报价后收口,不依赖是否已超过报价截止时间。
+ """
+ from apps.pissupplier.models import QuotationMaster # 避免 apps 间循环 import
+
+ inq_no = (inquiry_no or "").strip()
+ if not inq_no:
+ return False
+ inq = cls.objects.filter(inquiry_no=inq_no, status__in=(3, 4)).first()
+ if not inq:
+ return False
+
+ raw_codes = (
+ InquirySupplier.objects.filter(inquiry_no=inq_no)
+ .values_list("supplier_code", flat=True)
+ .distinct()
+ )
+ supplier_codes = {str(c).strip() for c in raw_codes if c and str(c).strip()}
+ if not supplier_codes:
+ return False
+
+ for code in supplier_codes:
+ qm = QuotationMaster.objects.filter(inquiry_no=inq_no, supplier_code=code).first()
+ st = int(qm.status) if qm is not None and qm.status is not None else None
+ if st != 3:
+ return False
+
+ old_status = int(inq.status if inq.status is not None else 0)
+ now = timezone.now()
+ username = (str(actor_username).strip()[:20] if actor_username else None) or None
+ inq.status = 5
+ inq.update_time = now
+ inq.update_datetime = now
+ if username:
+ inq.update_user = username
+ inq.save(update_fields=["status", "update_time", "update_user", "update_datetime"])
+ qn = (quotation_no or "-").strip()[:20] or "-"
+ RFQOperationLogs.try_append(
+ inquiry_no=inq.inquiry_no,
+ purchase_type=int(inq.purchase_type),
+ operation_type=6,
+ operation_user=username,
+ quotation_no=qn,
+ per_status=old_status,
+ cur_status=5,
+ operation_desc="全部供应商已提交报价,询价单同步为报价结束",
+ )
+ return True
+
class InquirySupplier(models.Model):
"""杂采询价单-供应商关联表"""
diff --git a/backend/apps/pisadmin/miscprocurement/views.py b/backend/apps/pisadmin/miscprocurement/views.py
index 6bdfe42..d3a5c59 100644
--- a/backend/apps/pisadmin/miscprocurement/views.py
+++ b/backend/apps/pisadmin/miscprocurement/views.py
@@ -247,6 +247,14 @@ class CostEstimateTemplateViewSet(CustomModelViewSet):
class InquiryViewSet(CustomModelViewSet):
+ """
+ 杂采询价单 CRUD/发布等。
+
+ 与供应商报价单联动:当 `pissupplier.QuotationMasterViewSet.sync_expired` 将超时报价置为已过期后,
+ 若该询价下已无待报价/报价中的报价单,且主表状态仍为「发布」(3) 或「报价中」(4),
+ 会由 `Inquiry.sync_to_quote_closed_when_no_open_quotations` 将询价单置为「报价结束」(5)。
+ """
+
queryset = Inquiry.objects.all().prefetch_related(
"suppliers",
"attachments",
@@ -572,7 +580,7 @@ class InquiryViewSet(CustomModelViewSet):
20,
) or None
current_time = timezone.now()
- quote_deadline = inquiry.quote_deadline.strftime("%Y-%m-%d %H:%M:%S") if inquiry.quote_deadline else None
+ quote_deadline = inquiry.quote_deadline
inquiry_materials = list(inquiry.material_costs.all())
inquiry_processes = list(inquiry.process_costs.all())
diff --git a/backend/apps/pissupplier/models.py b/backend/apps/pissupplier/models.py
index 669ef57..153102d 100644
--- a/backend/apps/pissupplier/models.py
+++ b/backend/apps/pissupplier/models.py
@@ -38,7 +38,7 @@ class QuotationMaster(models.Model):
contact_person = models.CharField( max_length=20, null=True, blank=True, verbose_name="联系人" )
contact_phone = models.CharField( max_length=20, null=True, blank=True, verbose_name="联系人电话" )
contact_email = models.CharField( max_length=20, null=True, blank=True, verbose_name="联系人邮件" )
- quote_deadline = models.CharField( max_length=20, null=True, blank=True, verbose_name="报价截止时间" )
+ quote_deadline = models.DateTimeField( null=True, blank=True, verbose_name="报价截止时间" )
validity_days = models.IntegerField( null=True, blank=True, verbose_name="有效天数" )
delivery_days = models.IntegerField( null=True, blank=True, verbose_name="交货周期" )
payment_method = models.IntegerField( choices=PAYMENT_METHOD_CHOICES, null=True, blank=True, verbose_name="付款方式" )
@@ -48,7 +48,12 @@ class QuotationMaster(models.Model):
null=True,
blank=True,
verbose_name="状态",
- help_text="1 待报价 2 已报价 3 已过期;置为已报价请走供应商端 POST quotation_master/{id}/submit/",
+ help_text=(
+ "1 待报价 2 报价中 3 已报价 4 已过期;"
+ "置为已报价请走 POST quotation_master/{id}/submit/;"
+ "超时批量置过期及询价单收口见 sync_expired / Inquiry.sync_to_quote_closed_when_no_open_quotations;"
+ "名单内供应商均已提交见 submit 与 Inquiry.sync_to_quote_closed_when_all_suppliers_quoted。"
+ ),
)
is_awarded = models.IntegerField( choices=AWARD_STATUS_CHOICES, default=0, null=True, blank=True, verbose_name="报价中标否" )
createuser = models.CharField( max_length=20, null=True, blank=True, verbose_name="创建人员" )
diff --git a/backend/apps/pissupplier/serializers.py b/backend/apps/pissupplier/serializers.py
index 0ad4857..4f29777 100644
--- a/backend/apps/pissupplier/serializers.py
+++ b/backend/apps/pissupplier/serializers.py
@@ -376,13 +376,24 @@ class SupplierInquiryAttachmentSerializer(serializers.ModelSerializer):
class QuotationMasterSerializer(BusinessAuditSerializer):
- """杂采报价单主表序列化器"""
+ """杂采报价单主表序列化器
+
+ `status`:1 待报价、2 报价中、3 已报价、4 已过期;超时由 POST `quotation_master/sync_expired/` 批量更新。
+ 同步过期后,若询价单 `Inquiry` 仍为「发布」(3) 或「报价中」(4) 且该询价下已无任何待报价/报价中单,
+ 则询价单会置为「报价结束」(5),见 `Inquiry.sync_to_quote_closed_when_no_open_quotations`。
+ 全部供应商名单均已正式提交报价(均为已报价)时由 `POST …/submit/` 内 `Inquiry.sync_to_quote_closed_when_all_suppliers_quoted` 收口。
+ """
creattime = serializers.DateTimeField(
format="%Y-%m-%d %H:%M:%S",
required=False,
allow_null=True,
)
+ quote_deadline = serializers.DateTimeField(
+ format="%Y-%m-%d %H:%M:%S",
+ required=False,
+ allow_null=True,
+ )
bid_start_time = serializers.DateTimeField(
format="%Y-%m-%d %H:%M:%S",
required=False,
@@ -503,7 +514,11 @@ class QuotationMasterCreateUpdateSerializer(BusinessAuditSerializer):
contact_person = serializers.CharField(max_length=20, required=False, allow_blank=True, allow_null=True)
contact_phone = serializers.CharField(max_length=20, required=False, allow_blank=True, allow_null=True)
contact_email = serializers.CharField(max_length=20, required=False, allow_blank=True, allow_null=True)
- quote_deadline = serializers.CharField(max_length=20, required=False, allow_blank=True, allow_null=True)
+ quote_deadline = serializers.DateTimeField(
+ format="%Y-%m-%d %H:%M:%S",
+ required=False,
+ allow_null=True,
+ )
validity_days = serializers.IntegerField(required=False, allow_null=True)
delivery_days = serializers.IntegerField(required=False, allow_null=True)
payment_method = serializers.IntegerField(required=False, allow_null=True)
diff --git a/backend/apps/pissupplier/views.py b/backend/apps/pissupplier/views.py
index b74e834..afd9b76 100644
--- a/backend/apps/pissupplier/views.py
+++ b/backend/apps/pissupplier/views.py
@@ -4,7 +4,7 @@ from django.db import transaction
from django.utils import timezone
from rest_framework.decorators import action
-from dvadmin.utils.json_response import DetailResponse, ErrorResponse
+from dvadmin.utils.json_response import DetailResponse, ErrorResponse, SuccessResponse
from dvadmin.utils.viewset import CustomModelViewSet
from apps.pisadmin.miscprocurement.models import Inquiry, RFQOperationLogs
@@ -60,6 +60,32 @@ class QuotationMasterViewSet(CustomModelViewSet):
)
ordering = ("-creattime", "-autoid")
+ @action(methods=["post"], detail=False, url_path="sync_expired")
+ def sync_expired(self, request):
+ """
+ 将当前用户数据权限范围内、状态为待报价(1)或报价中(2)、且已超过报价截止时间的报价单
+ 更新为已过期(4)。若某询价单下已无任何待报价/报价中的报价单,且询价单仍为「发布」或「报价中」,
+ 则将该询价单置为「报价结束」(5)。供供应商端列表加载前调用。
+ """
+ now = timezone.now()
+ qs = self.filter_queryset(self.get_queryset()).filter(
+ status__in=(1, 2),
+ quote_deadline__isnull=False,
+ quote_deadline__lt=now,
+ )
+ inquiry_nos = list(qs.values_list("inquiry_no", flat=True).distinct())
+ username = getattr(getattr(request, "user", None), "username", None)
+ with transaction.atomic():
+ updated = qs.update(status=4)
+ inquiries_closed = Inquiry.sync_to_quote_closed_when_no_open_quotations(
+ inquiry_nos,
+ actor_username=username,
+ )
+ return SuccessResponse(
+ data={"updated": updated, "inquiries_closed": inquiries_closed},
+ msg="同步成功",
+ )
+
def perform_create(self, serializer):
serializer.save()
@@ -126,6 +152,9 @@ class QuotationMasterViewSet(CustomModelViewSet):
instance = self.get_object()
if instance.status not in (1, 2):
return ErrorResponse(msg="仅未报价状态可进入报价中")
+ dl = getattr(instance, "quote_deadline", None)
+ if dl is not None and dl < timezone.now():
+ return ErrorResponse(msg="已超过报价截止时间")
instance.status = 2
instance.quotetime = timezone.now()
username = getattr(getattr(request, "user", None), "username", None)
@@ -147,14 +176,27 @@ class QuotationMasterViewSet(CustomModelViewSet):
instance = self.get_object()
if instance.status != 2:
return ErrorResponse(msg="仅报价中状态可提交报价")
+ dl = getattr(instance, "quote_deadline", None)
+ if dl is not None and dl < timezone.now():
+ return ErrorResponse(msg="已超过报价截止时间")
instance.status = 3
instance.quotetime = timezone.now()
username = getattr(getattr(request, "user", None), "username", None)
if username:
instance.quoteuser = username
- instance.save(update_fields=["status", "quotetime", "quoteuser"])
+ inquiry_quote_closed = False
+ with transaction.atomic():
+ instance.save(update_fields=["status", "quotetime", "quoteuser"])
+ inquiry_quote_closed = Inquiry.sync_to_quote_closed_when_all_suppliers_quoted(
+ instance.inquiry_no,
+ actor_username=username,
+ quotation_no=getattr(instance, "quotation_no", None),
+ )
serializer = self.get_serializer(instance)
- return DetailResponse(data=serializer.data, msg="提交成功")
+ payload = dict(serializer.data)
+ payload["inquiry_quote_closed"] = inquiry_quote_closed
+ msg = "提交成功" + (",询价单已进入报价结束" if inquiry_quote_closed else "")
+ return DetailResponse(data=payload, msg=msg)
class QuotationAttachmentViewSet(CustomModelViewSet):
diff --git a/web/src/views/pissupplier/quotation/api.ts b/web/src/views/pissupplier/quotation/api.ts
index 2ba5504..0878479 100644
--- a/web/src/views/pissupplier/quotation/api.ts
+++ b/web/src/views/pissupplier/quotation/api.ts
@@ -22,7 +22,7 @@ export const PISSUPPLIER_ROUTES = {
const baseUrl = PISSUPPLIER_ROUTES.quotation_master
-export type QuotationStatusCode = 1 | 2 | 3
+export type QuotationStatusCode = 1 | 2 | 3 | 4
export type PaymentCode = 1 | 2 | 3 | 4
/**
@@ -36,6 +36,10 @@ export type PaymentCode = 1 | 2 | 3 | 4
* 成本结构须提交 material_costs、process_costs、other_costs、profit_costs(与 QuotationMasterCreateUpdateSerializer),
* 勿使用 cost_items——后端不识别,子表不会更新。
*/
+/** 将待报价/报价中且已超过截止时间的报价单置为已过期(4),与列表权限范围一致 */
+export const syncExpiredQuotations = () =>
+ request({ url: `${baseUrl}sync_expired/`, method: 'post', data: {} })
+
export const getList = (params: any) => request({ url: baseUrl, method: 'get', params })
export const getDetail = (id: string | number) => request({ url: `${baseUrl}${id}/`, method: 'get' })
export const create = (data: any) => request({ url: baseUrl, method: 'post', data })
diff --git a/web/src/views/pissupplier/quotation/crud.tsx b/web/src/views/pissupplier/quotation/crud.tsx
index bb074d3..c977367 100644
--- a/web/src/views/pissupplier/quotation/crud.tsx
+++ b/web/src/views/pissupplier/quotation/crud.tsx
@@ -46,6 +46,7 @@ type Quote = {
bidEndTime?: string
template: string
currency: string
+ /** 与后端 `quote_deadline`(DateTime)对应的展示用字符串,经 `formatQuoteDeadlineDisplay` 规范化 */
quoteDeadline: string
quoteAmount: string
quoteTime: string
@@ -90,10 +91,17 @@ export function formatQuoteDeadlineDisplay(value: unknown) {
if (!value) return ''
const text = String(value).trim()
if (!text) return ''
- const matched = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2})/)
- if (matched) {
- return `${matched[1]} ${matched[2]}:00`
+ // 匹配完整日期时间:yyyy-mm-dd HH:MM:SS 或 yyyy-mm-ddTHH:MM:SS
+ const fullMatched = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})/)
+ if (fullMatched) {
+ return `${fullMatched[1]} ${fullMatched[2]}:${fullMatched[3]}`
}
+ // 匹配只到小时:yyyy-mm-dd HH
+ const hourMatched = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2})/)
+ if (hourMatched) {
+ return `${hourMatched[1]} ${hourMatched[2]}:00`
+ }
+ // 只有日期:yyyy-mm-dd
const dateOnly = text.match(/^(\d{4}-\d{2}-\d{2})$/)
if (dateOnly) {
return `${dateOnly[1]} 00:00`
@@ -101,6 +109,47 @@ export function formatQuoteDeadlineDisplay(value: unknown) {
return text
}
+/** 将后端返回的 `quote_deadline`(ISO 或 `YYYY-MM-DD HH:mm:ss`)规范为列表/表单展示用字符串 */
+export function normalizeQuoteDeadlineFromApi(value: unknown): string {
+ return formatQuoteDeadlineDisplay(value)
+}
+
+/** 解析报价截止时间用于本地筛选(与 `YYYY-MM-DD HH:mm:ss` / ISO 字符串兼容) */
+export function parseQuoteDeadlineToMs(value: unknown): number {
+ if (value == null || value === '') return NaN
+ const text = String(value).trim()
+ if (!text) return NaN
+ const normalized = formatQuoteDeadlineDisplay(text)
+ if (!normalized) return NaN
+ const forDate = normalized.replace(/^(\d{4}-\d{2}-\d{2})\s+/, '$1T')
+ const d = new Date(forDate)
+ const t = d.getTime()
+ return Number.isNaN(t) ? NaN : t
+}
+
+/**
+ * 保存主表时提交 `quote_deadline`:后端为 DateTimeField,提交 `YYYY-MM-DD HH:mm:ss` 或省略
+ */
+export function quoteDeadlineToApiPayload(value: unknown): string | undefined {
+ if (value == null || value === '') return undefined
+ const raw = String(value).trim()
+ if (!raw) return undefined
+ const normalized = formatQuoteDeadlineDisplay(raw)
+ if (!normalized) return undefined
+ const m = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}):(\d{2})(?::(\d{2}))?$/)
+ if (m) {
+ const sec = m[4] ?? '00'
+ return `${m[1]} ${m[2]}:${m[3]}:${sec}`
+ }
+ const m2 = normalized.match(/^(\d{4}-\d{2}-\d{2})$/)
+ if (m2) return `${m2[1]} 00:00:00`
+ const isoCandidate = raw.includes('T') || raw.includes('Z') ? raw : raw.replace(/^(\d{4}-\d{2}-\d{2})\s+/, '$1T')
+ const d = new Date(isoCandidate)
+ if (Number.isNaN(d.getTime())) return undefined
+ const pad = (n: number) => String(n).padStart(2, '0')
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+}
+
/** 列表「投标开始/截止」列:采购方式=询价 时显示「-」 */
export function formatBidTimeColumn(row: any, value: unknown) {
if (Number(row?.buyingMethod ?? row?.buying_method) === 1) return '-'
@@ -1339,7 +1388,9 @@ export function useQuoteCrud(options?: { onChange?: () => void }) {
const tpl = inv.template || inv.template_code
if (tpl) q.template = tpl
if (inv.currency) q.currency = inv.currency
- if (inv.quote_deadline) q.quoteDeadline = inv.quote_deadline
+ if (inv.quote_deadline != null && String(inv.quote_deadline).trim() !== '') {
+ q.quoteDeadline = normalizeQuoteDeadlineFromApi(inv.quote_deadline)
+ }
if (q.buyingMethod == null && inv.buying_method != null && inv.buying_method !== '') {
const n = Number(inv.buying_method)
if (Number.isFinite(n)) q.buyingMethod = n
@@ -1397,7 +1448,7 @@ export function useQuoteCrud(options?: { onChange?: () => void }) {
const raw = item.bid_end_time ?? item.bidEndTime
return raw != null && String(raw).trim() !== '' ? String(raw).trim() : ''
})(),
- quoteDeadline: item.quote_deadline || item.quoteDeadline || '',
+ quoteDeadline: normalizeQuoteDeadlineFromApi(item.quote_deadline ?? item.quoteDeadline ?? ''),
quoteAmount:
item.quote_amount ??
item.quoteAmount ??
@@ -1534,6 +1585,11 @@ export function useQuoteCrud(options?: { onChange?: () => void }) {
loading.value = true
try {
await ensureTemplateNameLookup()
+ try {
+ await api.syncExpiredQuotations()
+ } catch (e) {
+ console.warn('同步已过期报价单状态失败', e)
+ }
const res = await api.getList({ page: 1, page_size: 200 })
const list = res?.data?.results || res?.data?.data?.results || res?.data?.list || res?.data || []
const mapped = (Array.isArray(list) ? list : []).map(mapBackendQuote)
@@ -1585,7 +1641,13 @@ export function useQuoteCrud(options?: { onChange?: () => void }) {
filters.buyingMethod === '' || filters.buyingMethod === undefined || filters.buyingMethod === null
? true
: Number(q.buyingMethod) === Number(filters.buyingMethod)
- const matchDate = !start || !end || (new Date(q.quoteDeadline) >= start && new Date(q.quoteDeadline) <= end)
+ const matchDate =
+ !start ||
+ !end ||
+ (() => {
+ const t = parseQuoteDeadlineToMs(q.quoteDeadline)
+ return !Number.isNaN(t) && t >= start.getTime() && t <= end.getTime()
+ })()
return (
matchQuoteNo &&
matchInquiryCode &&
@@ -1819,7 +1881,9 @@ export function useQuoteCrud(options?: { onChange?: () => void }) {
quoteData.inquiryTitle = inquiry.title || quoteData.inquiryTitle
quoteData.template = inquiry.template || inquiry.template_code || quoteData.template
quoteData.currency = inquiry.currency || quoteData.currency
- quoteData.quoteDeadline = inquiry.quote_deadline || quoteData.quoteDeadline
+ if (inquiry.quote_deadline != null && String(inquiry.quote_deadline).trim() !== '') {
+ quoteData.quoteDeadline = normalizeQuoteDeadlineFromApi(inquiry.quote_deadline)
+ }
const icc = inquiry.company_code != null && inquiry.company_code !== '' ? String(inquiry.company_code).trim() : ''
if (icc) quoteData.inquiryCompanyCode = icc
quoteData.inquiryStatus = inquiry.status || inquiry.inquiry_status || quoteData.inquiryStatus
@@ -2138,13 +2202,17 @@ export function useQuoteCrud(options?: { onChange?: () => void }) {
loading.value = true
try {
const res: any = await api.submitOfficial(row.id)
- const data = res?.data?.data ?? res?.data ?? res
- const updated = mapBackendQuote(data)
+ const raw = res?.data?.data ?? res?.data ?? res
+ const inquiryClosed = raw?.inquiry_quote_closed === true
+ const updated = mapBackendQuote(raw)
await enrichQuotesWithInquiryData([updated])
const idx = quotes.value.findIndex((q) => q.id === updated.id)
if (idx >= 0) quotes.value.splice(idx, 1, updated)
else quotes.value.unshift(updated)
- ElMessage.success('报价已提交')
+ const tip =
+ res?.data?.msg ||
+ (inquiryClosed ? '报价已提交,询价单已进入报价结束' : '报价已提交')
+ ElMessage.success(tip)
options?.onChange?.()
} catch (err) {
console.error('提交报价失败', err)
@@ -2240,7 +2308,7 @@ export function useQuoteCrud(options?: { onChange?: () => void }) {
const payload: any = {
quotation_no: q.quoteNo || undefined,
inquiry_no: q.inquiryCode || undefined,
- quote_deadline: q.quoteDeadline || undefined,
+ quote_deadline: quoteDeadlineToApiPayload(q.quoteDeadline),
supplier_code: q.supplierCode || undefined,
supplier_name: q.supplierName || undefined,
contact_person: q.base.contact || undefined,
diff --git a/web/src/views/pissupplier/quotation/index.vue b/web/src/views/pissupplier/quotation/index.vue
index 5cfbf49..9365059 100644
--- a/web/src/views/pissupplier/quotation/index.vue
+++ b/web/src/views/pissupplier/quotation/index.vue
@@ -45,8 +45,8 @@
-
-
+
+
@@ -320,9 +320,11 @@ import {
formatAwardBidStatus,
type InquiryAttachmentRow,
buyingMethodDict,
- formatBidTimeColumn
+ formatBidTimeColumn,
+ formatQuoteDeadlineDisplay
} from './crud'
+
const INQUIRY_FILE_TYPE_ORDER = [1, 2, 3] as const
const INQUIRY_FILE_TYPE_LABELS: Record = {
1: '产品图纸',
@@ -539,10 +541,10 @@ const crudOptions = {
form: { labelWidth: '110px' },
search: { show: true, labelWidth: '90px' },
actionbar: { show: false },
- table: { rowKey: 'id', size: 'small' },
+ table: { rowKey: 'id', size: 'medium' },
rowHandle: {
fixed: 'right',
- width: 320,
+ width: 240,
buttons: {
// Hide fast-crud default actions; use custom actions below instead
view: { show: false },
@@ -630,7 +632,7 @@ const crudOptions = {
}
},
quoteDeadline: {
- title: '报价截止日',
+ title: '报价截止时间',
type: 'datetime',
search: {
show: true,
@@ -645,7 +647,7 @@ const crudOptions = {
}
}
},
- column: { width: 150 }
+ column: { width: 150, formatter: ({ value }: any) => formatQuoteDeadlineDisplay(value) }
},
bidStartTime: {
title: '投标开始时间',