m0_70156489 1 hete
szülő
commit
b107e80904
27 módosított fájl, 1361 hozzáadás és 619 törlés
  1. 500 188
      application/admin/controller/Procuremen.php
  2. 1 1
      application/admin/model/Purchasesmstemplate.php
  3. 20 5
      application/admin/view/procuremen/audit_issue.html
  4. 12 4
      application/admin/view/procuremen/details_fragment.html
  5. 33 81
      application/admin/view/procuremen/index.html
  6. 52 6
      application/admin/view/procuremen/outward_detail.html
  7. 374 0
      application/common/library/ProcuremenFormat.php
  8. 128 6
      application/common/library/ProcuremenSupplierScore.php
  9. 0 3
      application/extra/customer_monthly_score.sql
  10. 0 27
      application/extra/purchase_content_install.sql
  11. 0 3
      application/extra/purchase_order_delivery_deadline_install.sql
  12. 0 3
      application/extra/purchase_order_detail_lead_days_install.sql
  13. 0 3
      application/extra/purchase_order_detail_remark_install.sql
  14. 0 21
      application/extra/purchase_order_inbound_score_install.sql
  15. 0 3
      application/extra/purchase_order_notify_salesman_install.sql
  16. 0 11
      application/extra/purchase_order_oper_log_rename_en.sql
  17. 0 25
      application/extra/rfq_salesman_email_template_install.sql
  18. 1 1
      application/extra/supplier_score_install.sql
  19. 0 12
      application/extra/supplier_score_int_columns.sql
  20. 0 11
      application/extra/supplier_score_lead_columns.sql
  21. 0 38
      application/extra/supplier_score_rule_name_install.sql
  22. 0 28
      application/extra/supplier_service_final_score.sql
  23. 0 4
      application/extra/supplier_service_score_weight_columns.sql
  24. 92 30
      application/index/view/index/deliveryscore.html
  25. 17 3
      application/index/view/index/inboundscore.html
  26. 17 4
      application/index/view/index/rfqadd.html
  27. 114 98
      public/assets/js/backend/procuremen.js

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 500 - 188
application/admin/controller/Procuremen.php


+ 1 - 1
application/admin/model/Purchasesmstemplate.php

@@ -93,7 +93,7 @@ class Purchasesmstemplate extends Model
             ['tag' => '{delivery_deadline}', 'label' => '交货截止日期', 'example' => '2026-08-26', 'scenes' => '交货情况通知生产部'],
             ['tag' => '{category}', 'label' => '业务分类', 'example' => '出版物印刷', 'scenes' => '协助下发'],
             ['tag' => '{deadline}', 'label' => '截止时间', 'example' => '2026-05-18 14:30', 'scenes' => '协助下发/供应商询价'],
-            ['tag' => '{process_lines}', 'label' => '订单工序明细(文本)', 'example' => "订单号:202605732S\n印件名称:越剧唱腔\n1.工序名称:数字胶订 单位:册 工作量:32\n\n订单号:202605713S\n印件名称:……\n1.工序名称:数字覆膜 单位:张 工作量:545", 'scenes' => '全部'],
+            ['tag' => '{process_lines}', 'label' => '订单工序明细(文本)', 'example' => "订单号:202605732S\n印件名称:越剧唱腔\n工序名称:数字打包 单位:册 本次数量:40\n工序名称:数字平张单色 单位:P 本次数量:200\n\n订单号:202605713S\n印件名称:……\n工序名称:数字平张单色 单位:P 本次数量:100", 'scenes' => '全部'],
             ['tag' => '{process_lines_html}', 'label' => '订单工序明细(表格,多订单按单分组)', 'example' => '<table>…</table>', 'scenes' => '协助下发/供应商询价邮箱'],
             ['tag' => '{order_type}', 'label' => '订单类型', 'example' => '普通订单(价格优先)', 'scenes' => '协助下发邮箱'],
             ['tag' => '{score_weights}', 'label' => '评分比例', 'example' => '质量30%、价格50%、交货20%', 'scenes' => '协助下发邮箱'],

+ 20 - 5
application/admin/view/procuremen/audit_issue.html

@@ -10,6 +10,20 @@
         font-size: 13px;
         color: #333;
     }
+    .audit-issue-wrap .audit-batch-head {
+        margin: 0 0 8px;
+        padding: 8px 10px;
+        font-size: 13px;
+        font-weight: 600;
+        color: #333;
+        background: #f5f7fa;
+        border: 1px solid #e5e5e5;
+        border-radius: 3px;
+    }
+    .audit-issue-wrap .audit-batch-head .audit-batch-no {
+        color: #337ab7;
+        font-weight: 700;
+    }
     .audit-issue-wrap .audit-notify-tip {
         margin: 0 0 10px;
         padding: 8px 12px;
@@ -449,6 +463,10 @@
     }
 </style>
 <div class="audit-issue-wrap">
+    <div class="audit-batch-head">
+        <span class="audit-batch-no">批次号:{notempty name="orderBatchNo"}{$orderBatchNo|htmlentities}{/notempty}</span>
+    </div>
+    {if $processDisplayRows}
     <div class="audit-table-wrap">
         <table class="table table-bordered table-condensed audit-process-table">
             <colgroup>
@@ -491,6 +509,7 @@
             </tbody>
         </table>
     </div>
+    {/if}
     <form id="audit-issue-form" class="form-horizontal" role="form" style="margin-bottom:0;">
         {:token()}
         <input type="hidden" name="scydgy_id" value="{$scydgyId|htmlentities}"/>
@@ -573,10 +592,6 @@
     {if !empty($quoteVisible)}
     <p class="audit-score-rule-tip">
         <i class="fa fa-info-circle"></i>
-        {if !empty($orderScoreRuleName)}
-        <strong>订单类型:</strong>{$orderScoreRuleName|htmlentities}
-        <span class="text-muted">|</span>
-        {/if}
         <strong>评分规则:</strong>{$supplierScoreRuleText|default='(总分)=(质量得分×50%)+(价格得分);价格得分=(最低单价合计/本供应商单价合计)×50%×100'|htmlentities}
     </p>
     {/if}
@@ -590,7 +605,7 @@
                 <th class="text-center" style="width:88px;">质量得分</th>
                 <th class="text-center" style="width:80px;" title="价格得分=(最低单价合计/本供应商单价合计)×价格权重%×100">价格得分</th>
                 {if !empty($showServiceLeadScore)}
-                <th class="text-center" style="width:80px;" title="交货得分取月度评审(当月按上月起往前查,无则默认40)">交货得分</th>
+                <th class="text-center" style="width:80px;" title="交货得分=月度交货分×50%+报价交期分×50%(按订单算,一批多订单平均;报价交期满分40,提前+1/滞后-1)">交货得分</th>
                 {/if}
                 <th class="text-center" style="width:72px;">总分</th>
                 <th style="width:130px;">工序名称</th>

+ 12 - 4
application/admin/view/procuremen/details_fragment.html

@@ -132,6 +132,11 @@
         color: #222;
         margin: 0;
     }
+    .procuremen-details-wrap .procuremen-order-info-head .procuremen-batch-no {
+        font-weight: 700;
+        font-size: 14px;
+        color: #337ab7;
+    }
     .procuremen-details-wrap .procuremen-order-grid {
         width: 100%;
         border-collapse: collapse;
@@ -500,7 +505,9 @@
     {/notempty}
 
     <div class="procuremen-order-info-block">
-    <div class="procuremen-order-info-head">本单工序({$processCount|default='1'} 项)</div>
+    <div class="procuremen-order-info-head">
+        <span class="procuremen-batch-no">批次号:{notempty name="orderBatchNo"}{$orderBatchNo|htmlentities}{/notempty}</span>
+    </div>
     <div class="details-process-table-wrap">
         <table class="table table-bordered table-condensed details-process-table">
             <thead>
@@ -562,7 +569,7 @@
                 <th class="text-center" style="width:88px;">质量得分</th>
                 <th class="text-center" style="width:80px;" title="价格得分=(最低单价合计/本供应商单价合计)×价格权重%×100">价格得分</th>
                 {if !empty($showServiceLeadScore)}
-                <th class="text-center" style="width:80px;" title="交货得分取月度评审(当月按上月起往前查,无则默认40)">交货得分</th>
+                <th class="text-center" style="width:80px;" title="交货得分=月度交货分×50%+报价交期分×50%(按订单算,一批多订单平均;报价交期满分40,提前+1/滞后-1)">交货得分</th>
                 {/if}
                 <th class="text-center" style="width:72px;">总分</th>
                 <th style="width:100px;">工序名称</th>
@@ -623,12 +630,13 @@
             {if !empty($sg.has_remark)}
             <tr class="detail-supplier-remark-row{if !empty($sg.is_void)} text-muted{/if}">
                 <td class="detail-remark-label"><strong>投标备注</strong></td>
-                <td class="detail-remark-text{if !empty($sg.remark_quote_pending)} detail-quote-pending{/if}" colspan="6">{$sg.remark|default=''|htmlentities}</td>
+                <td class="detail-remark-text{if !empty($sg.remark_quote_pending)} detail-quote-pending{/if}" colspan="{if !empty($showServiceLeadScore)}12{else /}11{/if}">{$sg.remark|default=''|htmlentities}</td>
             </tr>
             {/if}
             {if !empty($sg.has_total)}
             <tr class="active detail-supplier-total-row">
-                <td><strong>总计</strong></td>
+                <td colspan="{if !empty($showServiceLeadScore)}6{else /}5{/if}"><strong>总计</strong></td>
+                <td></td>
                 <td></td>
                 <td></td>
                 <td></td>

+ 33 - 81
application/admin/view/procuremen/index.html

@@ -626,7 +626,6 @@
     #procuremen-layout .bootstrap-table .table > tbody > tr > td[data-field="CGYMC"],
     #procuremen-layout .bootstrap-table .table > tbody > tr > td[data-field="CDF"],
     #procuremen-layout .bootstrap-table .table > tbody > tr > td[data-field="cGzzxMc"],
-    #procuremen-layout .bootstrap-table .table > tbody > tr > td[data-field="MBZ"],
     #procuremen-layout .bootstrap-table .table > tbody > tr > td[data-field="notify_supplier_text"],
     #procuremen-layout .bootstrap-table .table > tbody > tr > td[data-field="picked_supplier_name"] {
         white-space: normal !important;
@@ -636,38 +635,42 @@
         line-height: 1.4 !important;
         vertical-align: middle !important;
     }
-    /* 操作列按钮不可省略裁切 */
-    #procuremen-layout .bootstrap-table thead th[data-field="operate"],
-    #procuremen-layout .bootstrap-table tbody td[data-field="operate"],
-    #procuremen-layout .bootstrap-table .fixed-columns-right td[data-field="operate"],
-    #procuremen-layout .bootstrap-table .fixed-columns-right th[data-field="operate"] {
-        overflow: visible !important;
-        text-overflow: clip !important;
+    /* 招标备注:确认/审批页用 autocontent 省略+展开,勿强制换行撑高行高 */
+    #procuremen-layout .bootstrap-table .table > tbody > tr > td[data-field="MBZ"].autocontent {
         white-space: nowrap !important;
-    }
-    /* 固定右列:外层定宽即可,内部必须是整表 + scrollLeft 到最右,禁止压成单列宽的假表 */
-    #procuremen-layout .bootstrap-table .fixed-columns-right {
-        min-width: 188px;
+        word-break: normal !important;
         overflow: hidden !important;
