Supplierservicescore.php 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336
  1. <?php
  2. namespace app\admin\controller;
  3. use app\common\controller\Backend;
  4. use app\common\library\ProcuremenSupplierScore;
  5. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  6. use PhpOffice\PhpSpreadsheet\Style\Alignment;
  7. use PhpOffice\PhpSpreadsheet\Style\Border;
  8. use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
  9. /**
  10. * 供应商评审表(月度记录:质量得分 / 价格得分 / 交货得分 / 最终得分)
  11. * 订单经入库评分(合格/不合格)完结后,可在此对供应商做月度评分
  12. *
  13. * @icon fa fa-trophy
  14. */
  15. class Supplierservicescore extends Backend
  16. {
  17. /** @var \app\admin\model\Supplierservicescore */
  18. protected $model = null;
  19. protected $searchFields = 'company_name';
  20. /** 有列表权限即可导出/批量保存最终得分/查看本月订单 */
  21. protected $noNeedRight = ['export', 'savefinalbatch', 'monthorders'];
  22. public function _initialize()
  23. {
  24. parent::_initialize();
  25. ProcuremenSupplierScore::ensureSchema();
  26. $this->model = new \app\admin\model\Supplierservicescore;
  27. }
  28. public function index()
  29. {
  30. $this->request->filter(['strip_tags', 'trim']);
  31. if ($this->request->isAjax()) {
  32. $ym = $this->resolveRequestYm();
  33. $keyword = $this->resolveSearchKeyword();
  34. $offset = max(0, (int)$this->request->get('offset', 0));
  35. $limit = (int)$this->request->get('limit', 50);
  36. if ($limit <= 0) {
  37. $limit = 50;
  38. }
  39. // 默认只读月度表,避免进页全量重算卡住;点「刷新」带 resync=1 再按规则同步
  40. $forceSync = (string)$this->request->param('resync', '0') === '1';
  41. $list = $keyword !== ''
  42. ? $this->loadSupplierScoreSearchList($keyword)
  43. : $this->loadMonthlySupplierScoreList($ym, '', $forceSync);
  44. // 按列表行月份批量统计中标订单数(与「本月订单明细」口径一致)
  45. $winCountByYmCompany = $this->loadMonthWinOrderCountMapForList($list);
  46. $total = count($list);
  47. $pageRows = array_slice($list, $offset, $limit);
  48. $seqBase = $offset;
  49. $rows = [];
  50. foreach ($pageRows as $i => $item) {
  51. $rowYm = (string)($item['ym'] ?? $ym);
  52. $companyName = (string)($item['company_name'] ?? '');
  53. $winKey = $rowYm . "\0" . $companyName;
  54. $rows[] = [
  55. 'id' => $seqBase + $i + 1,
  56. 'seq_no' => $seqBase + $i + 1,
  57. 'company_name' => $companyName,
  58. 'quality_score' => $item['quality_score'] ?? null,
  59. 'quality_score_text' => (string)($item['quality_score_text'] ?? ''),
  60. 'price_score' => $item['price_score'] ?? null,
  61. 'price_score_text' => (string)($item['price_score_text'] ?? ''),
  62. 'delivery_score' => $item['delivery_score'] ?? null,
  63. 'delivery_score_text' => (string)($item['delivery_score_text'] ?? ''),
  64. 'value_added_score' => $item['value_added_score'] ?? null,
  65. 'value_added_score_text' => (string)($item['value_added_score_text'] ?? ''),
  66. 'final_score' => $item['final_score'] ?? null,
  67. 'final_score_text' => (string)($item['final_score_text'] ?? ''),
  68. 'final_score_saved' => !empty($item['final_score_saved']) ? 1 : 0,
  69. 'score_grade' => (string)($item['score_grade'] ?? ''),
  70. 'score_date' => (string)(($item['score_date'] ?? '') !== '' ? $item['score_date'] : $rowYm),
  71. 'ym' => $rowYm,
  72. 'win_order_count' => (int)($winCountByYmCompany[$winKey] ?? 0),
  73. ];
  74. }
  75. return json(['total' => $total, 'rows' => $rows]);
  76. }
  77. $defaultYm = ProcuremenSupplierScore::latestReviewYm();
  78. if (!preg_match('/^\d{4}-\d{2}$/', $defaultYm)) {
  79. $defaultYm = date('Y-m');
  80. }
  81. $this->assign('defaultYm', $defaultYm);
  82. $this->assignconfig('supplierScoreDefaultYm', $defaultYm);
  83. return $this->view->fetch();
  84. }
  85. /**
  86. * 批量保存当月最终得分与评分等级
  87. * POST: ym, rows=[{company_name, final_score, score_grade}, ...] 或 rows_json
  88. */
  89. public function savefinalbatch()
  90. {
  91. if (!$this->auth->check('supplierservicescore/index') && !$this->auth->isSuperAdmin()) {
  92. $this->error(__('You have no permission'));
  93. }
  94. ProcuremenSupplierScore::ensureFinalScoreTable();
  95. $ym = trim((string)$this->request->post('ym', ''));
  96. if ($ym === '') {
  97. $ym = trim((string)$this->request->param('ym', ''));
  98. }
  99. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  100. $this->error('请选择查询月份');
  101. }
  102. $rows = $this->request->post('rows/a', []);
  103. if (!is_array($rows) || $rows === []) {
  104. $rowsJson = trim((string)$this->request->post('rows_json', ''));
  105. if ($rowsJson !== '') {
  106. $decoded = json_decode($rowsJson, true);
  107. if (is_array($decoded)) {
  108. $rows = $decoded;
  109. }
  110. }
  111. }
  112. if (!is_array($rows) || $rows === []) {
  113. $this->error('没有可保存的数据');
  114. }
  115. // 按行自带 ym 分组(跨月搜索结果可同页保存)
  116. $byYm = [];
  117. foreach ($rows as $row) {
  118. if (!is_array($row)) {
  119. continue;
  120. }
  121. $rowYm = trim((string)($row['ym'] ?? ''));
  122. if (!preg_match('/^\d{4}-\d{2}$/', $rowYm)) {
  123. $rowYm = $ym;
  124. }
  125. if (!isset($byYm[$rowYm])) {
  126. $byYm[$rowYm] = [];
  127. }
  128. $byYm[$rowYm][] = $row;
  129. }
  130. $done = 0;
  131. $err = '';
  132. foreach ($byYm as $saveYm => $chunk) {
  133. $ret = ProcuremenSupplierScore::saveFinalScoresForYm($saveYm, $chunk);
  134. $done += (int)($ret['done'] ?? 0);
  135. if ($err === '') {
  136. $err = trim((string)($ret['error'] ?? ''));
  137. }
  138. }
  139. if ($done < 1) {
  140. $this->error($err !== '' ? $err : '保存失败,请检查填写的分数');
  141. }
  142. $this->success('操作成功');
  143. }
  144. /**
  145. * 本月订单明细(某供应商 + 年月)
  146. */
  147. public function monthorders()
  148. {
  149. if (!$this->auth->check('supplierservicescore/index') && !$this->auth->isSuperAdmin()) {
  150. $this->error(__('You have no permission'));
  151. }
  152. $this->request->filter(['strip_tags', 'trim']);
  153. $ym = trim((string)$this->request->param('ym', ''));
  154. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  155. $ym = date('Y-m');
  156. }
  157. $company = trim((string)$this->request->param('company_name', ''));
  158. if ($company === '') {
  159. $company = trim((string)$this->request->param('company', ''));
  160. }
  161. $rows = $company !== '' ? $this->loadMonthOrdersForSupplier($ym, $company) : [];
  162. $this->view->assign('ym', $ym);
  163. $this->view->assign('company_name', $company);
  164. $this->view->assign('orderRows', $rows);
  165. $this->view->assign('orderCount', count($rows));
  166. return $this->view->fetch();
  167. }
  168. /**
  169. * 导出供应商评审表(图二样式)
  170. * 列:序号、供应商名称、质量、交货、价格、增值服务、总分、评审等级
  171. */
  172. public function export()
  173. {
  174. if (!$this->auth->check('supplierservicescore/index') && !$this->auth->isSuperAdmin()) {
  175. $this->error(__('You have no permission'));
  176. }
  177. $ym = trim((string)$this->request->param('ym', ''));
  178. if (!preg_match('/^(\d{4})-(\d{2})$/', $ym, $m)) {
  179. $this->error('请选择查询月份');
  180. }
  181. $year = (int)$m[1];
  182. $month = (int)$m[2];
  183. if ($month < 1 || $month > 12) {
  184. $this->error('查询月份无效');
  185. }
  186. $list = $this->loadMonthlySupplierScoreList($ym, '', false);
  187. $title = $year . '年' . $month . '月外协加工供应商评审表';
  188. $spreadsheet = new Spreadsheet();
  189. $sheet = $spreadsheet->getActiveSheet();
  190. $sheet->setTitle('供应商评审表');
  191. $fontName = '宋体';
  192. $borderStyle = [
  193. 'borders' => [
  194. 'allBorders' => [
  195. 'borderStyle' => Border::BORDER_THIN,
  196. 'color' => ['rgb' => '000000'],
  197. ],
  198. ],
  199. ];
  200. $centerAlign = [
  201. 'horizontal' => Alignment::HORIZONTAL_CENTER,
  202. 'vertical' => Alignment::VERTICAL_CENTER,
  203. 'wrapText' => true,
  204. ];
  205. $sheet->mergeCells('A1:H1');
  206. $sheet->setCellValue('A1', $title);
  207. $sheet->getRowDimension(1)->setRowHeight(36);
  208. $sheet->getStyle('A1')->getFont()->setName($fontName)->setBold(true)->setSize(20);
  209. $sheet->getStyle('A1')->getAlignment()->applyFromArray($centerAlign);
  210. $headers = ['序号', '供应商名称', '质量', '交货', '价格', '增值服务', '总分', '评审等级'];
  211. foreach ($headers as $idx => $label) {
  212. $col = chr(ord('A') + $idx);
  213. $sheet->setCellValue($col . '2', $label);
  214. }
  215. $sheet->getRowDimension(2)->setRowHeight(26);
  216. $sheet->getStyle('A2:H2')->getFont()->setName($fontName)->setBold(true)->setSize(14);
  217. $sheet->getStyle('A2:H2')->getAlignment()->applyFromArray($centerAlign);
  218. $rowNum = 3;
  219. if ($list === []) {
  220. $sheet->mergeCells('A3:H3');
  221. $sheet->setCellValue('A3', '(该月暂无供应商评审记录)');
  222. $sheet->getRowDimension(3)->setRowHeight(26);
  223. $sheet->getStyle('A3')->getFont()->setName($fontName)->setSize(12);
  224. $sheet->getStyle('A3')->getAlignment()->applyFromArray($centerAlign);
  225. $rowNum = 4;
  226. } else {
  227. $i = 1;
  228. foreach ($list as $item) {
  229. $sheet->setCellValue('A' . $rowNum, $i);
  230. $sheet->setCellValue('B' . $rowNum, (string)($item['company_name'] ?? ''));
  231. $sheet->setCellValue('C' . $rowNum, (string)($item['quality_score_text'] ?? ''));
  232. $sheet->setCellValue('D' . $rowNum, (string)($item['delivery_score_text'] ?? ''));
  233. $sheet->setCellValue('E' . $rowNum, (string)($item['price_score_text'] ?? ''));
  234. $sheet->setCellValue('F' . $rowNum, (string)($item['value_added_score_text'] ?? ''));
  235. $sheet->setCellValue('G' . $rowNum, (string)($item['final_score_text'] ?? ''));
  236. $sheet->setCellValue('H' . $rowNum, (string)($item['score_grade'] ?? ''));
  237. $sheet->getRowDimension($rowNum)->setRowHeight(24);
  238. $rowNum++;
  239. $i++;
  240. }
  241. }
  242. $lastRow = max(2, $rowNum - 1);
  243. $sheet->getStyle('A1:H' . $lastRow)->applyFromArray($borderStyle);
  244. $sheet->getStyle('A3:H' . $lastRow)->getFont()->setName($fontName)->setSize(12);
  245. $sheet->getStyle('A3:H' . $lastRow)->getAlignment()->applyFromArray($centerAlign);
  246. $sheet->getColumnDimension('A')->setWidth(10);
  247. $sheet->getColumnDimension('B')->setWidth(48);
  248. $sheet->getColumnDimension('C')->setWidth(12);
  249. $sheet->getColumnDimension('D')->setWidth(12);
  250. $sheet->getColumnDimension('E')->setWidth(12);
  251. $sheet->getColumnDimension('F')->setWidth(12);
  252. $sheet->getColumnDimension('G')->setWidth(12);
  253. $sheet->getColumnDimension('H')->setWidth(12);
  254. $sheet->getPageSetup()->setFitToPage(true)->setFitToWidth(1)->setFitToHeight(0);
  255. $filenameCn = $title . '.xlsx';
  256. $filenameAscii = 'supplier_review_' . $ym . '.xlsx';
  257. header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  258. header(
  259. 'Content-Disposition: attachment; filename="' . $filenameAscii . '"; filename*=UTF-8\'\'' . rawurlencode($filenameCn)
  260. );
  261. header('Cache-Control: max-age=0');
  262. header('X-Content-Type-Options: nosniff');
  263. $writer = new Xlsx($spreadsheet);
  264. $writer->save('php://output');
  265. $spreadsheet->disconnectWorksheets();
  266. unset($spreadsheet);
  267. exit;
  268. }
  269. /**
  270. * 月度评审记录列表(仅展示,最终得分可人工填写)
  271. *
  272. * @param bool $resync 是否先按规则重算(列表/导出默认 false;点刷新传 true)
  273. * @return array<int, array<string, mixed>>
  274. */
  275. protected function loadMonthlySupplierScoreList(string $ym, string $keyword = '', bool $resync = false): array
  276. {
  277. $ym = trim($ym);
  278. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  279. return [];
  280. }
  281. ProcuremenSupplierScore::ensureSchema();
  282. $monthlyMap = ProcuremenSupplierScore::loadMonthlyMapByYm($ym, $resync);
  283. $keyword = trim($keyword);
  284. $list = [];
  285. foreach ($monthlyMap as $name => $item) {
  286. if ($keyword !== '' && mb_stripos($name, $keyword) === false) {
  287. continue;
  288. }
  289. $list[] = $this->buildSupplierScoreListItem($name, $item, $ym);
  290. }
  291. usort($list, [$this, 'sortSupplierScoreListItems']);
  292. return $list;
  293. }
  294. /**
  295. * 按供应商关键词跨月搜索(不限查询月份)
  296. *
  297. * @return array<int, array<string, mixed>>
  298. */
  299. protected function loadSupplierScoreSearchList(string $keyword): array
  300. {
  301. $keyword = trim($keyword);
  302. if ($keyword === '') {
  303. return [];
  304. }
  305. ProcuremenSupplierScore::ensureSchema();
  306. ProcuremenSupplierScore::ensureFinalScoreTable();
  307. try {
  308. $rows = \think\Db::table(ProcuremenSupplierScore::TABLE_FINAL)
  309. ->where('company_name', 'like', '%' . addcslashes($keyword, '%_\\') . '%')
  310. ->field('ym,company_name,score,quality_score,price_score,delivery_score,value_added_score,final_score,score_grade')
  311. ->order('ym', 'desc')
  312. ->order('company_name', 'asc')
  313. ->select();
  314. } catch (\Throwable $e) {
  315. return [];
  316. }
  317. if (!is_array($rows)) {
  318. return [];
  319. }
  320. $list = [];
  321. foreach ($rows as $r) {
  322. if (!is_array($r)) {
  323. continue;
  324. }
  325. $name = trim((string)($r['company_name'] ?? ''));
  326. $rowYm = ProcuremenSupplierScore::formatScoreYm($r['ym'] ?? '');
  327. if ($name === '' || $rowYm === '') {
  328. continue;
  329. }
  330. $quality = ProcuremenSupplierScore::readOptionalScoreValue($r['quality_score'] ?? null);
  331. $price = ProcuremenSupplierScore::readOptionalScoreValue($r['price_score'] ?? null);
  332. $delivery = ProcuremenSupplierScore::readOptionalScoreValue($r['delivery_score'] ?? null);
  333. $valueAdded = ProcuremenSupplierScore::readOptionalScoreValue($r['value_added_score'] ?? null);
  334. $final = ProcuremenSupplierScore::readOptionalScoreValue($r['final_score'] ?? null);
  335. $item = [
  336. 'quality_score' => $quality,
  337. 'price_score' => $price,
  338. 'delivery_score' => $delivery,
  339. 'value_added_score' => $valueAdded,
  340. 'final_score' => $final,
  341. 'final_saved' => $final !== null ? 1 : 0,
  342. 'score_grade' => ProcuremenSupplierScore::normalizeScoreGrade($r['score_grade'] ?? ''),
  343. 'score_date' => $rowYm,
  344. ];
  345. $list[] = $this->buildSupplierScoreListItem($name, $item, $rowYm);
  346. }
  347. usort($list, [$this, 'sortSupplierScoreListItems']);
  348. return $list;
  349. }
  350. /**
  351. * @param array<string, mixed> $item
  352. * @return array<string, mixed>
  353. */
  354. protected function buildSupplierScoreListItem(string $name, array $item, string $ym): array
  355. {
  356. $quality = array_key_exists('quality_score', $item) ? $item['quality_score'] : null;
  357. $price = array_key_exists('price_score', $item) ? $item['price_score'] : null;
  358. $delivery = array_key_exists('delivery_score', $item) ? $item['delivery_score'] : null;
  359. $valueAdded = array_key_exists('value_added_score', $item) ? $item['value_added_score'] : null;
  360. if ($quality !== null) {
  361. $quality = (float)$quality;
  362. }
  363. if ($price !== null) {
  364. $price = (float)$price;
  365. }
  366. if ($delivery !== null) {
  367. $delivery = (float)$delivery;
  368. }
  369. if ($valueAdded !== null) {
  370. $valueAdded = (float)$valueAdded;
  371. }
  372. $hasFinal = !empty($item['final_saved']);
  373. $final = $hasFinal ? (float)($item['final_score'] ?? 0) : null;
  374. if ($final === null) {
  375. $final = ProcuremenSupplierScore::sumFinalScore($quality, $price, $delivery, $valueAdded);
  376. }
  377. $grade = ProcuremenSupplierScore::normalizeScoreGrade($item['score_grade'] ?? '');
  378. if ($grade === '' && $final !== null) {
  379. $grade = ProcuremenSupplierScore::gradeFromFinalScore($final);
  380. }
  381. $sortScore = $final !== null
  382. ? (float)$final
  383. : (($quality ?? 0) + ($price ?? 0) + ($delivery ?? 0) + ($valueAdded ?? 0));
  384. $scoreDate = (string)($item['score_date'] ?? '');
  385. if ($scoreDate === '') {
  386. $scoreDate = ProcuremenSupplierScore::formatScoreYm($ym);
  387. }
  388. return [
  389. 'company_name' => $name,
  390. 'ym' => ProcuremenSupplierScore::formatScoreYm($ym),
  391. 'quality_score' => $quality,
  392. 'quality_score_text' => ProcuremenSupplierScore::formatOptionalScore($quality),
  393. 'price_score' => $price,
  394. 'price_score_text' => ProcuremenSupplierScore::formatOptionalScore($price),
  395. 'delivery_score' => $delivery,
  396. 'delivery_score_text' => ProcuremenSupplierScore::formatOptionalScore($delivery),
  397. 'value_added_score' => $valueAdded,
  398. 'value_added_score_text' => ProcuremenSupplierScore::formatOptionalScore($valueAdded),
  399. 'final_score' => $final,
  400. 'final_score_text' => $final !== null ? ProcuremenSupplierScore::formatOptionalScore($final) : '',
  401. 'final_score_saved' => $hasFinal ? 1 : 0,
  402. 'score_grade' => $grade,
  403. 'score_date' => $scoreDate,
  404. 'sort_score' => $sortScore,
  405. ];
  406. }
  407. /**
  408. * @param array<string, mixed> $a
  409. * @param array<string, mixed> $b
  410. */
  411. protected function sortSupplierScoreListItems(array $a, array $b): int
  412. {
  413. $ya = (string)($a['ym'] ?? '');
  414. $yb = (string)($b['ym'] ?? '');
  415. if ($ya !== $yb) {
  416. return strcmp($yb, $ya);
  417. }
  418. $sa = (float)($a['sort_score'] ?? 0);
  419. $sb = (float)($b['sort_score'] ?? 0);
  420. if ($sa !== $sb) {
  421. return $sb <=> $sa;
  422. }
  423. return strcmp((string)($a['company_name'] ?? ''), (string)($b['company_name'] ?? ''));
  424. }
  425. protected function resolveRequestYm(): string
  426. {
  427. $ym = trim((string)$this->request->get('ym', ''));
  428. if (preg_match('/^\d{4}-\d{2}$/', $ym)) {
  429. return $ym;
  430. }
  431. $filterRaw = $this->request->get('filter', '');
  432. if (is_string($filterRaw) && $filterRaw !== '') {
  433. $filter = json_decode($filterRaw, true);
  434. if (is_array($filter)) {
  435. $fy = trim((string)($filter['ym'] ?? ''));
  436. if (preg_match('/^\d{4}-\d{2}$/', $fy)) {
  437. return $fy;
  438. }
  439. }
  440. }
  441. if (preg_match('/^(\d{4})\D+(\d{1,2})/', $ym, $m)) {
  442. return sprintf('%04d-%02d', (int)$m[1], (int)$m[2]);
  443. }
  444. return ProcuremenSupplierScore::latestReviewYm();
  445. }
  446. protected function resolveSearchKeyword(): string
  447. {
  448. $keyword = trim((string)$this->request->get('search', ''));
  449. if ($keyword !== '') {
  450. return $keyword;
  451. }
  452. $filterRaw = $this->request->get('filter', '');
  453. if (is_string($filterRaw) && $filterRaw !== '') {
  454. $filter = json_decode($filterRaw, true);
  455. if (is_array($filter)) {
  456. return trim((string)($filter['company_name'] ?? ''));
  457. }
  458. }
  459. return '';
  460. }
  461. /**
  462. * 列表页批量统计:各供应商在对应月份的中标订单数(审核通过 + 中标)
  463. * key = ym + "\0" + company_name
  464. *
  465. * @param array<int, array<string, mixed>> $list
  466. * @return array<string, int>
  467. */
  468. protected function loadMonthWinOrderCountMapForList(array $list): array
  469. {
  470. /** @var array<string, array<string, true>> $need ym => company => true */
  471. $need = [];
  472. foreach ($list as $item) {
  473. if (!is_array($item)) {
  474. continue;
  475. }
  476. $rowYm = trim((string)($item['ym'] ?? ''));
  477. $cn = trim((string)($item['company_name'] ?? ''));
  478. if (!preg_match('/^\d{4}-\d{2}$/', $rowYm) || $cn === '') {
  479. continue;
  480. }
  481. if (!isset($need[$rowYm])) {
  482. $need[$rowYm] = [];
  483. }
  484. $need[$rowYm][$cn] = true;
  485. }
  486. if ($need === []) {
  487. return [];
  488. }
  489. $out = [];
  490. foreach ($need as $ym => $companySet) {
  491. $map = $this->loadMonthWinOrderCountMap($ym, array_keys($companySet));
  492. foreach ($map as $cn => $cnt) {
  493. $out[$ym . "\0" . $cn] = (int)$cnt;
  494. }
  495. foreach (array_keys($companySet) as $cn) {
  496. $k = $ym . "\0" . $cn;
  497. if (!isset($out[$k])) {
  498. $out[$k] = 0;
  499. }
  500. }
  501. }
  502. return $out;
  503. }
  504. /**
  505. * 某月各供应商中标订单数(按订单号去重)
  506. * 口径与 loadMonthOrdersForSupplier 一致:审核通过 + pick 中标,且(下发月=本月 或 本月开标评分表有该单)
  507. *
  508. * @param string[] $companyNames
  509. * @return array<string, int>
  510. */
  511. protected function loadMonthWinOrderCountMap(string $ym, array $companyNames): array
  512. {
  513. $ym = trim($ym);
  514. $names = [];
  515. foreach ($companyNames as $n) {
  516. $n = trim((string)$n);
  517. if ($n !== '') {
  518. $names[$n] = true;
  519. }
  520. }
  521. $companyNames = array_keys($names);
  522. if (!preg_match('/^\d{4}-\d{2}$/', $ym) || $companyNames === []) {
  523. return [];
  524. }
  525. ProcuremenSupplierScore::ensureSchema();
  526. $approvedVals = \app\common\library\ProcuremenStatus::wflowApprovedValues();
  527. $softDel = '(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')';
  528. /** @var array<string, array<string, true>> $seen company => ccydh => true */
  529. $seen = [];
  530. // A) 下发时间落在本月的中标单
  531. try {
  532. $poRows = \think\Db::table('purchase_order')
  533. ->where('pick_company_name', 'in', $companyNames)
  534. ->where('wflow_status', 'in', $approvedVals)
  535. ->whereRaw($softDel)
  536. ->whereRaw('pick_time IS NOT NULL AND TRIM(CAST(pick_time AS CHAR(32))) <> \'\' AND TRIM(CAST(pick_time AS CHAR(32))) NOT LIKE \'0000-00-00%\'')
  537. ->whereRaw('DATE_FORMAT(pick_time, \'%Y-%m\') = ?', [$ym])
  538. ->field('CCYDH,pick_company_name')
  539. ->select();
  540. } catch (\Throwable $e) {
  541. $poRows = [];
  542. }
  543. if (is_array($poRows)) {
  544. foreach ($poRows as $po) {
  545. if (!is_array($po)) {
  546. continue;
  547. }
  548. $cn = trim((string)($po['pick_company_name'] ?? ''));
  549. $ccydh = trim((string)($po['CCYDH'] ?? ''));
  550. if ($cn === '' || $ccydh === '' || !isset($names[$cn])) {
  551. continue;
  552. }
  553. if (!isset($seen[$cn])) {
  554. $seen[$cn] = [];
  555. }
  556. $seen[$cn][$ccydh] = true;
  557. }
  558. }
  559. // B) 本月开标评分表有记录、且采购单仍为该供应商中标并已审核
  560. try {
  561. $bidRows = \think\Db::table(ProcuremenSupplierScore::TABLE_SCORE)
  562. ->where('ym', $ym)
  563. ->where('company_name', 'in', $companyNames)
  564. ->field('ccydh,company_name')
  565. ->select();
  566. } catch (\Throwable $e) {
  567. $bidRows = [];
  568. }
  569. $bidCcydhByCompany = [];
  570. if (is_array($bidRows)) {
  571. foreach ($bidRows as $br) {
  572. if (!is_array($br)) {
  573. continue;
  574. }
  575. $cn = trim((string)($br['company_name'] ?? ''));
  576. $ccydh = trim((string)($br['ccydh'] ?? ''));
  577. if ($cn === '' || $ccydh === '' || !isset($names[$cn])) {
  578. continue;
  579. }
  580. if (!isset($bidCcydhByCompany[$cn])) {
  581. $bidCcydhByCompany[$cn] = [];
  582. }
  583. $bidCcydhByCompany[$cn][$ccydh] = true;
  584. }
  585. }
  586. $allBidCcydh = [];
  587. foreach ($bidCcydhByCompany as $set) {
  588. foreach (array_keys($set) as $c) {
  589. $allBidCcydh[$c] = true;
  590. }
  591. }
  592. if ($allBidCcydh !== []) {
  593. try {
  594. $poBid = \think\Db::table('purchase_order')
  595. ->where('CCYDH', 'in', array_keys($allBidCcydh))
  596. ->where('pick_company_name', 'in', $companyNames)
  597. ->where('wflow_status', 'in', $approvedVals)
  598. ->whereRaw($softDel)
  599. ->field('CCYDH,pick_company_name')
  600. ->select();
  601. } catch (\Throwable $e) {
  602. $poBid = [];
  603. }
  604. if (is_array($poBid)) {
  605. foreach ($poBid as $po) {
  606. if (!is_array($po)) {
  607. continue;
  608. }
  609. $cn = trim((string)($po['pick_company_name'] ?? ''));
  610. $ccydh = trim((string)($po['CCYDH'] ?? ''));
  611. if ($cn === '' || $ccydh === '' || !isset($names[$cn])) {
  612. continue;
  613. }
  614. if (!isset($bidCcydhByCompany[$cn][$ccydh])) {
  615. continue;
  616. }
  617. if (!isset($seen[$cn])) {
  618. $seen[$cn] = [];
  619. }
  620. $seen[$cn][$ccydh] = true;
  621. }
  622. }
  623. }
  624. $out = [];
  625. foreach ($companyNames as $cn) {
  626. $out[$cn] = isset($seen[$cn]) ? count($seen[$cn]) : 0;
  627. }
  628. return $out;
  629. }
  630. /**
  631. * 本月订单明细:
  632. * 1)该供应商「审核通过」且中标的订单(按选定时间落在本月,或本月开标评分表有该单);
  633. * 2)叠加「完结月=本月且已质量评分」的订单(有分可看合格/交货等)。
  634. * 未到入库评分阶段的单:仍显示中标,分项/合格等为空,进度为待入库评分。
  635. *
  636. * @return array<int, array<string, mixed>>
  637. */
  638. protected function loadMonthOrdersForSupplier(string $ym, string $companyName): array
  639. {
  640. $ym = trim($ym);
  641. $companyName = trim($companyName);
  642. if (!preg_match('/^\d{4}-\d{2}$/', $ym) || $companyName === '') {
  643. return [];
  644. }
  645. ProcuremenSupplierScore::ensureSchema();
  646. /** @var array<string, array<string, mixed>> $byCcydh */
  647. $byCcydh = [];
  648. // ——— A) 审核通过 + 中标(本月)———
  649. $approvedVals = \app\common\library\ProcuremenStatus::wflowApprovedValues();
  650. $bidCcydhSet = [];
  651. try {
  652. $bidCcydhRows = \think\Db::table(ProcuremenSupplierScore::TABLE_SCORE)
  653. ->where('ym', $ym)
  654. ->where('company_name', $companyName)
  655. ->field('ccydh')
  656. ->select();
  657. if (is_array($bidCcydhRows)) {
  658. foreach ($bidCcydhRows as $br) {
  659. if (!is_array($br)) {
  660. continue;
  661. }
  662. $c = trim((string)($br['ccydh'] ?? ''));
  663. if ($c !== '') {
  664. $bidCcydhSet[$c] = true;
  665. }
  666. }
  667. }
  668. } catch (\Throwable $e) {
  669. }
  670. try {
  671. $poWinQuery = \think\Db::table('purchase_order')
  672. ->where('pick_company_name', $companyName)
  673. ->where('wflow_status', 'in', $approvedVals)
  674. ->whereRaw('(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')')
  675. ->field('CCYDH,CYJMC,CGYMC,CCLBMMC,CDW,pick_company_name,wflow_status,status,pick_time,scydgy_id,order_batch_no');
  676. $poWinRows = $poWinQuery->order('id', 'asc')->select();
  677. } catch (\Throwable $e) {
  678. try {
  679. $poWinRows = \think\Db::table('purchase_order')
  680. ->where('pick_company_name', $companyName)
  681. ->where('wflow_status', 'in', $approvedVals)
  682. ->whereRaw('(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')')
  683. ->field('CCYDH,CYJMC,CGYMC,CCLBMMC,CDW,pick_company_name,wflow_status,status,pick_time,scydgy_id')
  684. ->order('id', 'asc')
  685. ->select();
  686. } catch (\Throwable $e2) {
  687. $poWinRows = [];
  688. }
  689. }
  690. if (!is_array($poWinRows)) {
  691. $poWinRows = [];
  692. }
  693. foreach ($poWinRows as $po) {
  694. if (!is_array($po)) {
  695. continue;
  696. }
  697. $ccydh = trim((string)($po['CCYDH'] ?? ''));
  698. if ($ccydh === '') {
  699. continue;
  700. }
  701. $pickTime = trim((string)($po['pick_time'] ?? ''));
  702. $pickYm = '';
  703. if ($pickTime !== '' && !preg_match('/^0000-00-00/', $pickTime)) {
  704. $ts = strtotime($pickTime);
  705. if ($ts !== false) {
  706. $pickYm = date('Y-m', $ts);
  707. }
  708. }
  709. $inMonth = ($pickYm === $ym) || isset($bidCcydhSet[$ccydh]);
  710. if (!$inMonth) {
  711. continue;
  712. }
  713. if (!isset($byCcydh[$ccydh])) {
  714. $byCcydh[$ccydh] = [
  715. 'CCYDH' => $ccydh,
  716. 'order_batch_no' => trim((string)($po['order_batch_no'] ?? '')),
  717. 'scydgy_id' => (int)($po['scydgy_id'] ?? 0),
  718. 'scydgy_ids' => [],
  719. 'CYJMC' => trim((string)($po['CYJMC'] ?? '')),
  720. 'CCLBMMC' => trim((string)($po['CCLBMMC'] ?? '')),
  721. 'CDW' => trim((string)($po['CDW'] ?? '')),
  722. 'CGYMC_list' => [],
  723. 'pick_company_name' => $companyName,
  724. 'wflow_status' => \app\common\library\ProcuremenStatus::WFLOW_APPROVED,
  725. 'status' => trim((string)($po['status'] ?? '')),
  726. 'pick_time' => $pickTime,
  727. 'completed' => \app\common\library\ProcuremenStatus::isPoCompleted($po['status'] ?? ''),
  728. 'picked_match' => true,
  729. 'has_inbound_score' => 0,
  730. 'inbound_result' => '',
  731. 'delivery_status' => '',
  732. 'delivery_date' => '',
  733. 'customer_complaint' => 0,
  734. 'complaint_remark' => '',
  735. 'order_interrupt' => 0,
  736. 'inbound_remark' => '',
  737. 'score_time' => '',
  738. ];
  739. }
  740. $g = &$byCcydh[$ccydh];
  741. $sid = (int)($po['scydgy_id'] ?? 0);
  742. if ($sid > 0) {
  743. if ($g['scydgy_id'] <= 0) {
  744. $g['scydgy_id'] = $sid;
  745. }
  746. if (!in_array($sid, $g['scydgy_ids'], true)) {
  747. $g['scydgy_ids'][] = $sid;
  748. }
  749. }
  750. $batchNo = trim((string)($po['order_batch_no'] ?? ''));
  751. if ($batchNo !== '' && ($g['order_batch_no'] ?? '') === '') {
  752. $g['order_batch_no'] = $batchNo;
  753. }
  754. if ($g['CYJMC'] === '') {
  755. $g['CYJMC'] = trim((string)($po['CYJMC'] ?? ''));
  756. }
  757. if ($g['CCLBMMC'] === '') {
  758. $g['CCLBMMC'] = trim((string)($po['CCLBMMC'] ?? ''));
  759. }
  760. if ($g['CDW'] === '') {
  761. $g['CDW'] = trim((string)($po['CDW'] ?? ''));
  762. }
  763. $gymc = trim((string)($po['CGYMC'] ?? ''));
  764. if ($gymc !== '' && !in_array($gymc, $g['CGYMC_list'], true)) {
  765. $g['CGYMC_list'][] = $gymc;
  766. }
  767. if ($pickTime !== '' && !preg_match('/^0000-00-00/', $pickTime)) {
  768. if ($g['pick_time'] === '' || strcmp($pickTime, $g['pick_time']) < 0) {
  769. $g['pick_time'] = $pickTime;
  770. }
  771. }
  772. if (\app\common\library\ProcuremenStatus::isPoCompleted($po['status'] ?? '')) {
  773. $g['completed'] = true;
  774. $g['status'] = \app\common\library\ProcuremenStatus::PO_COMPLETED;
  775. }
  776. unset($g);
  777. }
  778. // ——— B) 入库质量评分(完结月=本月)叠加 ———
  779. try {
  780. $scoreRowsRaw = \think\Db::table('purchase_order_inbound_score')
  781. ->where('company_name', $companyName)
  782. ->field('scydgy_id,ccydh,cyjmc,cgymc,result,delivery_status,delivery_date,customer_complaint,complaint_remark,order_interrupt,remark,createtime,updatetime')
  783. ->order('id', 'desc')
  784. ->select();
  785. } catch (\Throwable $e) {
  786. try {
  787. $scoreRowsRaw = \think\Db::table('purchase_order_inbound_score')
  788. ->where('company_name', $companyName)
  789. ->field('scydgy_id,ccydh,cyjmc,cgymc,result,delivery_status,customer_complaint,order_interrupt,remark,createtime,updatetime')
  790. ->order('id', 'desc')
  791. ->select();
  792. } catch (\Throwable $e2) {
  793. try {
  794. $scoreRowsRaw = \think\Db::table('purchase_order_inbound_score')
  795. ->where('company_name', $companyName)
  796. ->field('scydgy_id,ccydh,cyjmc,cgymc,result,remark,createtime,updatetime')
  797. ->order('id', 'desc')
  798. ->select();
  799. } catch (\Throwable $e3) {
  800. $scoreRowsRaw = [];
  801. }
  802. }
  803. }
  804. if (!is_array($scoreRowsRaw)) {
  805. $scoreRowsRaw = [];
  806. }
  807. $sidSetForYm = [];
  808. $inboundTimeBySid = [];
  809. foreach ($scoreRowsRaw as $r) {
  810. if (!is_array($r)) {
  811. continue;
  812. }
  813. $sid = (int)($r['scydgy_id'] ?? 0);
  814. if ($sid <= 0) {
  815. continue;
  816. }
  817. $sidSetForYm[$sid] = true;
  818. $inboundTimeBySid[$sid] = [
  819. 'createtime' => $r['createtime'] ?? null,
  820. 'updatetime' => $r['updatetime'] ?? null,
  821. ];
  822. }
  823. $completedYmMap = $sidSetForYm !== []
  824. ? ProcuremenSupplierScore::loadCompletedYmMapByScydgyIds(array_keys($sidSetForYm), $inboundTimeBySid)
  825. : [];
  826. foreach ($scoreRowsRaw as $r) {
  827. if (!is_array($r)) {
  828. continue;
  829. }
  830. $ccydh = trim((string)($r['ccydh'] ?? ''));
  831. $result = trim((string)($r['result'] ?? ''));
  832. if ($ccydh === '' || ($result !== '合格' && $result !== '不合格')) {
  833. continue;
  834. }
  835. $sid = (int)($r['scydgy_id'] ?? 0);
  836. $rowYm = $sid > 0 ? (string)($completedYmMap[$sid] ?? '') : '';
  837. if ($rowYm === '' && $sid <= 0) {
  838. $t1 = \app\common\library\ProcuremenTime::parseToTimestamp($r['updatetime'] ?? null);
  839. $t2 = \app\common\library\ProcuremenTime::parseToTimestamp($r['createtime'] ?? null);
  840. $ts = max($t1, $t2);
  841. $rowYm = $ts > 946684800 ? date('Y-m', $ts) : '';
  842. }
  843. if ($rowYm !== $ym) {
  844. continue;
  845. }
  846. $scoreTime = trim((string)($r['updatetime'] ?? ''));
  847. if ($scoreTime === '' || preg_match('/^0000-00-00/', $scoreTime)) {
  848. $scoreTime = trim((string)($r['createtime'] ?? ''));
  849. }
  850. if (!isset($byCcydh[$ccydh])) {
  851. $ds = trim((string)($r['delivery_status'] ?? ''));
  852. if ($ds !== '准时' && $ds !== '滞后') {
  853. $ds = '';
  854. }
  855. $byCcydh[$ccydh] = [
  856. 'CCYDH' => $ccydh,
  857. 'order_batch_no' => '',
  858. 'scydgy_id' => $sid > 0 ? $sid : 0,
  859. 'scydgy_ids' => $sid > 0 ? [$sid] : [],
  860. 'CYJMC' => trim((string)($r['cyjmc'] ?? '')),
  861. 'CCLBMMC' => '',
  862. 'CDW' => '',
  863. 'CGYMC_list' => [],
  864. 'pick_company_name' => $companyName,
  865. 'wflow_status' => \app\common\library\ProcuremenStatus::WFLOW_APPROVED,
  866. 'status' => '',
  867. 'pick_time' => '',
  868. 'completed' => true,
  869. 'picked_match' => true,
  870. 'has_inbound_score' => 1,
  871. 'inbound_result' => $result,
  872. 'delivery_status' => $ds,
  873. 'delivery_date' => '',
  874. 'customer_complaint' => (int)($r['customer_complaint'] ?? 0) === 1 ? 1 : 0,
  875. 'complaint_remark' => '',
  876. 'order_interrupt' => (int)($r['order_interrupt'] ?? 0) === 1 ? 1 : 0,
  877. 'inbound_remark' => trim((string)($r['remark'] ?? '')),
  878. 'score_time' => $scoreTime,
  879. ];
  880. }
  881. $g = &$byCcydh[$ccydh];
  882. $g['has_inbound_score'] = 1;
  883. if ($result === '不合格') {
  884. $g['inbound_result'] = '不合格';
  885. } elseif ($g['inbound_result'] === '') {
  886. $g['inbound_result'] = $result;
  887. }
  888. if ($sid > 0) {
  889. if ($g['scydgy_id'] <= 0) {
  890. $g['scydgy_id'] = $sid;
  891. }
  892. if (!in_array($sid, $g['scydgy_ids'], true)) {
  893. $g['scydgy_ids'][] = $sid;
  894. }
  895. }
  896. $gymc = trim((string)($r['cgymc'] ?? ''));
  897. if ($gymc !== '' && !in_array($gymc, $g['CGYMC_list'], true)) {
  898. $g['CGYMC_list'][] = $gymc;
  899. }
  900. if ($g['CYJMC'] === '') {
  901. $g['CYJMC'] = trim((string)($r['cyjmc'] ?? ''));
  902. }
  903. if ($g['inbound_remark'] === '') {
  904. $g['inbound_remark'] = trim((string)($r['remark'] ?? ''));
  905. }
  906. if ($g['delivery_status'] === '') {
  907. $ds = trim((string)($r['delivery_status'] ?? ''));
  908. if ($ds === '准时' || $ds === '滞后') {
  909. $g['delivery_status'] = $ds;
  910. }
  911. }
  912. $dd = trim((string)($r['delivery_date'] ?? ''));
  913. if ($dd !== '' && preg_match('/^(\d{4}-\d{2}-\d{2})/', $dd, $mdd) && $g['delivery_date'] === '') {
  914. $g['delivery_date'] = $mdd[1];
  915. }
  916. if ((int)($r['customer_complaint'] ?? 0) === 1) {
  917. $g['customer_complaint'] = 1;
  918. }
  919. $crm = trim((string)($r['complaint_remark'] ?? ''));
  920. if ($crm !== '' && $g['complaint_remark'] === '') {
  921. $g['complaint_remark'] = $crm;
  922. }
  923. if ((int)($r['order_interrupt'] ?? 0) === 1) {
  924. $g['order_interrupt'] = 1;
  925. }
  926. if ($scoreTime !== '' && ($g['score_time'] === '' || strcmp($scoreTime, $g['score_time']) > 0)) {
  927. $g['score_time'] = $scoreTime;
  928. }
  929. unset($g);
  930. }
  931. if ($byCcydh === []) {
  932. return [];
  933. }
  934. // 补采购单展示字段(入库评分带来的单可能缺 PO 信息)
  935. $needPo = [];
  936. foreach ($byCcydh as $c => $g) {
  937. if (($g['CCLBMMC'] ?? '') === ''
  938. || ($g['pick_time'] ?? '') === ''
  939. || empty($g['CGYMC_list'])
  940. || ($g['order_batch_no'] ?? '') === ''
  941. ) {
  942. $needPo[$c] = true;
  943. }
  944. }
  945. if ($needPo !== []) {
  946. try {
  947. $poRows = \think\Db::table('purchase_order')
  948. ->where('CCYDH', 'in', array_keys($needPo))
  949. ->whereRaw('(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')')
  950. ->field('CCYDH,CYJMC,CGYMC,CCLBMMC,CDW,pick_company_name,wflow_status,status,pick_time,scydgy_id,order_batch_no')
  951. ->order('id', 'asc')
  952. ->select();
  953. } catch (\Throwable $e) {
  954. try {
  955. $poRows = \think\Db::table('purchase_order')
  956. ->where('CCYDH', 'in', array_keys($needPo))
  957. ->whereRaw('(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')')
  958. ->field('CCYDH,CYJMC,CGYMC,CCLBMMC,CDW,pick_company_name,wflow_status,status,pick_time,scydgy_id')
  959. ->order('id', 'asc')
  960. ->select();
  961. } catch (\Throwable $e2) {
  962. $poRows = [];
  963. }
  964. }
  965. if (is_array($poRows)) {
  966. foreach ($poRows as $po) {
  967. if (!is_array($po)) {
  968. continue;
  969. }
  970. $c = trim((string)($po['CCYDH'] ?? ''));
  971. if ($c === '' || !isset($byCcydh[$c])) {
  972. continue;
  973. }
  974. $g = &$byCcydh[$c];
  975. $sid = (int)($po['scydgy_id'] ?? 0);
  976. if ($g['scydgy_id'] <= 0 && $sid > 0) {
  977. $g['scydgy_id'] = $sid;
  978. }
  979. if ($sid > 0 && !in_array($sid, $g['scydgy_ids'], true)) {
  980. $g['scydgy_ids'][] = $sid;
  981. }
  982. $batchNo = trim((string)($po['order_batch_no'] ?? ''));
  983. if ($batchNo !== '' && ($g['order_batch_no'] ?? '') === '') {
  984. $g['order_batch_no'] = $batchNo;
  985. }
  986. if ($g['CYJMC'] === '') {
  987. $g['CYJMC'] = trim((string)($po['CYJMC'] ?? ''));
  988. }
  989. if ($g['CCLBMMC'] === '') {
  990. $g['CCLBMMC'] = trim((string)($po['CCLBMMC'] ?? ''));
  991. }
  992. if ($g['CDW'] === '') {
  993. $g['CDW'] = trim((string)($po['CDW'] ?? ''));
  994. }
  995. $gymc = trim((string)($po['CGYMC'] ?? ''));
  996. if ($gymc !== '' && !in_array($gymc, $g['CGYMC_list'], true)) {
  997. $g['CGYMC_list'][] = $gymc;
  998. }
  999. $pt = trim((string)($po['pick_time'] ?? ''));
  1000. if ($pt !== '' && !preg_match('/^0000-00-00/', $pt)) {
  1001. if ($g['pick_time'] === '' || strcmp($pt, $g['pick_time']) < 0) {
  1002. $g['pick_time'] = $pt;
  1003. }
  1004. }
  1005. if (\app\common\library\ProcuremenStatus::isPoCompleted($po['status'] ?? '')) {
  1006. $g['completed'] = true;
  1007. $g['status'] = \app\common\library\ProcuremenStatus::PO_COMPLETED;
  1008. } elseif ($g['status'] === '' && trim((string)($po['status'] ?? '')) !== '') {
  1009. $g['status'] = trim((string)$po['status']);
  1010. }
  1011. if (\app\common\library\ProcuremenStatus::isWflowApproved($po['wflow_status'] ?? '')) {
  1012. $g['wflow_status'] = \app\common\library\ProcuremenStatus::WFLOW_APPROVED;
  1013. }
  1014. unset($g);
  1015. }
  1016. }
  1017. }
  1018. // 开标分项(可选)
  1019. $bidByCcydh = [];
  1020. try {
  1021. $bidRows = \think\Db::table(ProcuremenSupplierScore::TABLE_SCORE)
  1022. ->where('ym', $ym)
  1023. ->where('company_name', $companyName)
  1024. ->where('ccydh', 'in', array_keys($byCcydh))
  1025. ->field('ccydh,order_batch_no,score,rank_no,quality_score,price_score,price_sum,lead_score,lead_days_sum,createtime')
  1026. ->order('createtime', 'desc')
  1027. ->select();
  1028. } catch (\Throwable $e) {
  1029. try {
  1030. $bidRows = \think\Db::table(ProcuremenSupplierScore::TABLE_SCORE)
  1031. ->where('ym', $ym)
  1032. ->where('company_name', $companyName)
  1033. ->where('ccydh', 'in', array_keys($byCcydh))
  1034. ->field('ccydh,score,rank_no,quality_score,price_score,price_sum,lead_score,lead_days_sum,createtime')
  1035. ->order('createtime', 'desc')
  1036. ->select();
  1037. } catch (\Throwable $e2) {
  1038. $bidRows = [];
  1039. }
  1040. }
  1041. if (is_array($bidRows)) {
  1042. foreach ($bidRows as $br) {
  1043. if (!is_array($br)) {
  1044. continue;
  1045. }
  1046. $c = trim((string)($br['ccydh'] ?? ''));
  1047. if ($c === '' || isset($bidByCcydh[$c])) {
  1048. continue;
  1049. }
  1050. $bidByCcydh[$c] = $br;
  1051. $bn = trim((string)($br['order_batch_no'] ?? ''));
  1052. if ($bn !== '' && isset($byCcydh[$c]) && ($byCcydh[$c]['order_batch_no'] ?? '') === '') {
  1053. $byCcydh[$c]['order_batch_no'] = $bn;
  1054. }
  1055. }
  1056. }
  1057. $out = [];
  1058. foreach ($byCcydh as $ccydh => $g) {
  1059. $bid = $bidByCcydh[$ccydh] ?? [];
  1060. $gymcList = is_array($g['CGYMC_list'] ?? null) ? $g['CGYMC_list'] : [];
  1061. $hasInbound = !empty($g['has_inbound_score']);
  1062. $batch = [
  1063. 'completed' => !empty($g['completed']) || $hasInbound,
  1064. 'status' => (string)($g['status'] ?? ''),
  1065. 'wflow_status' => (string)($g['wflow_status'] ?? ''),
  1066. ];
  1067. $progressText = $this->formatMonthOrderProgressText($batch);
  1068. // 已质量评分:进度优先显示已完结
  1069. if ($hasInbound && $progressText === '待入库评分') {
  1070. $progressText = \app\common\library\ProcuremenStatus::PO_COMPLETED;
  1071. }
  1072. $pickTimeRaw = trim((string)($g['pick_time'] ?? ''));
  1073. if ($pickTimeRaw === '') {
  1074. $pickTimeRaw = trim((string)($g['score_time'] ?? ''));
  1075. }
  1076. $sid = (int)($g['scydgy_id'] ?? 0);
  1077. $out[] = [
  1078. 'seq_no' => 0,
  1079. 'scydgy_id' => $sid,
  1080. 'CCYDH' => $ccydh,
  1081. 'order_batch_no' => (string)($g['order_batch_no'] ?? ''),
  1082. 'CYJMC' => (string)($g['CYJMC'] ?? ''),
  1083. 'CCLBMMC' => (string)($g['CCLBMMC'] ?? ''),
  1084. 'CGYMC' => $gymcList !== [] ? implode('、', $gymcList) : '',
  1085. 'CDW' => (string)($g['CDW'] ?? ''),
  1086. 'pick_company_name' => (string)(($g['pick_company_name'] ?? '') !== '' ? $g['pick_company_name'] : $companyName),
  1087. 'wflow_status' => (string)($g['wflow_status'] ?? ''),
  1088. 'status' => (string)($g['status'] ?? ''),
  1089. 'progress_text' => $progressText,
  1090. 'pick_time' => $pickTimeRaw,
  1091. 'issue_time' => $pickTimeRaw !== ''
  1092. ? \app\common\library\ProcuremenTime::formatDisplayDateTime($pickTimeRaw)
  1093. : '',
  1094. 'score' => $bid['score'] ?? null,
  1095. 'rank_no' => (int)($bid['rank_no'] ?? 0),
  1096. 'quality_score' => $bid['quality_score'] ?? null,
  1097. 'price_score' => $bid['price_score'] ?? null,
  1098. 'price_sum' => $bid['price_sum'] ?? null,
  1099. 'lead_score' => $bid['lead_score'] ?? null,
  1100. 'lead_days_sum' => $bid['lead_days_sum'] ?? null,
  1101. 'is_picked' => 1,
  1102. 'show_bid_result' => 1,
  1103. 'has_inbound_score' => $hasInbound ? 1 : 0,
  1104. 'inbound_result' => $hasInbound ? (string)($g['inbound_result'] ?? '') : '',
  1105. 'delivery_status' => $hasInbound ? (string)($g['delivery_status'] ?? '') : '',
  1106. 'delivery_date' => $hasInbound ? (string)($g['delivery_date'] ?? '') : '',
  1107. 'customer_complaint' => $hasInbound ? (int)($g['customer_complaint'] ?? 0) : 0,
  1108. 'complaint_remark' => $hasInbound ? (string)($g['complaint_remark'] ?? '') : '',
  1109. 'order_interrupt' => $hasInbound ? (int)($g['order_interrupt'] ?? 0) : 0,
  1110. 'inbound_remark' => $hasInbound ? (string)($g['inbound_remark'] ?? '') : '',
  1111. ];
  1112. }
  1113. usort($out, static function ($a, $b) {
  1114. $ta = (string)($a['pick_time'] ?? '');
  1115. $tb = (string)($b['pick_time'] ?? '');
  1116. if ($ta !== $tb) {
  1117. return strcmp($tb, $ta);
  1118. }
  1119. return strcmp((string)($a['CCYDH'] ?? ''), (string)($b['CCYDH'] ?? ''));
  1120. });
  1121. $total = count($out);
  1122. $seq = $total;
  1123. foreach ($out as &$row) {
  1124. $row['seq_no'] = $seq;
  1125. $seq--;
  1126. }
  1127. unset($row);
  1128. return $out;
  1129. }
  1130. /**
  1131. * 批量取各工序首次「下发」操作时间(用于本月明细按批次拆行)
  1132. *
  1133. * @param int[] $scydgyIds
  1134. * @return array<int, string> scydgy_id => Y-m-d H:i:s
  1135. */
  1136. protected function loadFirstIssueTimeByScydgyIds(array $scydgyIds): array
  1137. {
  1138. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds), static function ($id) {
  1139. return $id > 0;
  1140. })));
  1141. if ($scydgyIds === []) {
  1142. return [];
  1143. }
  1144. $acts = \app\common\library\ProcuremenOperLog::expandActionQueryValues('issue_submit');
  1145. if ($acts === []) {
  1146. return [];
  1147. }
  1148. try {
  1149. $logs = \think\Db::table('purchase_order_oper_log')
  1150. ->where(\app\common\library\ProcuremenOperLog::COL_SCYDGY_ID, 'in', $scydgyIds)
  1151. ->where(\app\common\library\ProcuremenOperLog::COL_ACTION, 'in', $acts)
  1152. ->field(\app\common\library\ProcuremenOperLog::COL_SCYDGY_ID . ',' . \app\common\library\ProcuremenOperLog::COL_TIME)
  1153. ->order('id', 'asc')
  1154. ->select();
  1155. } catch (\Throwable $e) {
  1156. return [];
  1157. }
  1158. $map = [];
  1159. if (!is_array($logs)) {
  1160. return [];
  1161. }
  1162. foreach ($logs as $lg) {
  1163. if (!is_array($lg)) {
  1164. continue;
  1165. }
  1166. $sid = (int)($lg[\app\common\library\ProcuremenOperLog::COL_SCYDGY_ID] ?? 0);
  1167. if ($sid <= 0 || isset($map[$sid])) {
  1168. continue;
  1169. }
  1170. $t = trim((string)($lg[\app\common\library\ProcuremenOperLog::COL_TIME] ?? ''));
  1171. if ($t === '' || preg_match('/^0000-00-00/', $t)) {
  1172. continue;
  1173. }
  1174. if (is_numeric($t) && (int)$t > 946684800) {
  1175. $t = date('Y-m-d H:i:s', (int)$t);
  1176. } else {
  1177. $t = \app\common\library\ProcuremenTime::formatDisplayDateTime($t);
  1178. }
  1179. if ($t === '') {
  1180. continue;
  1181. }
  1182. $map[$sid] = $t;
  1183. }
  1184. return $map;
  1185. }
  1186. /**
  1187. * 当前进度:已完结优先;终审通过也显示已完结;其余展示流程阶段
  1188. *
  1189. * @param array<string, mixed> $po
  1190. */
  1191. protected function formatMonthOrderProgressText(array $po): string
  1192. {
  1193. if (!empty($po['completed']) || \app\common\library\ProcuremenStatus::isPoCompleted($po['status'] ?? '')) {
  1194. return \app\common\library\ProcuremenStatus::PO_COMPLETED;
  1195. }
  1196. // 采购终审通过后待入库评分,合格/不合格后才完结
  1197. if (\app\common\library\ProcuremenStatus::isWflowApproved($po['wflow_status'] ?? '')) {
  1198. return '待入库评分';
  1199. }
  1200. $w = \app\common\library\ProcuremenStatus::normalizeWflowStatus($po['wflow_status'] ?? '');
  1201. if ($w === \app\common\library\ProcuremenStatus::WFLOW_PENDING_APPROVAL) {
  1202. return '待采购确认';
  1203. }
  1204. if ($w === \app\common\library\ProcuremenStatus::WFLOW_PENDING_CONFIRM) {
  1205. return '待确认供应商';
  1206. }
  1207. if ($w === \app\common\library\ProcuremenStatus::WFLOW_PENDING_ISSUE) {
  1208. return '待协助下发';
  1209. }
  1210. return $w !== '' ? $w : '—';
  1211. }
  1212. /**
  1213. * 批量读取质量评分明细(是否合格 / 交货情况 / 客户投诉 / 订单中断 / 备注)
  1214. *
  1215. * @param int[] $scydgyIds
  1216. * @return array<int, array{result:string,delivery_status:string,customer_complaint:int,order_interrupt:int,remark:string}>
  1217. */
  1218. protected function loadInboundResultByScydgyIds(array $scydgyIds): array
  1219. {
  1220. $ids = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  1221. if ($ids === []) {
  1222. return [];
  1223. }
  1224. $out = [];
  1225. try {
  1226. $rows = \think\Db::table('purchase_order_inbound_score')
  1227. ->where('scydgy_id', 'in', $ids)
  1228. ->field('scydgy_id,result,delivery_status,customer_complaint,order_interrupt,remark')
  1229. ->select();
  1230. } catch (\Throwable $e) {
  1231. // 兼容旧表无新列
  1232. try {
  1233. $rows = \think\Db::table('purchase_order_inbound_score')
  1234. ->where('scydgy_id', 'in', $ids)
  1235. ->field('scydgy_id,result,customer_complaint,order_interrupt,remark')
  1236. ->select();
  1237. } catch (\Throwable $e2) {
  1238. try {
  1239. $rows = \think\Db::table('purchase_order_inbound_score')
  1240. ->where('scydgy_id', 'in', $ids)
  1241. ->field('scydgy_id,result,remark')
  1242. ->select();
  1243. } catch (\Throwable $e3) {
  1244. return [];
  1245. }
  1246. }
  1247. }
  1248. if (!is_array($rows)) {
  1249. return [];
  1250. }
  1251. foreach ($rows as $r) {
  1252. if (!is_array($r)) {
  1253. continue;
  1254. }
  1255. $sid = (int)($r['scydgy_id'] ?? 0);
  1256. $result = trim((string)($r['result'] ?? ''));
  1257. if ($sid <= 0 || ($result !== '合格' && $result !== '不合格')) {
  1258. continue;
  1259. }
  1260. $ds = trim((string)($r['delivery_status'] ?? ''));
  1261. if ($ds !== '准时' && $ds !== '滞后') {
  1262. $ds = '';
  1263. }
  1264. $out[$sid] = [
  1265. 'result' => $result,
  1266. 'delivery_status' => $ds,
  1267. 'customer_complaint' => (int)($r['customer_complaint'] ?? 0) === 1 ? 1 : 0,
  1268. 'order_interrupt' => (int)($r['order_interrupt'] ?? 0) === 1 ? 1 : 0,
  1269. 'remark' => trim((string)($r['remark'] ?? '')),
  1270. ];
  1271. }
  1272. return $out;
  1273. }
  1274. }