Procuremenexport.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. <?php
  2. namespace app\admin\controller;
  3. use app\common\controller\Backend;
  4. use app\common\library\ProcuremenOperLog;
  5. use app\common\library\ProcuremenStatus;
  6. use app\common\library\ProcuremenTime;
  7. use think\Db;
  8. /**
  9. * 协助采购 — 月度报表导出列表
  10. *
  11. * @icon fa fa-file-excel-o
  12. */
  13. class Procuremenexport extends Backend
  14. {
  15. protected $searchFields = 'CCYDH,CYJMC,CGYMC';
  16. public function _initialize()
  17. {
  18. parent::_initialize();
  19. $canExport = $this->hasProcuremenSiblingPerm(['export_month_outward', 'exportMonthOutward']);
  20. $canExportQuote = $this->hasProcuremenSiblingPerm(['export_month_quote', 'exportMonthQuote', 'export_month_outward', 'exportMonthOutward']);
  21. $canDetails = $this->hasProcuremenSiblingPerm(['details']);
  22. $this->view->assign('canExportMonthOutward', $canExport);
  23. $this->view->assign('canExportMonthQuote', $canExportQuote);
  24. $this->assignconfig('procuremenAuth', [
  25. 'details' => $canDetails,
  26. 'export_month_outward' => $canExport,
  27. 'export_month_quote' => $canExportQuote,
  28. ]);
  29. }
  30. /**
  31. * 月度导出页引用的采购子权限(规则名在 procuremen 控制器下)
  32. *
  33. * @param string[] $actions
  34. */
  35. protected function hasProcuremenSiblingPerm(array $actions): bool
  36. {
  37. foreach ($actions as $action) {
  38. $action = strtolower(trim((string)$action));
  39. if ($action === '') {
  40. continue;
  41. }
  42. if (strpos($action, 'procuremen/') !== 0) {
  43. $action = 'procuremen/' . $action;
  44. }
  45. if ($this->auth->check($action)) {
  46. return true;
  47. }
  48. }
  49. $suffixes = [];
  50. foreach ($actions as $action) {
  51. $action = strtolower(trim((string)$action));
  52. $action = preg_replace('#^procuremen/#', '', $action);
  53. if ($action !== '') {
  54. $suffixes[$action] = true;
  55. }
  56. }
  57. if ($suffixes === []) {
  58. return false;
  59. }
  60. try {
  61. foreach ($this->auth->getRuleList() as $rule) {
  62. $rule = strtolower((string)$rule);
  63. if (strpos($rule, 'procuremen/') !== 0) {
  64. continue;
  65. }
  66. foreach (array_keys($suffixes) as $suffix) {
  67. if ($rule === 'procuremen/' . $suffix || strpos($rule, 'procuremen/' . $suffix . '/') === 0) {
  68. return true;
  69. }
  70. }
  71. }
  72. } catch (\Throwable $e) {
  73. }
  74. return false;
  75. }
  76. public function index()
  77. {
  78. $this->relationSearch = false;
  79. $this->request->filter(['strip_tags', 'trim']);
  80. if ($this->request->isAjax()) {
  81. $ym = trim((string)$this->request->request('ym', date('Y-m')));
  82. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  83. $ym = date('Y-m');
  84. }
  85. try {
  86. // 有搜索条件时跨月份查询(用户常不知完结月份)
  87. $hasSearch = $this->exportListHasActiveSearch();
  88. $rows = $this->buildExportableOrderRows($hasSearch ? '' : $ym);
  89. $rows = $this->filterExportableRowsBySearch($rows);
  90. $total = count($rows);
  91. $offset = max(0, (int)$this->request->get('offset', 0));
  92. $limit = (int)$this->request->get('limit', 0);
  93. if ($limit > 0) {
  94. $rows = array_slice($rows, $offset, $limit);
  95. }
  96. return json([
  97. 'total' => $total,
  98. 'rows' => $rows,
  99. 'ym' => $ym,
  100. 'cross_month'=> $hasSearch ? 1 : 0,
  101. ]);
  102. } catch (\Throwable $e) {
  103. return json(['total' => 0, 'rows' => [], 'ym' => $ym, 'msg' => $e->getMessage()]);
  104. }
  105. }
  106. return $this->view->fetch();
  107. }
  108. /**
  109. * 列表是否带有有效搜索(快速搜索或通用搜索)
  110. */
  111. protected function exportListHasActiveSearch(): bool
  112. {
  113. $keyword = trim((string)$this->request->request('search', ''));
  114. if ($keyword === '') {
  115. $keyword = trim((string)$this->request->get('search', ''));
  116. }
  117. if ($keyword !== '') {
  118. return true;
  119. }
  120. $filter = $this->request->get('filter', '');
  121. if (is_string($filter) && $filter !== '') {
  122. $filterArr = (array)json_decode($filter, true);
  123. } else {
  124. $filterArr = is_array($filter) ? $filter : [];
  125. }
  126. foreach ($filterArr as $val) {
  127. if (trim((string)$val) !== '') {
  128. return true;
  129. }
  130. }
  131. return false;
  132. }
  133. /**
  134. * 已完结、可按月份导出的订单(合并为一单一行)
  135. */
  136. public function exportable()
  137. {
  138. if (!$this->request->isAjax()) {
  139. $this->error(__('Invalid parameters'));
  140. }
  141. $ym = trim((string)$this->request->get('ym', date('Y-m')));
  142. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  143. $ym = date('Y-m');
  144. }
  145. try {
  146. $rows = $this->buildExportableOrderRows($ym);
  147. return json(['total' => count($rows), 'rows' => $rows, 'ym' => $ym]);
  148. } catch (\Throwable $e) {
  149. return json(['total' => 0, 'rows' => [], 'ym' => $ym, 'msg' => $e->getMessage()]);
  150. }
  151. }
  152. /**
  153. * 快速搜索 / 通用搜索过滤(本页数据已按月聚合,在内存中筛选)
  154. *
  155. * @param array<int, array<string, mixed>> $rows
  156. * @return array<int, array<string, mixed>>
  157. */
  158. protected function filterExportableRowsBySearch(array $rows): array
  159. {
  160. $keyword = trim((string)$this->request->request('search', ''));
  161. if ($keyword === '') {
  162. $keyword = trim((string)$this->request->get('search', ''));
  163. }
  164. $filter = $this->request->get('filter', '');
  165. $op = $this->request->get('op', '');
  166. if (is_string($filter) && $filter !== '') {
  167. $filterArr = (array)json_decode($filter, true);
  168. } else {
  169. $filterArr = is_array($filter) ? $filter : [];
  170. }
  171. if (is_string($op) && $op !== '') {
  172. $opArr = (array)json_decode($op, true);
  173. } else {
  174. $opArr = is_array($op) ? $op : [];
  175. }
  176. if ($keyword === '' && $filterArr === []) {
  177. return $rows;
  178. }
  179. $searchFields = ['CCYDH', 'CYJMC', 'CGYMC', 'createtime_text', 'ym'];
  180. $out = [];
  181. foreach ($rows as $row) {
  182. if (!is_array($row)) {
  183. continue;
  184. }
  185. if ($keyword !== '') {
  186. $hit = false;
  187. foreach ($searchFields as $f) {
  188. if (mb_stripos((string)($row[$f] ?? ''), $keyword) !== false) {
  189. $hit = true;
  190. break;
  191. }
  192. }
  193. if (!$hit) {
  194. continue;
  195. }
  196. }
  197. $passFilter = true;
  198. foreach ($filterArr as $field => $val) {
  199. $field = (string)$field;
  200. $val = trim((string)$val);
  201. if ($field === '' || $val === '') {
  202. continue;
  203. }
  204. $cell = (string)($row[$field] ?? '');
  205. $mode = strtoupper((string)($opArr[$field] ?? 'LIKE'));
  206. if ($mode === '=' || $mode === 'EQUAL') {
  207. if (strcasecmp($cell, $val) !== 0) {
  208. $passFilter = false;
  209. break;
  210. }
  211. } elseif (mb_stripos($cell, $val) === false) {
  212. $passFilter = false;
  213. break;
  214. }
  215. }
  216. if (!$passFilter) {
  217. continue;
  218. }
  219. $out[] = $row;
  220. }
  221. return $out;
  222. }
  223. /**
  224. * 已完结订单(与历史存证一致),按完结月份筛选后合并为一单一行
  225. *
  226. * @param string $ym 完结月份 Y-m;空字符串表示不限月份(用于跨月搜索)
  227. * @return array<int, array<string, mixed>>
  228. */
  229. protected function buildExportableOrderRows(string $ym): array
  230. {
  231. try {
  232. $query = Db::table('purchase_order');
  233. $query->whereRaw(ProcuremenStatus::sqlPoCompleted('status'));
  234. $dbRows = $query
  235. ->field('id,scydgy_id,CCYDH,CYJMC,CGYMC,pick_time,createtime,dStamp,pick_company_name')
  236. ->order('id', 'desc')
  237. ->select();
  238. } catch (\Throwable $e) {
  239. \think\Log::write('月度导出列表查询异常:' . $e->getMessage(), 'error');
  240. $dbRows = [];
  241. }
  242. if (!is_array($dbRows)) {
  243. $dbRows = [];
  244. }
  245. $sidList = [];
  246. foreach ($dbRows as $r) {
  247. if (!is_array($r)) {
  248. continue;
  249. }
  250. $sid = (int)($r['scydgy_id'] ?? 0);
  251. if ($sid > 0) {
  252. $sidList[$sid] = true;
  253. }
  254. }
  255. $completeTsMap = ProcuremenTime::loadOperLogTimestampMap(array_keys($sidList), ['purchase_confirm', 'mark_complete']);
  256. $pickedSupplierMap = $this->loadPickedSupplierNamesByScydgyIds(array_keys($sidList));
  257. $pool = [];
  258. foreach ($dbRows as $r) {
  259. if (!is_array($r)) {
  260. continue;
  261. }
  262. $done = ProcuremenTime::resolveCompletedDone($r, $completeTsMap);
  263. if ($done['ts'] <= 0) {
  264. continue;
  265. }
  266. $rowYm = date('Y-m', $done['ts']);
  267. if ($ym !== '' && $rowYm !== $ym) {
  268. continue;
  269. }
  270. $sid = (int)($r['scydgy_id'] ?? 0);
  271. $pickedName = trim((string)($pickedSupplierMap[$sid] ?? ''));
  272. if ($pickedName === '') {
  273. $pickedName = trim((string)($r['pick_company_name'] ?? ''));
  274. }
  275. $pool[] = [
  276. 'id' => (int)($r['id'] ?? 0),
  277. 'scydgy_id' => $sid,
  278. 'CCYDH' => trim((string)($r['CCYDH'] ?? '')),
  279. 'CYJMC' => trim((string)($r['CYJMC'] ?? '')),
  280. 'CGYMC' => trim((string)($r['CGYMC'] ?? '')),
  281. 'ym' => $rowYm,
  282. 'createtime_text' => $done['text'],
  283. 'picked_supplier' => $pickedName,
  284. ];
  285. }
  286. $merged = $this->collapseExportRowsByOrder($pool);
  287. foreach ($merged as &$row) {
  288. $names = [];
  289. if (!empty($row['_merge_rows']) && is_array($row['_merge_rows'])) {
  290. foreach ($row['_merge_rows'] as $mr) {
  291. if (!is_array($mr)) {
  292. continue;
  293. }
  294. $n = trim((string)($mr['picked_supplier'] ?? ''));
  295. if ($n !== '' && !in_array($n, $names, true)) {
  296. $names[] = $n;
  297. }
  298. if (trim((string)($row['ym'] ?? '')) === '') {
  299. $ymv = trim((string)($mr['ym'] ?? ''));
  300. if ($ymv !== '') {
  301. $row['ym'] = $ymv;
  302. }
  303. }
  304. }
  305. } else {
  306. $n = trim((string)($row['picked_supplier'] ?? ''));
  307. if ($n !== '') {
  308. $names[] = $n;
  309. }
  310. }
  311. $row['picked_supplier'] = implode('、', $names);
  312. unset($row['_merge_rows']);
  313. }
  314. unset($row);
  315. return $merged;
  316. }
  317. /**
  318. * 中标/已选供应商名称(按工序 scydgy_id)
  319. *
  320. * @param int[] $scydgyIds
  321. * @return array<int, string>
  322. */
  323. protected function loadPickedSupplierNamesByScydgyIds(array $scydgyIds): array
  324. {
  325. $out = [];
  326. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  327. if ($scydgyIds === []) {
  328. return $out;
  329. }
  330. try {
  331. $rows = Db::table('purchase_order_detail')
  332. ->where('scydgy_id', 'in', $scydgyIds)
  333. ->whereIn('status', ProcuremenStatus::podPickedValues())
  334. ->field('scydgy_id,company_name')
  335. ->select();
  336. } catch (\Throwable $e) {
  337. $rows = [];
  338. }
  339. if (!is_array($rows)) {
  340. return $out;
  341. }
  342. $namesBySid = [];
  343. foreach ($rows as $r) {
  344. if (!is_array($r)) {
  345. continue;
  346. }
  347. $sid = (int)($r['scydgy_id'] ?? 0);
  348. $cn = trim((string)($r['company_name'] ?? ''));
  349. if ($sid <= 0 || $cn === '') {
  350. continue;
  351. }
  352. if (!isset($namesBySid[$sid])) {
  353. $namesBySid[$sid] = [];
  354. }
  355. if (!in_array($cn, $namesBySid[$sid], true)) {
  356. $namesBySid[$sid][] = $cn;
  357. }
  358. }
  359. foreach ($namesBySid as $sid => $names) {
  360. $out[$sid] = implode('、', $names);
  361. }
  362. return $out;
  363. }
  364. /**
  365. * 工序行金额合计:优先已选定明细 status=1,否则汇总有效明细上的加工金额
  366. *
  367. * @param int[] $scydgyIds
  368. * @return array<int, float>
  369. */
  370. protected function loadDetailAmountSumByScydgyIds(array $scydgyIds): array
  371. {
  372. $picked = $this->loadPickedAmountSumByScydgyIds($scydgyIds);
  373. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  374. $missing = [];
  375. foreach ($scydgyIds as $sid) {
  376. if ($sid > 0 && !isset($picked[$sid])) {
  377. $missing[] = $sid;
  378. }
  379. }
  380. if ($missing === []) {
  381. return $picked;
  382. }
  383. try {
  384. $rows = Db::table('purchase_order_detail')
  385. ->where('scydgy_id', 'in', $missing)
  386. ->whereRaw(ProcuremenStatus::sqlPodNotVoid('status'))
  387. ->field('scydgy_id,amount')
  388. ->select();
  389. } catch (\Throwable $e) {
  390. $rows = [];
  391. }
  392. if (!is_array($rows)) {
  393. return $picked;
  394. }
  395. foreach ($rows as $r) {
  396. if (!is_array($r)) {
  397. continue;
  398. }
  399. $sid = (int)($r['scydgy_id'] ?? 0);
  400. $am = trim((string)($r['amount'] ?? ''));
  401. if ($sid <= 0 || $am === '' || $am === '0' || $am === '0.00') {
  402. continue;
  403. }
  404. if (!isset($picked[$sid])) {
  405. $picked[$sid] = 0.0;
  406. }
  407. $picked[$sid] += (float)$am;
  408. }
  409. return $picked;
  410. }
  411. /**
  412. * @param int[] $scydgyIds
  413. * @return array<int, float>
  414. */
  415. protected function loadPickedAmountSumByScydgyIds(array $scydgyIds): array
  416. {
  417. $out = [];
  418. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  419. if ($scydgyIds === []) {
  420. return $out;
  421. }
  422. try {
  423. $rows = Db::table('purchase_order_detail')
  424. ->where('scydgy_id', 'in', $scydgyIds)
  425. ->whereIn('status', ProcuremenStatus::podPickedValues())
  426. ->field('scydgy_id,amount')
  427. ->select();
  428. } catch (\Throwable $e) {
  429. $rows = [];
  430. }
  431. if (!is_array($rows)) {
  432. return $out;
  433. }
  434. foreach ($rows as $r) {
  435. if (!is_array($r)) {
  436. continue;
  437. }
  438. $sid = (int)($r['scydgy_id'] ?? 0);
  439. $am = trim((string)($r['amount'] ?? ''));
  440. if ($sid <= 0 || $am === '' || $am === '0' || $am === '0.00') {
  441. continue;
  442. }
  443. if (!isset($out[$sid])) {
  444. $out[$sid] = 0.0;
  445. }
  446. $out[$sid] += (float)$am;
  447. }
  448. return $out;
  449. }
  450. /**
  451. * 归属月份:优先采购确认/完结操作时间,其次 pick_time、createtime、dStamp
  452. *
  453. * @param array<string, mixed> $row
  454. * @param array<int, int> $completeTsMap scydgy_id => unix 完结时间
  455. */
  456. protected function resolvePurchaseOrderRowYm(array $row, array $completeTsMap = []): string
  457. {
  458. $ts = $this->resolvePurchaseOrderCompleteTimestamp($row, $completeTsMap);
  459. if ($ts > 0) {
  460. return date('Y-m', $ts);
  461. }
  462. return '';
  463. }
  464. /**
  465. * @param array<string, mixed> $row
  466. * @param array<int, int> $completeTsMap
  467. */
  468. protected function resolvePurchaseOrderCompleteTimestamp(array $row, array $completeTsMap = []): int
  469. {
  470. $sid = (int)($row['scydgy_id'] ?? 0);
  471. if ($sid > 0 && isset($completeTsMap[$sid]) && (int)$completeTsMap[$sid] > 946684800) {
  472. return (int)$completeTsMap[$sid];
  473. }
  474. foreach (['pick_time', 'createtime', 'dStamp'] as $key) {
  475. $raw = $row[$key] ?? null;
  476. if ($raw === null || $raw === '') {
  477. continue;
  478. }
  479. if (is_numeric($raw) && (int)$raw > 946684800) {
  480. return (int)$raw;
  481. }
  482. $t = trim((string)$raw);
  483. if ($t !== '' && stripos($t, '0000-00-00') !== 0) {
  484. $parsed = (int)strtotime($t);
  485. return $parsed > 946684800 ? $parsed : 0;
  486. }
  487. }
  488. return 0;
  489. }
  490. /**
  491. * @param array<int, string> $actions
  492. * @return array<int, int> scydgy_id => 最近操作 unix 时间
  493. */
  494. protected function loadOperLogTimestampMap(array $scydgyIds, array $actions): array
  495. {
  496. $out = [];
  497. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  498. $actions = array_values(array_filter(array_map('strval', $actions)));
  499. if ($scydgyIds === [] || $actions === []) {
  500. return $out;
  501. }
  502. try {
  503. $logs = Db::table('purchase_order_oper_log')
  504. ->where(ProcuremenOperLog::COL_SCYDGY_ID, 'in', $scydgyIds)
  505. ->where(ProcuremenOperLog::COL_ACTION, 'in', ProcuremenOperLog::expandActionQueryValues($actions))
  506. ->field(ProcuremenOperLog::COL_SCYDGY_ID . ',' . ProcuremenOperLog::COL_TIME)
  507. ->order('id', 'asc')
  508. ->select();
  509. } catch (\Throwable $e) {
  510. $logs = [];
  511. }
  512. if (!is_array($logs)) {
  513. return $out;
  514. }
  515. foreach ($logs as $log) {
  516. if (!is_array($log)) {
  517. continue;
  518. }
  519. $log = ProcuremenOperLog::normalizeRow($log);
  520. $sid = (int)($log['scydgy_id'] ?? 0);
  521. $ct = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
  522. if ($sid > 0 && $ct > 946684800) {
  523. $out[$sid] = isset($out[$sid]) ? max($out[$sid], $ct) : $ct;
  524. }
  525. }
  526. return $out;
  527. }
  528. /**
  529. * @param array<string, mixed> $row
  530. * @param array<int, int> $completeTsMap
  531. */
  532. protected function formatPurchaseOrderDoneTime(array $row, array $completeTsMap = []): string
  533. {
  534. $ts = $this->resolvePurchaseOrderCompleteTimestamp($row, $completeTsMap);
  535. if ($ts > 0) {
  536. return date('Y-m-d H:i:s', $ts);
  537. }
  538. return '';
  539. }
  540. /**
  541. * @param array<int, array<string, mixed>> $rows
  542. * @return array<int, array<string, mixed>>
  543. */
  544. protected function collapseExportRowsByOrder(array $rows): array
  545. {
  546. if ($rows === []) {
  547. return [];
  548. }
  549. $groups = [];
  550. foreach ($rows as $row) {
  551. if (!is_array($row)) {
  552. continue;
  553. }
  554. $dh = trim((string)($row['CCYDH'] ?? ''));
  555. $key = $dh !== '' ? $dh : ('_id_' . (int)($row['id'] ?? 0));
  556. if (!isset($groups[$key])) {
  557. $groups[$key] = [];
  558. }
  559. $groups[$key][] = $row;
  560. }
  561. $out = [];
  562. foreach ($groups as $groupRows) {
  563. if ($groupRows === []) {
  564. continue;
  565. }
  566. usort($groupRows, function ($a, $b) {
  567. return ((int)($a['id'] ?? 0)) <=> ((int)($b['id'] ?? 0));
  568. });
  569. $head = $groupRows[0];
  570. $gymcList = [];
  571. foreach ($groupRows as $r) {
  572. $g = trim((string)($r['CGYMC'] ?? ''));
  573. if ($g !== '' && !in_array($g, $gymcList, true)) {
  574. $gymcList[] = $g;
  575. }
  576. }
  577. $merged = $head;
  578. $merged['_merge_rows'] = $groupRows;
  579. if (count($groupRows) > 1) {
  580. $merged['CGYMC'] = implode('、', $gymcList);
  581. }
  582. $latestText = '';
  583. $latestTs = 0;
  584. foreach ($groupRows as $r) {
  585. $t = trim((string)($r['createtime_text'] ?? ''));
  586. $ts = ProcuremenTime::parseToTimestamp($t);
  587. if ($ts > $latestTs) {
  588. $latestTs = $ts;
  589. $latestText = ProcuremenTime::formatDisplayDateTime($t);
  590. }
  591. }
  592. if ($latestText !== '') {
  593. $merged['createtime_text'] = $latestText;
  594. }
  595. $maxId = (int)($head['id'] ?? 0);
  596. foreach ($groupRows as $r) {
  597. $maxId = max($maxId, (int)($r['id'] ?? 0));
  598. }
  599. $merged['id'] = $maxId;
  600. $out[] = $merged;
  601. }
  602. usort($out, function ($a, $b) {
  603. return ((int)($b['id'] ?? 0)) <=> ((int)($a['id'] ?? 0));
  604. });
  605. return $out;
  606. }
  607. }