m0_70156489 1 month ago
parent
commit
c061c623ae

+ 85 - 0
application/admin/command/ProcuremenRegenPdf.php

@@ -0,0 +1,85 @@
+<?php
+
+namespace app\admin\command;
+
+use app\admin\controller\Procuremen;
+use app\common\library\ProcuremenStatus;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Db;
+
+/**
+ * 按订单号重生成已完结存证 PDF
+ * 用法:php think procuremen:regen-pdf --ccydh=202605584L,202605694S
+ */
+class ProcuremenRegenPdf extends Command
+{
+    protected function configure()
+    {
+        $this->setName('procuremen:regen-pdf')
+            ->addOption('ccydh', null, Option::VALUE_REQUIRED, '订单号,多个用英文逗号分隔')
+            ->setDescription('Regenerate archive PDF for completed procuremen orders');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $raw = trim((string)$input->getOption('ccydh'));
+        if ($raw === '') {
+            $output->writeln('<error>请传 --ccydh=订单号</error>');
+
+            return;
+        }
+        $list = [];
+        foreach (preg_split('/[,,\s]+/u', $raw) as $one) {
+            $one = trim((string)$one);
+            if ($one !== '') {
+                $list[$one] = true;
+            }
+        }
+        if ($list === []) {
+            $output->writeln('<error>订单号无效</error>');
+
+            return;
+        }
+        foreach (array_keys($list) as $ccydh) {
+            try {
+                $rows = Db::table('purchase_order')
+                    ->where('CCYDH', $ccydh)
+                    ->field('id,scydgy_id,CCYDH,status,pdf_url')
+                    ->select();
+            } catch (\Throwable $e) {
+                $output->writeln('<error>' . $ccydh . ' 查询失败:' . $e->getMessage() . '</error>');
+                continue;
+            }
+            if (!is_array($rows) || $rows === []) {
+                $output->writeln('<comment>' . $ccydh . ' 未找到 purchase_order</comment>');
+                continue;
+            }
+            foreach ($rows as $po) {
+                if (!is_array($po)) {
+                    continue;
+                }
+                $sid = (int)($po['scydgy_id'] ?? 0);
+                $status = (string)($po['status'] ?? '');
+                $completed = ProcuremenStatus::isPoCompleted($status);
+                $output->writeln(sprintf(
+                    '处理 %s scydgy_id=%d status=%s completed=%s',
+                    $ccydh,
+                    $sid,
+                    $status !== '' ? $status : '(空)',
+                    $completed ? 'yes' : 'no'
+                ));
+                // 强制按当前详情重存(含操作记录),不强制要求已完结标记
+                $path = Procuremen::regenerateArchivePdfWithoutAuth($sid, false);
+                if ($path === '') {
+                    $output->writeln('<error>  PDF 生成失败 scydgy_id=' . $sid . '</error>');
+                } else {
+                    $output->writeln('<info>  PDF 已更新:' . $path . '</info>');
+                }
+            }
+        }
+        $output->writeln('<info>完成</info>');
+    }
+}

+ 177 - 15
application/admin/controller/Procuremen.php

@@ -4759,7 +4759,8 @@ class Procuremen extends Backend
             ];
             $qtySnap = trim((string)($data['This_quantity'] ?? ''));
             $priceSnap = trim((string)($data['ceilingPrice'] ?? ''));
-            if (!$exists && $qtySnap === '' && $priceSnap === '') {
+            $mbzSnap = trim((string)($data['MBZ'] ?? ''));
+            if (!$exists && $qtySnap === '' && $priceSnap === '' && $mbzSnap === '') {
                 $this->success('操作成功');
 
                 return;
@@ -4783,7 +4784,7 @@ class Procuremen extends Backend
 
     /**
      * 未发列表
-     * 「完结」或「仅保存本次数量/最高限价」:有则改、无则插
+     * 「完结」或「仅保存本次数量/最高限价/备注」:有则改、无则插
      * POST finish=1(默认):并置 status=1;finish=0:更新时不改 status;新增不写 status。
      */
     public function completeDirectly()
@@ -4845,7 +4846,8 @@ class Procuremen extends Backend
 
             $qtyCd = trim((string)($data['This_quantity'] ?? ''));
             $priceCd = trim((string)($data['ceilingPrice'] ?? ''));
-            if (!$asComplete && !$exists && $qtyCd === '' && $priceCd === '') {
+            $mbzCd = trim((string)($data['MBZ'] ?? ''));
+            if (!$asComplete && !$exists && $qtyCd === '' && $priceCd === '' && $mbzCd === '') {
                 $this->success('操作成功');
 
                 return;
@@ -4894,13 +4896,15 @@ class Procuremen extends Backend
             }
         } else {
             $gymc = trim((string)($data['CGYMC'] ?? $row['CGYMC'] ?? ''));
+            $mbz = isset($data['MBZ']) ? trim((string)$data['MBZ']) : '';
             $qtyPart = '本次数量「' . ($q !== '' ? $q : '') . '」';
             $pricePart = '最高限价「' . ($p !== '' ? $p : '') . '」';
-            $detail = ($gymc !== '' ? ($gymc . $qtyPart) : $qtyPart) . ',' . $pricePart;
+            $mbzPart = '备注「' . ($mbz !== '' ? $mbz : '') . '」';
+            $detail = ($gymc !== '' ? ($gymc . $qtyPart) : $qtyPart) . ',' . $pricePart . ',' . $mbzPart;
             $this->addOrderLog(
                 $ids,
                 'save_qty_price',
-                '保存本次数量、最高限价:' . $detail,
+                '保存本次数量、最高限价、备注:' . $detail,
                 $poIdLog
             );
         }
@@ -4909,7 +4913,7 @@ class Procuremen extends Backend
     }
 
     /**
-     * 未发列表:从 purchase_order 合并已填的本次数量、最高限价(若表中有对应列)
+     * 未发列表:从 purchase_order 合并已填的本次数量、最高限价、备注(若表中有对应列)
      *
      * @param array<int, array> $rows 引用传递当前页行
      */
@@ -4936,7 +4940,7 @@ class Procuremen extends Backend
         try {
             $list = Db::table('purchase_order')
                 ->where('scydgy_id', 'in', $idList)
-                ->field('scydgy_id,This_quantity,ceilingPrice')
+                ->field('scydgy_id,This_quantity,ceilingPrice,MBZ')
                 ->select();
         } catch (\Throwable $e) {
             return;
@@ -4960,7 +4964,7 @@ class Procuremen extends Backend
                 continue;
             }
             $db = $byId[$sid];
-            // 有 purchase_order 记录时同步数量/限价(含清空后的空串,避免刷新后又冒出来)
+            // 有 purchase_order 记录时同步数量/限价/备注(含清空后的空串,避免刷新后又冒出来)
             if (array_key_exists('This_quantity', $db)) {
                 $rw['This_quantity'] = $db['This_quantity'] === null ? '' : $db['This_quantity'];
             }
@@ -4969,6 +4973,9 @@ class Procuremen extends Backend
             } elseif (array_key_exists('ceiling_price', $db)) {
                 $rw['ceilingPrice'] = $db['ceiling_price'] === null ? '' : $db['ceiling_price'];
             }