-        background: #fff;
+        text-overflow: ellipsis !important;
+        vertical-align: middle !important;
     }
-    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-header,
-    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-body {
-        overflow-x: hidden !important;
-        overflow-y: hidden !important; /* 与主表同步滚动,勿再出第二条竖滚动条 */
-        scrollbar-width: none;
-        -ms-overflow-style: none;
+    #procuremen-layout .bootstrap-table .table > tbody > tr > td[data-field="MBZ"].autocontent > .autocontent-item {
+        max-width: 100% !important;
     }
-    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-body::-webkit-scrollbar {
-        width: 0 !important;
-        height: 0 !important;
+    /* 操作列:sticky 吸右,横向滚动时始终可见;禁用 fixed-columns 空白遮罩 */
+    #procuremen-layout .bootstrap-table .fixed-columns-right,
+    #procuremen-layout .bootstrap-table .fixed-columns {
         display: none !important;
+        visibility: hidden !important;
+        pointer-events: none !important;
     }
-    #procuremen-layout .bootstrap-table .fixed-columns-right table {
-        /* 勿强制单列宽,否则只能看到左侧复选框/订单号 */
-        width: auto !important;
-        min-width: 100% !important;
-        max-width: none !important;
+    #procuremen-layout .bootstrap-table thead th[data-field="operate"],
+    #procuremen-layout .bootstrap-table tbody td[data-field="operate"],
+    #procuremen-layout .bootstrap-table thead th.procuremen-operate-col,
+    #procuremen-layout .bootstrap-table tbody td.procuremen-operate-col {
+        position: sticky !important;
+        right: 0 !important;
+        z-index: 6 !important;
+        background: #fff !important;
+        overflow: visible !important;
+        text-overflow: clip !important;
+        white-space: nowrap !important;
+        box-shadow: -4px 0 8px rgba(0, 0, 0, 0.04);
+        border-left: 1px solid #e8e8e8 !important;
+    }
+    #procuremen-layout .bootstrap-table thead th[data-field="operate"],
+    #procuremen-layout .bootstrap-table thead th.procuremen-operate-col {
+        z-index: 7 !important;
+        background: #f5f5f5 !important;
     }
     #procuremen-layout .bootstrap-table thead th[data-field="operate"],
     #procuremen-layout .bootstrap-table tbody td[data-field="operate"] {
@@ -680,13 +683,10 @@
         text-align: center !important;
     }
     /* 确认页(audit):操作列略窄,减少右侧空白 */
-    #procuremen-layout[data-procuremen-stage="audit"] .bootstrap-table .fixed-columns-right {
-        min-width: 168px;
-    }
     #procuremen-layout[data-procuremen-stage="audit"] .bootstrap-table thead th[data-field="operate"],
     #procuremen-layout[data-procuremen-stage="audit"] .bootstrap-table tbody td[data-field="operate"],
-    #procuremen-layout[data-procuremen-stage="audit"] .bootstrap-table .fixed-columns-right td[data-field="operate"],
-    #procuremen-layout[data-procuremen-stage="audit"] .bootstrap-table .fixed-columns-right th[data-field="operate"] {
+    #procuremen-layout[data-procuremen-stage="audit"] .bootstrap-table thead th.procuremen-operate-col,
+    #procuremen-layout[data-procuremen-stage="audit"] .bootstrap-table tbody td.procuremen-operate-col {
         width: 176px !important;
         min-width: 168px !important;
         max-width: 188px !important;
@@ -771,55 +771,7 @@
         padding: 2px 6px;
         font-size: 12px;
     }
-    /*
-     * 右固定列与主表 .fixed-table-body 为同级兄弟,主表全宽时最后一列会叠在固定层「下面」。
-     * 全局 backend.css 里 .fixed-columns-right 仅 z-index:2,易被主表内层盖住,表现为要点多次才打开弹窗。
-     * 抬高固定列整体叠层,并把主表滚动区压在 z-index:0,保证命中的是固定层上的按钮。
-     */
-    #procuremen-layout .bootstrap-table .fixed-table-container > .fixed-table-body {
-        position: relative;
-        z-index: 0;
-    }
-    #procuremen-layout .bootstrap-table .fixed-columns-right {
-        box-shadow: none !important;
-        border-left: 1px solid #e3e6ea;
-        background-color: #fff;
-        z-index: 18 !important;
-        /* 盖住主表横向滚动条伸到操作列下方的那段,避免「操作按钮下面还能滑」 */
-        height: 100% !important;
-        bottom: 0 !important;
-    }
-    #procuremen-layout .bootstrap-table .fixed-columns-right::after {
-        content: '';
-        position: absolute;
-        left: 0;
-        right: 0;
-        bottom: 0;
-        height: 17px;
-        background: #fff;
-        pointer-events: none;
-        z-index: 1;
-    }
-    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-header,
-    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-body {
-        background: #fff;
-    }
-    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-header table thead th,
-    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-body table tbody td {
-        border-left-color: transparent;
-    }
-    /* 操作列按钮在固定层内置于可点区域之上,减少与主表透明重叠导致的「多点一次」 */
-    /* 固定列操作按钮:抬高层级 + 明确可点,减轻与主表空白区域叠层的「点了没反应」 */
-    #procuremen-layout .bootstrap-table .fixed-columns-right .fixed-table-body {
-        pointer-events: auto;
-    }
-    #procuremen-layout .bootstrap-table .fixed-columns-right a.procuremen-op-open {
-        position: relative;
-        z-index: 6;
-        pointer-events: auto;
-        display: inline-block;
-        vertical-align: middle;
-    }
+    /* fixed-columns 已停用(改 sticky),残留样式不再需要 */
     #procuremen-layout .procuremen-op-btns .btn + .btn {
         margin-left: 6px;
     }

+ 52 - 6
application/admin/view/procuremen/outward_detail.html

@@ -14,6 +14,20 @@
     .outward-detail-head strong {
         color: #333;
     }
+    .outward-batch-head {
+        margin: 0 0 8px;
+        padding: 8px 10px;
+        font-size: 13px;
+        font-weight: 600;
+        color: #333;
+        background: #f5f7fa;
+        border: 1px solid #e5e5e5;
+        border-radius: 3px;
+    }
+    .outward-batch-head .outward-batch-no {
+        color: #337ab7;
+        font-weight: 700;
+    }
     .outward-detail-table-wrap,
     .outward-confirm-process-wrap {
         /* 不在表格区域内部纵向滚动,由弹窗页面整体滚动 */
@@ -219,6 +233,30 @@
         vertical-align: middle;
         white-space: nowrap;
     }
