m0_70156489 пре 14 часа
родитељ
комит
de8240a7bb

+ 165 - 22
application/admin/controller/Procuremen.php

@@ -8704,7 +8704,7 @@ class Procuremen extends Backend
             . "印件名称:{cyjmc}\n"
             . "工序名称:{cgymc}\n"
             . "本次数量:{this_quantity}\n"
-            . "供应商:{supplier_name}\n"
+            . "供应商报价:\n{supplier_quotes}\n"
             . "需求发起人:{cywyxm}\n";
         try {
             $row = Db::table('purchase_sms_template')
@@ -8716,10 +8716,14 @@ class Procuremen extends Backend
         $now = date('Y-m-d H:i:s');
         if (is_array($row) && $row !== []) {
             $old = (string)($row['content'] ?? '');
-            $needMigrate = (strpos($old, '需求编号:{ccydh}') !== false)
-                || (strpos($old, '{supplier_name}') === false && strpos($old, '供应商:') === false);
+            // 仍用旧的「供应商:{supplier_name}」或没有报价占位符时,补上单价表格
+            $needMigrate = (strpos($old, '{supplier_quotes}') === false)
+                || (strpos($old, '供应商:{supplier_name}') !== false);
             if ($needMigrate) {
                 $data = ['content' => $content];
+                if (trim((string)($row['title'] ?? '')) === '' || trim((string)($row['title'] ?? '')) === '询价通知') {
+                    $data['title'] = '业务员询价通知';
+                }
                 try {
                     $cols = Db::query("SHOW COLUMNS FROM `purchase_sms_template` LIKE 'updatetime'");
                     if (!empty($cols)) {
@@ -8740,7 +8744,7 @@ class Procuremen extends Backend
         try {
             $data = [
                 'scene'   => 'rfq_salesman_email',
-                'title'   => '询价邮箱通知',
+                'title'   => '业务员询价通知',
                 'content' => $content,
                 'remark'  => '查询询价-通知业务员邮箱;模版名称作邮件主题',
                 'status'  => 1,
@@ -8772,6 +8776,84 @@ class Procuremen extends Backend
         }
     }
 
+    /**
+     * 通知业务员邮件:供应商报价明细行(仅名称+单价)
+     *
+     * @return array<int, array{name:string,amount:string}>
+     */
+    protected function loadRfqSalesmanSupplierQuoteRows(int $sid, string $supplierRaw = ''): array
+    {
+        $list = [];
+        $seen = [];
+        try {
+            $rows = Db::table('purchase_order_detail')
+                ->where('scydgy_id', $sid)
+                ->field('company_name,amount')
+                ->order('id', 'asc')
+                ->select();
+            if (is_array($rows)) {
+                foreach ($rows as $r) {
+                    if (!is_array($r)) {
+                        continue;
+                    }
+                    $cn = trim((string)($r['company_name'] ?? ''));
+                    if ($cn === '' || isset($seen[$cn])) {
+                        continue;
+                    }
+                    $seen[$cn] = true;
+                    $amount = trim((string)($r['amount'] ?? ''));
+                    $list[] = [
+                        'name'   => $cn,
+                        'amount' => $amount !== '' ? $amount : '未报价',
+                    ];
+                }
+            }
+        } catch (\Throwable $e) {
+        }
+        if ($list === [] && $supplierRaw !== '') {
+            foreach ($this->splitRfqSalesmanNames($supplierRaw) as $cn) {
+                if ($cn === '' || isset($seen[$cn])) {
+                    continue;
+                }
+                $seen[$cn] = true;
+                $list[] = [
+                    'name'   => $cn,
+                    'amount' => '未报价',
+                ];
+            }
+        }
+
+        return $list;
+    }
+
+    /**
+     * 通知业务员邮件:供应商报价 HTML 表格(名称+单价,不含时间)
+     */
+    protected function buildRfqSalesmanSupplierQuotesHtml(int $sid, string $supplierRaw = ''): string
+    {
+        $rows = $this->loadRfqSalesmanSupplierQuoteRows($sid, $supplierRaw);
+        if ($rows === []) {
+            return '<p style="margin:8px 0;color:#888;">暂无报价</p>';
+        }
+        $html = '<table cellpadding="0" cellspacing="0" border="1" '
+            . 'style="border-collapse:collapse;border-color:#ddd;width:100%;max-width:560px;font-size:14px;margin:8px 0;">'
+            . '<thead><tr style="background:#f5f5f5;">'
+            . '<th style="padding:8px 10px;text-align:center;width:56px;">序号</th>'
+            . '<th style="padding:8px 10px;text-align:left;">供应商名称</th>'
+            . '<th style="padding:8px 10px;text-align:center;width:100px;">单价</th>'
+            . '</tr></thead><tbody>';
+        foreach ($rows as $i => $row) {
+            $html .= '<tr>'
+                . '<td style="padding:8px 10px;text-align:center;">' . ($i + 1) . '</td>'
+                . '<td style="padding:8px 10px;">' . htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8') . '</td>'
+                . '<td style="padding:8px 10px;text-align:center;">' . htmlspecialchars($row['amount'], ENT_QUOTES, 'UTF-8') . '</td>'
+                . '</tr>';
+        }
+        $html .= '</tbody></table>';
+
+        return $html;
+    }
+
     /**
      * 询价多选字段:数组或字符串统一为顿号拼接
      *
@@ -9617,12 +9699,16 @@ class Procuremen extends Backend
         $ccydh = trim((string)($po['CCYDH'] ?? ''));
         $cyjmc = trim((string)($po['CYJMC'] ?? ''));
         $cgymc = trim((string)($po['CGYMC'] ?? ''));
-        $qty = trim((string)($po['This_quantity'] ?? ''));
+        $cdw = trim((string)($po['CDW'] ?? ''));
+        $qty = trim((string)$this->resolveProcuremenThisQuantity($po));
+        $ceiling = trim((string)$this->resolveProcuremenCeilingPrice($po));
+        $mbz = trim((string)($po['MBZ'] ?? ''));
         $cywyxm = trim((string)($po['cywyxm'] ?? ''));
         $supplier = trim((string)($po['cGzzxMc'] ?? ''));
         if ($supplier === '') {
             $supplier = trim((string)($po['pick_company_name'] ?? ''));
         }
+        $supplierQuotesHtml = $this->buildRfqSalesmanSupplierQuotesHtml($sid, $supplier);
         try {
             $mailSubjectTpl = $this->resolveProcuremenEmailSubject('rfq_salesman_email');
         } catch (\Throwable $e) {
@@ -9634,21 +9720,29 @@ class Procuremen extends Backend
             $toEmail = trim((string)($contact['email'] ?? ''));
             $toName = trim((string)($contact['name'] ?? ''));
             $notifyVars = [
-                'contact_name'   => $toName,
-                'salesman_name'  => $toName,
-                'company_name'   => $toName,
-                'email'          => $toEmail,
-                'ccydh'          => $ccydh,
-                'cyjmc'          => $cyjmc,
-                'cgymc'          => $cgymc,
-                'this_quantity'  => $qty,
-                'supplier_name'  => $supplier,
-                'cywyxm'         => $cywyxm,
+                'contact_name'    => $toName,
+                'salesman_name'   => $toName,
+                'company_name'    => $toName,
+                'email'           => $toEmail,
+                'ccydh'           => $ccydh,
+                'cyjmc'           => $cyjmc,
+                'cgymc'           => $cgymc,
+                'cdw'             => $cdw,
+                'this_quantity'   => $qty,
+                'ceilingPrice'    => $ceiling,
+                'MBZ'             => $mbz,
+                'supplier_name'   => $supplier,
+                // 占位符,转 HTML 后再替换成表格,避免被 htmlspecialchars 转义
+                'supplier_quotes' => '__RFQ_SUPPLIER_QUOTES_TABLE__',
+                'cywyxm'          => $cywyxm,
             ];
             try {
                 $mailPlain = $this->renderNotifyTemplate('rfq_salesman_email', $notifyVars);
                 $mailSubject = $mailSubjectTpl;
                 foreach ($notifyVars as $k => $v) {
+                    if ($k === 'supplier_quotes') {
+                        continue;
+                    }
                     $mailSubject = str_replace('{' . $k . '}', (string)$v, $mailSubject);
                 }
                 $mailSubject = preg_replace('/\{[a-zA-Z0-9_]+\}/', '', $mailSubject);
@@ -9657,10 +9751,12 @@ class Procuremen extends Backend
                     $mailSubject = '协助采购询价通知' . ($ccydh !== '' ? ' - ' . $ccydh : '');
                 }
                 $mailBody = $this->plainTextToHtmlEmailBody($mailPlain);
+                $mailBody = str_replace('__RFQ_SUPPLIER_QUOTES_TABLE__', $supplierQuotesHtml, $mailBody);
+                $mailPlainForLog = str_replace('__RFQ_SUPPLIER_QUOTES_TABLE__', strip_tags(str_replace(['</tr>', '</p>'], "\n", $supplierQuotesHtml)), $mailPlain);
                 $bundle = [
                     'company_name'                => $toName,
                     'email'                       => $toEmail,
-                    'mail_plain'                  => $mailPlain,
+                    'mail_plain'                  => $mailPlainForLog,
                     'mail_body'                   => $mailBody,
                     'ccydh'                       => $ccydh,
                     'email_log_scene'             => 'rfq_salesman',
@@ -9727,6 +9823,50 @@ class Procuremen extends Backend
             $supplierRaw = trim((string)($po['pick_company_name'] ?? ''));
         }
         $names = $this->splitRfqSalesmanNames($supplierRaw);
+
+        // 已成功发过询价邮件的供应商(含历史 pick_issue 场景)
+        $notifiedMap = [];
+        try {
+            $this->ensurePurchaseEmailSendLogTableOnce();
+            $logRows = Db::table('purchase_email_send_log')
+                ->where('scydgy_id', $sid)
+                ->where('status', 1)
+                ->where('scene', 'in', ['rfq_supplier', 'pick_issue'])
+                ->field('company_name')
+                ->select();
+            if (is_array($logRows)) {
+                foreach ($logRows as $lr) {
+                    if (!is_array($lr)) {
+                        continue;
+                    }
+                    $cn = trim((string)($lr['company_name'] ?? ''));
+                    if ($cn !== '') {
+                        $notifiedMap[$cn] = true;
+                    }
+                }
+            }
+        } catch (\Throwable $e) {
+        }
+        // 兼容:明细已生成(曾下发)也视为已通知
+        try {
+            $detRows = Db::table('purchase_order_detail')
+                ->where('scydgy_id', $sid)
+                ->field('company_name')
+                ->select();
+            if (is_array($detRows)) {
+                foreach ($detRows as $dr) {
+                    if (!is_array($dr)) {
+                        continue;
+                    }
+                    $cn = trim((string)($dr['company_name'] ?? ''));
+                    if ($cn !== '') {
+                        $notifiedMap[$cn] = true;
+                    }
+                }
+            }
+        } catch (\Throwable $e) {
+        }
+
         $list = [];
         foreach ($names as $name) {
             // 展示用:无邮箱/手机也返回档案信息(与供应商分组一致)
@@ -9735,13 +9875,16 @@ class Procuremen extends Backend
             $phone = is_array($contact) ? trim((string)($contact['phone'] ?? '')) : '';
             $username = is_array($contact) ? trim((string)($contact['username'] ?? '')) : '';
             $okEmail = ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL));
+            $notified = !empty($notifiedMap[$name]) ? 1 : 0;
             $list[] = [
-                'name'       => $name,
-                'username'   => $username,
-                'email'      => $email,
-                'phone'      => $phone,
-                'can_notify' => $okEmail ? 1 : 0,
-                'found'      => is_array($contact) ? 1 : 0,
+                'name'               => $name,
+                'username'           => $username,
+                'email'              => $email,
+                'phone'              => $phone,
+                'can_notify'         => $okEmail ? 1 : 0,
+                'found'              => is_array($contact) ? 1 : 0,
+                'notified'           => $notified,
+                'notify_status_text' => $notified ? '已通知' : '未通知',
             ];
         }
 

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

@@ -73,7 +73,11 @@ class Purchasesmstemplate extends Model
             ['tag' => '{cyjmc}', 'label' => '印件名称', 'example' => '藏书票2', 'scenes' => '全部'],
             ['tag' => '{cgymc}', 'label' => '工序名称(单道工序)', 'example' => '骑马订', 'scenes' => '协助下发/询价通知'],
             ['tag' => '{this_quantity}', 'label' => '本次数量', 'example' => '200', 'scenes' => '询价通知'],
-            ['tag' => '{supplier_name}', 'label' => '供应商', 'example' => '台州某某有限公司', 'scenes' => '询价通知业务员'],
+            ['tag' => '{ceilingPrice}', 'label' => '最高限价', 'example' => '0.7', 'scenes' => '询价通知业务员'],
+            ['tag' => '{cdw}', 'label' => '单位', 'example' => '本', 'scenes' => '询价通知业务员'],
+            ['tag' => '{MBZ}', 'label' => '备注', 'example' => '封面颜色深', 'scenes' => '询价通知业务员'],
+            ['tag' => '{supplier_name}', 'label' => '报价供应商', 'example' => '台州某某有限公司', 'scenes' => '询价通知业务员'],
+            ['tag' => '{supplier_quotes}', 'label' => '供应商报价表格(名称+单价)', 'example' => '自动生成表格', 'scenes' => '询价通知业务员'],
             ['tag' => '{cywyxm}', 'label' => '需求发起人', 'example' => '管理员', 'scenes' => '询价通知业务员'],
             ['tag' => '{category}', 'label' => '业务分类', 'example' => '出版物印刷', 'scenes' => '协助下发'],
             ['tag' => '{deadline}', 'label' => '截止时间', 'example' => '2026-05-18 14:30', 'scenes' => '协助下发/供应商询价'],

+ 6 - 0
application/admin/view/procuremen/audit_issue.html

@@ -551,6 +551,9 @@
                 <th class="text-center col-rank" style="width:64px;">排名</th>
                 <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="交货得分取月度评审(当月按上月起往前查,无则默认50)">交货得分</th>
+                {/if}
                 <th class="text-center" style="width:72px;">总分</th>
                 <th style="width:130px;">工序名称</th>
                 <th class="text-center" style="width:130px;">单价</th>
@@ -580,6 +583,9 @@
                 <td class="text-center col-rank"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-size:17px;font-weight:700;">{if condition="!empty($quoteVisible)"}{$co.service_rank_text|default=''|htmlentities}{/if}</td>
                 <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($quoteVisible)"}{$co.service_quality_score_text|default=''|htmlentities}{/if}</td>
                 <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;"{if condition="!empty($quoteVisible)"} title="单价合计 {$co.service_price_sum_text|default=''|htmlentities}"{/if}>{if condition="!empty($quoteVisible)"}{$co.service_price_score_text|default=''|htmlentities}{/if}</td>
+                {if !empty($showServiceLeadScore)}
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($quoteVisible)"}{$co.service_lead_score_text|default=''|htmlentities}{/if}</td>
+                {/if}
                 <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-weight:600;">{if condition="!empty($quoteVisible)"}{$co.service_score_text|default=''|htmlentities}{/if}</td>
                 {/if}
                 <td>{$ln.cgymc|default=''|htmlentities}</td>

+ 6 - 0
application/admin/view/procuremen/details_fragment.html

@@ -555,6 +555,9 @@
                 <th class="text-center col-rank" style="width:64px;">排名</th>
                 <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="交货得分取月度评审(当月按上月起往前查,无则默认50)">交货得分</th>
+                {/if}
                 <th class="text-center" style="width:72px;">总分</th>
                 <th style="width:100px;">工序名称</th>
                 <th class="text-center col-unit-price">单价</th>
@@ -580,6 +583,9 @@
                 <td class="text-center col-rank"{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if}>{$sg.service_rank_text|default=''|htmlentities}</td>
                 <td class="text-center"{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;">{$sg.service_quality_score_text|default=''|htmlentities}</td>
                 <td class="text-center"{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;" title="单价合计 {$sg.service_price_sum_text|default=''|htmlentities}">{$sg.service_price_score_text|default=''|htmlentities}</td>
+                {if !empty($showServiceLeadScore)}
+                <td class="text-center"{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;">{$sg.service_lead_score_text|default=''|htmlentities}</td>
+                {/if}
                 <td class="text-center"{if condition="$sg.display_rowspan gt 1"} rowspan="{$sg.display_rowspan}"{/if} style="vertical-align:middle;font-weight:600;">{$sg.service_score_text|default=''|htmlentities}</td>
                 {/if}
                 <td>{$ln.cgymc|default=''|htmlentities}</td>

+ 16 - 0
application/admin/view/procuremen/outward_detail.html

@@ -480,6 +480,16 @@
         <i class="fa fa-exclamation-triangle"></i>
         <strong>重要提示:</strong>审批通过:将向中标供应商发送「已通过」短信、向未中标供应商发送「未通过」短信。
     </p>
+    {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}
     <p class="outward-confirm-block-title">供应商({$confirmPickCount|default=0} 家)</p>
     <div class="outward-confirm-company-wrap">
         <table class="table table-bordered table-condensed outward-confirm-company-table outward-confirm-readonly">
@@ -493,6 +503,9 @@
                 <th class="text-center col-rank" style="width:64px;">排名</th>
                 <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="交货得分取月度评审(当月按上月起往前查,无则默认50)">交货得分</th>
+                {/if}
                 <th class="text-center" style="width:72px;">总分</th>
                 <th style="width:120px;">工序名称</th>
                 <th class="text-center" style="width:112px;">单价</th>
@@ -521,6 +534,9 @@
                 <td class="text-center col-rank"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-size:17px;font-weight:700;">{if condition="!empty($quoteVisible)"}{$co.service_rank_text|default=''|htmlentities}{/if}</td>
                 <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($quoteVisible)"}{$co.service_quality_score_text|default=''|htmlentities}{/if}</td>
                 <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;"{if condition="!empty($quoteVisible)"} title="单价合计 {$co.service_price_sum_text|default=''|htmlentities}"{/if}>{if condition="!empty($quoteVisible)"}{$co.service_price_score_text|default=''|htmlentities}{/if}</td>
+                {if !empty($showServiceLeadScore)}
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($quoteVisible)"}{$co.service_lead_score_text|default=''|htmlentities}{/if}</td>
+                {/if}
                 <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-weight:600;">{if condition="!empty($quoteVisible)"}{$co.service_score_text|default=''|htmlentities}{/if}</td>
                 {/if}
                 <td class="outward-process-name">{$ql.cgymc|default=''|htmlentities}</td>

+ 45 - 29
application/common/library/ProcuremenSupplierScore.php

@@ -13,6 +13,7 @@ use think\Log;
  * - supplier_service_final_score:月度评审表(四项自动统计,满分 105;qp_manual=1 手工锁定不覆盖)
  *
  * 开标单笔总分 = 质量得分×质量% + 价格得分 [+ 交货得分×交货%]
+ * 开标质量/交货得分:均取月度评审表(当月按上月起往前查,无则默认 50)
  * 开标价格得分 = (本单最低单价合计 / 本供应商单价合计) × 价格权重% × 100
  *
  * 月度评审(与开标单笔分离):
@@ -2275,12 +2276,13 @@ class ProcuremenSupplierScore
     }
 
     /**
-     * 从月度评审表取商务技术分:当月按上月起往前查,都没有则默认 50
+     * 从月度评审表取指定分项:当月按上月起往前查,都没有则默认 50
      *
      * @param array<int, string> $companyNames
-     * @return array<string, float> company_name => quality_score
+     * @param 'quality_score'|'delivery_score' $field
+     * @return array<string, float> company_name => score
      */
-    public static function loadReviewQualityScoreMap(array $companyNames, ?string $asOfYm = null): array
+    protected static function loadReviewFieldScoreMap(array $companyNames, string $field, ?string $asOfYm = null): array
     {
         $asOfYm = self::normalizeYm($asOfYm);
         $default = 50.0;
@@ -2293,17 +2295,17 @@ class ProcuremenSupplierScore
                 $map[$n] = $default;
             }
         }
-        if ($names === []) {
+        if ($names === [] || !in_array($field, ['quality_score', 'delivery_score'], true)) {
             return $map;
         }
         self::ensureFinalScoreTable();
         try {
-            // 当月按上个月起查:ym < asOfYm,且商务技术分已填写
+            // 当月按上个月起查:ym < asOfYm,且该分项已填写
             $rows = Db::table(self::TABLE_FINAL)
                 ->where('company_name', 'in', array_keys($names))
                 ->where('ym', '<', $asOfYm)
-                ->whereNotNull('quality_score')
-                ->field('company_name,ym,quality_score')
+                ->whereNotNull($field)
+                ->field('company_name,ym,' . $field)
                 ->order('ym', 'desc')
                 ->select();
         } catch (\Throwable $e) {
@@ -2321,7 +2323,7 @@ class ProcuremenSupplierScore
             if ($cn === '' || isset($seen[$cn])) {
                 continue;
             }
-            $score = self::readOptionalScoreValue($r['quality_score'] ?? null);
+            $score = self::readOptionalScoreValue($r[$field] ?? null);
             if ($score === null) {
                 continue;
             }
@@ -2332,6 +2334,28 @@ class ProcuremenSupplierScore
         return $map;
     }
 
+    /**
+     * 从月度评审表取质量得分:当月按上月起往前查,都没有则默认 50
+     *
+     * @param array<int, string> $companyNames
+     * @return array<string, float> company_name => quality_score
+     */
+    public static function loadReviewQualityScoreMap(array $companyNames, ?string $asOfYm = null): array
+    {
+        return self::loadReviewFieldScoreMap($companyNames, 'quality_score', $asOfYm);
+    }
+
+    /**
+     * 从月度评审表取交货得分:当月按上月起往前查,都没有则默认 50(与质量得分口径一致)
+     *
+     * @param array<int, string> $companyNames
+     * @return array<string, float> company_name => delivery_score
+     */
+    public static function loadReviewDeliveryScoreMap(array $companyNames, ?string $asOfYm = null): array
+    {
+        return self::loadReviewFieldScoreMap($companyNames, 'delivery_score', $asOfYm);
+    }
+
     /**
      * 按公司名批量取供应商 id(兼容旧调用保留评分字段结构)
      *
@@ -2420,10 +2444,10 @@ class ProcuremenSupplierScore
         }
         $custMap = self::loadCustomerScoreMap($names);
         $reviewMap = self::loadReviewQualityScoreMap($names, $asOfYm);
+        $deliveryMap = self::loadReviewDeliveryScoreMap($names, $asOfYm);
 
         $items = [];
         $priceSums = [];
-        $leadSums = [];
         foreach ($quoteGroups as $g) {
             if (!is_array($g)) {
                 continue;
@@ -2463,8 +2487,9 @@ class ProcuremenSupplierScore
                     }
                 }
             }
-            // 商务/技术得分:月度评审「商务技术分」,当月按上月起往前,默认 50
+            // 质量/交货得分:月度评审表,当月按上月起往前,默认 50
             $quality = (float)($reviewMap[$cn] ?? 50);
+            $deliveryScore = (float)($deliveryMap[$cn] ?? 50);
             $psum = $hasPrice ? round($priceSum, 2) : 0;
             $lsum = $hasLead ? (int)$leadSum : 0;
             $items[$cn] = [
@@ -2484,8 +2509,8 @@ class ProcuremenSupplierScore
                 'lead_weight'          => (int)$lw,
                 'lead_enabled'         => $leadEnabled,
                 'has_lead'             => $hasLead,
-                'lead_score'           => 0.0,
-                'lead_score_text'      => '0',
+                'lead_score'           => $leadEnabled ? $deliveryScore : 0.0,
+                'lead_score_text'      => $leadEnabled ? self::formatScore($deliveryScore) : '0',
                 'score'                => 0.0,
                 'rank_no'              => 0,
                 'score_text'           => '',
@@ -2494,9 +2519,6 @@ class ProcuremenSupplierScore
             if ($hasPrice && $priceSum > 0) {
                 $priceSums[$cn] = $priceSum;
             }
-            if ($hasLead && $leadSum > 0) {
-                $leadSums[$cn] = $leadSum;
-            }
         }
 
         $minSum = null;
@@ -2505,12 +2527,6 @@ class ProcuremenSupplierScore
                 $minSum = $ps;
             }
         }
-        $minLead = null;
-        foreach ($leadSums as $ls) {
-            if ($minLead === null || $ls < $minLead) {
-                $minLead = $ls;
-            }
-        }
         foreach ($items as $cn => &$it) {
             // 单价未填写:价格得分与总分均为 0
             if ($pw > 0 && (empty($it['has_price']) || (float)($it['price_sum'] ?? 0) <= 0)) {
@@ -2532,15 +2548,15 @@ class ProcuremenSupplierScore
             }
             $it['price_score_text'] = self::formatScore($it['price_score']);
 
-            if ($leadEnabled && !empty($it['has_lead']) && $it['lead_days_sum'] > 0 && $minLead !== null && $minLead > 0) {
-                $leadRatio = round($minLead / (float)$it['lead_days_sum'], 4);
-                $it['lead_score'] = round($leadRatio * 100, 2);
-            } else {
+            // 交货得分已在上方取自月度评审(与质量得分同口径)
+            if (!$leadEnabled) {
                 $it['lead_score'] = 0.0;
+                $it['lead_score_text'] = '0';
+            } else {
+                $it['lead_score_text'] = self::formatScore($it['lead_score']);
             }
-            $it['lead_score_text'] = self::formatScore($it['lead_score']);
 
-            // 总分 = 商务/技术得分×质量% + 价格得分(已含价格权重) [+ 交货得分×交货%]
+            // 总分 = 质量得分×质量% + 价格得分(已含价格权重) [+ 交货得分×交货%]
             $total = 0.0;
             if ($qw > 0) {
                 $total += $it['quality_score'] * ($qw / 100);
@@ -2549,7 +2565,7 @@ class ProcuremenSupplierScore
                 $total += (float)$it['price_score'];
             }
             if ($lw > 0) {
-                $total += $it['lead_score'] * ($lw / 100);
+                $total += (float)$it['lead_score'] * ($lw / 100);
             }
             $it['score'] = round($total, 2);
             $it['score_text'] = self::formatScore($it['score']);
@@ -2668,7 +2684,7 @@ class ProcuremenSupplierScore
         $formula = $parts === []
             ? '(总分)=(质量得分×50%)+(价格得分)'
             : '(总分)=' . implode('+', $parts);
-        $formula .= ';质量得分取月度评审「质量得分」(当月按上月起往前查,无则默认50)';
+        $formula .= ';质量得分、交货得分取月度评审(当月按上月起往前查,无则默认50)';
         if ($pw > 0) {
             $formula .= ';价格得分=(最低单价合计/本供应商单价合计)×'
                 . self::formatWeightPercent($pw) . '%×100';

+ 8 - 1
public/assets/js/backend/procuremen.js

@@ -3370,6 +3370,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         + '<th style="width:72px;">姓名</th>'
                         + '<th style="width:150px;">邮箱</th>'
                         + '<th style="width:100px;">手机号</th>'
+                        + '<th style="width:72px;" class="text-center">状态</th>'
                         + '<th style="width:96px;" class="text-center">操作</th>'
                         + '</tr></thead><tbody>';
                     list.forEach(function (item, idx) {
@@ -3380,12 +3381,17 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         var emailShow = email ? maskProcuremenEmail(email) : '';
                         var phoneShow = phone ? maskProcuremenPhone(phone) : '';
                         var canNotify = item && parseInt(item.can_notify, 10) === 1;
-                        html += '<tr>'
+                        var notified = item && parseInt(item.notified, 10) === 1;
+                        var statusHtml = notified
+                            ? '<span class="text-success">已通知</span>'
+                            : '<span class="text-muted">未通知</span>';
+                        html += '<tr data-company-name="' + rfqEscAttr(name) + '">'
                             + '<td class="text-center">' + (idx + 1) + '</td>'
                             + '<td>' + rfqEscHtml(name) + '</td>'
                             + '<td>' + (username ? rfqEscHtml(username) : '') + '</td>'
                             + '<td style="word-break:break-all;">' + (emailShow ? rfqEscHtml(emailShow) : '') + '</td>'
                             + '<td>' + (phoneShow ? rfqEscHtml(phoneShow) : '') + '</td>'
+                            + '<td class="text-center rfq-notify-status">' + statusHtml + '</td>'
                             + '<td class="text-center">';
                         if (canNotify) {
                             html += '<a href="javascript:;" class="btn btn-xs btn-default btn-rfq-send-mail" data-scydgy-id="'
@@ -3572,6 +3578,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 }, function (data, ret) {
                     var msg = (ret && ret.msg) ? ret.msg : '邮件已发送';
                     Toastr.success(msg);
+                    $btn.closest('tr').find('.rfq-notify-status').html('<span class="text-success">已通知</span>');
                     table.bootstrapTable('refresh');
                     return false;
                 });