+            if (array_key_exists('MBZ', $db)) {
+                $rw['MBZ'] = $db['MBZ'] === null ? '' : $db['MBZ'];
+            }
         }
         unset($rw);
     }
@@ -5540,6 +5547,7 @@ class Procuremen extends Backend
 
     /**
      * 获取当前登录用户信息 [id, 展示名]
+     * admin_name 一律记「名称」(昵称),无昵称再回退账号
      */
     protected function GetUseName(): array
     {
@@ -5551,8 +5559,24 @@ class Procuremen extends Backend
                 if (is_array($u)) {
                     $id = (int)($u['id'] ?? 0);
                     $name = trim((string)($u['nickname'] ?? ''));
+                    // 会话昵称缺失或等于登录账号时,再查库取正式昵称
+                    $sessionUser = trim((string)($u['username'] ?? ''));
+                    if ($id > 0 && ($name === '' || ($sessionUser !== '' && strcasecmp($name, $sessionUser) === 0))) {
+                        try {
+                            $row = Db::name('admin')->where('id', $id)->field('nickname,username')->find();
+                            if (is_array($row)) {
+                                $nick = trim((string)($row['nickname'] ?? ''));
+                                if ($nick !== '') {
+                                    $name = $nick;
+                                } elseif ($name === '') {
+                                    $name = trim((string)($row['username'] ?? ''));
+                                }
+                            }
+                        } catch (\Throwable $e) {
+                        }
+                    }
                     if ($name === '') {
-                        $name = trim((string)($u['username'] ?? ''));
+                        $name = $sessionUser;
                     }
                 }
             }
@@ -6392,6 +6416,7 @@ class Procuremen extends Backend
         return [
             'ok'    => $main !== [] || $details !== [],
             'ccydh' => $ccydh,
+            'steps' => $bundle['steps'],
         ];
     }
 
@@ -6409,6 +6434,20 @@ class Procuremen extends Backend
             $this->error(__('Invalid parameters'));
         }
         $this->assertManualOrderDetailsViewable((int)$ids);
+
+        // 已完结:打开详情时按最新进度/操作记录重存存证 PDF(含质量/交货评分)
+        $sid = (int)$ids;
+        if ($this->isValidScydgyRowId($sid)) {
+            try {
+                $poCheck = Db::table('purchase_order')->where('scydgy_id', $sid)->find();
+                if (is_array($poCheck) && ProcuremenStatus::isPoCompleted($poCheck['status'] ?? '')) {
+                    $this->refreshCompletedOrderArchivePdf($sid, true);
+                }
+            } catch (\Throwable $e) {
+                Log::write('详情页刷新完结存证PDF失败 scydgy_id=' . $sid . ' ' . $e->getMessage(), 'notice');
+            }
+        }
+
         $this->prepareProcuremenDetailsView($ids);
 
         /* 弹层内不套 default 布局,避免出现「控制台 / Control panel」整块标题区 */
@@ -6423,6 +6462,74 @@ class Procuremen extends Backend
         }
     }
 
+    /**
+     * 已完结订单重生成存证 PDF(与详情页一致,含质量/交货评分与「已完结」节点)
+     *
+     * @param bool $requireCompleted true 时仅已完结才重生成;false 强制按当前详情重存
+     * @return string 本地 web 路径或 OSS URL,失败为空串
+     */
+    protected function refreshCompletedOrderArchivePdf(int $scydgyId, bool $requireCompleted = true): string
+    {
+        if (!$this->isValidScydgyRowId($scydgyId)) {
+            return '';
+        }
+        try {
+            $po = Db::table('purchase_order')->where('scydgy_id', $scydgyId)->find();
+        } catch (\Throwable $e) {
+            $po = null;
+        }
+        if (!is_array($po) || $po === []) {
+            return '';
+        }
+        if ($requireCompleted && !ProcuremenStatus::isPoCompleted($po['status'] ?? '')) {
+            return trim((string)($po['pdf_url'] ?? ''));
+        }
+        $poId = (int)($po['id'] ?? 0);
+
+        return $this->savePurchaseConfirmDetailPdf($scydgyId, $poId);
+    }
+
+    /**
+     * CLI / 手机端评分完结后调用:无后台登录态重生成存证 PDF
+     */
+    public static function regenerateArchivePdfWithoutAuth(int $scydgyId, bool $requireCompleted = true): string
+    {
+        if ((int)$scydgyId === 0) {
+            return '';
+        }
+        try {
+            $ctl = new class extends Procuremen {
+                public function _initialize()
+                {
+                    $this->request = \think\Request::instance();
+                    $tpl = \think\Config::get('template');
+                    if (!is_array($tpl)) {
+                        $tpl = [];
+                    }
+                    // CLI / 跨模块调用时无 admin 视图目录
+                    $tpl['view_path'] = APP_PATH . 'admin' . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR;
+                    $this->view = new \think\View($tpl, \think\Config::get('view_replace_str'));
+                    $this->model = new \app\admin\model\Procuremen();
+                    try {
+                        ProcuremenSchema::ensureAll();
+                    } catch (\Throwable $e) {
+                    }
+                }
+
+                public function runRegen(int $sid, bool $requireCompleted): string
+                {
+                    return $this->refreshCompletedOrderArchivePdf($sid, $requireCompleted);
+                }
+            };
+
+            return $ctl->runRegen($scydgyId, $requireCompleted);
+        } catch (\Throwable $e) {
+            Log::write('regenerateArchivePdfWithoutAuth 失败 scydgy_id=' . $scydgyId . ' ' . $e->getMessage(), 'error');
+
+            return '';
+        }
+    }
+
     /**
      * 解析合并审核工序行(订单号须一致,且均未协助)
      *
@@ -13409,9 +13516,10 @@ class Procuremen extends Backend
     }
 
     /**
-     * 采购确认成功后:用与「详情」弹窗相同的模板片段渲染 HTML,再存为 PDF(改 details_fragment 后 PDF 同步变化)。
+     * 采购确认成功后 / 已完结刷新:用与「详情」弹窗相同的模板片段渲染 HTML,再存为 PDF(改 details_fragment 后 PDF 同步变化)。
      * 优先上传至阿里云 OSS(application/config.php 的 oss 节点);失败或未配置时回退到 public 下与 objectKey 相同目录结构。
      * 成功后将相对路径写入 purchase_order.pdf_url(形如 /xinhua/年/月/日/scydgy_id/订单号_scydgy_id.pdf)。
+     * 说明:审批通过时会先存一版;质量+交货评分使订单「已完结」后,详情页打开或评分保存完成时会再重存,以含最终步骤与操作记录。
      *
      * @return string OSS 返回 https 完整 URL;本地回退为以 / 开头的 Web 路径;失败返回空串
      */
@@ -13431,10 +13539,56 @@ class Procuremen extends Backend
         $objectKey = $paths['objectKey'];
         $webPath = $paths['webPath'];
 
-        $meta = sprintf('工序行ID %s | 主表订单ID %d | PDF生成时间 %s', $ids, (int)$purchaseOrderId, date('Y-m-d H:i:s'));
+        $cyjmc = '';
+        try {
+            $poMeta = Db::table('purchase_order')->where('scydgy_id', $scydgyId)->field('CYJMC')->find();
+            if (is_array($poMeta)) {
+                $cyjmc = trim((string)($poMeta['CYJMC'] ?? ''));
+            }
+        } catch (\Throwable $e) {
+            $cyjmc = '';
+        }
+        $genTime = date('Y-m-d H:i:s');
+        $metaIds = '工序行ID ' . $ids . ' | 主表订单ID ' . (int)$purchaseOrderId;
+        $metaLeft = $metaIds . ($cyjmc !== '' ? ' | ' . $cyjmc : '');
+        // PDF 进度:去掉「未发」;节点用纯色圆点(不用图片,避免 mPDF 破图)
+        $pdfSteps = [];
+        $viewSteps = $prep['steps'] ?? [];
+        if (is_array($viewSteps)) {
+            foreach ($viewSteps as $st) {
+                if (!is_array($st)) {
+                    continue;
+                }
+                if (trim((string)($st['title'] ?? '')) === '未发') {
+                    continue;
+                }
+                $pdfSteps[] = $st;
+            }
+        }
+        $pdfStepW = count($pdfSteps) > 0 ? (round(100 / count($pdfSteps), 2) . '%') : '16.66%';
+        $nPdf = count($pdfSteps);
+        foreach ($pdfSteps as $idx => &$pst) {
+            if ($idx === 0) {
+                $pst['pdf_left_bg'] = '';
+            } else {
+                $pst['pdf_left_bg'] = !empty($pdfSteps[$idx - 1]['done']) ? '#1890ff' : '#e0e0e0';
+            }
+            if ($idx >= $nPdf - 1) {
+                $pst['pdf_right_bg'] = '';
+            } else {
+                $pst['pdf_right_bg'] = !empty($pst['done']) ? '#1890ff' : '#e0e0e0';
+            }
+        }
+        unset($pst);
         $this->view->assign([
-            'pdf_export'   => 1,
-            'pdfMetaLine' => $meta,
+            'pdf_export'    => 1,
+            'pdfMetaLine'   => $metaLeft,
+            'pdfMetaLeft'   => $metaLeft,
+            'pdfMetaIds'    => $metaIds,
+            'pdfMetaTitle'  => $cyjmc,
+            'pdfMetaTime'   => 'PDF生成时间 ' . $genTime,
+            'pdfSteps'      => $pdfSteps,
+            'pdfStepWidth'  => $pdfStepW,
         ]);
         // 关闭后台 layout,避免 default 布局里的「控制台」面包屑等被打进 PDF
         $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false;
@@ -13446,14 +13600,22 @@ class Procuremen extends Backend
                 $this->view->engine->layout($restoreLayout);
             }
             Log::write('采购确认PDF模板渲染失败: ' . $e->getMessage(), 'error');
-            $this->view->assign(['pdf_export' => '', 'pdfMetaLine' => '']);
+            $this->view->assign([
+                'pdf_export' => '', 'pdfMetaLine' => '', 'pdfMetaLeft' => '',
+                'pdfMetaIds' => '', 'pdfMetaTitle' => '', 'pdfMetaTime' => '',
+                'pdfSteps' => [], 'pdfStepWidth' => '',
+            ]);
 
             return '';
         }
         if ($restoreLayout) {
             $this->view->engine->layout($restoreLayout);
         }
-        $this->view->assign(['pdf_export' => '', 'pdfMetaLine' => '']);
+        $this->view->assign([
+            'pdf_export' => '', 'pdfMetaLine' => '', 'pdfMetaLeft' => '',
+            'pdfMetaIds' => '', 'pdfMetaTitle' => '', 'pdfMetaTime' => '',
+            'pdfSteps' => [], 'pdfStepWidth' => '',
+        ]);
 
         $tempDir = ROOT_PATH . 'runtime' . DIRECTORY_SEPARATOR . 'mpdf_tmp';
         if (!is_dir($tempDir)) {

+ 111 - 43
application/admin/controller/Procuremenarchive.php

@@ -117,13 +117,16 @@ class Procuremenarchive extends Backend
                 }
             };
 
-            $sortField = preg_match('/^[a-zA-Z0-9_]+$/', (string)$sort) ? $sort : 'id';
+            $sortField = preg_match('/^[a-zA-Z0-9_]+$/', (string)$sort) ? $sort : 'complete_time';
             $orderDir = strtoupper((string)$order) === 'ASC' ? 'ASC' : 'DESC';
+            // 完结/审批时间为计算字段,库表排序仅用真实列
+            $dbSortAllow = ['id', 'CCYDH', 'CYJMC', 'CGYMC', 'pick_company_name', 'createtime', 'pick_time', 'dStamp'];
+            $dbSortField = in_array($sortField, $dbSortAllow, true) ? $sortField : 'id';
             $listQuery = Db::table('purchase_order');
             $applyFilters($listQuery);
             $rows = $listQuery
                 ->field('id,scydgy_id,CCYDH,CYJMC,CGYMC,pick_company_name,createtime,dStamp,pick_time')
-                ->order($sortField, $orderDir)
+                ->order($dbSortField, $orderDir)
                 ->select();
             if (!is_array($rows)) {
                 $rows = [];
@@ -139,16 +142,22 @@ class Procuremenarchive extends Backend
                     $sidList[$sid] = true;
                 }
             }
+            // 审批时间 = 审核确认供应商;完结时间 = 直接完结 / 质量评分(与详情「已完结」一致)
+            $approveTsMap = ProcuremenTime::loadOperLogTimestampMap(
+                array_keys($sidList),
+                ['purchase_confirm']
+            );
             $completeTsMap = ProcuremenTime::loadOperLogTimestampMap(
                 array_keys($sidList),
-                ['purchase_confirm', 'mark_complete', 'inbound_score']
+                ['mark_complete', 'inbound_score']
             );
             $inboundMap = [];
+            $inboundTimeMap = [];
             if ($sidList !== []) {
                 try {
                     $inRows = Db::table('purchase_order_inbound_score')
                         ->where('scydgy_id', 'in', array_keys($sidList))
-                        ->field('scydgy_id,result')
+                        ->field('scydgy_id,result,updatetime,createtime')
                         ->select();
                 } catch (\Throwable $e) {
                     $inRows = [];
@@ -159,8 +168,13 @@ class Procuremenarchive extends Backend
                             continue;
                         }
                         $isid = (int)($ir['scydgy_id'] ?? 0);
-                        if ($isid !== 0) {
-                            $inboundMap[$isid] = trim((string)($ir['result'] ?? ''));
+                        if ($isid === 0) {
+                            continue;
+                        }
+                        $inboundMap[$isid] = trim((string)($ir['result'] ?? ''));
+                        $its = ProcuremenTime::parseToTimestamp($ir['updatetime'] ?? $ir['createtime'] ?? null);
+                        if ($its > 946684800) {
+                            $inboundTimeMap[$isid] = $its;
                         }
                     }
                 }
@@ -172,41 +186,75 @@ class Procuremenarchive extends Backend
                     continue;
                 }
                 $sid = (int)($r['scydgy_id'] ?? 0);