+    .outward-bid-open-tag {
+        flex-shrink: 0;
+        display: inline-block;
+        height: 32px;
+        line-height: 32px;
+        padding: 0;
+        border: 0;
+        border-radius: 0;
+        background: transparent;
+        font-size: 15px;
+        font-weight: 600;
+        white-space: nowrap;
+    }
+    .outward-bid-open-tag.is-ok {
+        color: #3c763d;
+    }
+    .outward-bid-open-tag.is-wait {
+        color: #8a6d3b;
+    }
+    .outward-bid-open-tag .fa {
+        font-size: 16px;
+        margin-right: 4px;
+        vertical-align: -1px;
+    }
     .outward-manual-pick-inline {
         display: flex;
         flex-direction: column;
@@ -402,6 +440,9 @@
 
     {if $showPurchaseConfirm}
 
+    <div class="outward-batch-head">
+        <span class="outward-batch-no">批次号:{notempty name="orderBatchNo"}{$orderBatchNo|htmlentities}{/notempty}</span>
+    </div>
     {if $processDisplayRows}
     <div class="outward-confirm-process-wrap">
         <table class="table table-bordered table-condensed outward-confirm-process-table">
@@ -472,9 +513,18 @@
             <span class="outward-confirm-order-type-text" title="{$supplierScoreRuleText|default=''|htmlentities}">{$orderScoreRuleName|htmlentities}</span>
         </div>
         {/if}
+        <div class="outward-confirm-deadline-item outward-bid-open-status-item">
+            <label class="deadline-label">开标状态:</label>
+            {if !empty($bidOpenVerified)}
+            <span class="outward-bid-open-tag is-ok"><i class="fa fa-check-circle"></i> 已开标</span>
+            {elseif !empty($manualPicked) /}
+            <span class="outward-bid-open-tag is-ok"><i class="fa fa-check-circle"></i> 已指定供应商</span>
+            {else /}
+            <span class="outward-bid-open-tag is-wait"><i class="fa fa-clock-o"></i> 待开标</span>
+            {/if}
+        </div>
         {if condition="$manualPicked"}
         <div class="outward-confirm-deadline-item outward-manual-pick-item">
-            <span class="outward-manual-pick-tag"><i class="fa fa-check-circle"></i> 已指定供应商</span>
             <span class="outward-manual-pick-inline">
                 {if condition="$pickedCompanyName"}
                 <span class="outward-manual-pick-line"><strong>指定供应商:</strong>{$pickedCompanyName|htmlentities}</span>
@@ -494,10 +544,6 @@
     {if !empty($quoteVisible)}
     <p class="audit-score-rule-tip">
         <i class="fa fa-info-circle"></i>
-        {if !empty($orderScoreRuleName)}
-        <strong>订单类型:</strong>{$orderScoreRuleName|htmlentities}
-        <span class="text-muted">|</span>
-        {/if}
         <strong>评分规则:</strong>{$supplierScoreRuleText|default='(总分)=(质量得分×50%)+(价格得分);价格得分=(最低单价合计/本供应商单价合计)×50%×100'|htmlentities}
     </p>
     {/if}
@@ -512,7 +558,7 @@
                 <th class="text-center" style="width:88px;">质量得分</th>
                 <th class="text-center" style="width:80px;" title="价格得分=(最低单价合计/本供应商单价合计)×价格权重%×100">价格得分</th>
                 {if !empty($showServiceLeadScore)}
-                <th class="text-center" style="width:80px;" title="交货得分取月度评审(当月按上月起往前查,无则默认40)">交货得分</th>
+                <th class="text-center" style="width:80px;" title="交货得分=月度交货分×50%+报价交期分×50%(按订单算,一批多订单平均;报价交期满分40,提前+1/滞后-1)">交货得分</th>
                 {/if}
                 <th class="text-center" style="width:72px;">总分</th>
                 <th style="width:120px;">工序名称</th>

+ 374 - 0
application/common/library/ProcuremenFormat.php

@@ -0,0 +1,374 @@
+<?php
+
+namespace app\common\library;
+
+/**
+ * 协助采购:纯展示/解析格式化(无请求、无写库副作用)
+ * 从 Procuremen 控制器抽出,便于复用与减负;行为须与原 protected 方法一致。
+ */
+class ProcuremenFormat
+{
+    /**
+     * 工序行主键:ERP 为正 scydgy.ID;手工新增为负 purchase_order.scydgy_id
+     */
+    public static function extractScydgyRowId(array $row): int
+    {
+        if (array_key_exists('scydgy_id', $row) && $row['scydgy_id'] !== null && $row['scydgy_id'] !== '') {
+            return (int)$row['scydgy_id'];
+        }
+        if (array_key_exists('SCYDGY_ID', $row) && $row['SCYDGY_ID'] !== null && $row['SCYDGY_ID'] !== '') {
+            return (int)$row['SCYDGY_ID'];
+        }
+
+        return (int)($row['ID'] ?? $row['id'] ?? 0);
+    }
+
+    public static function rowLabel(array $row): string
+    {
+        $dh = trim((string)($row['CCYDH'] ?? ''));
+        $gymc = trim((string)($row['CGYMC'] ?? ''));
+        if ($dh !== '' && $gymc !== '') {
+            return '订单「' . $dh . '」、工序「' . $gymc . '」';
+        }
+        if ($dh !== '') {
+            return '订单「' . $dh . '」';
+        }
+        if ($gymc !== '') {
+            return '工序「' . $gymc . '」';
+        }
+        $id = self::extractScydgyRowId($row);
+
+        return $id !== 0 ? ('工序编号 ' . $id) : '所选工序';
+    }
+
+    /** 主表/明细时间字段统一为可读字符串 */
+    public static function detailTime($v): string
+    {
+        if ($v === null || $v === '') {
+            return '';
+        }
+        if (is_numeric($v) && (int)$v > 946684800) {
+            return date('Y-m-d H:i:s', (int)$v);
+        }
+
+        return ProcuremenTime::formatDisplayDateTime($v);
+    }
+
+    /** 交货日期仅展示 YYYY-MM-DD */
+    public static function deliveryYmd($v): string
+    {
+        if ($v === null || $v === '') {
+            return '';
+        }
+        $s = trim((string)$v);
+        if ($s === '' || preg_match('/^0000-00-00/i', $s)) {
+            return '';
+        }
+        if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) {
+            return $m[1];
+        }
+        $ts = strtotime($s);
+
+        return ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : $s;
+    }
+
+    /** 解析金额/数量(去千分位等非数字字符) */
+    public static function moneyNumber($value): ?float
+    {
+        if ($value === null || $value === '') {
+            return null;
+        }
+        if (is_numeric($value)) {
+            return (float)$value;
+        }
+        $v = preg_replace('/[^\d\.\-]/', '', (string)$value);
+
+        return ($v !== '' && is_numeric($v)) ? (float)$v : null;
+    }
+
+    public static function moneyDisplay(?float $amount): string
+    {
+        if ($amount === null) {
+            return '';
+        }
+        $s = rtrim(rtrim(sprintf('%.5F', (float)$amount), '0'), '.');
+
+        return $s === '' ? '0' : $s;
+    }
+
+    /** 工作量为 0 时显示空 */
+    public static function workloadDisplay($value): string
+    {
+        if ($value === null || $value === '') {
+            return '';
+        }
+        $s = trim((string)$value);
+        if ($s === '') {
+            return '';
+        }
+        if (is_numeric($s) && (float)$s == 0) {
+            return '';
+        }
+
+        return $s;
+    }
+
+    /**
+     * @param string[] $names
+     */
+    public static function notifySupplierDisplay(array $names): string
+    {
+        $names = array_values(array_unique(array_filter(array_map(function ($n) {
+            return trim((string)$n);
+        }, $names))));
+        if ($names === []) {
+            return '';
+        }
+        sort($names, SORT_STRING);
+
+        return implode("\n", $names);
+    }
+
+    /**
+     * @param array{all?: array<string, bool>, quoted?: array<string, bool>} $bucket
+     */
+    public static function quotedSupplierLines(array $bucket): string
+    {
+        $all = array_keys($bucket['all'] ?? []);
+        if ($all === []) {
+            return '';
+        }
+        sort($all, SORT_STRING);
+        $lines = [];
+        foreach ($all as $cn) {
+            $status = !empty($bucket['quoted'][$cn]) ? '已报价' : '未报价';
+            $lines[] = $cn . '(' . $status . ')';
+        }
+
+        return implode("\n", $lines);
+    }
+
+    /** @param int|null|string $days */
+    public static function leadDaysText($days): string
+    {
+        if ($days === null || $days === '') {
+            return '未填写';
+        }
+        if (!is_numeric($days)) {
+            return '未填写';
+        }
+
+        return ((int)$days) . '天';
+    }
+
+    public static function deliveryDeadlineForInput($value): string
+    {
+        $s = trim((string)$value);
+        if ($s === '' || stripos($s, '0000-00-00') === 0) {
+            return '';
+        }
+        if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) {
+            return $m[1];
+        }
+        $ts = strtotime(str_replace('/', '-', $s));
+
+        return ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : '';
+    }
+
+    public static function sysRqForInput($sysRq): string
+    {
+        $sr = trim((string)$sysRq);
+        if ($sr === '' || stripos($sr, '0000-00-00') === 0) {
+            return '';
+        }
+        $ts = strtotime($sr);
+
+        return ($ts !== false && $ts > 0) ? date('Y-m-d H:i:s', $ts) : $sr;
+    }
+
+    public static function sysRqForDisplay($sysRq): string
+    {
+        $sr = self::sysRqForInput($sysRq);
+        if ($sr === '') {
+            return '—';
+        }
+        $ts = strtotime($sr);
+
+        return ($ts !== false && $ts > 0) ? date('Y/m/d H:i', $ts) : $sr;
+    }
+
+    /**
+     * @param array{quality_weight?:mixed,price_weight?:mixed,lead_weight?:mixed} $rule
+     */
+    public static function scoreRuleWeightsText(array $rule): string
+    {
+        $parts = [];
+        $qw = max(0, (float)($rule['quality_weight'] ?? 0));
+        $pw = max(0, (float)($rule['price_weight'] ?? 0));
+        $lw = max(0, (float)($rule['lead_weight'] ?? 0));
+        $fmt = static function ($n) {
+            $s = rtrim(rtrim(sprintf('%.2F', (float)$n), '0'), '.');
+
+            return $s === '' ? '0' : $s;
+        };
+        if ($qw > 0) {
+            $parts[] = '质量' . $fmt($qw) . '%';
+        }
+        if ($pw > 0) {
+            $parts[] = '价格' . $fmt($pw) . '%';
+        }
+        if ($lw > 0) {
+            $parts[] = '交货' . $fmt($lw) . '%';
+        }
+
+        return implode('、', $parts);
+    }
+
+    public static function mprocYmdForNotify($raw): string
+    {
+        $s = trim((string)$raw);
+        if ($s === '' || preg_match('/^0000-00-00/i', $s)) {
+            return '';
+        }
+        $s = str_replace(['/', '.'], '-', $s);
+        if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) {
+            return $m[1];
+        }
+        $ts = strtotime(str_replace('T', ' ', $s));
+        if ($ts === false || $ts <= 0) {
+            return '';
+        }
+
+        return date('Y-m-d', $ts);
+    }
+
+    /** 导出用:工序列文案 */
+    public static function exportGxText(array $r): string
+    {
+        $a = trim((string)($r['CDXMC'] ?? ''));
+        $b = trim((string)($r['CGYMC'] ?? ''));
+        if ($a !== '' && $b !== '') {
+            return $a . ':' . $b;
+        }
+
+        return $a !== '' ? $a : $b;
+    }
+
+    /** 导出用:加工金额 */
+    public static function exportAmount(array $r): float
+    {
+        foreach (['amount', 'jje', 'jgje', 'processing_amount'] as $k) {
+            if (!array_key_exists($k, $r)) {
+                continue;
+            }
+            $v = $r[$k];
+            if ($v === null || $v === '') {
+                continue;
+            }
+            if (is_numeric($v)) {
+                return (float)$v;
+            }
+            $v = preg_replace('/[^\d\.\-]/', '', (string)$v);
+            if ($v !== '' && is_numeric($v)) {
+                return (float)$v;
+            }
+        }
+
+        return 0.0;
+    }
+
+    /** 订单号等用于 PDF 文件名片段 */
+    public static function sanitizePdfOrderKey(string $ccydh): string
+    {
+        $s = trim($ccydh);
+        if ($s === '') {
+            return 'ORDER';
+        }
+        $s = preg_replace('@[\\\\/:*?"<>|\\s]+@u', '_', $s);
+        $s = trim($s, '._-');
+        if ($s === '') {
+            return 'ORDER';
+        }
+        if (function_exists('mb_substr')) {
+            return mb_substr($s, 0, 80, 'UTF-8');
+        }
+
+        return strlen($s) <= 80 ? $s : substr($s, 0, 80);
+    }
+
+    /**
+     * @return array{objectKey: string, webPath: string}
+     */
+    public static function buildConfirmPdfPaths(int $scydgyId, string $ccydhRaw): array
+    {
+        $sid = (int)$scydgyId;
+        $safeOrder = self::sanitizePdfOrderKey($ccydhRaw);
+        $y = date('Y');
+        $m = date('m');
+        $d = date('d');
+        $basename = $safeOrder . '_' . $sid . '.pdf';
+        $objectKey = 'xinhua/' . $y . '/' . $m . '/' . $d . '/' . $sid . '/' . $basename;
+
+        return [
+            'objectKey' => $objectKey,
+            'webPath'   => '/' . $objectKey,
+        ];
+    }
+
+    /**
+     * @param array<string, string> $vars
+     * @return array<string, string>
+     */
+    public static function normalizeSmsTemplateVars(array $vars): array
+    {
+        $out = [];
+        foreach ($vars as $k => $v) {
+            $out[(string)$k] = trim((string)$v);
+        }
+        if (($out['contact_name'] ?? '') === '' && ($out['company_name'] ?? '') !== '') {
+            $out['contact_name'] = $out['company_name'];
+        }
+
+        return $out;
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    public static function emailLogSceneLabelMap(): array
+    {
+        return [
+            'pick_issue'      => '初选下发',
+            'audit_resend'    => '确认页重发',
+            'audit_append'    => '确认页追加供应商',
+            'rfq_salesman'    => '询价通知业务员',
+            'rfq_supplier'    => '供应商询价通知',
+            'delivery_notify' => '交货情况通知生产部',
+            'confirm_result'  => '确认结果通知',
+            'confirm_ok'      => '确认已通过通知',
+            'confirm_fail'    => '确认未通过通知',
+        ];
+    }
+
+    public static function normalizeEmailLogSceneForStore(string $scene): string
+    {
+        $scene = trim($scene);
+        $map = self::emailLogSceneLabelMap();
+        if ($scene === '') {
+            return $map['pick_issue'];
+        }
+        if (isset($map[$scene])) {
+            return $map[$scene];
+        }
+        if (in_array($scene, array_values($map), true)) {
+            return $scene;
+        }
+
+        return $scene;
+    }
+
+    public static function emailLogSceneText(string $scene): string
+    {
+        return self::normalizeEmailLogSceneForStore($scene);
+    }
+}

+ 128 - 6
application/common/library/ProcuremenSupplierScore.php

@@ -13,7 +13,9 @@ use think\Log;
  * - supplier_service_final_score:月度评审表(四项自动统计,满分 105;qp_manual=1 手工锁定不覆盖)
  *
  * 开标单笔总分 = 质量得分×质量% + 价格得分 [+ 交货得分×交货%]
- * 开标质量/交货得分:均取月度评审表(当月按上月起往前查,无则默认 40)
+ * 开标质量得分:取月度评审表(当月按上月起往前查,无则默认 40)
+ * 开标交货得分:月度交货分×50% + 本单报价交期分×50%(按订单算,一批多订单取平均)
+ *   报价交期分:满分40,相对订单交货截止日每提前1天+1、滞后1天-1
  * 开标价格得分 = (本单最低单价合计 / 本供应商单价合计) × 价格权重% × 100
  *
  * 月度评审(与开标单笔分离):
