From 67b6cae72367f5603c28312742d15b49ebcd57e2 Mon Sep 17 00:00:00 2001 From: nebula_chen Date: Thu, 2 Apr 2026 20:12:32 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=82=AE=E4=BB=B6?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=EF=BC=9B=E4=BF=AE=E6=94=B9=E4=BE=9B=E5=BA=94?= =?UTF-8?q?=E5=95=86=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=EF=BC=9B=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E7=BC=96=E8=BE=91=E6=A8=A1=E5=BC=8F=E4=B8=8B?= =?UTF-8?q?=E5=8F=AF=E4=BF=AE=E6=94=B9=E4=BF=A1=E6=81=AF=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=9A=84bug=EF=BC=9B=E8=B0=83=E6=95=B4=E6=AF=94=E4=BB=B7?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/apps/pisadmin/basicinfo/admin.py | 2 +- backend/apps/pisadmin/basicinfo/models.py | 14 +- .../basicinfo/views/email_template.py | 120 +++- .../pisadmin/basicinfo/views/email_utils.py | 76 ++- .../apps/pisadmin/miscprocurement/views.py | 18 +- backend/apps/pissupplier/serializers.py | 3 + backend/apps/pissupplier/views.py | 6 +- .../emails/bids_publish_template.html | 41 ++ .../templates/emails/quote_ended_subject.txt | 1 - ...ed_body.html => quote_ended_template.html} | 1 + .../emails/quote_timeout_subject.txt | 1 - ..._body.html => quote_timeout_template.html} | 1 + .../templates/emails/rfs_publish_subject.txt | 1 - ...sh_body.html => rfs_publish_template.html} | 1 + .../views/pisadmin/basicinfo/company/crud.tsx | 3 + .../pisadmin/basicinfo/currency/crud.tsx | 5 + .../miscprocurement/misc_stations/crud.tsx | 3 + .../rfqmiscellaneous/comparePrice.vue | 532 ++++++++---------- .../miscprocurement/rfqmiscellaneous/crud.tsx | 24 + .../rfqmiscellaneous/miscInquiryDetail.vue | 210 ++++++- 20 files changed, 728 insertions(+), 335 deletions(-) create mode 100644 backend/templates/emails/bids_publish_template.html delete mode 100644 backend/templates/emails/quote_ended_subject.txt rename backend/templates/emails/{quote_ended_body.html => quote_ended_template.html} (92%) delete mode 100644 backend/templates/emails/quote_timeout_subject.txt rename backend/templates/emails/{quote_timeout_body.html => quote_timeout_template.html} (92%) delete mode 100644 backend/templates/emails/rfs_publish_subject.txt rename backend/templates/emails/{rfs_publish_body.html => rfs_publish_template.html} (93%) diff --git a/backend/apps/pisadmin/basicinfo/admin.py b/backend/apps/pisadmin/basicinfo/admin.py index 64f2e0b..7fcd190 100644 --- a/backend/apps/pisadmin/basicinfo/admin.py +++ b/backend/apps/pisadmin/basicinfo/admin.py @@ -25,7 +25,7 @@ class SupplierAdmin(admin.ModelAdmin): @admin.register(SupplierUser) class SupplierUserAdmin(admin.ModelAdmin): - list_display = ['supplier_id', 'supplier_name', 'user_email', 'user_name', 'company_code', 'status'] + list_display = ['supplier_id', 'supplier_name', 'user_email', 'user_name', 'status'] search_fields = ['supplier_id', 'supplier_name', 'user_email', 'user_name'] diff --git a/backend/apps/pisadmin/basicinfo/models.py b/backend/apps/pisadmin/basicinfo/models.py index 4d81d9e..d2ac2a8 100644 --- a/backend/apps/pisadmin/basicinfo/models.py +++ b/backend/apps/pisadmin/basicinfo/models.py @@ -74,9 +74,14 @@ class Supplier(CoreModel): class SupplierUser(CoreModel): - """供应商用户信息""" - company_code = models.CharField(max_length=10, verbose_name="交易厂区", help_text="交易厂区(公司)", null=True, blank=True) - supplier_id = models.CharField(max_length=20, unique=True, verbose_name="供应商唯一ID", help_text="供应商唯一ID") + """供应商用户信息(独立主数据;通过 supplier_id 与供应商主档逻辑关联,非数据库外键)""" + + supplier_id = models.CharField( + max_length=20, + db_index=True, + verbose_name="供应商唯一ID", + help_text="与供应商信息表 supplier_id 同值,可多条(多联系人)", + ) supplier_name = models.CharField(max_length=100, verbose_name="供应商全称", help_text="供应商全称") supplier_role = models.IntegerField(verbose_name="供应商角色", help_text="供应商角色(1:报价)") user_email = models.CharField(max_length=100, verbose_name="联络人邮箱", help_text="联络人邮箱") @@ -89,9 +94,6 @@ class SupplierUser(CoreModel): verbose_name = "供应商用户信息" verbose_name_plural = verbose_name ordering = ("-create_datetime", "id") - indexes = [ - models.Index(fields=["company_code"]), - ] def __str__(self): return self.supplier_name diff --git a/backend/apps/pisadmin/basicinfo/views/email_template.py b/backend/apps/pisadmin/basicinfo/views/email_template.py index e91a555..fc263a4 100644 --- a/backend/apps/pisadmin/basicinfo/views/email_template.py +++ b/backend/apps/pisadmin/basicinfo/views/email_template.py @@ -2,14 +2,16 @@ """邮件正文/主题模板:按业务场景 key 渲染,供询价发布、报价结束、询价截止提醒等流程调用。 扩展方式: -1. 在下方注册 ``EMAIL_TEMPLATE_FILES``(主题模板路径 + HTML 正文路径), - 再实现 ``build_context_<场景>(...)``,通过 ``render_email(TEMPLATE_xxx, ctx)`` 调用。 -2. 若需完全自定义渲染逻辑(非一对 .txt/.html),将 ``TEMPLATE_xxx`` 登记到 ``_CUSTOM_RENDERERS``。 +1. 推荐:单文件 ``emails/_template.html``,首行 ```` 为主题, + 余下为 HTML 正文;在 ``_CUSTOM_RENDERERS`` 中注册渲染函数(通常调用 ``_split_combined_subject_html``)。 +2. 或:在 ``EMAIL_TEMPLATE_FILES`` 登记 (subject.txt, body.html) 路径对,由 ``render_template_pair`` 渲染。 +3. 运行时注册:``register_email_template(..., renderer=...)`` 或 ``register_email_template(..., sub, body)``。 """ from __future__ import annotations import os +import re from typing import Any, Callable, Dict, Optional, Tuple from django.conf import settings @@ -19,6 +21,8 @@ from django.utils import timezone # 杂采询价单发布通知(新询价邀请) TEMPLATE_RFS_PUBLISH = "RFS_publish" +# 招标方式发布通知(单文件 HTML:首行注释解析主题 + 正文) +TEMPLATE_BIDS_PUBLISH = "Bids_publish" # 全部供应商已报价 → 询价单进入「报价结束」— 通知采购负责人 TEMPLATE_QUOTE_ENDED = "Quote_ended" # 报价截止后无待报价/报价中单 → 询价单收口为「报价结束」(典型:sync_expired)— 询价截止提醒 @@ -127,6 +131,56 @@ def build_context_rfs_publish( } +def build_context_bids_publish( + inquiry: Any, + supplier_group: Dict[str, Any], + *, + purchaser_company_name: str = "", +) -> Dict[str, Any]: + """ + 招标邀请邮件(``bids_publish_template.html``):在 ``build_context_rfs_publish`` 基础上增加 + ``bid_time_range``、``material_short``、``portal_url``、``purchaser_name`` 等变量。 + """ + ctx = build_context_rfs_publish(inquiry, supplier_group, purchaser_company_name=purchaser_company_name) + + bs = getattr(inquiry, "bid_start_time", None) + be = getattr(inquiry, "bid_end_time", None) + if bs and be: + bsl = _for_local_display(bs) + bel = _for_local_display(be) + if bsl and bel and bsl.date() == bel.date(): + bid_time_range = f"{bsl.strftime('%Y-%m-%d %H:%M')}~{bel.strftime('%H:%M')}" + elif bsl and bel: + bid_time_range = f"{bsl.strftime('%Y-%m-%d %H:%M')} ~ {bel.strftime('%Y-%m-%d %H:%M')}" + else: + bid_time_range = "—" + elif be: + bel = _for_local_display(be) + bid_time_range = bel.strftime("%Y-%m-%d %H:%M") if bel else "请登录系统查看" + elif bs: + bsl = _for_local_display(bs) + bid_time_range = (bsl.strftime("%Y-%m-%d %H:%M") + " 起") if bsl else "请登录系统查看" + else: + bid_time_range = str(ctx.get("deadline_time") or "请登录系统查看") + + mat_plain = str(ctx.get("material_info") or "") + material_short = mat_plain[:120] + ("…" if len(mat_plain) > 120 else "") + + purchaser_name = _quote_ended_purchaser_name(inquiry) + portal_url = str(ctx.get("system_link") or "").strip() + + ctx.update( + { + "bid_time_range": bid_time_range, + "material_short": material_short, + "portal_url": portal_url, + "purchaser_name": purchaser_name, + "purchaser_phone": str(ctx.get("contact_phone") or "").strip(), + } + ) + return ctx + + def _quote_ended_purchaser_name(inquiry: Any) -> str: """采购负责人展示名:优先系统用户姓名,否则采购负责人字段原文。""" buyer = (getattr(inquiry, "buyer", None) or "").strip() @@ -310,13 +364,10 @@ def build_context_quote_timeout(inquiry: Any) -> Dict[str, Any]: # --------------------------------------------------------------------------- -# 模板文件注册:template_key -> (subject 相对 templates/, body 相对 templates/) -# 新增一类邮件时:在此增加一行,并放置对应 templates/emails/*.txt / *.html +# 可选:template_key -> (subject 相对 templates/, body 相对 templates/),由 render_template_pair 渲染。 +# 内置场景均已改为单文件 *_template.html,见 _CUSTOM_RENDERERS。 # --------------------------------------------------------------------------- -EMAIL_TEMPLATE_FILES: Dict[str, Tuple[str, str]] = { - TEMPLATE_QUOTE_ENDED: ("emails/quote_ended_subject.txt", "emails/quote_ended_body.html"), - TEMPLATE_QUOTE_TIMEOUT: ("emails/quote_timeout_subject.txt", "emails/quote_timeout_body.html"), -} +EMAIL_TEMPLATE_FILES: Dict[str, Tuple[str, str]] = {} def render_template_pair( @@ -334,7 +385,7 @@ def render_template_pair( def _render_rfs_publish_body(ctx: Dict[str, Any]) -> Tuple[str, str]: - """RFS_publish:主题需 material_short,在渲染前注入。""" + """RFS_publish:单文件 ``rfs_publish_template.html``;主题需 material_short,在渲染前注入。""" mat_plain = str(ctx.get("material_info") or "") material_short = mat_plain[:120] + ("…" if len(mat_plain) > 120 else "") @@ -342,15 +393,52 @@ def _render_rfs_publish_body(ctx: Dict[str, Any]) -> Tuple[str, str]: **ctx, "material_short": material_short, } - return render_template_pair( - "emails/rfs_publish_subject.txt", - "emails/rfs_publish_body.html", - render_ctx, - ) + html = render_to_string("emails/rfs_publish_template.html", render_ctx) + return _split_combined_subject_html(html) + + +def _render_quote_ended_combined(ctx: Dict[str, Any]) -> Tuple[str, str]: + """Quote_ended:单文件 ``quote_ended_template.html``。""" + html = render_to_string("emails/quote_ended_template.html", ctx) + return _split_combined_subject_html(html) + + +def _render_quote_timeout_combined(ctx: Dict[str, Any]) -> Tuple[str, str]: + """Quote_timeout:单文件 ``quote_timeout_template.html``。""" + html = render_to_string("emails/quote_timeout_template.html", ctx) + return _split_combined_subject_html(html) + + +# 首行必须为:(主题内勿含连续两个减号 ``--``,以免破坏 HTML 注释) +_COMBINED_EMAIL_SUBJECT_RE = re.compile( + r"^\s*\s*", + re.IGNORECASE | re.DOTALL, +) + + +def _split_combined_subject_html(html: str) -> Tuple[str, str]: + """从合并模板中解析 (subject, body_html)。""" + m = _COMBINED_EMAIL_SUBJECT_RE.match(html) + if not m: + raise ValueError( + "合并邮件模板必须以 开头(首行),参见 emails/*_template.html" + ) + subject = m.group(1).replace("\r", " ").replace("\n", " ").strip() + body = html[m.end() :].lstrip() + return subject, body + + +def _render_bids_publish_combined(ctx: Dict[str, Any]) -> Tuple[str, str]: + """Bids_publish:单文件 HTML,首行注释为主题。""" + html = render_to_string("emails/bids_publish_template.html", ctx) + return _split_combined_subject_html(html) _CUSTOM_RENDERERS: Dict[str, Callable[[Dict[str, Any]], Tuple[str, str]]] = { TEMPLATE_RFS_PUBLISH: _render_rfs_publish_body, + TEMPLATE_BIDS_PUBLISH: _render_bids_publish_combined, + TEMPLATE_QUOTE_ENDED: _render_quote_ended_combined, + TEMPLATE_QUOTE_TIMEOUT: _render_quote_timeout_combined, } @@ -359,7 +447,7 @@ def render_email(template_key: str, context: Dict[str, Any]) -> Tuple[str, str]: 按模板类型渲染邮件。 解析顺序: - 1. ``_CUSTOM_RENDERERS`` 中注册的完全自定义渲染器; + 1. ``_CUSTOM_RENDERERS``(单文件 ``*_template.html`` 等); 2. ``EMAIL_TEMPLATE_FILES`` 中的 (subject, body) 路径对; 否则抛出 ``ValueError``。 diff --git a/backend/apps/pisadmin/basicinfo/views/email_utils.py b/backend/apps/pisadmin/basicinfo/views/email_utils.py index 353ed44..a5dd0e3 100644 --- a/backend/apps/pisadmin/basicinfo/views/email_utils.py +++ b/backend/apps/pisadmin/basicinfo/views/email_utils.py @@ -4,13 +4,15 @@ import tempfile import urllib.parse import urllib.request from datetime import timedelta -from typing import Tuple, Dict, Any, List +from typing import Any, Dict, List, Tuple from rest_framework import serializers from rest_framework.decorators import action +from rest_framework.response import Response from django.conf import settings from django.core.mail import EmailMessage, get_connection from django.utils import timezone +from django.utils.dateparse import parse_datetime from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet from django.contrib.auth import get_user_model @@ -85,6 +87,7 @@ def resolve_inquiry_purchaser_emails(inquiry) -> list: def send_quote_ended_notice_to_purchaser(inquiry, *, last_quotation_no: str = "") -> bool: """ 询价单进入「报价结束」时通知采购负责人(HTML 邮件 + EmailNotice 记录)。 + 正文与主题由 ``emails/quote_ended_template.html`` 首行 ```` 解析。 无有效收件人时跳过发送,返回 False。 """ from apps.pisadmin.basicinfo.views.email_template import ( @@ -155,6 +158,7 @@ def send_quote_timeout_notice_to_purchaser(inquiry) -> bool: """ 询价截止提醒(HTML + EmailNotice):由 ``notify_purchasers_quote_timeout_for_inquiries`` 在 供应商端 ``POST .../quotation_master/sync_expired/`` 将超期未报价单置为已过期之后按需调用。 + 正文与主题由 ``emails/quote_timeout_template.html`` 首行 ```` 解析。 无有效收件人时跳过发送,返回 False。 """ from apps.pisadmin.basicinfo.views.email_template import ( @@ -229,6 +233,76 @@ def notify_purchasers_quote_timeout_for_inquiries(inquiry_nos: List[str]) -> int return sent +def send_bids_publish_notice_to_supplier( + inquiry, + supplier_group: Dict[str, Any], + *, + purchaser_company_name: str = "", +) -> Tuple[bool, EmailNotice]: + """ + 招标邀请邮件(单文件 ``bids_publish_template.html``):向单个供应商分组发送 HTML 邮件并写入 EmailNotice。 + + 无有效收件人时仍创建 notice 并标记失败,返回 (False, notice);成功发送返回 (True, notice)。 + """ + from apps.pisadmin.basicinfo.views.email_template import ( + TEMPLATE_BIDS_PUBLISH, + build_context_bids_publish, + render_email, + ) + + email = (supplier_group.get("contact_email") or "").strip() + to_list = [email] if email else [] + + ctx = build_context_bids_publish( + inquiry, + supplier_group, + purchaser_company_name=purchaser_company_name or "", + ) + subject, body = render_email(TEMPLATE_BIDS_PUBLISH, ctx) + + notice = EmailNotice.objects.create( + subject=subject, + body=body, + to_emails=to_list, + cc_emails=[], + bcc_emails=[], + attachments=[], + biz_type="inquiry_bids_publish", + biz_id=getattr(inquiry, "inquiry_no", None) or "", + status="pending", + payload={ + "template_key": TEMPLATE_BIDS_PUBLISH, + "is_html": True, + "inquiry_no": ctx.get("rfq_number"), + "inquiry_title": ctx.get("inquiry_title"), + "bid_time_range": ctx.get("bid_time_range"), + "supplier_name": (supplier_group.get("supplier_name") or "").strip(), + }, + ) + + if not to_list: + notice.status = "failed" + notice.last_error = "缺少供应商邮箱" + notice.save(update_fields=["status", "last_error", "update_datetime"]) + return False, notice + + notice.status = "sending" + notice.save(update_fields=["status", "update_datetime"]) + + success, detail = send_email_notice(notice) + notice.response = detail or {} + if success: + notice.status = "success" + notice.sent_at = timezone.now() + notice.last_error = None + else: + notice.status = "failed" + notice.last_error = detail.get("error") if isinstance(detail, dict) else str(detail) + + notice.save(update_fields=["status", "sent_at", "response", "last_error", "update_datetime"]) + return success, notice + + def send_email_notice(notice) -> Tuple[bool, Dict[str, Any]]: """Send an email based on EmailNotice instance. diff --git a/backend/apps/pisadmin/miscprocurement/views.py b/backend/apps/pisadmin/miscprocurement/views.py index 71d5584..77bed00 100644 --- a/backend/apps/pisadmin/miscprocurement/views.py +++ b/backend/apps/pisadmin/miscprocurement/views.py @@ -16,7 +16,7 @@ from apps.pisadmin.basicinfo.views.email_template import ( build_context_rfs_publish, render_email, ) -from apps.pisadmin.basicinfo.views.email_utils import send_email_notice +from apps.pisadmin.basicinfo.views.email_utils import send_bids_publish_notice_to_supplier, send_email_notice from apps.pissupplier.models import ( QuotationMaster, @@ -150,12 +150,13 @@ def _rfq_qty_decimal(inquiry: Inquiry, pid: str) -> Optional[Decimal]: def _sync_misc_low_price_records(inquiry: Inquiry, part_id: str) -> None: """ 制程最低价落库(与比价展开明细一致): + - 前端比价展开不依赖成本结构模板:材料按材质分组仅展示重量/单价/材料费用,加工按工站分组仅展示加工费;落库口径仍按下列规则。 - 询价单下**每个上阶物料料号**单独一套主/次表数据(多料号互不合并)。 - **材料**:次表两行分别记录最低重量、最低单价;souce_no 为取得该最小值对应的报价单单号,单价若来自「杂采材料信息」 则存交易厂区(factory);主表材料行 souce_no 聚合为 W:…;U:…(重量来源与单价来源可能不同)。 - **加工**:仅主表一行(无次表、不按工站);每报价单合计加工费后取最小,全空/全 0 仍写入 MinPrice=0。 - 注意:材料行必须用 quotation_no__in=报价单号列表 过滤,勿用 QuerySet(QuotationMaster) 作 __in,否则 ORM 按主键匹配会查不到材料行。 - - **其它**:包装费、运输费分列取 min(优先 sup_quotation_other;无则用上阶物料 total_other_expense 回退)。 + - **其它**:包装费、运输费分列取 min(优先 sup_quotation_other;无则用上阶物料 total_other_expense 回退)。采购端比价主表「其它成本」行制程最低价列与「加工成本」相同,可链向合计最低的报价单详情。 - **利润率**:cost_type=6,各报价单该料号利润率取 min。杂采不落库管销研(cost_type=5)。 """ inquiry_no = (inquiry.inquiry_no or "").strip() @@ -363,6 +364,8 @@ class MiscMaterialViewSet(CustomModelViewSet): update_serializer_class = MiscMaterialCreateUpdateSerializer search_fields = ["materialtype", "factory"] ordering = ["-create_datetime"] + # 与询价单 `Inquiry.company_code` 一致,须精确匹配 `factory`,避免 sz_avc 与 sz_avcx 串数据 + filter_fields = ("factory", "status", "materialtype") class MiscStationViewSet(CustomModelViewSet): @@ -372,6 +375,7 @@ class MiscStationViewSet(CustomModelViewSet): update_serializer_class = MiscStationCreateUpdateSerializer search_fields = ["stationname", "stationcode", "company_code"] ordering = ["-create_datetime"] + filter_fields = ("company_code", "status", "stationcode", "stationname") class MiscPartViewSet(CustomModelViewSet): @@ -783,6 +787,7 @@ class InquiryViewSet(CustomModelViewSet): 按询价单供应商子表 `InquirySupplier` 汇总:同一 `supplier_code`(空代码时用行级占位)合并多料号 `part_id`, 供 `_create_supplier_quotations` 为每个供应商生成一份 `QuotationMaster`。 返回条目的 supplier_code / supplier_name / 联系人字段均来自子表行数据(截断至报价主表字段长度)。 + 采购端保存时,联系人/邮箱/电话由前端在「供应商用户信息」表中按 supplier_id 选择后写入子表,此处不再与主档做 ORM 关联校验。 """ supplier_map = {} for row in inquiry.suppliers.all(): @@ -1320,11 +1325,20 @@ class InquiryViewSet(CustomModelViewSet): return purchaser_company_name = self._resolve_purchaser_company_name(inquiry) + is_bidding = int(getattr(inquiry, "buying_method", 1) or 1) == 2 for vendor in supplier_groups: if not isinstance(vendor, dict): continue + if is_bidding: + send_bids_publish_notice_to_supplier( + inquiry, + vendor, + purchaser_company_name=purchaser_company_name, + ) + continue + supplier_name = (vendor.get("supplier_name") or "").strip() email = (vendor.get("contact_email") or "").strip() to_list = [email] if email else [] diff --git a/backend/apps/pissupplier/serializers.py b/backend/apps/pissupplier/serializers.py index e40bd39..6895777 100644 --- a/backend/apps/pissupplier/serializers.py +++ b/backend/apps/pissupplier/serializers.py @@ -266,6 +266,8 @@ class NestedQuotationProcessSerializer(QuotationProcessSerializer): class QuotationOtherSerializer(serializers.ModelSerializer): + """报价单其它成本子表:按料号维度存包装费、运输费;采购端比价展开「其它成本」明细与此字段一致。""" + class Meta: model = QuotationOther fields = [ @@ -447,6 +449,7 @@ class QuotationMasterSerializer(BusinessAuditSerializer): return cache[inquiry_no] def get_inquiry_company_code(self, obj): + """与 `Inquiry.company_code` 一致;供应商端拉取杂采材质/工站列表时须按此精确过滤厂区。""" code, _ = self._inquiry_plant_tuple(getattr(obj, "inquiry_no", None)) return code diff --git a/backend/apps/pissupplier/views.py b/backend/apps/pissupplier/views.py index d4c8114..21d71ef 100644 --- a/backend/apps/pissupplier/views.py +++ b/backend/apps/pissupplier/views.py @@ -43,8 +43,8 @@ def _supplier_bidding_window_error(instance: QuotationMaster): be = getattr(instance, "bid_end_time", None) if bs is None or be is None: return ErrorResponse(msg="招标项目未设置投标开始或截止时间,无法报价或提交") - if now < bs: - return ErrorResponse(msg="投标尚未开始") + # if now < bs: + # return ErrorResponse(msg="投标尚未开始") if now > be: return ErrorResponse(msg="已超过投标截止时间") return None @@ -53,7 +53,7 @@ def _supplier_bidding_window_error(instance: QuotationMaster): class QuotationMasterViewSet(CustomModelViewSet): """杂采报价单主表管理接口 - 详情 GET 与采购端比价弹窗「供应商报价预览」共用:返回 `QuotationMasterSerializer` 及嵌套材料/加工/其它/利润/上阶物料等。 + 详情 GET 与采购端比价弹窗「供应商报价预览」共用:返回 `QuotationMasterSerializer` 及嵌套材料/加工/其它(包装费、运输费)/利润/上阶物料等。 """ queryset = QuotationMaster.objects.prefetch_related( diff --git a/backend/templates/emails/bids_publish_template.html b/backend/templates/emails/bids_publish_template.html new file mode 100644 index 0000000..cae4724 --- /dev/null +++ b/backend/templates/emails/bids_publish_template.html @@ -0,0 +1,41 @@ + + + + + +