-                $done = ProcuremenTime::resolveCompletedDone($r, $completeTsMap);
+                $approve = ProcuremenTime::resolveCompletedDone($r, $approveTsMap);
+                // 完结:操作日志优先;无日志时用入库评分表时间兜底
+                $completeMap = $completeTsMap;
+                if ($sid > 0 && empty($completeMap[$sid]) && isset($inboundTimeMap[$sid])) {
+                    $completeMap[$sid] = $inboundTimeMap[$sid];
+                }
+                $complete = ProcuremenTime::resolveCompletedDone($r, $completeMap);
+                // 未评分完结时勿用 pick_time 冒充完结:无完结日志则留空(审批时间仍单独展示)
+                if ($sid > 0 && empty($completeTsMap[$sid]) && empty($inboundTimeMap[$sid])) {
+                    $complete = ['ts' => 0, 'text' => ''];
+                }
                 $out[] = [
-                    'id'                => (int)($r['id'] ?? 0),
-                    'scydgy_id'         => $sid,
-                    'CCYDH'             => trim((string)($r['CCYDH'] ?? '')),
-                    'CYJMC'             => trim((string)($r['CYJMC'] ?? '')),
-                    'CGYMC'             => trim((string)($r['CGYMC'] ?? '')),
-                    'pick_company_name' => trim((string)($r['pick_company_name'] ?? '')),
-                    'inbound_result'    => $inboundMap[$sid] ?? '',
-                    'createtime'        => $done['ts'],
-                    'createtime_text'   => $done['text'],
+                    'id'                 => (int)($r['id'] ?? 0),
+                    'scydgy_id'          => $sid,
+                    'CCYDH'              => trim((string)($r['CCYDH'] ?? '')),
+                    'CYJMC'              => trim((string)($r['CYJMC'] ?? '')),
+                    'CGYMC'              => trim((string)($r['CGYMC'] ?? '')),
+                    'pick_company_name'  => trim((string)($r['pick_company_name'] ?? '')),
+                    'inbound_result'     => $inboundMap[$sid] ?? '',
+                    'createtime'         => $approve['ts'],
+                    'createtime_text'    => $approve['text'],
+                    'complete_time'      => $complete['ts'],
+                    'complete_time_text' => $complete['text'],
                 ];
             }
 
             $merged = $this->collapseArchiveRowsByOrder($out);
             $nMerged = count($merged);