@@ -2439,6 +2441,117 @@ class ProcuremenSupplierScore
         return self::loadReviewFieldScoreMap($companyNames, 'delivery_score', $asOfYm);
     }
 
+    /**
+     * 日期规范为 Y-m-d;无效/"未填写" 返回空串
+     */
+    public static function normalizeScoreYmd($value): string
+    {
+        $s = trim((string)$value);
+        if ($s === '' || $s === '未填写' || stripos($s, '0000-00-00') === 0) {
+            return '';
+        }
+        if (preg_match('/开标|验证后|查看/u', $s)) {
+            return '';
+        }
+        if (preg_match('/^(\d{4})[\/\-.](\d{1,2})[\/\-.](\d{1,2})/', $s, $m)) {
+            return sprintf('%04d-%02d-%02d', (int)$m[1], (int)$m[2], (int)$m[3]);
+        }
+        $ts = strtotime(str_replace('/', '-', $s));
+
+        return ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : '';
+    }
+
+    /**
+     * 本单报价交期分:满分 40;相对截止日每提前1天+1、滞后1天-1;未填报价交货日或无截止日=0
+     */
+    public static function calcQuoteDeliveryPartScore(string $deadlineYmd, string $quoteYmd): float
+    {
+        $deadlineYmd = self::normalizeScoreYmd($deadlineYmd);
+        $quoteYmd = self::normalizeScoreYmd($quoteYmd);
+        if ($quoteYmd === '' || $deadlineYmd === '') {
+            return 0.0;
+        }
+        $d1 = strtotime($deadlineYmd . ' 00:00:00');
+        $d2 = strtotime($quoteYmd . ' 00:00:00');
+        if ($d1 === false || $d2 === false) {
+            return 0.0;
+        }
+        $daysEarly = (int)round(($d1 - $d2) / 86400);
+
+        return (float)(40 + $daysEarly);
+    }
+
+    /**
+     * 订单交货分 = 月度×50% + 报价交期分×50%
+     */
+    public static function calcOrderDeliveryScore(float $monthlyScore, float $quotePartScore): float
+    {
+        return round($monthlyScore * 0.5 + $quotePartScore * 0.5, 2);
+    }
+
+    /**
+     * 从报价行按订单汇总交货得分,一批多订单取平均
+     *
+     * @param array<int, array<string, mixed>> $lineRows
+     */
+    public static function calcCompanyDeliveryScoreFromQuoteLines(array $lineRows, float $monthlyScore): float
+    {
+        $byOrder = [];
+        foreach ($lineRows as $ln) {
+            if (!is_array($ln)) {
+                continue;
+            }
+            $dh = trim((string)($ln['ccydh'] ?? $ln['CCYDH'] ?? ''));
+            if ($dh === '') {
+                $dh = '_';
+            }
+            if (!isset($byOrder[$dh])) {
+                $byOrder[$dh] = ['deadline' => '', 'quote' => ''];
+            }
+            $deadline = self::normalizeScoreYmd(
+                $ln['order_delivery_deadline'] ?? $ln['delivery_deadline'] ?? ''
+            );
+            if ($deadline !== '' && $byOrder[$dh]['deadline'] === '') {
+                $byOrder[$dh]['deadline'] = $deadline;
+            }
+            // 开标前遮罩行不计入报价交期
+            if (!empty($ln['delivery_quote_pending']) || !empty($ln['lead_days_quote_pending'])) {
+                continue;
+            }
+            if ($byOrder[$dh]['quote'] !== '') {
+                continue;
+            }
+            $quote = '';
+            foreach (['delivery_ymd', 'delivery_show', 'delivery_text', 'delivery'] as $k) {
+                if (!array_key_exists($k, $ln) || $ln[$k] === null || $ln[$k] === '') {
+                    continue;
+                }
+                $quote = self::normalizeScoreYmd($ln[$k]);
+                if ($quote !== '') {
+                    break;
+                }
+            }
+            if ($quote !== '') {
+                $byOrder[$dh]['quote'] = $quote;
+            }
+        }
+        if ($byOrder === []) {
+            return self::calcOrderDeliveryScore($monthlyScore, 0.0);
+        }
+        $sum = 0.0;
+        $n = 0;
+        foreach ($byOrder as $info) {
+            $quotePart = self::calcQuoteDeliveryPartScore(
+                (string)($info['deadline'] ?? ''),
+                (string)($info['quote'] ?? '')
+            );
+            $sum += self::calcOrderDeliveryScore($monthlyScore, $quotePart);
+            $n++;
+        }
+
+        return $n > 0 ? round($sum / $n, 2) : self::calcOrderDeliveryScore($monthlyScore, 0.0);
+    }
+
     /**
      * 按公司名批量取供应商 id(兼容旧调用保留评分字段结构)
      *
@@ -2570,9 +2683,13 @@ class ProcuremenSupplierScore
                     }
                 }
             }
-            // 质量/交货得分:月度评审表,当月按上月起往前,默认 40
-            $quality = (float)($reviewMap[$cn] ?? 50);
-            $deliveryScore = (float)($deliveryMap[$cn] ?? 50);
+            // 质量得分:月度评审表,当月按上月起往前,默认 40
+            $quality = (float)($reviewMap[$cn] ?? 40);
+            $monthlyDelivery = (float)($deliveryMap[$cn] ?? 40);
+            // 交货得分:月度50% + 本单报价交期50%(按订单算,一批多订单平均)
+            $deliveryScore = $leadEnabled
+                ? self::calcCompanyDeliveryScoreFromQuoteLines($lineRows, $monthlyDelivery)
+                : 0.0;
             $psum = $hasPrice ? round($priceSum, 2) : 0;
             $lsum = $hasLead ? (int)$leadSum : 0;
             $items[$cn] = [
@@ -2631,7 +2748,7 @@ class ProcuremenSupplierScore
             }
             $it['price_score_text'] = self::formatScore($it['price_score']);
 
-            // 交货得分已在上方取自月度评审(与质量得分同口径)
+            // 交货得分已在上方按「月度50%+报价交期50%」算好
             if (!$leadEnabled) {
                 $it['lead_score'] = 0.0;
                 $it['lead_score_text'] = '0';
@@ -2767,11 +2884,16 @@ class ProcuremenSupplierScore
         $formula = $parts === []
             ? '(总分)=(质量得分×50%)+(价格得分)'
             : '(总分)=' . implode('+', $parts);
-        $formula .= ';质量得分、交货得分取月度评审(当月按上月起往前查,无则默认40)';
+        $formula .= ';质量得分取月度评审(当月按上月起往前查,无则默认40)';
+        // 说明顺序与表头一致:价格在前,交货在后
         if ($pw > 0) {
             $formula .= ';价格得分=(最低单价合计/本供应商单价合计)×'
                 . self::formatWeightPercent($pw) . '%×100';
         }
+        if ($lw > 0) {
+            $formula .= ';交货得分=月度交货分×50%+报价交期分×50%(按订单算,一批多订单平均;'
+                . '报价交期满分40,相对截止日每提前1天+1、滞后1天-1)';
+        }
 
         return $formula;
     }

+ 0 - 3
application/extra/customer_monthly_score.sql

@@ -1,3 +0,0 @@
--- 供应商(customer)增加月度评分
-ALTER TABLE `customer`
-  ADD COLUMN `monthly_score` int DEFAULT NULL COMMENT '月度评分' AFTER `score`;

+ 0 - 27
application/extra/purchase_content_install.sql

@@ -1,27 +0,0 @@
--- 协助采购 — 通知公告(执行一次;也可由后台首次访问自动建表)
--- 菜单需在「权限管理」中手动添加,或访问 purchasecontent/install(超级管理员)
-
-CREATE TABLE IF NOT EXISTS `purchase_content` (
-  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
-  `creator_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '创建人',
-  `sender_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '投递人',
-  `subject` varchar(255) NOT NULL DEFAULT '' COMMENT '主题',
-  `content` mediumtext COMMENT '内容',
-  `createtime` datetime DEFAULT NULL COMMENT '投递时间',
-  `updatetime` datetime DEFAULT NULL COMMENT '更新时间',
-  PRIMARY KEY (`id`),
-  KEY `idx_creator` (`creator_id`),
-  KEY `idx_sender` (`sender_id`),
-  KEY `idx_ct` (`createtime`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='协助采购通知公告';
-
-CREATE TABLE IF NOT EXISTS `purchase_content_recipient` (
-  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
-  `content_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '公告ID',
-  `admin_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '接收人',
-  `read_time` datetime DEFAULT NULL COMMENT '首次阅读时间',
-  `createtime` datetime DEFAULT NULL COMMENT '创建时间',
-  PRIMARY KEY (`id`),
-  UNIQUE KEY `uk_content_admin` (`content_id`,`admin_id`),
-  KEY `idx_admin` (`admin_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知公告接收人';

+ 0 - 3
application/extra/purchase_order_delivery_deadline_install.sql

@@ -1,3 +0,0 @@
--- 采购订单交货截止日期(执行一次即可)
-ALTER TABLE `purchase_order`
-  ADD COLUMN `delivery_deadline` date DEFAULT NULL COMMENT '交货截止日期' AFTER `sys_rq`;

+ 0 - 3
application/extra/purchase_order_detail_lead_days_install.sql

@@ -1,3 +0,0 @@
--- 供应商报价预估工期(天),执行一次即可
-ALTER TABLE `purchase_order_detail`
-  ADD COLUMN `lead_days` int(10) unsigned DEFAULT NULL COMMENT '预估工期(天)' AFTER `delivery`;

+ 0 - 3
application/extra/purchase_order_detail_remark_install.sql

@@ -1,3 +0,0 @@
--- 手机端供应商整单备注(执行一次)
-ALTER TABLE `purchase_order_detail`
-  ADD COLUMN `remark` varchar(500) DEFAULT NULL COMMENT '供应商整单备注' AFTER `status_name`;

+ 0 - 21
application/extra/purchase_order_inbound_score_install.sql

@@ -1,21 +0,0 @@
--- 外协入库评分(手机端管理员填写合格/不合格)
-CREATE TABLE IF NOT EXISTS `purchase_order_inbound_score` (
-  `id` int unsigned NOT NULL AUTO_INCREMENT,
-  `scydgy_id` int NOT NULL DEFAULT 0 COMMENT '工序ID',
-  `purchase_order_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'purchase_order.id',
-  `ccydh` varchar(64) NOT NULL DEFAULT '' COMMENT '订单号',
-  `cyjmc` varchar(255) NOT NULL DEFAULT '' COMMENT '印件名称',
-  `cgymc` varchar(255) NOT NULL DEFAULT '' COMMENT '工序名称',
-  `company_name` varchar(255) NOT NULL DEFAULT '' COMMENT '中标供应商',
-  `result` varchar(16) NOT NULL DEFAULT '' COMMENT '合格/不合格',
-  `admin_id` int unsigned NOT NULL DEFAULT 0,
-  `admin_name` varchar(64) NOT NULL DEFAULT '',
-  `remark` varchar(500) NOT NULL DEFAULT '',
-  `createtime` datetime DEFAULT NULL,
-  `updatetime` datetime DEFAULT NULL,
-  PRIMARY KEY (`id`),
-  UNIQUE KEY `uk_scydgy_id` (`scydgy_id`),
-  KEY `idx_ccydh` (`ccydh`),
-  KEY `idx_result` (`result`),
-  KEY `idx_updatetime` (`updatetime`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='外协入库评分';

+ 0 - 3
application/extra/purchase_order_notify_salesman_install.sql

@@ -1,3 +0,0 @@
--- 手工询价:通知业务员(可多选,顿号拼接)
-ALTER TABLE `purchase_order`
-  ADD COLUMN `notify_salesman` varchar(255) NOT NULL DEFAULT '' COMMENT '通知业务员(可多选)' AFTER `cywyxm`;

+ 0 - 11
application/extra/purchase_order_oper_log_rename_en.sql

@@ -1,11 +0,0 @@
--- purchase_order_oper_log:字段名用英文,注释/内容用中文
--- 若库中已是中文列名,可执行本脚本改回英文(数据不丢)
-
-ALTER TABLE `purchase_order_oper_log`
-  CHANGE COLUMN `工序ID` `scydgy_id` int NOT NULL COMMENT '工序行 ID(scydgy.ID)',
-  CHANGE COLUMN `采购订单ID` `purchase_order_id` int unsigned NULL DEFAULT NULL COMMENT '采购订单 ID(purchase_order.id)',
-  CHANGE COLUMN `操作人ID` `admin_id` int unsigned NOT NULL DEFAULT 0 COMMENT '操作人 ID(admin.id)',
-  CHANGE COLUMN `操作人` `admin_name` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人姓名/账号快照',
-  CHANGE COLUMN `操作类型` `action` varchar(64) NOT NULL DEFAULT '' COMMENT '操作类型(中文)',
-  CHANGE COLUMN `操作内容` `content` varchar(1000) NOT NULL DEFAULT '' COMMENT '操作说明(详情页展示)',
-  CHANGE COLUMN `创建时间` `createtime` varchar(19) NOT NULL DEFAULT '' COMMENT '操作时间';

+ 0 - 25
application/extra/rfq_salesman_email_template_install.sql

@@ -1,25 +0,0 @@
--- 查询询价:通知业务员邮箱模版(scene=rfq_salesman_email)
--- 变量:{contact_name}/{salesman_name}/{cyjmc}/{cgymc}/{this_quantity}/{supplier_name}/{cywyxm}/{email}
-INSERT INTO `purchase_sms_template` (`scene`, `title`, `content`, `remark`, `status`, `createtime`, `updatetime`)
-SELECT
-  'rfq_salesman_email',
-  '询价邮箱通知',
-  '您好,{contact_name}:\n\n有新的协助采购询价需求,请知悉。\n印件名称:{cyjmc}\n工序名称:{cgymc}\n本次数量:{this_quantity}\n供应商:{supplier_name}\n需求发起人:{cywyxm}\n',
-  '查询询价-通知业务员邮箱;模版名称作邮件主题',
-  1,
-  NOW(),
-  NOW()
-FROM DUAL
-WHERE NOT EXISTS (
-  SELECT 1 FROM `purchase_sms_template` WHERE `scene` = 'rfq_salesman_email' LIMIT 1
-);
-
--- 已有旧模版时:去掉需求编号,补上供应商
-UPDATE `purchase_sms_template`
-SET `content` = '您好,{contact_name}:\n\n有新的协助采购询价需求,请知悉。\n印件名称:{cyjmc}\n工序名称:{cgymc}\n本次数量:{this_quantity}\n供应商:{supplier_name}\n需求发起人:{cywyxm}\n',
-    `updatetime` = NOW()
-WHERE `scene` = 'rfq_salesman_email'
-  AND (
-    `content` LIKE '%需求编号:{ccydh}%'
-    OR (`content` NOT LIKE '%{supplier_name}%' AND `content` NOT LIKE '%供应商:%')
-  );

+ 1 - 1
application/extra/supplier_score_install.sql

@@ -42,4 +42,4 @@ CREATE TABLE IF NOT EXISTS `supplier_service_score` (
   KEY `idx_ym` (`ym`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商服务评分订单明细';
 
--- 月度汇总见 supplier_service_final_score.sql(score=月度总分,final_score=最终得分)
+-- 月度汇总表 supplier_service_final_score 由 ProcuremenSupplierScore::ensureFinalScoreTable() 自动创建

+ 0 - 12
application/extra/supplier_score_int_columns.sql

@@ -1,12 +0,0 @@
--- 评分相关字段改为整数(去掉 .00)
--- 规则表
-ALTER TABLE `supplier_score_rule`
-  MODIFY COLUMN `quality_weight` int unsigned NOT NULL DEFAULT 50 COMMENT '上年度质量评分权重(%)',
-  MODIFY COLUMN `price_weight` int unsigned NOT NULL DEFAULT 50 COMMENT '单价评分权重(%)';
-
--- 服务评分表(总分/单价合计/单价评分值保留小数)
-ALTER TABLE `supplier_service_score`
-  MODIFY COLUMN `quality_score` int NOT NULL DEFAULT 0 COMMENT '上年度质量评分',
-  MODIFY COLUMN `quality_weight` int unsigned NOT NULL DEFAULT 50 COMMENT '质量百分比',
-  MODIFY COLUMN `price_sum` decimal(12,4) NOT NULL DEFAULT 0.0000 COMMENT '本单工序单价合计',
-  MODIFY COLUMN `price_weight` int unsigned NOT NULL DEFAULT 50 COMMENT '价格百分比';

+ 0 - 11
application/extra/supplier_score_lead_columns.sql

@@ -1,11 +0,0 @@
--- 评分规则:交货期权重 + 是否纳入总分
-ALTER TABLE `supplier_score_rule`
-  ADD COLUMN `lead_weight` int unsigned NOT NULL DEFAULT 0 COMMENT '交货期得分权重(%)' AFTER `price_weight`,
-  ADD COLUMN `lead_enabled` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '1=交货期纳入总分' AFTER `lead_weight`;
-
--- 服务评分落库留痕
-ALTER TABLE `supplier_service_score`
-  ADD COLUMN `lead_days_sum` int NOT NULL DEFAULT 0 COMMENT '本单工序工期合计(天)' AFTER `price_score`,
-  ADD COLUMN `lead_weight` int unsigned NOT NULL DEFAULT 0 COMMENT '交货期百分比' AFTER `lead_days_sum`,
-  ADD COLUMN `lead_enabled` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '1=当时纳入总分' AFTER `lead_weight`,
-  ADD COLUMN `lead_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '交货期评分值' AFTER `lead_enabled`;

+ 0 - 38
application/extra/supplier_score_rule_name_install.sql

@@ -1,38 +0,0 @@
--- 评分规则表补「规则名称/订单类型」并写入三条权重方案
--- 可在 Navicat 中直接执行
-
--- 1) 加 name 字段(若已存在会报 Duplicate column,忽略后继续)
-ALTER TABLE `supplier_score_rule`
-  ADD COLUMN `name` varchar(100) NOT NULL DEFAULT '' COMMENT '规则名称/订单类型' AFTER `id`;
-
--- 2) 对齐/写入三条方案
-UPDATE `supplier_score_rule`
-SET `name`='普通订单(价格优先)', `quality_weight`=30, `price_weight`=50, `lead_weight`=20, `lead_enabled`=1, `status`='normal', `updatetime`=NOW()
-WHERE `id`=1
-   OR (`quality_weight`=30 AND `price_weight`=50 AND `lead_weight`=20);
-
-INSERT INTO `supplier_score_rule`
-(`name`,`quality_weight`,`price_weight`,`lead_weight`,`lead_enabled`,`is_default`,`status`,`createtime`,`updatetime`)
-SELECT '急单(交期优先)', 30, 20, 50, 1, 0, 'normal', NOW(), NOW()
-FROM DUAL
-WHERE NOT EXISTS (SELECT 1 FROM `supplier_score_rule` WHERE `name`='急单(交期优先)');
-
-INSERT INTO `supplier_score_rule`
-(`name`,`quality_weight`,`price_weight`,`lead_weight`,`lead_enabled`,`is_default`,`status`,`createtime`,`updatetime`)
-SELECT '重点产品(质量优先)', 50, 20, 30, 1, 0, 'normal', NOW(), NOW()
-FROM DUAL
-WHERE NOT EXISTS (SELECT 1 FROM `supplier_score_rule` WHERE `name`='重点产品(质量优先)');
-
--- 3) 非方案旧数据隐藏(例如原先 60/40/0)
-UPDATE `supplier_score_rule`
-SET `status`='hidden', `is_default`=0, `updatetime`=NOW()
-WHERE `status`='normal'
-  AND (`name`='' OR `name` IS NULL OR `name` NOT IN (
-    '普通订单(价格优先)',
-    '急单(交期优先)',
-    '重点产品(质量优先)'
-  ));
-
--- 4) 默认使用「普通订单(价格优先)」
-UPDATE `supplier_score_rule` SET `is_default`=0;
-UPDATE `supplier_score_rule` SET `is_default`=1 WHERE `name`='普通订单(价格优先)' AND `status`='normal';

+ 0 - 28
application/extra/supplier_service_final_score.sql

@@ -1,28 +0,0 @@
--- 供应商月度评审记录(仅本表展示/导出,不参与其它业务)
--- 商务技术分、价格分、交货分:默认来自当月订单平均,可人工修改;最终得分/评分等级:人工填写
-CREATE TABLE IF NOT EXISTS `supplier_service_final_score` (
-  `id` int unsigned NOT NULL AUTO_INCREMENT,
-  `ym` char(7) NOT NULL DEFAULT '' COMMENT '年月 YYYY-MM',
-  `company_name` varchar(128) NOT NULL DEFAULT '' COMMENT '供应商名称',
-  `score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '月度总分(兼容旧字段)',
-  `quality_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '商务技术分(当月订单平均)',
-  `price_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '价格分(当月订单平均)',
-  `delivery_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '交货分',
-  `value_added_score` decimal(10,2) DEFAULT NULL COMMENT '增值服务(人工,空=未填)',
-  `final_score` decimal(10,2) DEFAULT NULL COMMENT '最终得分(人工,空=未填)',
-  `score_grade` char(1) NOT NULL DEFAULT '' COMMENT '评分等级 A/B/C/D',
-  `qp_manual` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '1=商务技术分/价格分/交货分已人工修改',
-  `createtime` datetime DEFAULT NULL,
-  `updatetime` datetime DEFAULT NULL,
-  PRIMARY KEY (`id`),
-  UNIQUE KEY `uk_ym_company` (`ym`,`company_name`),
-  KEY `idx_ym` (`ym`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商月度评审记录';
-
--- 若表已存在,可执行:
--- ALTER TABLE `supplier_service_final_score` ADD COLUMN `quality_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '商务技术分(当月订单平均)' AFTER `score`;
--- ALTER TABLE `supplier_service_final_score` ADD COLUMN `price_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '价格分(当月订单平均)' AFTER `quality_score`;
--- ALTER TABLE `supplier_service_final_score` ADD COLUMN `delivery_score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '交货分' AFTER `price_score`;
--- ALTER TABLE `supplier_service_final_score` ADD COLUMN `value_added_score` decimal(10,2) DEFAULT NULL COMMENT '增值服务(人工,空=未填)' AFTER `delivery_score`;
--- ALTER TABLE `supplier_service_final_score` ADD COLUMN `score_grade` char(1) NOT NULL DEFAULT '' COMMENT '评分等级 A/B/C/D' AFTER `final_score`;
--- ALTER TABLE `supplier_service_final_score` ADD COLUMN `qp_manual` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '1=商务技术分/价格分/交货分已人工修改' AFTER `score_grade`;

+ 0 - 4
application/extra/supplier_service_score_weight_columns.sql

@@ -1,4 +0,0 @@
--- 历史补列(新环境请直接用 supplier_score_install.sql)
-ALTER TABLE `supplier_service_score`
-  ADD COLUMN `quality_weight` int unsigned NOT NULL DEFAULT 50 COMMENT '质量百分比' AFTER `quality_score`,
-  ADD COLUMN `price_weight` int unsigned NOT NULL DEFAULT 50 COMMENT '价格百分比' AFTER `price_sum`;

+ 92 - 30
application/index/view/index/deliveryscore.html

@@ -105,18 +105,42 @@
         .card.is-focus {
             box-shadow: 0 0 0 2px #3c8dbc, 0 1px 6px rgba(60,141,188,.22);
         }
+        .card .ord-row {
+            display: flex;
+            align-items: flex-start;
+            gap: 8px;
+            margin-bottom: 8px;
+        }
         .card .ord {
-            font-size: 15px; font-weight: 700; color: #222; margin-bottom: 8px;
+            flex: 1 1 auto;
+            min-width: 0;
+            font-size: 15px; font-weight: 700; color: #222;
             line-height: 1.45; word-break: break-all;
         }
-        .card .meta { font-size: 13px; color: #555; line-height: 1.55; }
+        .card .ord-status {
+            flex: 0 0 auto;
+            margin-top: 0;
+            padding: 4px 11px;
+            border-radius: 12px;
+            font-size: 14px;
+            font-weight: 700;
+            line-height: 1.45;
+            color: #27ae60;
+            background: #eaf8ef;
+            white-space: nowrap;
+        }
+        .card .ord-status.is-fail {
+            color: #e74c3c;
+            background: #fdeeee;
+        }
+        .card .meta { font-size: 13px; color: #555; line-height: 1.5; }
         .card .meta > div {
             display: flex; align-items: flex-start; gap: 8px;
-            margin: 0 0 4px;
+            margin: 0 0 3px;
         }
         .card .meta .meta-lab {
-            flex: 0 0 6.5em;
-            width: 6.5em;
+            flex: 0 0 5.8em;
+            width: 5.8em;
             color: #888;
             font-weight: 500;
             white-space: nowrap;
@@ -127,7 +151,34 @@
             color: #333;
             word-break: break-all;
         }
-        .actions { display: flex; gap: 8px; margin-top: 10px; justify-content: flex-end; }
+        .card .meta.score-summary {
+            margin-top: 8px;
+            padding-top: 8px;
+            border-top: 1px dashed #e6e6e6;
+        }
+        .card .meta.score-summary > .op-meta-row {
+            display: block;
+            margin-top: 2px;
+            font-size: 12px;
+            color: #888;
+            line-height: 1.5;
+            word-break: break-all;
+        }
+        .actions {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            margin-top: 10px;
+            justify-content: flex-end;
+        }
+        .actions .notified-tip {
+            flex: 1 1 auto;
+            min-width: 0;
+            font-size: 12px;
+            color: #888;
+            text-align: left;
+            word-break: break-all;
+        }
         .actions button {
             min-width: 72px; padding: 6px 14px; border: none; border-radius: 6px;
             font-size: 13px; font-weight: 600; color: #fff; font-family: inherit;
@@ -653,12 +704,19 @@
             {if $rows}
             {volist name="rows" id="row"}
             <div class="card{if $mprocFocusCcydh == $row.CCYDH} is-focus{/if}" data-ccydh="{$row.CCYDH|htmlentities}" data-delivery-deadline="{$row.delivery_deadline|default=''|htmlentities}" data-order-qty="{$row.order_qty|default=''|htmlentities}">
-                <div class="ord">{$row.CCYDH|default=''|htmlentities}{if $row.CYJMC} {$row.CYJMC|htmlentities}{/if}</div>
+                <div class="ord-row">
+                    <div class="ord">{$row.CCYDH|default=''|htmlentities}{if $row.CYJMC} {$row.CYJMC|htmlentities}{/if}</div>
+                    {eq name="mprocScoreTab" value="scored"}
+                    {notempty name="row.delivery_status"}
+                    <span class="ord-status{eq name='row.delivery_status' value='滞后'} is-fail{/eq}">{$row.delivery_status|htmlentities}</span>
+                    {/notempty}
+                    {/eq}
+                </div>
                 <div class="meta">
                     <div><span class="meta-lab">供应商</span><span class="meta-val">{$row.pick_company_name|default='—'|htmlentities}</span></div>
                     <div><span class="meta-lab">工序名称</span><span class="meta-val">{$row.CGYMC|default='—'|htmlentities}</span></div>
                     <div><span class="meta-lab">订单数量</span><span class="meta-val">{$row.order_qty|default=''|htmlentities}</span></div>
-                    <div><span class="meta-lab">交货截止日期</span><span class="meta-val">{$row.delivery_deadline|default=''|htmlentities}</span></div>
+                    <div><span class="meta-lab">交货截止</span><span class="meta-val">{$row.delivery_deadline|default=''|htmlentities}</span></div>
                 </div>
                 {eq name="mprocScoreTab" value="scored"}
                 <div class="meta score-summary"
@@ -667,20 +725,16 @@
                     data-delivery-date="{$row.delivery_date|default=''|htmlentities}"
                     data-remark="{$row.remark|default=''|htmlentities}">
                     <div><span class="meta-lab">库位</span><span class="meta-val">{$row.storage_location|default=''|htmlentities}</span></div>
-                    <div><span class="meta-lab">交货情况</span><span class="meta-val"><span class="res{eq name='row.delivery_status' value='滞后'} is-fail{/eq}">{$row.delivery_status|default=''|htmlentities}</span></span></div>
                     <div><span class="meta-lab">交货日期</span><span class="meta-val">{$row.delivery_date|default=''|htmlentities}</span></div>
                     <div><span class="meta-lab">备注</span><span class="meta-val">{$row.remark|default=''|htmlentities}</span></div>
-                    {notempty name="row.score_operator"}
-                    <div><span class="meta-lab">操作人</span><span class="meta-val">{$row.score_operator|htmlentities}</span></div>
-                    {/notempty}
-                    {notempty name="row.score_time"}
-                    <div><span class="meta-lab">操作时间</span><span class="meta-val">{$row.score_time|htmlentities}</span></div>
-                    {/notempty}
-                    {notempty name="row.arrival_notify_staff"}
-                    <div><span class="meta-lab">已通知</span><span class="meta-val">{$row.arrival_notify_staff|htmlentities}</span></div>
-                    {/notempty}
+                    {if condition="$row.score_operator || $row.score_time"}
+                    <div class="op-meta-row">{notempty name="row.score_operator"}操作人:{$row.score_operator|htmlentities}{/notempty}{notempty name="row.score_operator"}{notempty name="row.score_time"} {/notempty}{/notempty}{notempty name="row.score_time"}操作时间:{$row.score_time|substr=0,10|htmlentities}{/notempty}</div>
+                    {/if}
                 </div>
                 <div class="actions">
+                    {notempty name="row.arrival_notify_staff"}
+                    <span class="notified-tip">已通知:{$row.arrival_notify_staff|htmlentities}</span>
+                    {/notempty}
                     <button type="button" class="btn-notify">通知</button>
                 </div>
                 {else/}
@@ -1074,12 +1128,19 @@
             var hl = (focusCcydh && no === focusCcydh) ? ' is-focus' : '';
             var deadline = toYmdDisplay(row.delivery_deadline || '');
             html += '<div class="card' + hl + '" data-ccydh="' + esc(row.CCYDH || '') + '" data-delivery-deadline="' + esc(deadline) + '" data-order-qty="' + esc(row.order_qty || '') + '">';
-            html += '<div class="ord">' + esc(titleText(row)) + '</div>';
+            html += '<div class="ord-row"><div class="ord">' + esc(titleText(row)) + '</div>';
+            if (!pending) {
+                var dsHead = String(row.delivery_status || '').trim();
+                if (dsHead) {
+                    html += '<span class="ord-status' + (dsHead === '滞后' ? ' is-fail' : '') + '">' + esc(dsHead) + '</span>';
+                }
+            }
+            html += '</div>';
             html += '<div class="meta">';
             html += '<div><span class="meta-lab">供应商</span><span class="meta-val">' + esc(row.pick_company_name || '—') + '</span></div>';
             html += '<div><span class="meta-lab">工序名称</span><span class="meta-val">' + esc(row.CGYMC || '—') + '</span></div>';
             html += '<div><span class="meta-lab">订单数量</span><span class="meta-val">' + esc(row.order_qty || '') + '</span></div>';
-            html += '<div><span class="meta-lab">交货截止日期</span><span class="meta-val">' + esc(deadline) + '</span></div>';
+            html += '<div><span class="meta-lab">交货截止</span><span class="meta-val">' + esc(deadline) + '</span></div>';
             html += '</div>';
             if (pending) {
                 html += '<div class="actions"><button type="button" class="btn-score">确认交货</button></div>';
@@ -1097,20 +1158,21 @@
                     + ' data-delivery-date="' + esc(dd) + '"'
                     + ' data-remark="' + esc(remark) + '">'
                     + '<div><span class="meta-lab">库位</span><span class="meta-val">' + esc(loc || '') + '</span></div>'
-                    + '<div><span class="meta-lab">交货情况</span><span class="meta-val"><span class="res' + (ds === '滞后' ? ' is-fail' : '') + '">' + esc(ds || '') + '</span></span></div>'
                     + '<div><span class="meta-lab">交货日期</span><span class="meta-val">' + esc(dd || '') + '</span></div>'
                     + '<div><span class="meta-lab">备注</span><span class="meta-val">' + esc(remark) + '</span></div>';
-                if (opName) {
-                    html += '<div><span class="meta-lab">操作人</span><span class="meta-val">' + esc(opName) + '</span></div>';
-                }
-                if (opTime) {
-                    html += '<div><span class="meta-lab">操作时间</span><span class="meta-val">' + esc(opTime) + '</span></div>';
+                if (opName || opTime) {
+                    html += '<div class="op-meta-row">'
+                        + (opName ? ('操作人:' + esc(opName)) : '')
+                        + (opName && opTime ? ' ' : '')
+                        + (opTime ? ('操作时间:' + esc(opTime)) : '')
+                        + '</div>';
                 }
+                html += '</div>';
+                html += '<div class="actions">';
                 if (notified) {
-                    html += '<div><span class="meta-lab">已通知</span><span class="meta-val">' + esc(notified) + '</span></div>';
+                    html += '<span class="notified-tip">已通知:' + esc(notified) + '</span>';
                 }
-                html += '</div>';
-                html += '<div class="actions"><button type="button" class="btn-notify">通知</button></div>';
+                html += '<button type="button" class="btn-notify">通知</button></div>';
             }
             html += '</div>';
         });
@@ -1972,7 +2034,7 @@
                 var val = textOf(valEl) || '—';
                 if (lab === '工序名称' || lab === '工序') gymc = val;
                 if (lab === '供应商') company = val;
-                if (lab === '订单数量' && orderQty === '') orderQty = val === '—' ? '' : val;
+                if ((lab === '订单数量' || lab === '数量') && orderQty === '') orderQty = val === '—' ? '' : val;
                 continue;
             }
             var t = textOf(metaLines[i]);

+ 17 - 3
application/index/view/index/inboundscore.html

@@ -96,7 +96,13 @@
         }
         .card .score-summary .res.is-fail { color: #e74c3c; }
         .card .score-extra { margin-top: 6px; font-size: 12px; color: #666; line-height: 1.5; }
-        .card .score-time { margin-top: 6px; font-size: 12px; color: #888; }
+        .card .score-time {
+            margin-top: 6px;
+            font-size: 12px;
+            color: #888;
+            line-height: 1.5;
+            word-break: break-all;
+        }
         .main {
             flex: 1; min-width: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
             padding: 10px; background: #f5f5f5;
@@ -645,6 +651,12 @@
         showToast._t = setTimeout(function () { el.className = 'toast'; }, 2800);
     }
 
+    function toYmdDisplay(v) {
+        var s = String(v || '').trim().replace(/\//g, '-');
+        var m = s.match(/^(\d{4}-\d{2}-\d{2})/);
+        return m ? m[1] : s;
+    }
+
     function esc(s) {
         return String(s == null ? '' : s)
             .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
@@ -716,9 +728,11 @@
                     html += '<div class="score-extra">备注:' + esc(remark) + '</div>';
                 }
                 if (row.score_time || opName) {
+                    var opTimeYmd = toYmdDisplay(row.score_time || '');
                     html += '<div class="score-time">'
-                        + (opName ? ('操作人:' + esc(opName) + ' ') : '')
-                        + (row.score_time ? ('操作时间:' + esc(row.score_time)) : '')
+                        + (opName ? ('操作人:' + esc(opName)) : '')
+                        + (opName && opTimeYmd ? ' ' : '')
+                        + (opTimeYmd ? ('操作时间:' + esc(opTimeYmd)) : '')
                         + '</div>';
                 }
                 html += '<div class="actions">'

+ 17 - 4
application/index/view/index/rfqadd.html

@@ -119,7 +119,13 @@
             color: #333;
             word-break: break-all;
         }
-        .card .score-time { margin-top: 6px; font-size: 12px; color: #888; }
+        .card .score-time {
+            margin-top: 6px;
+            font-size: 12px;
+            color: #888;
+            line-height: 1.5;
+            word-break: break-all;
+        }
         .card .tags {
             margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px 10px;
             font-size: 12px; color: #666; align-items: center; justify-content: flex-end;
@@ -373,7 +379,7 @@
                 {/notempty}
             </div>
             {if condition="$row.cywyxm || $row.createtime"}
-            <div class="score-time">{notempty name="row.cywyxm"}操作人:{$row.cywyxm|htmlentities} {/notempty}{notempty name="row.createtime"}操作时间:{$row.createtime|htmlentities}{/notempty}</div>
+            <div class="score-time">{notempty name="row.cywyxm"}操作人:{$row.cywyxm|htmlentities}{/notempty}{notempty name="row.cywyxm"}{notempty name="row.createtime"} {/notempty}{/notempty}{notempty name="row.createtime"}操作时间:{$row.createtime|substr=0,10|htmlentities}{/notempty}</div>
             {/if}
             {if condition="isset($rfqStatusTab) && $rfqStatusTab eq 'quoted'"}
             <div class="tags">
@@ -608,6 +614,12 @@
         showToast._t = setTimeout(function () { el.className = 'toast'; }, 2800);
     }
 
+    function toYmdDisplay(v) {
+        var s = String(v || '').trim().replace(/\//g, '-');
+        var m = s.match(/^(\d{4}-\d{2}-\d{2})/);
+        return m ? m[1] : s;
+    }
+
     function esc(s) {
         return String(s == null ? '' : s)
             .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
@@ -691,7 +703,7 @@
             var sid = parseInt(row.scydgy_id, 10) || 0;
             var hit = (focusSid < 0 && sid === focusSid) ? ' rfq-card-highlight' : '';
             var opName = String(row.cywyxm || '').trim();
-            var opTime = String(row.createtime || '').trim();
+            var opTime = toYmdDisplay(row.createtime || '');
             html += '<div class="card' + hit + '" data-scydgy-id="' + esc(sid) + '">'
                 + '<div class="ord">' + esc(title) + '</div>'
                 + '<div class="meta">'
@@ -705,7 +717,8 @@
             html += '</div>';
             if (opName || opTime) {
                 html += '<div class="score-time">'
-                    + (opName ? ('操作人:' + esc(opName) + ' ') : '')
+                    + (opName ? ('操作人:' + esc(opName)) : '')
+                    + (opName && opTime ? ' ' : '')
                     + (opTime ? ('操作时间:' + esc(opTime)) : '')
                     + '</div>';
             }

+ 114 - 98
public/assets/js/backend/procuremen.js

@@ -642,7 +642,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             var allOnlyCols = ['ID'];
 
             Controller.currYm = ($layout.data('defaultYm') || '').toString();
-            Controller.wffTab = ($layout.data('procuremenStage') || 'pick').toString();
+            // 优先读属性,避免 jQuery .data() 缓存旧值导致 stage 错成 pick、操作列整列变空
+            Controller.wffTab = ($layout.attr('data-procuremen-stage') || $layout.data('procuremenStage') || 'pick').toString();
             if (['pick', 'audit', 'confirm'].indexOf(Controller.wffTab) < 0) {
                 Controller.wffTab = 'pick';
             }
@@ -1158,6 +1159,21 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     + "<span class='procuremen-supplier-preview'>" + procuremenEscHtml(preview) + '</span>'
                     + '</div>';
             }
+            /** 多值直接换行展示(确认/审批订单号等) */
+            function procuremenMultiLinesWrapCell(value) {
+                var lines = procuremenParseSupplierLines(value);
+                if (lines.length === 0) {
+                    return '';
+                }
+                if (lines.length === 1) {
+                    return procuremenEscHtml(lines[0]);
+                }
+                return '<div class="procuremen-multi-lines-wrap" style="white-space:normal;line-height:1.55;text-align:center;">'
+                    + lines.map(function (line) {
+                        return '<div>' + procuremenEscHtml(line) + '</div>';
+                    }).join('')
+                    + '</div>';
+            }
             function procuremenReadSupplierLinesFromCell($item) {
                 if (!$item || !$item.length) {
                     return [];
@@ -1511,6 +1527,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 {field: 'CCYDH', title: __('订单号'), operate: 'LIKE', table: 'b', width: 120, align: 'center', sortable: true,
                     class: 'procuremen-th-sort-tight procuremen-supplier-lines-cell',
                     formatter: function (v) {
+                        var tab = Controller.wffTab || 'pick';
+                        // 确认/审批:多订单号直接换行,避免省略看不清
+                        if (tab === 'audit' || tab === 'confirm') {
+                            return procuremenMultiLinesWrapCell(v);
+                        }
                         return procuremenSupplierLinesCell(v, this.width || 120);
                     }
                 },
@@ -1569,6 +1590,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 },
                 {field: 'ceilingPrice', title: '最高限价', operate: 'LIKE', table: 'a', width: 90, align: 'center',
                     visible: true,
+                    class: 'procuremen-supplier-lines-cell',
                     formatter: function (value, row, index) {
                         var tab = Controller.wffTab || 'pick';
                         var v = value;
@@ -1583,16 +1605,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                                 + 'data-field="ceilingPrice" data-row-index="' + index + '" value="'
                                 + procuremenEscAttr(val) + '" placeholder="填写" autocomplete="off"/>';
                         }
-                        if (v == null || v === '') {
-                            return '';
-                        }
-                        var s = String(v).split(/[、,,]+/).map(function (x) {
-                            return $.trim(x);
-                        }).filter(Boolean).join('、');
-                        if (!s) {
-                            return '';
-                        }
-                        return '<span style="white-space:normal;line-height:1.5;">' + procuremenEscHtml(s) + '</span>';
+                        return procuremenSupplierLinesCell(v, this.width || 90);
                     }
                 },
                 {field: 'order_status_text', title: '订单状态', operate: false, table: 'a', width: 100, align: 'center',
@@ -1672,7 +1685,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 {field: 'CDW', title: __('单位'), operate: 'LIKE', table: 'a', width: 88, align: 'center'},
                 {field: 'CDF', title: __('订法'), operate: 'LIKE', table: 'a', width: 90, align: 'center', class: 'procuremen-cell-wrap'},
                 {field: 'cGzzxMc', title: __('外厂单位'), operate: 'LIKE', table: 'a', width: 200, align: 'center', class: 'procuremen-cell-wrap'},
-                {field: 'MBZ', title: '招标备注', operate: 'LIKE', table: 'a', width: 240, align: 'center', class: 'procuremen-cell-wrap',
+                {field: 'MBZ', title: '招标备注', operate: 'LIKE', table: 'a', width: 240, align: 'center',
+                    class: indexInitWffTab === 'pick' ? 'procuremen-cell-wrap' : 'autocontent',
                     formatter: function (v, row, index) {
                         var tab = Controller.wffTab || 'pick';
                         if (tab === 'pick') {
@@ -1682,10 +1696,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                                 + 'data-field="MBZ" data-row-index="' + index + '" value="'
                                 + procuremenEscAttr(val) + '" placeholder="填写" autocomplete="off"/>';
                         }
-                        if (v == null || v === '') {
-                            return '';
-                        }
-                        return '<span title="' + procuremenEscAttr(v) + '">' + procuremenEscHtml(String(v)) + '</span>';
+                        // 确认/审批:过长单行省略,悬停出现下拉箭头展开全文
+                        return procuremenAutocontentCell(v, this.width || 240);
                     }
                 },
                 {field: 'cywyxm', title: __('业务员'), operate: '=', searchList: procuremenSearchList('cywyxm'), table: 'b', width: 80, align: 'center'},
@@ -1726,46 +1738,61 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 {field: 'dputrecord', title: __('提交日期'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, table: 'b', width: 176, align: 'center', sortable: true, class: 'procuremen-th-sort-tight',
                     visible: indexInitWffTab === 'pick'
                 },
-                {field: 'operate',title: '操作',width: indexInitWffTab === 'audit' ? 178 : 220,align: 'center',fixed: 'right', operate: false,
+                {field: 'operate', title: '操作', width: indexInitWffTab === 'audit' ? 178 : 220, align: 'center', operate: false,
                     visible: indexInitWffTab !== 'pick',
+                    class: 'procuremen-operate-col',
                     table: table,
                     formatter: function (value, row, index) {
-                        var tab = Controller.wffTab || 'pick';
-                        var pk = row && (row.ID != null ? row.ID : row.id);
-                        if (!row || pk == null || pk === '' || String(pk) === '0') {
+                        // 用初始化 stage,避免运行期 wffTab 被改写后操作列突然变空
+                        var tab = indexInitWffTab || Controller.wffTab || 'pick';
+                        if (tab === 'pick' || !row) {
                             return '';
                         }
+                        // 合并行可能只有 scydgy_id / purchase_order_id,勿因缺 ID 整列变空
+                        var pk = row.ID != null ? row.ID : row.id;
+                        if (pk == null || pk === '' || String(pk) === '0') {
+                            pk = row.scydgy_id != null ? row.scydgy_id : row.purchase_order_id;
+                        }
+                        if (pk == null || pk === '' || String(pk) === '0') {
+                            pk = 'r' + index;
+                        }
                         var areaDetails = ' data-area=\'["100%","100%"]\'';
                         var areaAudit = ' data-area=\'["98%","92%"]\'';
                         var areaConfirmOutward = ' data-area=\'["100%","100%"]\'';
                         var primary = '';
-                        var detail = '';
-                        if (tab === 'pick') {
-                            return '';
-                        } else if (tab === 'audit') {
-                            var stA = row && row.order_status_text != null ? String(row.order_status_text).trim() : '';
+                        // 能进确认/审批列表即可看详情(接口仍会鉴权)
+                        var detail = '<a class="btn btn-xs btn-info procuremen-op-open procuremen-btn-details"' + areaDetails + ' href="procuremen/details" data-row-index="' + index + '" title="详情"><i class="fa fa-file-text-o"></i> 详情</a>';
+                        var canAuditIssue = procuremenCan('auditissue') || tab === 'audit';
+                        var canOutward = procuremenCan('outward_detail') || tab === 'confirm';
+                        if (tab === 'audit') {
+                            var stA = row.order_status_text != null ? String(row.order_status_text).trim() : '';
                             if (stA === '待审批') {
                                 stA = '待审核';
                             }
-                            if (stA === '待确认' && procuremenCan('auditissue')) {
+                            if (stA === '待确认' && canAuditIssue) {
                                 primary = '<a class="btn btn-xs btn-primary procuremen-op-open procuremen-btn-audit-issue"' + areaAudit + ' href="procuremen/auditissue" data-row-index="' + index + '" title=""><i class="fa fa-check"></i> 选择供应商</a>';
                             }
-                            if (procuremenCan('details')) {
-                                detail = '<a class="btn btn-xs btn-info procuremen-op-open procuremen-btn-details"' + areaDetails + ' href="procuremen/details" data-row-index="' + index + '" title="详情"><i class="fa fa-file-text-o"></i> 详情</a>';
-                            }
                         } else if (tab === 'confirm') {
-                            var stC = row && row.order_status_text != null ? String(row.order_status_text).trim() : '';
+                            var stC = row.order_status_text != null ? String(row.order_status_text).trim() : '';
                             if (stC === '待审批') {
                                 stC = '待审核';
                             }
-                            if (stC === '待审核' && procuremenCan('outward_detail')) {
-                                primary = '<a class="btn btn-xs btn-success procuremen-op-open procuremen-btn-outward"' + areaConfirmOutward + ' href="procuremen/outward_detail?wff_tab=confirm" data-row-index="' + index + '" title="审批"><i class="fa fa-gavel"></i> 审批</a>';
+                            // 待开标:不可审批(须已开标或已指定供应商)
+                            var bidText = row.bid_open_status_text != null ? String(row.bid_open_status_text).trim() : '';
+                            if (!bidText) {
+                                if (parseInt(row.bid_open_verified, 10) === 1) {
+                                    bidText = '已开标';
+                                } else if (parseInt(row.manual_picked, 10) === 1) {
+                                    bidText = '已指定供应商';
+                                } else {
+                                    bidText = '待开标';
+                                }
                             }
-                            if (procuremenCan('details')) {
-                                detail = '<a class="btn btn-xs btn-info procuremen-op-open procuremen-btn-details"' + areaDetails + ' href="procuremen/details" data-row-index="' + index + '" title="详情"><i class="fa fa-file-text-o"></i> 详情</a>';
+                            var bidReady = (bidText === '已开标' || bidText === '已指定供应商');
+                            if (stC === '待审核' && bidReady && canOutward) {
+                                primary = '<a class="btn btn-xs btn-success procuremen-op-open procuremen-btn-outward"' + areaConfirmOutward + ' href="procuremen/outward_detail?wff_tab=confirm" data-row-index="' + index + '" title="审批"><i class="fa fa-gavel"></i> 审批</a>';
                             }
-                        }
-                        if (!primary && !detail) {
+                        } else {
                             return '';
                         }
                         // 双槽位:左侧主操作(无则占位),右侧详情固定竖线对齐;整体居中
@@ -1778,8 +1805,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 }
             ];
 
-            // audit / confirm 右侧固定操作列,避免横向滚动才能点按钮
-            var useFixedColumns = indexInitWffTab === 'audit' || indexInitWffTab === 'confirm';
+            // 操作列改用 CSS sticky,禁用 fixed-columns 插件(克隆列宽错位会让操作按钮看起来“没了”)
+            var useFixedColumns = false;
             var procuremenTableHeightCached = 0;
             var procuremenHeightSyncTimer = null;
 
@@ -1936,55 +1963,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 }
                 $headerWrap.scrollLeft(sl);
 
-                // 右侧固定操作列:插件克隆整表再 scrollLeft 到最右;切勿 scrollLeft(0) 或把 table 压成单列宽
-                    var $fixedRight = $bt.find('.fixed-columns-right');
-                if ($fixedRight.length) {
-                    var $fh = $fixedRight.find('.fixed-table-header').first();
-                    var $fb = $fixedRight.find('.fixed-table-body').first();
-                    var $fTable = $fh.find('table').first();
-                    if (!$fTable.length) {
-                        $fTable = $fb.find('table').first();
-                    }
-                    // 清掉历史错误内联样式,恢复整表宽度
-                    $fixedRight.find('table').each(function () {
-                        $(this).css({width: '', minWidth: '', maxWidth: '', tableLayout: ''});
-                    });
-                    $fixedRight.find('thead th, tbody td').css({
-                        width: '',
-                        minWidth: '',
-                        maxWidth: '',
-                        overflow: '',
-                        whiteSpace: ''
-                    });
-                    $fixedRight.find('colgroup').remove();
-
-                    var opW = 168;
-                    var opMin = Controller.wffTab === 'audit' ? 160 : 156;
-                    var $opCell = $bt.find('.fixed-table-body table thead th[data-field="operate"]').first();
-                    if (!$opCell.length) {
-                        $opCell = $headerTable.find('th[data-field="operate"]').first();
-                    }
-                    if ($opCell.length) {
-                        var measured = $opCell.outerWidth();
-                        if (measured > 0) {
-                            opW = Math.max(Math.round(measured), opMin);
-                        }
-                    }
-                    $fixedRight.css({
-                        width: opW + 'px',
-                        minWidth: opW + 'px',
-                        maxWidth: opW + 'px',
-                        display: 'block',
-                        visibility: 'visible',
-                        overflow: 'hidden'
-                    });
-
-                    var fullW = $fTable.outerWidth() || $fTable.width() || 0;
-                    if (fullW > 0) {
-                        $fh.scrollLeft(fullW);
-                        $fb.scrollLeft(fullW);
-                    }
-                }
+                // 已改用 sticky 操作列:隐藏插件残留的右侧固定层,避免空白遮罩盖住按钮
+                $bt.find('.fixed-columns-right, .fixed-columns').hide();
             }
 
             function procuremenScheduleAlign() {
@@ -4709,9 +4689,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 FastRef.api.ajax({
                     url: FastRef.api.fixurl(isPickMode ? 'procuremen/picksubmit' : 'procuremen/review'),
                     type: 'POST',
-                    data: postData
+                    data: postData,
+                    timeout: isPickMode ? 180000 : 90000
                 }, function (data, ret) {
                     var msg = (ret && ret.msg) ? ret.msg : '操作成功';
+                    var notifyFailed = data && (parseInt(data.notify_failed, 10) === 1);
                     try {
                         sessionStorage.removeItem('procuremen_merge_rows');
                     } catch (ignoreClr) {
@@ -4720,10 +4702,17 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     if (typeof parent !== 'undefined' && parent.Layer && index) {
                         parent.Layer.close(index);
                     }
-                    if (typeof parent !== 'undefined' && parent.Toastr) {
-                        parent.Toastr.success(msg);
-                    } else if (typeof Toastr !== 'undefined') {
-                        Toastr.success(msg);
+                    // 下发已成功:通知未送达用 warning,避免被当成整单失败
+                    var toast = (typeof parent !== 'undefined' && parent.Toastr) ? parent.Toastr
+                        : (typeof Toastr !== 'undefined' ? Toastr : null);
+                    if (toast) {
+                        if (notifyFailed && toast.warning) {
+                            toast.warning(msg);
+                        } else {
+                            toast.success(msg);
+                        }
+                    } else if (notifyFailed && typeof parent !== 'undefined' && parent.Layer) {
+                        parent.Layer.msg(msg, {icon: 0, time: 5000});
                     }
                     if (parent && parent.$ && parent.$('#table').length) {
                         parent.$('#table').bootstrapTable('refresh', {silent: true});
@@ -4731,8 +4720,30 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     return false;
                 }, function (data, ret) {
                     $submitBtn.prop('disabled', false).html(submitBtnHtml);
-                    var errMsg = (ret && ret.msg) ? ret.msg : '提交失败,请重试';
-                    procuremenReviewWarn(errMsg);
+                    var errMsg = (ret && ret.msg) ? String(ret.msg) : '';
+                    var softFail = /timeout|timed out|网络|abort|gateway|504|502|503/i.test(errMsg)
+                        || errMsg === ''
+                        || (ret && (ret.code === -1 || typeof ret.code === 'undefined'));
+                    if (isPickMode && softFail) {
+                        try { sessionStorage.removeItem('procuremen_merge_rows'); } catch (e0) {}
+                        var tip = '提交时间较长。请刷新列表:若已进入「待确认」,说明下发已成功;个别未收到通知时,请到「协助审批下发」重发。';
+                        if (typeof parent !== 'undefined' && parent.Toastr && parent.Toastr.warning) {
+                            parent.Toastr.warning(tip);
+                        } else if (typeof Toastr !== 'undefined' && Toastr.warning) {
+                            Toastr.warning(tip);
+                        } else {
+                            procuremenReviewWarn(tip);
+                        }
+                        if (parent && parent.$ && parent.$('#table').length) {
+                            parent.$('#table').bootstrapTable('refresh', {silent: true});
+                        }
+                        try {
+                            var idxClose = parent.Layer.getFrameIndex(window.name);
+                            if (idxClose) { parent.Layer.close(idxClose); }
+                        } catch (e1) {}
+                        return false;
+                    }
+                    procuremenReviewWarn(errMsg || '提交未完成,请刷新列表确认后重试');
                     return false;
                 });
             }
@@ -5738,10 +5749,15 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     }
                 }, function (data, ret) {
                     var msg = (ret && ret.msg) ? ret.msg : '补加成功';
-                    if (typeof parent !== 'undefined' && parent.Toastr) {
-                        parent.Toastr.success(msg);
-                    } else {
-                        Toastr.success(msg);
+                    var notifyFailed = data && (parseInt(data.notify_failed, 10) === 1);
+                    var toast = (typeof parent !== 'undefined' && parent.Toastr) ? parent.Toastr
+                        : (typeof Toastr !== 'undefined' ? Toastr : null);
+                    if (toast) {
+                        if (notifyFailed && toast.warning) {
+                            toast.warning(msg);
+                        } else {
+                            toast.success(msg);
+                        }
                     }
                     setTimeout(function () {
                         try {

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott