diff --git a/backend/apps/pisadmin/basicinfo/admin.py b/backend/apps/pisadmin/basicinfo/admin.py index 18d8b97..64f2e0b 100644 --- a/backend/apps/pisadmin/basicinfo/admin.py +++ b/backend/apps/pisadmin/basicinfo/admin.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from django.contrib import admin from apps.pisadmin.basicinfo.models import ( - Unit, Currency, Supplier, Company + Unit, Currency, Supplier, SupplierUser, Company ) @@ -23,6 +23,12 @@ class SupplierAdmin(admin.ModelAdmin): search_fields = ['supplier_name', 'supplier_id', 'company_code'] +@admin.register(SupplierUser) +class SupplierUserAdmin(admin.ModelAdmin): + list_display = ['supplier_id', 'supplier_name', 'user_email', 'user_name', 'company_code', 'status'] + search_fields = ['supplier_id', 'supplier_name', 'user_email', 'user_name'] + + @admin.register(Company) class CompanyAdmin(admin.ModelAdmin): list_display = ['company_code', 'company_name', 'company_short_name', 'status'] diff --git a/backend/apps/pisadmin/basicinfo/models.py b/backend/apps/pisadmin/basicinfo/models.py index 468e335..4d81d9e 100644 --- a/backend/apps/pisadmin/basicinfo/models.py +++ b/backend/apps/pisadmin/basicinfo/models.py @@ -73,6 +73,30 @@ class Supplier(CoreModel): return self.supplier_name +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_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="联络人邮箱") + user_name = models.CharField(max_length=20, verbose_name="联络人", help_text="联络人") + user_phone = models.CharField(max_length=100, verbose_name="联络人电话", help_text="联络人电话") + status = models.IntegerField(default=1, verbose_name="有效否", help_text="有效否(1有效0无效)") + + class Meta: + db_table = table_prefix + "supplier_users" + verbose_name = "供应商用户信息" + verbose_name_plural = verbose_name + ordering = ("-create_datetime", "id") + indexes = [ + models.Index(fields=["company_code"]), + ] + + def __str__(self): + return self.supplier_name + + class Company(CoreModel): """公司信息""" company_code = models.CharField(max_length=20, verbose_name="公司代码", help_text="公司代码") diff --git a/backend/apps/pisadmin/basicinfo/urls.py b/backend/apps/pisadmin/basicinfo/urls.py index 29cfd90..d1f2dab 100644 --- a/backend/apps/pisadmin/basicinfo/urls.py +++ b/backend/apps/pisadmin/basicinfo/urls.py @@ -3,7 +3,7 @@ from rest_framework import routers from apps.pisadmin.basicinfo.views.unit import UnitViewSet from apps.pisadmin.basicinfo.views.currency import CurrencyViewSet -from apps.pisadmin.basicinfo.views.supplier import SupplierViewSet +from apps.pisadmin.basicinfo.views.supplier import SupplierViewSet, SupplierUserViewSet from apps.pisadmin.basicinfo.views.company import CompanyViewSet from apps.pisadmin.basicinfo.views.system_no_rule import SystemNoRuleViewSet from apps.pisadmin.basicinfo.views.email_utils import EmailNoticeViewSet @@ -12,6 +12,7 @@ basicinfo_url = routers.SimpleRouter() basicinfo_url.register(r'units', UnitViewSet) basicinfo_url.register(r'currencies', CurrencyViewSet) basicinfo_url.register(r'suppliers', SupplierViewSet) +basicinfo_url.register(r'supplier_users', SupplierUserViewSet) basicinfo_url.register(r'companies', CompanyViewSet) basicinfo_url.register(r'system_no_rules', SystemNoRuleViewSet) basicinfo_url.register(r'email_notice', EmailNoticeViewSet) diff --git a/backend/apps/pisadmin/basicinfo/views/email_template.py b/backend/apps/pisadmin/basicinfo/views/email_template.py index ef629f5..ab127a3 100644 --- a/backend/apps/pisadmin/basicinfo/views/email_template.py +++ b/backend/apps/pisadmin/basicinfo/views/email_template.py @@ -1,23 +1,25 @@ # -*- coding: utf-8 -*- -"""邮件正文/主题模板:按业务场景 key 渲染,供询价发布等流程调用。""" +"""邮件正文/主题模板:按业务场景 key 渲染,供询价发布、报价结束等流程调用。 + +扩展方式: +1. 在下方注册 ``EMAIL_TEMPLATE_FILES``(主题模板路径 + HTML 正文路径), + 再实现 ``build_context_<场景>(...)``,通过 ``render_email(TEMPLATE_xxx, ctx)`` 调用。 +2. 若需完全自定义渲染逻辑(非一对 .txt/.html),将 ``TEMPLATE_xxx`` 登记到 ``_CUSTOM_RENDERERS``。 +""" from __future__ import annotations -import html import os -from typing import Any, Callable, Dict, Tuple +from typing import Any, Callable, Dict, Optional, Tuple from django.conf import settings +from django.template.loader import render_to_string from django.utils import timezone # 杂采询价单发布通知(新询价邀请) TEMPLATE_RFS_PUBLISH = "RFS_publish" - - -def _h(text: Any) -> str: - if text is None: - return "" - return html.escape(str(text), quote=True) +# 全部供应商已报价 → 询价单进入「报价结束」— 通知采购负责人 +TEMPLATE_QUOTE_ENDED = "Quote_ended" def _for_local_display(dt: Any): @@ -41,6 +43,17 @@ def supplier_portal_base_url() -> str: return str(base).strip().rstrip("/") +def admin_portal_base_url() -> str: + """采购/管理端前端基址;优先 settings.PIS_ADMIN_PORTAL_URL,其次环境变量。""" + base = getattr(settings, "PIS_ADMIN_PORTAL_URL", None) or os.getenv("PIS_ADMIN_PORTAL_URL", "") or "" + return str(base).strip().rstrip("/") + + +def system_brand_name() -> str: + """邮件落款系统名称。""" + return str(getattr(settings, "PIS_EMAIL_SYSTEM_NAME", None) or "AVC PIS").strip() or "AVC PIS" + + def build_context_rfs_publish( inquiry: Any, supplier_group: Dict[str, Any], @@ -48,7 +61,7 @@ def build_context_rfs_publish( purchaser_company_name: str = "", ) -> Dict[str, Any]: """ - 组装 RFS_publish 模板变量(均为未转义纯文本,由渲染函数做 HTML 转义)。 + 组装 RFS_publish 模板变量(均为未转义纯文本,由 Django 模板引擎在渲染 HTML 时自动转义)。 supplier_group:与 `_group_inquiry_suppliers` 一致的结构。 """ vendor_name = (supplier_group.get("supplier_name") or "").strip() or "贵司" @@ -111,70 +124,81 @@ def build_context_rfs_publish( } -def _render_rfs_publish_body(ctx: Dict[str, Any]) -> Tuple[str, str]: - """返回 (subject, html_body)。""" - rfq = _h(ctx.get("rfq_number")) - mat = ctx.get("material_info") or "" - mat_plain = str(mat) - mid = mat_plain[:120] + ("…" if len(mat_plain) > 120 else "") - subject = f"【新询价邀请】{ctx.get('rfq_number') or '—'} - {mid} - 截止:{ctx.get('deadline_date_subject') or '—'}" +def build_context_quote_ended( + inquiry: Any, + *, + last_quotation_no: str = "", +) -> Dict[str, Any]: + """ + 报价结束通知(采购端):全部供应商已提交报价,询价单进入「报价结束」。 + """ + rfq_number = (getattr(inquiry, "inquiry_no", None) or "").strip() + title = (getattr(inquiry, "title", None) or "").strip() + buyer = (getattr(inquiry, "buyer", None) or "").strip() + purchaser_name = buyer or "采购同事" - vendor = _h(ctx.get("vendor_name")) - purchaser = _h(ctx.get("purchaser_company_name")) - material_html = _h(ctx.get("material_info")) - deadline_line = _h(ctx.get("deadline_time")) - link = ctx.get("system_link") or "" - link_h = _h(link) - contact_name = _h(ctx.get("contact_person")) - phone_raw = (ctx.get("contact_phone") or "").strip() - phone = _h(phone_raw) if phone_raw else "—" - closing_date = _h(ctx.get("current_date")) + now = _for_local_display(timezone.now()) + current_date = now.strftime("%Y-%m-%d") if now else "" + closed_time = now.strftime("%Y-%m-%d %H:%M:%S") if now else "—" - if link: - button_block = f""" -
- 立即前往报价 -
-若按钮无法点击,请复制以下链接到浏览器打开:
- """ - else: - button_block = """ -系统未配置供应商门户地址(PIS_SUPPLIER_PORTAL_URL),请联系管理员获取登录方式。
""" + base = admin_portal_base_url() + admin_link = base if base else "" - body = f""" - - -尊敬的 {vendor} 合作伙伴:
-您好!
-我司({purchaser})现正式发布新的采购询价单,诚邀贵司参与报价,我们非常期待与您的合作。
+ t_short = title[:40] + ("…" if len(title) > 40 else "") if title else "—" -请点击下方按钮登录系统查看详情并填写报价:
- {button_block} -祝商祺!
-{purchaser} 采购部
-{closing_date}
-此邮件由系统自动发送,请勿直接回复。
-""" + +def render_template_pair( + subject_template: str, + body_template: str, + context: Dict[str, Any], +) -> Tuple[str, str]: + """ + 通用:按一对 Django 模板路径渲染 (subject, html_body)。 + subject 一般为纯文本;HTML 正文由引擎对变量自动转义(HTML 安全)。 + """ + subject = render_to_string(subject_template, context).strip() + body = render_to_string(body_template, context) return subject, body -_RENDERERS: Dict[str, Callable[[Dict[str, Any]], Tuple[str, str]]] = { +def _render_rfs_publish_body(ctx: Dict[str, Any]) -> Tuple[str, str]: + """RFS_publish:主题需 material_short,在渲染前注入。""" + mat_plain = str(ctx.get("material_info") or "") + material_short = mat_plain[:120] + ("…" if len(mat_plain) > 120 else "") + + render_ctx: Dict[str, Any] = { + **ctx, + "material_short": material_short, + } + return render_template_pair( + "emails/rfs_publish_subject.txt", + "emails/rfs_publish_body.html", + render_ctx, + ) + + +_CUSTOM_RENDERERS: Dict[str, Callable[[Dict[str, Any]], Tuple[str, str]]] = { TEMPLATE_RFS_PUBLISH: _render_rfs_publish_body, } @@ -182,9 +206,42 @@ _RENDERERS: Dict[str, Callable[[Dict[str, Any]], Tuple[str, str]]] = { def render_email(template_key: str, context: Dict[str, Any]) -> Tuple[str, str]: """ 按模板类型渲染邮件。 + + 解析顺序: + 1. ``_CUSTOM_RENDERERS`` 中注册的完全自定义渲染器; + 2. ``EMAIL_TEMPLATE_FILES`` 中的 (subject, body) 路径对; + 否则抛出 ``ValueError``。 + :return: (subject, body);body 为 HTML 时由调用方在 EmailNotice.payload 中标记 is_html。 """ - renderer = _RENDERERS.get(template_key) - if not renderer: - raise ValueError(f"Unknown email template: {template_key!r}") - return renderer(context) + custom = _CUSTOM_RENDERERS.get(template_key) + if custom: + return custom(context) + + paths = EMAIL_TEMPLATE_FILES.get(template_key) + if paths: + sub_path, body_path = paths + return render_template_pair(sub_path, body_path, context) + + raise ValueError(f"Unknown email template: {template_key!r}") + + +def register_email_template( + template_key: str, + subject_template: str, + body_template: str, + *, + renderer: Optional[Callable[[Dict[str, Any]], Tuple[str, str]]] = None, +) -> None: + """ + 运行时注册(可选):用于插件或测试注入额外模板。 + + - 若提供 ``renderer``,则走自定义渲染,不再使用路径对。 + - 否则将 ``subject_template`` / ``body_template`` 登记到 ``EMAIL_TEMPLATE_FILES``。 + """ + if renderer is not None: + _CUSTOM_RENDERERS[template_key] = renderer + EMAIL_TEMPLATE_FILES.pop(template_key, None) + return + EMAIL_TEMPLATE_FILES[template_key] = (subject_template, body_template) + _CUSTOM_RENDERERS.pop(template_key, None) diff --git a/backend/apps/pisadmin/basicinfo/views/email_utils.py b/backend/apps/pisadmin/basicinfo/views/email_utils.py index 000618f..2736408 100644 --- a/backend/apps/pisadmin/basicinfo/views/email_utils.py +++ b/backend/apps/pisadmin/basicinfo/views/email_utils.py @@ -8,8 +8,11 @@ from rest_framework.decorators import action from django.conf import settings from django.core.mail import EmailMessage, get_connection +from django.utils import timezone from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet +from django.contrib.auth import get_user_model + from apps.pisadmin.basicinfo.models import EmailNotice @@ -42,6 +45,90 @@ def _collect_attachment_paths(raw_list: List[Any]) -> List[str]: return paths +def resolve_inquiry_purchaser_emails(inquiry) -> list: + """ + 根据询价单解析采购端收件人邮箱:优先「采购负责人」对应系统用户邮箱; + 若无则依次尝试创建人(creator / creator_id)、创建人账号(create_user)。 + """ + User = get_user_model() + + buyer = (getattr(inquiry, "buyer", None) or "").strip() + if buyer: + u = User.objects.filter(username=buyer).exclude(email__isnull=True).exclude(email="").first() + if not u: + u = User.objects.filter(name=buyer).exclude(email__isnull=True).exclude(email="").first() + if u and getattr(u, "email", None): + return [str(u.email).strip()] + + cr = getattr(inquiry, "creator", None) + if cr is not None and getattr(cr, "email", None): + return [str(cr.email).strip()] + cid = getattr(inquiry, "creator_id", None) + if cid: + u = User.objects.filter(id=cid).exclude(email__isnull=True).exclude(email="").first() + if u and getattr(u, "email", None): + return [str(u.email).strip()] + + cu = (getattr(inquiry, "create_user", None) or "").strip() + if cu: + u = User.objects.filter(username=cu).exclude(email__isnull=True).exclude(email="").first() + if u and getattr(u, "email", None): + return [str(u.email).strip()] + + return [] + + +def send_quote_ended_notice_to_purchaser(inquiry, *, last_quotation_no: str = "") -> bool: + """ + 询价单进入「报价结束」时通知采购负责人(HTML 邮件 + EmailNotice 记录)。 + 无有效收件人时跳过发送,返回 False。 + """ + from apps.pisadmin.basicinfo.views.email_template import ( + TEMPLATE_QUOTE_ENDED, + build_context_quote_ended, + render_email, + ) + + to_list = resolve_inquiry_purchaser_emails(inquiry) + if not to_list: + return False + + ctx = build_context_quote_ended(inquiry, last_quotation_no=last_quotation_no or "") + subject, body = render_email(TEMPLATE_QUOTE_ENDED, ctx) + + notice = EmailNotice.objects.create( + subject=subject, + body=body, + to_emails=to_list, + cc_emails=[], + bcc_emails=[], + attachments=[], + biz_type="inquiry_quote_ended", + biz_id=getattr(inquiry, "inquiry_no", None) or "", + status="pending", + payload={ + "template_key": TEMPLATE_QUOTE_ENDED, + "is_html": True, + "inquiry_no": getattr(inquiry, "inquiry_no", None), + }, + ) + 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 + + def send_email_notice(notice) -> Tuple[bool, Dict[str, Any]]: """Send an email based on EmailNotice instance. diff --git a/backend/apps/pisadmin/basicinfo/views/supplier.py b/backend/apps/pisadmin/basicinfo/views/supplier.py index fd56ff7..7737feb 100644 --- a/backend/apps/pisadmin/basicinfo/views/supplier.py +++ b/backend/apps/pisadmin/basicinfo/views/supplier.py @@ -1,19 +1,50 @@ # -*- coding: utf-8 -*- from rest_framework import serializers +from rest_framework.decorators import action -from apps.pisadmin.basicinfo.models import Supplier +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 + fields = [f.name for f in SupplierUser._meta.concrete_fields] + read_only_fields = ["id"] + + +class SupplierUserCreateUpdateSerializer(CustomModelSerializer): + """供应商用户信息 创建/更新""" + + class Meta: + model = SupplierUser + fields = "__all__" + + class SupplierSerializer(CustomModelSerializer): - """供应商信息-序列化器""" + """供应商信息-序列化器(含关联 supplier_user)""" + + supplier_user = serializers.SerializerMethodField(read_only=True) class Meta: model = Supplier - fields = "__all__" + fields = [f.name for f in Supplier._meta.concrete_fields] + ["supplier_user"] 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): """供应商信息 创建/更新""" @@ -41,3 +72,72 @@ class SupplierViewSet(CustomModelViewSet): update_serializer_class = SupplierCreateUpdateSerializer 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 关联)""" + + 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"] + 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 3ddafc9..2551408 100644 --- a/backend/apps/pisadmin/miscprocurement/models.py +++ b/backend/apps/pisadmin/miscprocurement/models.py @@ -377,6 +377,12 @@ class Inquiry(CoreModel): cur_status=5, operation_desc="全部供应商已提交报价,询价单同步为报价结束", ) + try: + from apps.pisadmin.basicinfo.views.email_utils import send_quote_ended_notice_to_purchaser + + send_quote_ended_notice_to_purchaser(inq, last_quotation_no=qn) + except Exception: + logger.exception("报价结束通知邮件发送失败") return True @@ -808,7 +814,11 @@ class MiscNegotiationRecords(CoreModel): class RFQOperationLogs(CoreModel): - """询价单操作日志表""" + """询价单操作日志表。 + + 按询价单号拉取全部记录:采购端 ``InquiryViewSet.operation_logs``( + ``GET .../inquiry/{pk}/operation_logs/``)。 + """ # (01询价单创;02询价单确认;03询价单发布;04询价单还原;05询价发送通知;06报价;07比议价;08议价审核提交;09议价审核完成;10议价审核驳回) OPERATION_TYPE_CHOICES = ( @@ -816,8 +826,8 @@ class RFQOperationLogs(CoreModel): (2, "询价单确认"), (3, "询价单发布"), (4, "询价单还原"), - (5, "询价发送通知"), - (6, "报价"), + # (5, "询价发送通知"), + (6, "供应商报价"), (7, "比议价"), (8, "议价审核提交"), (9, "议价审核完成"), diff --git a/backend/templates/emails/quote_ended_body.html b/backend/templates/emails/quote_ended_body.html new file mode 100644 index 0000000..37f72ad --- /dev/null +++ b/backend/templates/emails/quote_ended_body.html @@ -0,0 +1,34 @@ + + + + +尊敬的 {{ purchaser_name }}:
+您好!
+询价单「{{ inquiry_title }}」(单号:{{ rfq_number }})已报价结束:所有受邀供应商均已提交报价。
+ ++ 立即前往采购端查看 +
+若按钮无法点击,请复制以下链接到浏览器打开:
+ + {% else %} +系统未配置采购端门户地址(PIS_ADMIN_PORTAL_URL),请登录系统查看该询价单。
+ {% endif %} + +祝工作顺利!
+{{ system_name }}
+{{ current_date }}
+此邮件由系统自动发送,请勿直接回复。
+ + diff --git a/backend/templates/emails/quote_ended_subject.txt b/backend/templates/emails/quote_ended_subject.txt new file mode 100644 index 0000000..f4d06bb --- /dev/null +++ b/backend/templates/emails/quote_ended_subject.txt @@ -0,0 +1 @@ +{% autoescape off %}【报价结束】{{ rfq_number|default:"—" }} {{ inquiry_title_short|default:"—" }}{% endautoescape %} diff --git a/backend/templates/emails/rfs_publish_body.html b/backend/templates/emails/rfs_publish_body.html new file mode 100644 index 0000000..37d3f8c --- /dev/null +++ b/backend/templates/emails/rfs_publish_body.html @@ -0,0 +1,40 @@ + + + + +尊敬的 {{ vendor_name }} 合作伙伴:
+您好!
+我司({{ purchaser_company_name }})现正式发布新的采购询价单,诚邀贵司参与报价,我们非常期待与您的合作。
+ +请点击下方按钮登录系统查看详情并填写报价:
+ {% if system_link %} ++ 立即前往报价 +
+若按钮无法点击,请复制以下链接到浏览器打开:
+ + {% else %} +系统未配置供应商门户地址(PIS_SUPPLIER_PORTAL_URL),请联系管理员获取登录方式。
+ {% endif %} + +祝商祺!
+{{ purchaser_company_name }} 采购部
+{{ current_date }}
+此邮件由系统自动发送,请勿直接回复。
+ + diff --git a/backend/templates/emails/rfs_publish_subject.txt b/backend/templates/emails/rfs_publish_subject.txt new file mode 100644 index 0000000..2a2d429 --- /dev/null +++ b/backend/templates/emails/rfs_publish_subject.txt @@ -0,0 +1 @@ +【新询价邀请】{{ rfq_number|default:"—" }} - {{ material_short }} - 截止:{{ deadline_date_subject|default:"—" }}