-            if ($nMerged > 1 && $sortField === 'id') {
-                usort($merged, function ($a, $b) use ($orderDir) {
-                    $cmp = ((int)($a['id'] ?? 0)) <=> ((int)($b['id'] ?? 0));
-                    if ($cmp === 0) {
-                        return strcmp((string)($a['CCYDH'] ?? ''), (string)($b['CCYDH'] ?? ''));
-                    }
+            if ($nMerged > 1) {
+                if ($sortField === 'id') {
+                    usort($merged, function ($a, $b) use ($orderDir) {
+                        $cmp = ((int)($a['id'] ?? 0)) <=> ((int)($b['id'] ?? 0));
+                        if ($cmp === 0) {
+                            return strcmp((string)($a['CCYDH'] ?? ''), (string)($b['CCYDH'] ?? ''));
+                        }
 
-                    return $orderDir === 'ASC' ? $cmp : -$cmp;
-                });
-            } elseif ($nMerged > 1 && $sortField === 'createtime') {
-                usort($merged, function ($a, $b) use ($orderDir) {
-                    $ta = (int)($a['createtime'] ?? 0);
-                    $tb = (int)($b['createtime'] ?? 0);
-                    if ($ta === $tb) {
-                        return ((int)($b['id'] ?? 0)) <=> ((int)($a['id'] ?? 0));
-                    }
+                        return $orderDir === 'ASC' ? $cmp : -$cmp;
+                    });
+                } elseif ($sortField === 'createtime') {
+                    usort($merged, function ($a, $b) use ($orderDir) {
+                        $ta = (int)($a['createtime'] ?? 0);
+                        $tb = (int)($b['createtime'] ?? 0);
+                        if ($ta === $tb) {
+                            return ((int)($b['id'] ?? 0)) <=> ((int)($a['id'] ?? 0));
+                        }
 
-                    return $orderDir === 'ASC' ? ($ta <=> $tb) : ($tb <=> $ta);
-                });
+                        return $orderDir === 'ASC' ? ($ta <=> $tb) : ($tb <=> $ta);
+                    });
+                } else {
+                    // 默认 / complete_time:按完结时间;无完结时间的排后面
+                    usort($merged, function ($a, $b) use ($orderDir) {
+                        $ta = (int)($a['complete_time'] ?? 0);
+                        $tb = (int)($b['complete_time'] ?? 0);
+                        if ($ta <= 0 && $tb <= 0) {
+                            return ((int)($b['id'] ?? 0)) <=> ((int)($a['id'] ?? 0));
+                        }
+                        if ($ta <= 0) {
+                            return 1;
+                        }
+                        if ($tb <= 0) {
+                            return -1;
+                        }
+                        if ($ta === $tb) {
+                            return ((int)($b['id'] ?? 0)) <=> ((int)($a['id'] ?? 0));
+                        }
+
+                        return $orderDir === 'ASC' ? ($ta <=> $tb) : ($tb <=> $ta);
+                    });
+                }
             }
 
             $pageRows = array_slice($merged, $offset, $limit);
@@ -273,24 +321,44 @@ class Procuremenarchive extends Backend
             $merged['pick_company_name'] = implode('、', $supplierList);
             $merged['inbound_result'] = $inboundList !== [] ? implode('、', $inboundList) : '';
             $merged['process_count'] = count($groupRows);
-            $latestTs = 0;
-            $latestText = '';
+            $latestApproveTs = 0;
+            $latestApproveText = '';
+            $latestCompleteTs = 0;
+            $latestCompleteText = '';
             foreach ($groupRows as $r) {
                 $ts = (int)($r['createtime'] ?? 0);
                 if ($ts <= 0 && !empty($r['createtime_text'])) {
                     $ts = (int)strtotime((string)$r['createtime_text']);
                 }
-                if ($ts > $latestTs) {
-                    $latestTs = $ts;
-                    $latestText = trim((string)($r['createtime_text'] ?? ''));
+                if ($ts > $latestApproveTs) {
+                    $latestApproveTs = $ts;
+                    $latestApproveText = trim((string)($r['createtime_text'] ?? ''));
+                }
+                $cts = (int)($r['complete_time'] ?? 0);
+                if ($cts <= 0 && !empty($r['complete_time_text'])) {
+                    $cts = (int)strtotime((string)$r['complete_time_text']);
+                }
+                if ($cts > $latestCompleteTs) {
+                    $latestCompleteTs = $cts;
+                    $latestCompleteText = trim((string)($r['complete_time_text'] ?? ''));
+                }
+            }
+            if ($latestApproveTs > 0) {
+                $merged['createtime'] = $latestApproveTs;
+                if ($latestApproveText === '') {
+                    $latestApproveText = date('Y-m-d H:i:s', $latestApproveTs);
                 }
+                $merged['createtime_text'] = ProcuremenTime::formatDisplayDateTime($latestApproveText);
             }
-            if ($latestTs > 0) {
-                $merged['createtime'] = $latestTs;
-                if ($latestText === '') {
-                    $latestText = date('Y-m-d H:i:s', $latestTs);
+            if ($latestCompleteTs > 0) {
+                $merged['complete_time'] = $latestCompleteTs;
+                if ($latestCompleteText === '') {
+                    $latestCompleteText = date('Y-m-d H:i:s', $latestCompleteTs);
                 }
-                $merged['createtime_text'] = ProcuremenTime::formatDisplayDateTime($latestText);
+                $merged['complete_time_text'] = ProcuremenTime::formatDisplayDateTime($latestCompleteText);
+            } else {
+                $merged['complete_time'] = 0;
+                $merged['complete_time_text'] = '';
             }
             $maxId = (int)($head['id'] ?? 0);
             foreach ($groupRows as $r) {

+ 46 - 48
application/admin/view/procuremen/details_fragment.html

@@ -212,7 +212,7 @@
     }
     .procuremen-details-wrap .procuremen-oper-log li {
         display: grid;
-        grid-template-columns: 14em 10em 8em minmax(0, 1fr);
+        grid-template-columns: 12em 10em 8em minmax(0, 1fr);
         column-gap: 6px;
         align-items: start;
         padding: 8px 12px;
@@ -407,53 +407,51 @@
         状态进度
     </div>
     {notempty name="pdf_export"}
-    <div class="proc-pdf-steps-outer" style="overflow:hidden;margin:0;padding:0;">
-    <table class="proc-steps-table-pdf" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;table-layout:fixed;width:100%;margin:0 0 18px;">
-        <tr>
-            {volist name="steps" id="st"}
-            <td style="vertical-align:top;text-align:center;padding:4px 1px 0;border:0 !important;width:14.28%;">
-                <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;height:152px;margin:0 auto;">
-                    <tr>
-                        <td style="vertical-align:top;padding:0;border:0 !important;">
-                            <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:0 auto 4px;">
-                                <tr style="height:28px;">
-                                    <td style="width:50%;padding:0;border:0 !important;vertical-align:middle;height:28px;">
-                                        {if $st.pdf_left_bg}
-                                        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;"><tr><td style="padding:0;border:0 !important;height:0;line-height:0;font-size:0;border-top:2px solid {$st.pdf_left_bg};">&#160;</td></tr></table>
-                                        {/if}
-                                    </td>
-                                    <td style="width:28px;padding:0;border:0 !important;vertical-align:middle;text-align:center;height:28px;">
-                                        {if $st.done}
-                                        <span style="display:inline-block;width:28px;height:28px;line-height:26px;text-align:center;border-radius:14px;border:2px solid #1890ff;background-color:#1890ff;color:#ffffff;font-size:11px;font-weight:bold;">&#10003;</span>
-                                        {elseif $st.current /}
-                                        <span style="display:inline-block;width:28px;height:28px;line-height:26px;text-align:center;border-radius:14px;border:2px solid #1890ff;background-color:#1890ff;color:#ffffff;font-size:11px;font-weight:bold;">{$i}</span>
-                                        {else /}
-                                        <span style="display:inline-block;width:28px;height:28px;line-height:26px;text-align:center;border-radius:14px;border:2px solid #dddddd;background-color:#f5f5f5;color:#999999;font-size:11px;font-weight:bold;">{$i}</span>
-                                        {/if}
-                                    </td>
-                                    <td style="width:50%;padding:0;border:0 !important;vertical-align:middle;height:28px;">
-                                        {if $st.pdf_right_bg}
-                                        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;"><tr><td style="padding:0;border:0 !important;height:0;line-height:0;font-size:0;border-top:2px solid {$st.pdf_right_bg};">&#160;</td></tr></table>
-                                        {/if}
-                                    </td>
-                                </tr>
-                            </table>
-                        </td>
-                    </tr>
-                    <tr>
-                        <td style="vertical-align:top;padding:2px 2px 0;border:0 !important;font-size:12px;color:#333;line-height:1.35;font-weight:600;text-align:center;">{$st.title|htmlentities}</td>
-                    </tr>
-                    <tr>
-                        <td style="vertical-align:top;padding:2px 2px 0;height:72px;border:0 !important;font-size:11px;color:#888;line-height:1.45;text-align:center;overflow:hidden;">{$st.subtitle|default=''|htmlentities|nl2br}</td>
-                    </tr>
-                    <tr>
-                        <td style="vertical-align:bottom;padding:2px 2px 0;border:0 !important;font-size:11px;color:#aaa;text-align:center;line-height:1.3;">{$st.time|default=''|htmlentities|nl2br}</td>
-                    </tr>
-                </table>
-            </td>
-            {/volist}
-        </tr>
-    </table>
+    <div class="proc-pdf-steps-outer" style="overflow:hidden;margin:0;padding:0 0 18px;">
+        <table class="proc-steps-table-pdf" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;table-layout:fixed;width:100%;margin:0;">
+            <tr>
+                {volist name="pdfSteps" id="st"}
+                <td style="vertical-align:top;text-align:center;padding:4px 1px 0;border:0 !important;width:{$pdfStepWidth|default='16.66%'|htmlentities};">
+                    <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;height:152px;margin:0 auto;">
+                        <tr>
+                            <td style="vertical-align:top;padding:0;border:0 !important;">
+                                <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:0 auto 6px;">
+                                    <tr style="height:16px;">
+                                        <td style="width:50%;padding:0;border:0 !important;vertical-align:middle;height:16px;">
+                                            {notempty name="st.pdf_left_bg"}
+                                            <div style="height:2px;background-color:{$st.pdf_left_bg};font-size:0;line-height:0;width:100%;">&nbsp;</div>
+                                            {/notempty}
+                                        </td>
+                                        <td style="width:16px;padding:0;border:0 !important;vertical-align:middle;text-align:center;height:16px;">
+                                            {if condition="!empty($st['done']) || !empty($st['current'])"}
+                                            <span style="display:inline-block;width:12px;height:12px;background-color:#1890ff;border:2px solid #1890ff;border-radius:8px;font-size:0;line-height:0;">&nbsp;</span>
+                                            {else /}
+                                            <span style="display:inline-block;width:12px;height:12px;background-color:#ffffff;border:2px solid #d9d9d9;border-radius:8px;font-size:0;line-height:0;">&nbsp;</span>
+                                            {/if}
+                                        </td>
+                                        <td style="width:50%;padding:0;border:0 !important;vertical-align:middle;height:16px;">
+                                            {notempty name="st.pdf_right_bg"}
+                                            <div style="height:2px;background-color:{$st.pdf_right_bg};font-size:0;line-height:0;width:100%;">&nbsp;</div>
+                                            {/notempty}
+                                        </td>
+                                    </tr>
+                                </table>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td style="vertical-align:top;padding:2px 2px 0;border:0 !important;font-size:12px;color:#333;line-height:1.35;font-weight:600;text-align:center;">{$st.title|htmlentities}</td>
+                        </tr>
+                        <tr>
+                            <td style="vertical-align:top;padding:2px 2px 0;height:72px;border:0 !important;font-size:11px;color:#888;line-height:1.45;text-align:center;overflow:hidden;">{$st.subtitle|default=''|htmlentities|nl2br}</td>
+                        </tr>
+                        <tr>
+                            <td style="vertical-align:bottom;padding:2px 2px 0;border:0 !important;font-size:11px;color:#aaa;text-align:center;line-height:1.3;">{$st.time|default=''|htmlentities|nl2br}</td>
+                        </tr>
+                    </table>
+                </td>
+                {/volist}
+            </tr>
+        </table>
     </div>
     {else /}
     <div class="proc-steps-wrap">

+ 52 - 3
application/admin/view/procuremen/details_pdf_shell.html

@@ -11,6 +11,40 @@
             padding: 6px 14px 0 14px;
             line-height: 1.5;
         }
+        .procuremen-pdf-meta-table {
+            width: 100%;
+            border-collapse: collapse;
+            border: 0;
+        }
+        .procuremen-pdf-meta-table td {
+            border: 0;
+            padding: 0;
+            vertical-align: middle;
+            line-height: 1.5;
+        }
+        .procuremen-pdf-meta-left {
+            text-align: left;
+            padding-right: 16px;
+            font-size: 9pt;
+            color: #666;
+        }
+        .procuremen-pdf-meta-title {
+            color: #000;
+            font-size: 14pt;
+            font-weight: 700;
+            display: block;
+            text-align: center;
+            padding: 0 0 6px 0;
+            line-height: 1.4;
+            word-break: break-all;
+        }
+        .procuremen-pdf-meta-time {
+            text-align: right;
+            white-space: nowrap;
+            width: 170px;
+            font-size: 9pt;
+            color: #666;
+        }
         body.procuremen-pdf-export .procuremen-oper-log {
             max-height: none !important;
             overflow: visible !important;
@@ -96,9 +130,24 @@
     </style>
 </head>
 <body class="is-dialog procuremen-pdf-export">
-{notempty name="pdfMetaLine"}
-<div class="procuremen-pdf-meta">{$pdfMetaLine|htmlentities}</div>
-{/notempty}
+{if condition="!empty($pdfMetaIds) || !empty($pdfMetaTitle) || !empty($pdfMetaTime) || !empty($pdfMetaLeft) || !empty($pdfMetaLine)"}
+<div class="procuremen-pdf-meta">
+    {notempty name="pdfMetaTitle"}
+    <div class="procuremen-pdf-meta-title">{$pdfMetaTitle|htmlentities}</div>
+    {/notempty}
+    <table class="procuremen-pdf-meta-table">
+        <tr>
+            <td class="procuremen-pdf-meta-left">
+                {notempty name="pdfMetaIds"}{$pdfMetaIds|htmlentities}
+                {else /}{empty name="pdfMetaTitle"}{$pdfMetaLeft|default=$pdfMetaLine|default=''|htmlentities}{/empty}{/notempty}
+            </td>
+            {notempty name="pdfMetaTime"}
+            <td class="procuremen-pdf-meta-time">{$pdfMetaTime|htmlentities}</td>
+            {/notempty}
+        </tr>
+    </table>
+</div>
+{/if}
 {include file="procuremen/details_fragment" /}
 </body>
 </html>

+ 1 - 0
application/command.php

@@ -17,4 +17,5 @@ return [
     'app\admin\command\Min',
     'app\admin\command\Addon',
     'app\admin\command\Api',
+    'app\admin\command\ProcuremenRegenPdf',
 ];

+ 30 - 7
application/common/library/ProcuremenDashboard.php

@@ -60,8 +60,8 @@ class ProcuremenDashboard
             ],
             // 兼容旧模板字段(图表面板仍可能引用)
             'confirm' => [
-                'today'     => self::countQuotedPendingConfirmOrders(),
-                'month'     => self::countQuotedPendingConfirmOrders(),
+                'today'     => self::countQuotedPendingConfirmOrders($todayStart, $todayEnd),
+                'month'     => self::countQuotedPendingConfirmOrders($monthStart, $monthEnd),
                 'link'      => 'procuremen/audit',
                 'link_text' => '供应商确认',
             ],
@@ -85,8 +85,8 @@ class ProcuremenDashboard
                 'link_text' => '采购供应商初选',
             ],
             'quote_pending' => [
-                'today'     => self::countQuotedPendingConfirmOrders(),
-                'month'     => self::countQuotedPendingConfirmOrders(),
+                'today'     => self::countQuotedPendingConfirmOrders($todayStart, $todayEnd),
+                'month'     => self::countQuotedPendingConfirmOrders($monthStart, $monthEnd),
                 'link'      => 'procuremen/audit',
                 'link_text' => '供应商确认',
             ],
@@ -140,11 +140,11 @@ class ProcuremenDashboard
     }
 
     /**
-     * 报价供应商待确认:当前待确认供应商,且至少一家已填单价
+     * 报价供应商待确认:当前待确认且至少一家已填单价;按下发时间(pick_time)落在区间内(今日数据)
      */
-    protected static function countQuotedPendingConfirmOrders(): int
+    protected static function countQuotedPendingConfirmOrders(string $start = '', string $end = ''): int
     {
-        $rows = self::loadPendingConfirmRows();
+        $rows = self::loadPendingStageRowsWithTime('confirm');
         if ($rows === []) {
             return 0;
         }
@@ -154,6 +154,13 @@ class ProcuremenDashboard
             if (!is_array($row)) {
                 continue;
             }
+            if ($start !== '' && $end !== '') {
+                $normalized = self::normalizeRowForListMonth($row);
+                if (!self::rowMatchesTimeRange($normalized, $start, $end, 'pick_time')) {
+                    continue;
+                }
+                $row = $normalized;
+            }
             $sid = (int)($row['scydgy_id'] ?? 0);
             if ($sid === 0) {
                 continue;
@@ -202,6 +209,22 @@ class ProcuremenDashboard
         return self::countDistinctOrders($matched);
     }
 
+    /**
+     * @param array<string, mixed> $row
+     */
+    protected static function rowMatchesTimeRange(array $row, string $start, string $end, string $primary = 'pick_time'): bool
+    {
+        $listTime = self::resolveRowListTime($row, $primary);
+        $ts = ProcuremenTime::parseToTimestamp($listTime);
+        $startTs = ProcuremenTime::parseToTimestamp($start);
+        $endTs = ProcuremenTime::parseToTimestamp($end);
+        if ($ts <= 0 || $startTs <= 0 || $endTs <= 0) {
+            return false;
+        }
+
+        return $ts >= $startTs && $ts <= $endTs;
+    }
+
     /**
      * 已入库打分:合格 / 不合格订单数(按订单号去重)
      *

+ 19 - 10
application/common/library/ProcuremenOperLog.php

@@ -35,6 +35,9 @@ class ProcuremenOperLog
             'bid_open_verify'        => '开标验证',
             'manual_pick'            => '指定供应商',
             'audit_append_supplier'  => '补加供应商',
+            'rfq_append_supplier'    => '补加供应商',
+            'rfq_notify_salesman'    => '询价通知业务员',
+            'rfq_append_salesman'    => '添加通知业务员',
             'audit_resend_sms'       => '重发短信',
             'audit_resend_email'     => '重发邮件',
             'manual_add'             => '手工新增',
@@ -61,7 +64,10 @@ class ProcuremenOperLog
             'purchase_reject'       => ['审核驳回', 'purchase_reject'],
             'bid_open_verify'       => ['开标验证', 'bid_open_verify'],
             'manual_pick'           => ['指定供应商', 'manual_pick'],
-            'audit_append_supplier' => ['补加供应商', 'audit_append_supplier'],
+            'audit_append_supplier' => ['补加供应商', 'audit_append_supplier', 'rfq_append_supplier'],
+            'rfq_append_supplier'   => ['补加供应商', 'rfq_append_supplier'],
+            'rfq_notify_salesman'   => ['询价通知业务员', '邮箱通知业务员', 'rfq_notify_salesman'],
+            'rfq_append_salesman'   => ['添加通知业务员', 'rfq_append_salesman'],
             'audit_resend_sms'      => ['重发短信', 'audit_resend_sms'],
             'audit_resend_email'    => ['重发邮件', 'audit_resend_email'],
             'manual_add'            => ['手工新增', 'manual_add'],
@@ -375,6 +381,8 @@ class ProcuremenOperLog
             '确认供应商'     => ['确认供应商'],
             '开标验证'       => ['开标验证'],
             '补加供应商'     => ['补加供应商'],
+            '询价通知业务员' => ['询价通知业务员', '邮箱通知业务员'],
+            '添加通知业务员' => ['添加通知业务员'],
             '直接完结'       => ['直接完结', '完结'],
             '审核驳回'       => ['审核驳回', '采购终审驳回'],
         ];
@@ -445,7 +453,8 @@ class ProcuremenOperLog
     }
 
     /**
-     * 操作人前加所属组别;说明文案中的人员名同样补组别
+     * 操作人展示:所属部门(按日志 admin_id 查角色组)+ 日志快照 admin_name
+     * 名称不取 admin.nickname,避免覆盖操作当时写入的名称
      *
      * @param array<int, array<string, mixed>> $logs
      * @return array<int, array<string, mixed>>
@@ -478,18 +487,18 @@ class ProcuremenOperLog
             }
             $id = (int)($lg[self::COL_ADMIN_ID] ?? 0);
             $info = ($id > 0 && isset($map[$id])) ? $map[$id] : null;
+            // 名称:日志 admin_name 快照(操作当时写入)
+            $snapshot = trim((string)($lg[self::COL_ADMIN_NAME] ?? ''));
             $nick = is_array($info) ? trim((string)($info['nickname'] ?? '')) : '';
-            if ($nick === '') {
-                $nick = trim((string)($lg[self::COL_ADMIN_NAME] ?? ''));
-            }
-            if ($nick === '') {
-                $nick = '未知用户';
+            $displayName = $snapshot !== '' ? $snapshot : $nick;
+            if ($displayName === '') {
+                $displayName = '未知用户';
             }
+            // 所属部门:仅用 admin_id → 角色组名
             $group = is_array($info) ? trim((string)($info['group_name'] ?? '')) : '';
             $lg['admin_group'] = $group !== '' ? $group : '—';
-            $lg['admin_nickname'] = $nick;
-            // 操作人显示「组别 昵称」
-            $lg[self::COL_ADMIN_NAME] = self::formatAdminWithGroup($group, $nick);
+            $lg['admin_nickname'] = $displayName;
+            $lg[self::COL_ADMIN_NAME] = self::formatAdminWithGroup($group, $displayName);
             $content = (string)($lg[self::COL_CONTENT] ?? '');
             if ($content !== '' && $nameToLabeled !== []) {
                 // 先还原历史误伤的供应商名

+ 42 - 3
application/index/controller/Index.php

@@ -4252,6 +4252,9 @@ class Index extends Frontend
             [$adminId, $adminName]
         );
         $this->mprocSyncMonthlyAfterInboundScore($poRows, $now);
+        if ($deliveryReady) {
+            $this->mprocRefreshCompletedArchivePdfs($poRows);
+        }
         $this->success('操作成功', '', [
             'ccydh'           => $ccydh,
             'result'          => $result,
@@ -4511,6 +4514,9 @@ class Index extends Frontend
             [$adminId, $adminName]
         );
         $this->mprocSyncMonthlyAfterInboundScore($poRows, $now);
+        if ($qualityReady) {
+            $this->mprocRefreshCompletedArchivePdfs($poRows);
+        }
         $this->success('操作成功', '', [
             'ccydh'           => $ccydh,
             'delivery_status' => $deliveryStatus,
@@ -4520,6 +4526,31 @@ class Index extends Frontend
         ]);
     }
 
+    /**
+     * 质量+交货均完成后,按最新详情重存存证 PDF
+     *
+     * @param array<int, mixed> $poRows
+     */
+    protected function mprocRefreshCompletedArchivePdfs(array $poRows): void
+    {
+        $done = [];
+        foreach ($poRows as $po) {
+            if (!is_array($po)) {
+                continue;
+            }
+            $sid = (int)($po['scydgy_id'] ?? 0);
+            if ($sid <= 0 || isset($done[$sid])) {
+                continue;
+            }
+            $done[$sid] = true;
+            try {
+                \app\admin\controller\Procuremen::regenerateArchivePdfWithoutAuth($sid, true);
+            } catch (\Throwable $e) {
+                Log::record('完结存证PDF刷新失败 scydgy_id=' . $sid . ' ' . $e->getMessage(), 'error');
+            }
+        }
+    }
+
     /**
      * @return array<int, array<string, mixed>>
      */
@@ -4561,6 +4592,8 @@ class Index extends Frontend
     }
 
     /**
+     * 操作人展示名:优先后台管理员昵称(名称),避免记成登录账号
+     *
      * @param array<string, mixed> $user
      */
     protected function mprocResolveAdminDisplayName(array $user, int $adminId): string
@@ -4570,14 +4603,20 @@ class Index extends Frontend
             try {
                 $adminRow = Db::name('admin')->where('id', $adminId)->field('nickname,username')->find();
                 if (is_array($adminRow)) {
-                    $adminName = trim((string)($adminRow['nickname'] ?? ''));
-                    if ($adminName === '') {
-                        $adminName = trim((string)($adminRow['username'] ?? ''));
+                    $nick = trim((string)($adminRow['nickname'] ?? ''));
+                    $uname = trim((string)($adminRow['username'] ?? ''));
+                    if ($nick !== '') {
+                        $adminName = $nick;
+                    } elseif ($uname !== '') {
+                        $adminName = $uname;
                     }
                 }
             } catch (\Throwable $e) {
             }
         }
+        if ($adminName === '') {
+            $adminName = trim((string)($user['nickname'] ?? ''));
+        }
         if ($adminName === '') {
             $adminName = trim((string)($user['username'] ?? ''));
         }

+ 35 - 11
public/assets/js/backend/procuremen.js

@@ -1413,7 +1413,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             + procuremenEscHtml(s) + '</span>';
                     }
                 },