尊敬的 {{ vendor_name }} 合作伙伴:

+

您好!

+

我司({{ purchaser_company_name }})现正式发布新的采购询价单,诚邀贵司参与投标,我们非常期待与您的合作。

+ +

📋 询价单概要

+ + +

💡 如何参与报价?

+

请点击下方按钮登录系统查看详情并填写报价:

+ {% if portal_url %} +

+ 立即前往投标 +

+

若按钮无法点击,请复制以下链接到浏览器打开:

+

{{ portal_url }}

+ {% else %} +

系统未配置供应商门户地址(PIS_SUPPLIER_PORTAL_URL),请联系管理员获取登录方式。

+ {% endif %} + +

⚠️ 注意事项

+ + +

祝商祺!

+

{{ purchaser_company_name }} 采购部

+

{{ current_date }}

+

此邮件由系统自动发送,请勿直接回复。

+ + diff --git a/backend/templates/emails/quote_ended_subject.txt b/backend/templates/emails/quote_ended_subject.txt deleted file mode 100644 index eaac796..0000000 --- a/backend/templates/emails/quote_ended_subject.txt +++ /dev/null @@ -1 +0,0 @@ -{% autoescape off %}【报价完成通知】询价单{{ inquiry_no }}-{{ material_or_project_short }}已全部报价,请进行比价{% endautoescape %} diff --git a/backend/templates/emails/quote_ended_body.html b/backend/templates/emails/quote_ended_template.html similarity index 92% rename from backend/templates/emails/quote_ended_body.html rename to backend/templates/emails/quote_ended_template.html index 5d2d7df..6183771 100644 --- a/backend/templates/emails/quote_ended_body.html +++ b/backend/templates/emails/quote_ended_template.html @@ -1,3 +1,4 @@ + diff --git a/backend/templates/emails/quote_timeout_subject.txt b/backend/templates/emails/quote_timeout_subject.txt deleted file mode 100644 index b322be3..0000000 --- a/backend/templates/emails/quote_timeout_subject.txt +++ /dev/null @@ -1 +0,0 @@ -{% autoescape off %}【询价截止提醒】询价单{{ inquiry_no }}-{{ material_or_project_short }}已截止,请进行比价{% endautoescape %} diff --git a/backend/templates/emails/quote_timeout_body.html b/backend/templates/emails/quote_timeout_template.html similarity index 92% rename from backend/templates/emails/quote_timeout_body.html rename to backend/templates/emails/quote_timeout_template.html index c94a4c2..08ef59b 100644 --- a/backend/templates/emails/quote_timeout_body.html +++ b/backend/templates/emails/quote_timeout_template.html @@ -1,3 +1,4 @@ + diff --git a/backend/templates/emails/rfs_publish_subject.txt b/backend/templates/emails/rfs_publish_subject.txt deleted file mode 100644 index 2a2d429..0000000 --- a/backend/templates/emails/rfs_publish_subject.txt +++ /dev/null @@ -1 +0,0 @@ -【新询价邀请】{{ rfq_number|default:"—" }} - {{ material_short }} - 截止:{{ deadline_date_subject|default:"—" }} diff --git a/backend/templates/emails/rfs_publish_body.html b/backend/templates/emails/rfs_publish_template.html similarity index 93% rename from backend/templates/emails/rfs_publish_body.html rename to backend/templates/emails/rfs_publish_template.html index 37d3f8c..5d6fe1f 100644 --- a/backend/templates/emails/rfs_publish_body.html +++ b/backend/templates/emails/rfs_publish_template.html @@ -1,3 +1,4 @@ + diff --git a/web/src/views/pisadmin/basicinfo/company/crud.tsx b/web/src/views/pisadmin/basicinfo/company/crud.tsx index f346b55..cb084eb 100644 --- a/web/src/views/pisadmin/basicinfo/company/crud.tsx +++ b/web/src/views/pisadmin/basicinfo/company/crud.tsx @@ -88,6 +88,9 @@ export const createCrudOptions = function ({ crudExpose }: Partial询价单号{{ displayTextEmpty(comparisonDialog.baseInfo.code) }}
采购件料号{{ displayTextEmpty(comparisonDialog.baseInfo.partNo) }}
采购件名称{{ displayTextEmpty(comparisonDialog.baseInfo.partName) }}
-
目标价格{{ displayNumericEmpty(comparisonDialog.baseInfo.targetPrice) }}
+
目标价格{{ compareNumericDisplay(comparisonDialog.baseInfo.targetPrice) }}
交易币别{{ displayTextEmpty(comparisonDialog.baseInfo.currency) }}
税率{{ displayPercentRate(comparisonDialog.baseInfo.taxRate) }}
-
当前成交价{{ displayNumericEmpty(comparisonDialog.baseInfo.dealPrice) }}
-
制程最低价{{ displayNumericEmpty(comparisonDialog.baseInfo.lowestProcessPrice) }}
+
当前成交价{{ compareNumericDisplay(comparisonDialog.baseInfo.dealPrice) }}
+
制程最低价{{ compareNumericDisplay(comparisonDialog.baseInfo.lowestProcessPrice) }}
-