From 6f2000041f6ac556d7df875bbbd08459c884dc5e Mon Sep 17 00:00:00 2001 From: nebula_chen Date: Fri, 27 Mar 2026 14:32:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=E6=9D=82=E9=87=87?= =?UTF-8?q?=E6=AF=94=E8=AE=AE=E4=BB=B7=E3=80=81=E8=AF=A2=E4=BB=B7=E5=B1=A5?= =?UTF-8?q?=E5=8E=86=E8=AE=B0=E5=BD=95=20=E5=AE=9E=E7=8E=B0=EF=BC=9B?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=83=A8=E5=88=86ui=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/pisadmin/miscprocurement/models.py | 250 +++++ .../pisadmin/miscprocurement/serializers.py | 175 ++++ backend/apps/pisadmin/miscprocurement/urls.py | 6 + .../apps/pisadmin/miscprocurement/views.py | 287 +++++- backend/apps/pissupplier/models.py | 8 + backend/apps/pissupplier/serializers.py | 21 + backend/apps/pissupplier/views.py | 26 +- .../miscprocurement/rfqmiscellaneous/api.ts | 21 + .../miscprocurement/rfqmiscellaneous/crud.tsx | 144 ++- .../rfqmiscellaneous/index.vue | 924 +++++++++++++++++- web/src/views/pissupplier/quotation/crud.tsx | 75 +- web/src/views/pissupplier/quotation/index.vue | 140 ++- 12 files changed, 2004 insertions(+), 73 deletions(-) diff --git a/backend/apps/pisadmin/miscprocurement/models.py b/backend/apps/pisadmin/miscprocurement/models.py index ba1ad50..2df9492 100644 --- a/backend/apps/pisadmin/miscprocurement/models.py +++ b/backend/apps/pisadmin/miscprocurement/models.py @@ -1,7 +1,13 @@ +import logging +from typing import Optional + from django.db import models +from django.utils import timezone from dvadmin.utils.models import CoreModel, table_prefix +logger = logging.getLogger(__name__) + class MiscProcurementMaterialInfo(CoreModel): """杂采材料信息""" @@ -205,6 +211,10 @@ class Inquiry(CoreModel): (3, "模具/夹具"), (4, "管"), ) + BUYING_METHOD_CHOICES = ( + (1, "询价"), + (2, "招标"), + ) inquiry_no = models.CharField(max_length=20, unique=True, db_index=True, verbose_name="询价单号") title = models.CharField(max_length=20, verbose_name="询价单名称") @@ -246,6 +256,9 @@ class Inquiry(CoreModel): create_user = models.CharField(max_length=20, db_column="createuser", null=True, blank=True, verbose_name="创建人") update_user = models.CharField(max_length=20, db_column="UpdateUser", null=True, blank=True, verbose_name="最后更新人") update_time = models.DateTimeField(db_column="UpdateTime", null=True, blank=True, verbose_name="最后更新时间") + buying_method = models.IntegerField(choices=BUYING_METHOD_CHOICES, null=True, blank=True, verbose_name="采购方式(寻源方式)") + bid_start_time = models.DateTimeField(null=True, blank=True, verbose_name="投标开始时间") + bid_end_time = models.DateTimeField(null=True, blank=True, verbose_name="投标截止时间") class Meta: db_table = table_prefix + "proc_inquiry_master" @@ -546,3 +559,240 @@ class InquiryRfqItem(models.Model): def __str__(self) -> str: # pragma: no cover - simple repr return f"{self.inquiry_no_id}-{self.part_id}" + + +class MiscLowPriceHeader(CoreModel): + """比价-制程最低价记录主表。""" + + # 成本类别: 1-材料; 2-加工; 3-包装费; 4-运输费; 5-管销研费用; 6-利润率(与库表 CostType varchar 一致) + COST_TYPE_CHOICES = ( + ("1", "材料"), + ("2", "加工"), + ("3", "包装费"), + ("4", "运输费"), + ("5", "管销研费用"), + ("6", "利润率"), + ) + + inquiry_no = models.CharField( + max_length=20, + db_index=True, + db_column="inquiry_no", + verbose_name="询价单号", + help_text="业务主键列之一;逻辑主键与 id 并存", + ) + part_id = models.CharField(max_length=50, db_column="PartId", verbose_name="产品料号") + # 来源单号(报价单&询价单);设计库字段名为 SouceNo + souce_no = models.CharField( + max_length=20, + null=True, + blank=True, + db_column="SouceNo", + verbose_name="来源单号(报价单&询价单)", + ) + cost_type = models.CharField( + max_length=50, + choices=COST_TYPE_CHOICES, + db_column="CostType", + verbose_name="成本类别", + ) + item_no = models.CharField( + max_length=100, + db_column="ItemNo", + verbose_name="项次名", + help_text="如:铝等项次名称", + ) + min_price = models.CharField( + max_length=10, + db_column="MinPrice", + verbose_name="最低价格", + help_text="表结构为 varchar,若需参与运算可在业务层转换", + ) + + class Meta: + db_table = table_prefix + "misc_low_price_header" + verbose_name = "比价-制程最低价记录主表" + verbose_name_plural = verbose_name + ordering = ("-create_datetime", "id") + indexes = [ + models.Index(fields=["inquiry_no", "part_id"]), + ] + + def __str__(self) -> str: + return f"{self.inquiry_no}-{self.part_id}-{self.cost_type}" + + +class MiscLowPriceDetail(CoreModel): + """比价-制程最低价记录次表""" + + ITEM_NO_CHOICES = ( + (1, "重量"), + (2, "单价"), + ) + inquiry_no = models.CharField(max_length=20, db_column="inquiry_no", db_index=True, verbose_name="询价单号") + part_id = models.CharField(max_length=50, db_column="PartId", verbose_name="产品料号") + cost_type = models.CharField( + max_length=50, + db_column="CostType", + verbose_name="成本类别", + help_text="仅支持1-材料", + ) + material_spec = models.CharField(max_length=50, db_column="materialspec", verbose_name="材料规格") + item_no = models.CharField( + max_length=100, + db_column="ItemNo", + verbose_name="项次名", + help_text="策采:重量、损耗、单价;杂采:重量、单价", + choices=ITEM_NO_CHOICES, + ) + value = models.CharField(max_length=10, db_column="Value", verbose_name="最小值") + souce_no = models.CharField( + max_length=20, + db_column="SouceNo", + verbose_name="来源单号(报价单&询价单)", + ) + + class Meta: + db_table = table_prefix + "misc_low_price_detail" + verbose_name = "比价-制程最低价记录次表" + verbose_name_plural = verbose_name + ordering = ("-create_datetime", "id") + indexes = [ + models.Index(fields=["inquiry_no", "part_id"]), + ] + + def __str__(self) -> str: + return f"{self.inquiry_no}-{self.part_id}-{self.item_no}" + + +class MiscNegotiationRecords(CoreModel): + """杂采议价记录表""" + + inquiry_no = models.CharField(max_length=20, db_column="inquiry_no", db_index=True, verbose_name="询价单号") + part_id = models.CharField(max_length=50, db_column="PartId", verbose_name="产品料号") + supplier_code = models.CharField(max_length=50, null=True, blank=True, db_column="SupplierCode", verbose_name="供应商代码") + bargaining_price = models.DecimalField(max_digits=12, decimal_places=4, null=True, blank=True, default=0, verbose_name="议价后价格") + bargaining_time = models.DateTimeField(null=True, blank=True, verbose_name="议价时间") + bargaining_user = models.CharField(max_length=20, null=True, blank=True, db_column="BargainingUser", verbose_name="议价人") + is_awarded = models.IntegerField(default=0, null=True, blank=True, verbose_name="是否中标", help_text="是否中标(1:是 0:否)") + quotation_no = models.CharField(max_length=20, null=True, blank=True, db_column="QuotationNo", verbose_name="报价单号") + total_price_excl_tax = models.DecimalField(max_digits=12, decimal_places=4, null=True, blank=True, default=0, verbose_name="议价前不含税总价") + total_price_incl_tax = models.DecimalField(max_digits=12, decimal_places=4, null=True, blank=True, default=0, verbose_name="议价前含税总价") + + class Meta: + db_table = table_prefix + "misc_negotiation_records" + verbose_name = "杂采议价记录表" + verbose_name_plural = verbose_name + ordering = ("-create_datetime", "id") + indexes = [ + models.Index(fields=["inquiry_no", "part_id"]), + models.Index( + fields=["inquiry_no", "part_id", "quotation_no"], + name="pis_misc_ne_inq_part_qtn_idx", + ), + ] + + def __str__(self) -> str: + return f"{self.inquiry_no}-{self.part_id}-{self.quotation_no or self.supplier_code}" + + +class RFQOperationLogs(CoreModel): + """询价单操作日志表""" + + # (01询价单创;02询价单确认;03询价单发布;04询价单还原;05询价发送通知;06报价;07比议价;08议价审核提交;09议价审核完成;10议价审核驳回) + OPERATION_TYPE_CHOICES = ( + (1, "询价单创建"), + (2, "询价单确认"), + (3, "询价单发布"), + (4, "询价单还原"), + (5, "询价发送通知"), + (6, "报价"), + (7, "比议价"), + (8, "议价审核提交"), + (9, "议价审核完成"), + (10, "议价审核驳回"), + ) + PURCHASE_TYPE_CHOICES = ( + (1, "策采"), + (2, "杂采"), + ) + + operation_type = models.CharField(max_length=20, db_column="OperationType", verbose_name="操作类型", choices=OPERATION_TYPE_CHOICES) + operation_user = models.CharField(max_length=20, null=True, blank=True, db_column="OperationUser", verbose_name="操作人") + operation_time = models.DateTimeField(null=True, blank=True, db_column="OperationTime", verbose_name="操作时间") + operation_desc = models.TextField(max_length=200, null=True, blank=True, db_column="OperationDesc", verbose_name="操作描述") + inquiry_no = models.CharField(max_length=20, db_column="InquiryNo", db_index=True, verbose_name="询价单号") + quotation_no = models.CharField(max_length=20, null=False, blank=False, db_column="QuotationNo", db_index=True, verbose_name="报价单号") + per_status = models.CharField(max_length=50, null=True, blank=True, db_column="PerStatus", verbose_name="作业前状态") + cur_status = models.TextField(max_length=100, null=True, blank=True, db_column="CurStatus", verbose_name="作业后状态") + is_show_user = models.IntegerField(null=True, blank=True, db_column="IsShowUser", verbose_name="履历显示否", help_text="履历显示否(1:是 0:否)") + purchase_type = models.IntegerField(choices=PURCHASE_TYPE_CHOICES, null=False, blank=False, db_column="PurchaseType", verbose_name="采购类别", help_text="采购类别(1:策采 2:杂采)") + + class Meta: + db_table = table_prefix + "rfq_operation_logs" + verbose_name = "询价单操作日志" + verbose_name_plural = verbose_name + ordering = ("-create_datetime", "id") + indexes = [ + models.Index(fields=["inquiry_no", "quotation_no"]), + ] + + @classmethod + def inquiry_status_display(cls, code: Optional[int]) -> str: + """询价单状态码 → 与 Inquiry.STATUS_CHOICES 一致的可读文案。""" + if code is None: + return "" + try: + c = int(code) + except (TypeError, ValueError): + return str(code) + return dict(Inquiry.STATUS_CHOICES).get(c, str(c)) + + @classmethod + def append( + cls, + *, + inquiry_no: str, + purchase_type: int, + operation_type: int, + operation_user: Optional[str] = None, + quotation_no: Optional[str] = None, + per_status: Optional[int] = None, + cur_status: Optional[int] = None, + operation_desc: Optional[str] = None, + is_show_user: int = 1, + ) -> None: + """ + 写入一条询价操作日志。quotation_no 无关联报价单时使用 \"-\"。 + operation_type 与 OPERATION_TYPE_CHOICES 取值 1–10 一致。 + """ + qn = (quotation_no or "-").strip()[:20] or "-" + op_user = (operation_user or "").strip()[:20] if operation_user else None + now = timezone.now() + per_str = cls.inquiry_status_display(per_status) if per_status is not None else None + cur_str = cls.inquiry_status_display(cur_status) if cur_status is not None else None + desc = (operation_desc or "").strip()[:200] if operation_desc else None + + cls.objects.create( + operation_type=str(int(operation_type)), + operation_user=op_user, + operation_time=now, + operation_desc=desc, + inquiry_no=(inquiry_no or "")[:20], + quotation_no=qn, + per_status=(per_str[:50] if per_str else None), + cur_status=(cur_str[:100] if cur_str else None), + is_show_user=is_show_user, + purchase_type=int(purchase_type), + ) + + @classmethod + def try_append(cls, **kwargs) -> None: + """写入失败不影响主流程,仅记录异常日志。""" + try: + cls.append(**kwargs) + except Exception: + logger.exception("写入询价操作日志失败") + + def __str__(self) -> str: + return f"{self.inquiry_no}-{self.quotation_no}" diff --git a/backend/apps/pisadmin/miscprocurement/serializers.py b/backend/apps/pisadmin/miscprocurement/serializers.py index 2f46b2c..f32dc16 100644 --- a/backend/apps/pisadmin/miscprocurement/serializers.py +++ b/backend/apps/pisadmin/miscprocurement/serializers.py @@ -9,6 +9,10 @@ from .models import ( MiscProcurementMaterialInfo, MiscProcurementStationInfo, MiscProcMaterial, + MiscLowPriceHeader, + MiscLowPriceDetail, + MiscNegotiationRecords, + RFQOperationLogs, Inquiry, InquirySupplier, InquiryAttachment, @@ -262,6 +266,141 @@ class MiscPartCreateUpdateSerializer(CustomModelSerializer): fields = "__all__" +class MiscLowPriceHeaderSerializer(CustomModelSerializer): + """比价-制程最低价记录主表""" + + class Meta: + model = MiscLowPriceHeader + fields = "__all__" + read_only_fields = [ + "id", + "create_datetime", + "update_datetime", + "creator", + "modifier", + "dept_belong_id", + ] + + +class MiscLowPriceDetailSerializer(CustomModelSerializer): + """比价-制程最低价记录次表""" + + class Meta: + model = MiscLowPriceDetail + fields = "__all__" + read_only_fields = [ + "id", + "create_datetime", + "update_datetime", + "creator", + "modifier", + "dept_belong_id", + ] + + +class MiscNegotiationRecordsSerializer(CustomModelSerializer): + """杂采议价记录表""" + + class Meta: + model = MiscNegotiationRecords + fields = "__all__" + read_only_fields = [ + "id", + "create_datetime", + "update_datetime", + "creator", + "modifier", + "dept_belong_id", + ] + + +class RFQOperationLogsSerializer(CustomModelSerializer): + """询价单操作日志(rfq_operation_logs)""" + + operation_type = serializers.IntegerField(required=True) + operation_time = serializers.DateTimeField( + format="%Y-%m-%d %H:%M:%S", + required=False, + allow_null=True, + ) + + class Meta: + model = RFQOperationLogs + fields = "__all__" + read_only_fields = [ + "id", + "create_datetime", + "update_datetime", + "creator", + "modifier", + "dept_belong_id", + ] + extra_kwargs = { + "inquiry_no": {"required": True, "allow_blank": False}, + "quotation_no": {"required": True, "allow_blank": False}, + "purchase_type": {"required": True}, + } + + def validate_operation_type(self, value): + valid_values = {choice[0] for choice in RFQOperationLogs.OPERATION_TYPE_CHOICES} + if value not in valid_values: + raise serializers.ValidationError("操作类型取值不合法") + return value + + def validate_purchase_type(self, value): + valid_values = {choice[0] for choice in RFQOperationLogs.PURCHASE_TYPE_CHOICES} + if value not in valid_values: + raise serializers.ValidationError("采购类别取值不合法") + return value + + def _normalize_operation_type(self, validated_data: dict) -> None: + ot = validated_data.get("operation_type") + if ot is not None and not isinstance(ot, str): + validated_data["operation_type"] = str(int(ot)) + + def create(self, validated_data): + self._normalize_operation_type(validated_data) + return super().create(validated_data) + + def update(self, instance, validated_data): + self._normalize_operation_type(validated_data) + return super().update(instance, validated_data) + + def to_representation(self, instance): + data = super().to_representation(instance) + ot = data.get("operation_type") + if ot is not None and ot != "": + try: + data["operation_type"] = int(ot) + except (TypeError, ValueError): + pass + return data + + +class MiscNegotiationRecordBatchItemSerializer(serializers.Serializer): + """比价保存时按报价单维度的议价行""" + + quotation_no = serializers.CharField(max_length=20) + supplier_code = serializers.CharField(max_length=50, required=False, allow_blank=True) + is_awarded = serializers.IntegerField(required=False, default=0) + bargaining_price = serializers.DecimalField( + max_digits=12, decimal_places=4, required=False, allow_null=True + ) + total_price_excl_tax = serializers.DecimalField( + max_digits=12, decimal_places=4, required=False, allow_null=True + ) + total_price_incl_tax = serializers.DecimalField( + max_digits=12, decimal_places=4, required=False, allow_null=True + ) + + +class MiscNegotiationSaveSerializer(serializers.Serializer): + """比价/议价结果批量写入杂采议价记录表""" + + part_id = serializers.CharField(max_length=50) + records = MiscNegotiationRecordBatchItemSerializer(many=True) + + class CostEstimateTemplateBodySerializer(serializers.ModelSerializer): supplier_behavior = serializers.SerializerMethodField() item_name_en = serializers.CharField(required=False, allow_blank=True, allow_null=True) @@ -890,6 +1029,16 @@ class InquirySerializer(CustomModelSerializer): rfq_items = InquiryRfqItemSerializer(many=True, required=False) # 列表/详情展示:company_code → 公司信息简称(同请求内按代码缓存) company_short_name = serializers.SerializerMethodField(read_only=True) + bid_start_time = serializers.DateTimeField( + format="%Y-%m-%d %H:%M:%S", + required=False, + allow_null=True, + ) + bid_end_time = serializers.DateTimeField( + format="%Y-%m-%d %H:%M:%S", + required=False, + allow_null=True, + ) def get_company_short_name(self, obj): code = (getattr(obj, "company_code", None) or "").strip() @@ -931,8 +1080,32 @@ class InquirySerializer(CustomModelSerializer): "inquiry_no": {"required": False, "allow_blank": True, "allow_null": True}, # 由 resolve_inquiry_template_version 根据 template 从 CostEstimateTemplateHead 写入 "template_version": {"required": False, "allow_null": True}, + "buying_method": {"required": False, "allow_null": True}, } + def validate_buying_method(self, value): + if value is None: + return value + valid_values = {choice[0] for choice in Inquiry.BUYING_METHOD_CHOICES} + if value not in valid_values: + raise serializers.ValidationError("采购方式(寻源方式)取值不合法") + return value + + def _apply_inquiry_bid_times_for_buying_method(self, attrs, instance): + """采购方式为询价(1)时,投标开始/截止时间不入库。""" + bm = attrs.get("buying_method") + if bm is None and instance is not None: + bm = getattr(instance, "buying_method", None) + if bm is None: + bm = 1 + try: + bm = int(bm) + except (TypeError, ValueError): + return + if bm == 1: + attrs["bid_start_time"] = None + attrs["bid_end_time"] = None + def validate(self, attrs): instance = getattr(self, "instance", None) @@ -941,6 +1114,7 @@ class InquirySerializer(CustomModelSerializer): # 局部更新且未改模板/版本:保持库中原值 if instance and not template_in_attrs and not tv_in_attrs: + self._apply_inquiry_bid_times_for_buying_method(attrs, instance) return attrs template_no = attrs.get("template") @@ -963,6 +1137,7 @@ class InquirySerializer(CustomModelSerializer): } ) attrs["template_version"] = resolved + self._apply_inquiry_bid_times_for_buying_method(attrs, instance) return attrs def _generate_code(self, validated_data: dict) -> str: diff --git a/backend/apps/pisadmin/miscprocurement/urls.py b/backend/apps/pisadmin/miscprocurement/urls.py index fae8b45..e628d8e 100644 --- a/backend/apps/pisadmin/miscprocurement/urls.py +++ b/backend/apps/pisadmin/miscprocurement/urls.py @@ -4,6 +4,8 @@ from .views import ( MiscMaterialViewSet, MiscStationViewSet, MiscPartViewSet, + MiscLowPriceHeaderViewSet, + MiscLowPriceDetailViewSet, InquiryViewSet, InquirySupplierViewSet, InquiryAttachmentViewSet, @@ -13,6 +15,7 @@ from .views import ( InquiryProfitCostViewSet, InquiryRfqItemViewSet, CostEstimateTemplateViewSet, + RFQOperationLogsViewSet, ) router = routers.SimpleRouter() @@ -27,7 +30,10 @@ router.register(r'inquiry_process_cost', InquiryProcessCostViewSet) router.register(r'inquiry_other_cost', InquiryOtherCostViewSet) router.register(r'inquiry_profit_cost', InquiryProfitCostViewSet) router.register(r'inquiry_rfq_item', InquiryRfqItemViewSet) +router.register(r'proc_low_price_header', MiscLowPriceHeaderViewSet) +router.register(r'proc_low_price_detail', MiscLowPriceDetailViewSet) router.register(r'cost_template', CostEstimateTemplateViewSet) +router.register(r'rfq_operation_logs', RFQOperationLogsViewSet) urlpatterns = [] urlpatterns += router.urls diff --git a/backend/apps/pisadmin/miscprocurement/views.py b/backend/apps/pisadmin/miscprocurement/views.py index 18c3554..6bdfe42 100644 --- a/backend/apps/pisadmin/miscprocurement/views.py +++ b/backend/apps/pisadmin/miscprocurement/views.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from django.db import transaction from django.db.models import Q from django.utils import timezone @@ -27,6 +29,10 @@ from .models import ( MiscProcurementMaterialInfo, MiscProcurementStationInfo, MiscProcMaterial, + MiscLowPriceHeader, + MiscLowPriceDetail, + MiscNegotiationRecords, + RFQOperationLogs, Inquiry, InquirySupplier, InquiryAttachment, @@ -45,6 +51,11 @@ from .serializers import ( MiscStationCreateUpdateSerializer, MiscPartSerializer, MiscPartCreateUpdateSerializer, + MiscLowPriceHeaderSerializer, + MiscLowPriceDetailSerializer, + MiscNegotiationRecordsSerializer, + MiscNegotiationSaveSerializer, + RFQOperationLogsSerializer, InquirySerializer, InquirySupplierSerializer, InquiryAttachmentSerializer, @@ -59,6 +70,20 @@ from .serializers import ( ) +def _negotiation_totals_from_quotation_item(quotation_no: str, part_id: str): + """从杂采报价单上阶物料明细取议价前含税/不含税总价(与 part_id 匹配行)。""" + if not quotation_no or not part_id: + return None, None + item = ( + QuotationItem.objects.filter(quotation_no=quotation_no, part_id=part_id) + .order_by("autoid") + .first() + ) + if not item: + return None, None + return item.total_price_excl_tax, item.total_price_incl_tax + + class MiscMaterialViewSet(CustomModelViewSet): queryset = MiscProcurementMaterialInfo.objects.all() serializer_class = MiscMaterialSerializer @@ -232,7 +257,16 @@ class InquiryViewSet(CustomModelViewSet): "rfq_items", ) serializer_class = InquirySerializer - filter_fields = ("inquiry_no", "title", "purchase_type", "template", "template_version", "status", "buyer") + filter_fields = ( + "inquiry_no", + "title", + "purchase_type", + "template", + "template_version", + "status", + "buyer", + "buying_method", + ) search_fields = ("inquiry_no", "title", "material_type", "buyer", "remark") ordering = ("-update_datetime",) @@ -326,7 +360,21 @@ class InquiryViewSet(CustomModelViewSet): return ErrorResponse(msg="当前询价单状态仅允许查看,不允许编辑或删除;如需修改请先还原为“开立”") return None - def _save_status(self, instance, *, status, confirm_user=None, confirm_time=None, release_user=None, release_time=None, comparison_user=None, comparison_time=None): + def _save_status( + self, + instance, + *, + status, + confirm_user=None, + confirm_time=None, + release_user=None, + release_time=None, + comparison_user=None, + comparison_time=None, + operation_type=None, + operation_desc=None, + ): + old_status = int(instance.status if instance.status is not None else 0) current_time = timezone.now() current_user = self._get_request_username() or getattr(instance, "update_user", None) instance.status = status @@ -353,6 +401,18 @@ class InquiryViewSet(CustomModelViewSet): "update_time", ] ) + new_status = int(status) + if operation_type is not None and old_status != new_status: + RFQOperationLogs.try_append( + inquiry_no=instance.inquiry_no, + purchase_type=int(instance.purchase_type), + operation_type=operation_type, + operation_user=current_user or None, + quotation_no="-", + per_status=old_status, + cur_status=new_status, + operation_desc=operation_desc, + ) serializer = self.get_serializer(instance) return DetailResponse(data=serializer.data, msg="状态更新成功") @@ -537,6 +597,9 @@ class InquiryViewSet(CustomModelViewSet): contact_phone=supplier["contact_phone"] or None, contact_email=supplier["contact_email"] or None, quote_deadline=quote_deadline, + buying_method=getattr(inquiry, "buying_method", None), + bid_start_time=getattr(inquiry, "bid_start_time", None), + bid_end_time=getattr(inquiry, "bid_end_time", None), delivery_days=getattr(inquiry, "lead_time_days", None), payment_method=getattr(inquiry, "payment_method", None), status=1, @@ -655,7 +718,17 @@ class InquiryViewSet(CustomModelViewSet): QuotationItem.objects.bulk_create(item_bulk) def perform_create(self, serializer): - serializer.save() + inquiry = serializer.save() + RFQOperationLogs.try_append( + inquiry_no=inquiry.inquiry_no, + purchase_type=int(inquiry.purchase_type), + operation_type=1, + operation_user=self._get_request_username() or None, + quotation_no="-", + per_status=None, + cur_status=int(inquiry.status) if inquiry.status is not None else 1, + operation_desc="询价单创建", + ) def perform_update(self, serializer): serializer.save() @@ -717,6 +790,8 @@ class InquiryViewSet(CustomModelViewSet): confirm_time=current_time, release_user=getattr(instance, "release_user", None), release_time=getattr(instance, "release_time", None), + operation_type=2, + operation_desc="询价单确认", ) @action(methods=["put"], detail=True) @@ -731,6 +806,8 @@ class InquiryViewSet(CustomModelViewSet): confirm_time=None, release_user=None, release_time=None, + operation_type=4, + operation_desc="询价单还原为开立", ) @action(methods=["put"], detail=True) @@ -751,6 +828,8 @@ class InquiryViewSet(CustomModelViewSet): confirm_time=getattr(instance, "confirm_time", None), release_user=current_user, release_time=current_time, + operation_type=3, + operation_desc="询价单发布", ) except serializers.ValidationError as exc: detail = getattr(exc, "detail", None) @@ -760,6 +839,160 @@ class InquiryViewSet(CustomModelViewSet): msg = str(detail or exc) return ErrorResponse(msg=msg) + @action(methods=["put"], detail=True) + def start_bargaining(self, request, pk=None): + """开启比价议价:从报价结束状态进入比议价中""" + instance = self.get_object() + if int(instance.status or self.STATUS_PUBLISHED) not in (self.STATUS_PUBLISHED, self.STATUS_QUOTING, self.STATUS_QUOTE_ENDED): + return ErrorResponse(msg='只有“发布”或“报价结束”状态的询价单才能开启比价') + current_user = self._get_request_username() or None + current_time = timezone.now() + return self._save_status( + instance, + status=self.STATUS_BARGaining, + confirm_user=getattr(instance, "confirm_user", None), + confirm_time=getattr(instance, "confirm_time", None), + release_user=getattr(instance, "release_user", None), + release_time=getattr(instance, "release_time", None), + comparison_user=current_user, + comparison_time=current_time, + operation_type=7, + operation_desc="开启比议价", + ) + + @action(methods=["put"], detail=True) + def confirm_negotiation(self, request, pk=None): + """确认议价:从比议价中进入议价确认/价格审核""" + instance = self.get_object() + if int(instance.status or self.STATUS_BARGaining) != self.STATUS_BARGaining: + return ErrorResponse(msg='只有“比议价中”状态的询价单才能确认议价') + current_user = self._get_request_username() or None + current_time = timezone.now() + return self._save_status( + instance, + status=self.STATUS_NEGOTIATED, + confirm_user=getattr(instance, "confirm_user", None), + confirm_time=getattr(instance, "confirm_time", None), + release_user=getattr(instance, "release_user", None), + release_time=getattr(instance, "release_time", None), + comparison_user=getattr(instance, "comparison_user", None), + comparison_time=getattr(instance, "comparison_time", None), + operation_type=8, + operation_desc="议价审核提交(进入价格审核)", + ) + + @action(methods=["put"], detail=True) + def submit_price_audit(self, request, pk=None): + """价格审核提交:由「价格审核」(7) 进入「核价通过」(8)。""" + instance = self.get_object() + if int(instance.status or 0) != self.STATUS_NEGOTIATED: + return ErrorResponse(msg='只有「价格审核」状态的询价单才能提交核价') + return self._save_status( + instance, + status=self.STATUS_APPROVED, + confirm_user=getattr(instance, "confirm_user", None), + confirm_time=getattr(instance, "confirm_time", None), + release_user=getattr(instance, "release_user", None), + release_time=getattr(instance, "release_time", None), + comparison_user=getattr(instance, "comparison_user", None), + comparison_time=getattr(instance, "comparison_time", None), + operation_type=9, + operation_desc="议价审核完成(核价通过)", + ) + + @action(methods=["get"], detail=True, url_path="negotiation_records") + def negotiation_records(self, request, pk=None): + """按询价单号查询杂采议价记录(可选 part_id)。""" + instance = self.get_object() + part_id = (request.query_params.get("part_id") or "").strip() + qs = MiscNegotiationRecords.objects.filter(inquiry_no=instance.inquiry_no) + if part_id: + qs = qs.filter(part_id=part_id) + data = MiscNegotiationRecordsSerializer(qs.order_by("id"), many=True).data + return DetailResponse(data=data, msg="success") + + @action(methods=["put"], detail=True, url_path="save_negotiation_records") + def save_negotiation_records(self, request, pk=None): + """按报价单写入杂采议价记录:议价结果 + 该报价单议价前含税/不含税总价快照(来自上阶物料明细)。""" + instance = self.get_object() + serializer = MiscNegotiationSaveSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + part_id = (serializer.validated_data.get("part_id") or "").strip() + if not part_id: + return ErrorResponse(msg="part_id 不能为空") + records = serializer.validated_data.get("records") or [] + username = self._get_request_username() or None + now = timezone.now() + with transaction.atomic(): + for row in records: + qn = (row.get("quotation_no") or "").strip() + if not qn: + return ErrorResponse(msg="records 中每条须包含 quotation_no(报价单号)") + code = (row.get("supplier_code") or "").strip() + if not code: + qm = QuotationMaster.objects.filter(quotation_no=qn).only("supplier_code").first() + if qm: + code = (qm.supplier_code or "").strip() + is_awarded = int(row.get("is_awarded") or 0) + # 议价价格独立于是否中标:用户填写即落库(清空时前端传 null) + bp = row.get("bargaining_price") + snap_ex, snap_in = _negotiation_totals_from_quotation_item(qn, part_id) + tex = row.get("total_price_excl_tax") + tin = row.get("total_price_incl_tax") + if tex is None: + tex = snap_ex + if tin is None: + tin = snap_in + if tex is None: + tex = Decimal("0") + if tin is None: + tin = Decimal("0") + defaults = { + "supplier_code": code or None, + "is_awarded": is_awarded, + "bargaining_user": username, + "bargaining_time": now, + "bargaining_price": bp, + "quotation_no": qn, + "total_price_excl_tax": tex, + "total_price_incl_tax": tin, + } + qs = MiscNegotiationRecords.objects.filter( + inquiry_no=instance.inquiry_no, + part_id=part_id, + quotation_no=qn, + ) + if qs.count() > 1: + keep_id = qs.order_by("id").first().id + qs.exclude(id=keep_id).delete() + obj = ( + MiscNegotiationRecords.objects.filter( + inquiry_no=instance.inquiry_no, + part_id=part_id, + quotation_no=qn, + ) + .order_by("id") + .first() + ) + if obj: + for k, v in defaults.items(): + setattr(obj, k, v) + obj.save() + else: + MiscNegotiationRecords.objects.create( + inquiry_no=instance.inquiry_no, + part_id=part_id, + **defaults, + ) + out_qs = MiscNegotiationRecords.objects.filter( + inquiry_no=instance.inquiry_no, + part_id=part_id, + ).order_by("id") + return DetailResponse( + data=MiscNegotiationRecordsSerializer(out_qs, many=True).data, + msg="议价记录已保存", + ) + def _notify_vendors_on_publish(self, inquiry: Inquiry): """ 按询价单供应商子表汇总(与 `_create_supplier_quotations` 相同的 `_group_inquiry_suppliers`), @@ -924,3 +1157,51 @@ class InquiryRfqItemViewSet(CustomModelViewSet): def perform_update(self, serializer): serializer.save() + + +class MiscLowPriceHeaderViewSet(CustomModelViewSet): + """比价-制程最低价记录主表""" + + queryset = MiscLowPriceHeader.objects.all() + serializer_class = MiscLowPriceHeaderSerializer + filter_fields = ("inquiry_no", "part_id", "souce_no", "cost_type", "item_no") + search_fields = ("inquiry_no", "part_id", "souce_no", "item_no") + ordering = ("-create_datetime", "id") + + +class MiscLowPriceDetailViewSet(CustomModelViewSet): + """比价-制程最低价记录次表""" + + queryset = MiscLowPriceDetail.objects.all() + serializer_class = MiscLowPriceDetailSerializer + filter_fields = ("inquiry_no", "part_id", "cost_type", "material_spec", "item_no", "souce_no") + search_fields = ("inquiry_no", "part_id", "material_spec", "item_no", "souce_no") + ordering = ("-create_datetime", "id") + + +class MiscNegotiationRecordsViewSet(CustomModelViewSet): + """杂采议价记录表""" + + queryset = MiscNegotiationRecords.objects.all() + serializer_class = MiscNegotiationRecordsSerializer + filter_fields = ("inquiry_no", "part_id", "supplier_code") + search_fields = ("inquiry_no", "part_id", "supplier_code") + ordering = ("-create_datetime", "id") + + +class RFQOperationLogsViewSet(CustomModelViewSet): + """询价单操作日志(rfq_operation_logs)""" + + queryset = RFQOperationLogs.objects.all() + serializer_class = RFQOperationLogsSerializer + create_serializer_class = RFQOperationLogsSerializer + update_serializer_class = RFQOperationLogsSerializer + filter_fields = ( + "inquiry_no", + "quotation_no", + "operation_type", + "operation_user", + "purchase_type", + ) + search_fields = ("inquiry_no", "quotation_no", "operation_user", "operation_desc") + ordering = ("-create_datetime", "-id") \ No newline at end of file diff --git a/backend/apps/pissupplier/models.py b/backend/apps/pissupplier/models.py index 2e8bbfc..669ef57 100644 --- a/backend/apps/pissupplier/models.py +++ b/backend/apps/pissupplier/models.py @@ -25,6 +25,11 @@ class QuotationMaster(models.Model): (1, "已中标"), ) + BUYING_METHOD_CHOICES = ( + (1, "询价"), + (2, "招标"), + ) + autoid = models.BigAutoField( primary_key=True, db_column="autoId", verbose_name="自增ID" ) quotation_no = models.CharField( max_length=20, unique=True, db_index=True, verbose_name="报价单单号" ) inquiry_no = models.CharField( max_length=20, db_index=True, verbose_name="询价单单号", help_text="报价单关联的询价单单号" ) @@ -53,6 +58,9 @@ class QuotationMaster(models.Model): null=True, blank=True, verbose_name="报价时间", help_text="正式提交报价时由 submit 接口写入当前时间" ) remark = models.TextField( null=True, blank=True, verbose_name="报价说明及备注" ) + buying_method = models.IntegerField(choices=BUYING_METHOD_CHOICES, null=True, blank=True, verbose_name="采购方式(寻源方式)") + bid_start_time = models.DateTimeField(null=True, blank=True, verbose_name="投标开始时间") + bid_end_time = models.DateTimeField(null=True, blank=True, verbose_name="投标截止时间") class Meta: db_table = table_prefix + "sup_quotation_master" diff --git a/backend/apps/pissupplier/serializers.py b/backend/apps/pissupplier/serializers.py index 00ce279..0ad4857 100644 --- a/backend/apps/pissupplier/serializers.py +++ b/backend/apps/pissupplier/serializers.py @@ -383,6 +383,16 @@ class QuotationMasterSerializer(BusinessAuditSerializer): required=False, allow_null=True, ) + bid_start_time = serializers.DateTimeField( + format="%Y-%m-%d %H:%M:%S", + required=False, + allow_null=True, + ) + bid_end_time = serializers.DateTimeField( + format="%Y-%m-%d %H:%M:%S", + required=False, + allow_null=True, + ) # 列表/详情:询价主表 company_code + 公司信息简称(同请求内按 inquiry_no 缓存,减轻重复查询) inquiry_company_code = serializers.SerializerMethodField(read_only=True) inquiry_company_short_name = serializers.SerializerMethodField(read_only=True) @@ -504,6 +514,9 @@ class QuotationMasterCreateUpdateSerializer(BusinessAuditSerializer): quoteuser = serializers.CharField(max_length=20, required=False, allow_blank=True, allow_null=True) quotetime = serializers.DateTimeField(required=False, allow_null=True) remark = serializers.CharField(required=False, allow_blank=True, allow_null=True) + buying_method = serializers.IntegerField(required=False, allow_null=True) + bid_start_time = serializers.DateTimeField(required=False, allow_null=True) + bid_end_time = serializers.DateTimeField(required=False, allow_null=True) audit_create_user_field = "createuser" audit_create_time_field = "creattime" @@ -548,6 +561,14 @@ class QuotationMasterCreateUpdateSerializer(BusinessAuditSerializer): raise serializers.ValidationError("中标状态值不合法") return value + def validate_buying_method(self, value): + if value is None: + return value + valid_values = {choice[0] for choice in QuotationMaster.BUYING_METHOD_CHOICES} + if value not in valid_values: + raise serializers.ValidationError("采购方式(寻源方式)取值不合法") + return value + def _upsert_attachments(self, quotation, attachments): QuotationAttachment.objects.filter(quotation_no=quotation).delete() if not attachments: diff --git a/backend/apps/pissupplier/views.py b/backend/apps/pissupplier/views.py index 89b17ba..b74e834 100644 --- a/backend/apps/pissupplier/views.py +++ b/backend/apps/pissupplier/views.py @@ -7,7 +7,7 @@ from rest_framework.decorators import action from dvadmin.utils.json_response import DetailResponse, ErrorResponse from dvadmin.utils.viewset import CustomModelViewSet -from apps.pisadmin.miscprocurement.models import Inquiry +from apps.pisadmin.miscprocurement.models import Inquiry, RFQOperationLogs from apps.pissupplier.models import ( QuotationMaster, QuotationAttachment, @@ -84,7 +84,12 @@ class QuotationMasterViewSet(CustomModelViewSet): return super().partial_update(request, *args, **kwargs) @staticmethod - def _sync_inquiry_when_quotation_quoting(inquiry_no: str, *, actor_username: Optional[str] = None): + def _sync_inquiry_when_quotation_quoting( + inquiry_no: str, + *, + actor_username: Optional[str] = None, + quotation_no: Optional[str] = None, + ): """ 任意报价单进入「报价中」(status=2) 时,若询价单仍为「发布」(3),同步为「报价中」(4)。 已进入报价中(4) 的询价单无需再改;不回退报价结束及之后状态。 @@ -95,6 +100,7 @@ class QuotationMasterViewSet(CustomModelViewSet): inq = Inquiry.objects.filter(inquiry_no=inq_no, status=3).first() if not inq: return + old_status = int(inq.status if inq.status is not None else 0) now = timezone.now() update_user = (str(actor_username).strip()[:20] if actor_username else None) or None inq.status = 4 @@ -103,6 +109,16 @@ class QuotationMasterViewSet(CustomModelViewSet): if update_user: inq.update_user = update_user 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=update_user, + quotation_no=(quotation_no or "-")[:20], + per_status=old_status, + cur_status=4, + operation_desc="供应商进入报价中,询价单同步为报价中", + ) @action(methods=["post"], detail=True, url_path="quote") def quote(self, request, pk=None): @@ -117,7 +133,11 @@ class QuotationMasterViewSet(CustomModelViewSet): instance.quoteuser = username with transaction.atomic(): instance.save(update_fields=["status", "quotetime", "quoteuser"]) - self._sync_inquiry_when_quotation_quoting(instance.inquiry_no, actor_username=username) + self._sync_inquiry_when_quotation_quoting( + instance.inquiry_no, + actor_username=username, + quotation_no=getattr(instance, "quotation_no", None), + ) serializer = self.get_serializer(instance) return DetailResponse(data=serializer.data, msg="报价中状态更新成功") diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts index 7f72b59..ed352d4 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts @@ -10,6 +10,27 @@ export const DelObj = (id: string | number) => request({ url: baseUrl + id + '/' export const ConfirmObj = (id: string | number) => request({ url: `${baseUrl}${id}/confirm/`, method: 'put' }) export const RestoreObj = (id: string | number) => request({ url: `${baseUrl}${id}/restore/`, method: 'put' }) export const PublishObj = (id: string | number) => request({ url: `${baseUrl}${id}/publish/`, method: 'put' }) +export const StartBargainingObj = (id: string | number) => request({ url: `${baseUrl}${id}/start_bargaining/`, method: 'put' }) +export const ConfirmNegotiationObj = (id: string | number) => request({ url: `${baseUrl}${id}/confirm_negotiation/`, method: 'put' }) +export const SubmitPriceAuditObj = (id: string | number) => request({ url: `${baseUrl}${id}/submit_price_audit/`, method: 'put' }) +/** 查询杂采议价记录(比价议价价格存此表,非报价明细「中标价格」) */ +export const GetNegotiationRecordsObj = (id: string | number, params: { part_id?: string }) => + request({ url: `${baseUrl}${id}/negotiation_records/`, method: 'get', params }) +/** 保存比价中的议价后价格、中标否至杂采议价记录表 */ +export const SaveNegotiationRecordsObj = ( + id: string | number, + data: { + part_id: string + records: { + quotation_no: string + supplier_code?: string + is_awarded: number + bargaining_price: number | null + total_price_excl_tax?: number | null + total_price_incl_tax?: number | null + }[] + } +) => request({ url: `${baseUrl}${id}/save_negotiation_records/`, method: 'put', data }) export const UploadFile = (data: FormData) => request({ url: '/api/system/file/', diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx index 5b982a2..e9ec0d2 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/crud.tsx @@ -15,6 +15,11 @@ const statusDict = [ { value: 0, label: '作废' } ] +const buyingMethodDict = [ + { value: 1, label: '询价' }, + { value: 2, label: '招标' } +] + const paymentMethods = [ { value: 1, label: '月结30天' }, { value: 2, label: '月结60天' }, @@ -59,8 +64,12 @@ type ExtraHooks = { onAdd?: () => void onEdit?: (row: any) => void onView?: (row: any) => void + /** 打开比价/议价弹窗(比议价中、价格审核、核价通过);保存时按报价单写入杂采议价记录(议价前总价快照等) */ + onComparison?: (row: any) => void /** 列表多选变化(用于后续多询价单比价等) */ onTableSelectionChange?: (rows: any[]) => void + /** 与勾选列同步:由页面 ref 维护当前选中行,工具栏按钮据此取行(fast-crud 的 tableRef 往往拿不到 selection) */ + getTableSelection?: () => any[] } const STATUS_OPEN = 1 @@ -97,6 +106,10 @@ const getRowStatus = (row: any) => Number(row?.status) const isOpenStatus = (row: any) => getRowStatus(row) === STATUS_OPEN const isConfirmedStatus = (row: any) => getRowStatus(row) === STATUS_CONFIRMED const canPublishStatus = (row: any) => isConfirmedStatus(row) +/** 报价中(4)、报价结束(5) 可开启比价 */ +const isQuotingOrEndedStatus = (row: any) => [4, 5].includes(getRowStatus(row)) +/** 比议价中(6)、价格审核(7)、核价通过(8) 可打开比价窗口 */ +const canOpenComparison = (row: any) => [6, 7, 8].includes(getRowStatus(row)) const getErrorMessage = (err: any, fallback: string) => formatRfqApiErrorMessage(err, fallback) /** 列表行含 `suppliers` 时可先做提示;未返回嵌套时交由接口校验 */ @@ -106,12 +119,12 @@ const rowHasSuppliersList = (row: any): boolean | null => { return list.length > 0 } -/** 成本模板版本号两位展示(与 pricetemplate 一致) */ -export const formatCostTemplateVersionTwoDigits = (v: unknown) => { - if (v == null || v === '') return '00' +/** 成本模板版本号展示(不做位数补零) */ +export const formatCostTemplateVersion = (v: unknown) => { + if (v == null || v === '') return '' const n = Number(v) - if (!Number.isFinite(n)) return String(v) - return String(Math.trunc(n)).padStart(2, '0') + if (!Number.isFinite(n)) return String(v).trim() + return String(Math.trunc(n)) } export const createCrudOptions = function ({ @@ -120,27 +133,23 @@ export const createCrudOptions = function ({ onAdd, onEdit, onView, - onTableSelectionChange + onComparison, + onTableSelectionChange, + getTableSelection }: Partial & ExtraHooks): CreateCrudOptionsRet { void context - const normalizeSelection = (raw: unknown): any[] => { - if (Array.isArray(raw)) return raw - if (raw != null && typeof raw === 'object') return [raw as any] - return [] - } - const getSelectedRows = () => { if (typeof getTableSelection === 'function') { - const rows = normalizeSelection(getTableSelection()) - if (rows.length > 0) return rows + const rows = getTableSelection() + if (Array.isArray(rows) && rows.length > 0) return rows } const expose = crudExpose as any // Element Plus:当前勾选行(与列表第一列选择列一致) const baseTable = expose?.getBaseTableRef?.() if (baseTable?.getSelectionRows) { - const fromEl = normalizeSelection(baseTable.getSelectionRows()) - if (fromEl.length) return fromEl + const fromEl = baseTable.getSelectionRows() + if (Array.isArray(fromEl) && fromEl.length) return fromEl } const tableRef: any = expose?.getTableRef?.() || expose?.tableRef const selection = @@ -149,20 +158,20 @@ export const createCrudOptions = function ({ tableRef?.getSelections?.() || tableRef?.getSelected?.() || [] - return normalizeSelection(selection) - } + if (Array.isArray(selection)) return selection + return [] } /** 工具栏批量操作:取当前勾选的全部行(与列表选择列一致) */ - const pickSelectedRows = (): any[] | null => { + const pickSelectedRow = (): any | null => { const rows = getSelectedRows() if (!rows.length) { ElMessage.warning('请先选择询价单') return null } - // if (rows.length > 1) { - // ElMessage.warning('仅支持单条操作') - // return null - // } + if (rows.length > 1) { + ElMessage.warning('仅支持单条操作') + return null + } return rows[0] } @@ -208,12 +217,39 @@ export const createCrudOptions = function ({ click() { onAdd && onAdd() } - } + }, + startBargaining: { + show: true, + text: '开启比价', + order: 2, + type: 'warning', + async click() { + const row = pickSelectedRow() + if (!row) return + if (!isQuotingOrEndedStatus(row)) { + ElMessage.warning('仅报价中或报价结束状态可开启比价') + return + } + try { + await ElMessageBox.confirm('确认将状态改为【比议价中】?', '提示', { + type: 'warning', + confirmButtonText: '确定', + cancelButtonText: '取消' + }) + await api.StartBargainingObj(row.id) + onComparison && onComparison(row) + crudExpose?.doRefresh?.() + } catch (err: any) { + if (err === 'cancel' || err === 'close') return + ElMessage.error(getErrorMessage(err, '开启比价失败')) + } + } + }, } }, rowHandle: { fixed: 'right', - width: 420, + width: 500, buttons: { view: { show: false }, edit: { show: false }, @@ -321,6 +357,17 @@ export const createCrudOptions = function ({ throw err } } + }, + viewComparison: { + text: '比价', + type: compute(({ row }) => (canOpenComparison(row) ? 'primary' : 'info')), + order: 0.25, + show: true, + disabled: compute(({ row }) => !canOpenComparison(row)), + click({ row }) { + if (!canOpenComparison(row)) return + onComparison && onComparison(row) + } } } }, @@ -337,7 +384,7 @@ export const createCrudOptions = function ({ columnSetDisabled: true } }, -company_short_name: { + company_short_name: { title: '交易厂区', type: 'text', form: { show: false }, @@ -351,6 +398,25 @@ company_short_name: { } } }, + buying_method: { + title: '采购方式', + type: 'dict-select', + dict: dict({ data: buyingMethodDict }), + search: { + show: true, + component: { props: { placeholder: '采购方式', clearable: true } } + }, + form: { show: false }, + column: { + width: 100, + formatter: ({ row, value }: { row: any; value: unknown }) => { + const v = value ?? row?.buying_method + const n = Number(v) + if (!Number.isFinite(n)) return v != null && v !== '' ? String(v) : '' + return buyingMethodDict.find((d) => d.value === n)?.label ?? String(v) + } + } + }, inquiry_no: { title: '询价单号', type: 'input', @@ -361,7 +427,7 @@ company_short_name: { form: { show: false }, - column: { minWidth: 140 } + column: { minWidth: 120, showOverflowTooltip: true } }, title: { title: '询价单名称', @@ -377,13 +443,13 @@ company_short_name: { title: '询价模版', type: 'input', column: { - minWidth: 180, + minWidth: 120, showOverflowTooltip: true, formatter: ({ row, value }: { row: any; value: unknown }) => { const code = String(value ?? row?.template ?? row?.template_code ?? '').trim() const verRaw = row?.template_version ?? row?.templateVersion ?? row?.cost_template_version if (!code) return '' - if (verRaw != null && verRaw !== '') return `${code}(V${formatCostTemplateVersionTwoDigits(verRaw)})` + if (verRaw != null && verRaw !== '') return `${code}(V${formatCostTemplateVersion(verRaw)})` return code } } @@ -392,10 +458,28 @@ company_short_name: { title: '报价截止时', type: 'datetime', column: { - width: 160, + width: 150, formatter: ({ value }: { value: unknown }) => formatQuoteDeadlineDisplay(value) } }, + bid_start_time: { + title: '投标开始时间', + type: 'datetime', + column: { + width: 150, + formatter: ({ row, value }: { row: any; value: unknown }) => + Number(row?.buying_method) === 1 ? '-' : formatQuoteDeadlineDisplay(value) + } + }, + bid_end_time: { + title: '投标截止时间', + type: 'datetime', + column: { + width: 150, + formatter: ({ row, value }: { row: any; value: unknown }) => + Number(row?.buying_method) === 1 ? '-' : formatQuoteDeadlineDisplay(value) + } + }, buyer: { title: '采购负责人', type: 'input', diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/index.vue b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/index.vue index 426c452..5a620d2 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/index.vue +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/index.vue @@ -80,6 +80,38 @@ + 保存 + + +
+
+
询价单号{{ comparisonDialog.baseInfo.code || '-' }}
+
采购件料号{{ comparisonDialog.baseInfo.partNo || '-' }}
+
采购件名称{{ comparisonDialog.baseInfo.partName || '-' }}
+
目标价格{{ comparisonDialog.baseInfo.targetPrice || '-' }}
+
交易币别{{ comparisonDialog.baseInfo.currency || '-' }}
+
税率{{ comparisonDialog.baseInfo.taxRate || '-' }}
+
当前成交价{{ comparisonDialog.baseInfo.dealPrice || '-' }}
+
制程最低价{{ comparisonDialog.baseInfo.lowestProcessPrice || '-' }}
+
+ + + + + + + + + + + + + + + + + + +
+ +