-                {field: 'czlyq', title: '类型', operate: '=', searchList: procuremenSearchList('czlyq'), table: 'b', width: 100, align: 'center',
+                {field: 'czlyq', title: '类型', operate: '=', searchList: procuremenSearchList('czlyq'), table: 'b', width: 90, align: 'center',
                     formatter: function (v) {
                         return v != null && v !== '' ? String(v) : '';
                     }
@@ -1531,9 +1531,24 @@ 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: 100, align: 'center', class: 'procuremen-cell-wrap'},
-                {field: 'cGzzxMc', title: __('外厂单位'), operate: 'LIKE', table: 'a', width: 220, align: 'center', class: 'procuremen-cell-wrap'},
-                {field: 'MBZ', title: __('备注'), operate: 'LIKE', table: 'a', width: 150, align: 'center', class: 'procuremen-cell-wrap'},
+                {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',
+                    formatter: function (v, row, index) {
+                        var tab = Controller.wffTab || 'pick';
+                        if (tab === 'pick') {
+                            var val = (v != null && v !== '') ? String(v) : '';
+                            return '<input type="text" class="form-control input-sm procuremen-po-field procuremen-po-mbz" '
+                                + 'style="min-width:180px;width:100%;max-width:240px;height:28px;padding:2px 6px;" '
+                                + '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>';
+                    }
+                },
                 {field: 'cywyxm', title: __('业务员'), operate: '=', searchList: procuremenSearchList('cywyxm'), table: 'b', width: 80, align: 'center'},
                 {field: 'issue_time', title: '下发时间', operate: false, table: 'a', width: 160, align: 'center',
                     visible: indexInitWffTab !== 'pick',
@@ -2106,12 +2121,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     if (rowIdx >= 0 && $btPo.length) {
                         var $qty = $btPo.find('.procuremen-po-qty[data-row-index="' + rowIdx + '"]');
                         var $price = $btPo.find('.procuremen-po-price[data-row-index="' + rowIdx + '"]');
+                        var $mbz = $btPo.find('.procuremen-po-mbz[data-row-index="' + rowIdx + '"]');
                         if ($qty.length) {
                             rowCopy.This_quantity = String($qty.val()).trim();
                         }
                         if ($price.length) {
                             rowCopy.ceilingPrice = String($price.val()).trim();
                         }
+                        if ($mbz.length) {
+                            rowCopy.MBZ = String($mbz.val()).trim();
+                        }
                     }
                     out.push(rowCopy);
                 });
@@ -2338,22 +2357,25 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 }
                 var $qty = $bt.find('.procuremen-po-qty[data-row-index="' + rowIdx + '"]');
                 var $price = $bt.find('.procuremen-po-price[data-row-index="' + rowIdx + '"]');
+                var $mbz = $bt.find('.procuremen-po-mbz[data-row-index="' + rowIdx + '"]');
                 var q = ($qty.length ? String($qty.val()) : '').trim();
                 var p = ($price.length ? String($price.val()) : '').trim();
+                var m = ($mbz.length ? String($mbz.val()) : '').trim();
                 var origQ = procuremenNormPoCell(baseRow.This_quantity);
                 var origP = procuremenNormPoCell(baseRow.ceilingPrice);
+                var origM = procuremenNormPoCell(baseRow.MBZ);
                 if (origP === '' && baseRow.ceiling_price != null && baseRow.ceiling_price !== '') {
                     origP = procuremenNormPoCell(baseRow.ceiling_price);
                 }
-                return {baseRow: baseRow, q: q, p: p, origQ: origQ, origP: origP};
+                return {baseRow: baseRow, q: q, p: p, m: m, origQ: origQ, origP: origP, origM: origM};
             }
             function procuremenPoRowDirty(rowIdx) {
                 var v = procuremenPoRowValues(rowIdx);
                 if (!v) {
                     return false;
                 }
-                // 允许清空:只要与库中原值不同即视为已修改(两个都删空也能保存)
-                return v.q !== v.origQ || v.p !== v.origP;
+                // 允许清空:只要与库中原值不同即视为已修改(都删空也能保存)
+                return v.q !== v.origQ || v.p !== v.origP || v.m !== v.origM;
             }
             function procuremenEnsurePoPopover() {
                 if ($poPopover && $poPopover.length) {
@@ -2393,7 +2415,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 }
                 var dirty = procuremenPoRowDirty(rowIdx);
                 procuremenEnsurePoPopover().find('.procuremen-po-pop-save').prop('disabled', !dirty);
-                $bt.find('.procuremen-po-qty[data-row-index="' + rowIdx + '"], .procuremen-po-price[data-row-index="' + rowIdx + '"]')
+                $bt.find('.procuremen-po-qty[data-row-index="' + rowIdx + '"], .procuremen-po-price[data-row-index="' + rowIdx + '"], .procuremen-po-mbz[data-row-index="' + rowIdx + '"]')
                     .toggleClass('procuremen-po-dirty', dirty);
             }
             function procuremenPoPopoverHide() {
@@ -2413,8 +2435,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 if (snap) {
                     $bt.find('.procuremen-po-qty[data-row-index="' + rowIdx + '"]').val(snap.q);
                     $bt.find('.procuremen-po-price[data-row-index="' + rowIdx + '"]').val(snap.p);
+                    $bt.find('.procuremen-po-mbz[data-row-index="' + rowIdx + '"]').val(snap.m);
                 }
-                $bt.find('.procuremen-po-qty[data-row-index="' + rowIdx + '"], .procuremen-po-price[data-row-index="' + rowIdx + '"]')
+                $bt.find('.procuremen-po-qty[data-row-index="' + rowIdx + '"], .procuremen-po-price[data-row-index="' + rowIdx + '"], .procuremen-po-mbz[data-row-index="' + rowIdx + '"]')
                     .removeClass('procuremen-po-dirty');
                 procuremenPoPopoverHide();
             }
@@ -2436,7 +2459,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     return;
                 }
                 procuremenPoActiveRow = rowIdx;
-                procuremenPoFocusSnapshot = {q: v.origQ, p: v.origP};
+                procuremenPoFocusSnapshot = {q: v.origQ, p: v.origP, m: v.origM};
                 procuremenPoPopoverSyncSaveBtn(rowIdx);
                 procuremenPoPopoverPosition($inp);
             }
@@ -2455,6 +2478,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 var row = $.extend({}, v.baseRow);
                 row.This_quantity = v.q;
                 row.ceilingPrice = v.p;
+                row.MBZ = v.m;
                 var $saveBtn = procuremenEnsurePoPopover().find('.procuremen-po-pop-save');
                 $saveBtn.prop('disabled', true);
                 Fast.api.ajax({
@@ -2469,7 +2493,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     try {
                         table.bootstrapTable('updateRow', {
                             index: rowIdx,
-                            row: $.extend({}, v.baseRow, {This_quantity: v.q, ceilingPrice: v.p})
+                            row: $.extend({}, v.baseRow, {This_quantity: v.q, ceilingPrice: v.p, MBZ: v.m})
                         });
                     } catch (ignore) {
                     }

+ 21 - 1
public/assets/js/backend/procuremenarchive.js

@@ -57,7 +57,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             table.bootstrapTable({
                 url: $.fn.bootstrapTable.defaults.extend.index_url,
                 pk: 'scydgy_id',
-                sortName: 'id',
+                sortName: 'complete_time',
                 sortOrder: 'desc',
                 commonSearch: true,
                 search: true,
@@ -141,6 +141,26 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                                 return String(value);
                             }
                         },
+                        {
+                            field: 'complete_time',
+                            title: '完结时间',
+                            operate: false,
+                            sortable: true,
+                            width: 165,
+                            formatter: function (value, row) {
+                                if (row.complete_time_text) {
+                                    return row.complete_time_text;
+                                }
+                                if (value == null || value === '' || value === 0 || value === '0') {
+                                    return '<span class="text-muted">—</span>';
+                                }
+                                var n = parseInt(value, 10);
+                                if (!isNaN(n) && n > 946684800) {
+                                    return Table.api.formatter.datetime.call(this, value, row);
+                                }
+                                return String(value);
+                            }
+                        },
                         {
                             field: 'operate',
                             title: '操作',