Procuremenexport.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <?php
  2. namespace app\admin\controller;
  3. use app\common\controller\Backend;
  4. use app\common\library\ProcuremenStatus;
  5. use think\Db;
  6. /**
  7. * 协助采购 — 月度报表导出列表
  8. *
  9. * @icon fa fa-file-excel-o
  10. */
  11. class Procuremenexport extends Backend
  12. {
  13. protected $searchFields = 'CCYDH,CYJMC,CGYMC';
  14. public function _initialize()
  15. {
  16. parent::_initialize();
  17. $this->assignconfig('procuremenAuth', [
  18. 'details' => $this->auth->check('procuremen/details'),
  19. 'export_month_outward' => $this->auth->check('procuremen/export_month_outward'),
  20. ]);
  21. }
  22. public function index()
  23. {
  24. $this->relationSearch = false;
  25. $this->request->filter(['strip_tags', 'trim']);
  26. if ($this->request->isAjax()) {
  27. $ym = trim((string)$this->request->request('ym', date('Y-m')));
  28. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  29. $ym = date('Y-m');
  30. }
  31. try {
  32. $rows = $this->buildExportableOrderRows($ym);
  33. return json(['total' => count($rows), 'rows' => $rows, 'ym' => $ym]);
  34. } catch (\Throwable $e) {
  35. return json(['total' => 0, 'rows' => [], 'ym' => $ym, 'msg' => $e->getMessage()]);
  36. }
  37. }
  38. return $this->view->fetch();
  39. }
  40. /**
  41. * 已完结、可按月份导出的订单(合并为一单一行)
  42. */
  43. public function exportable()
  44. {
  45. if (!$this->request->isAjax()) {
  46. $this->error(__('Invalid parameters'));
  47. }
  48. $ym = trim((string)$this->request->get('ym', date('Y-m')));
  49. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  50. $ym = date('Y-m');
  51. }
  52. try {
  53. $rows = $this->buildExportableOrderRows($ym);
  54. return json(['total' => count($rows), 'rows' => $rows, 'ym' => $ym]);
  55. } catch (\Throwable $e) {
  56. return json(['total' => 0, 'rows' => [], 'ym' => $ym, 'msg' => $e->getMessage()]);
  57. }
  58. }
  59. /**
  60. * 已完结订单(与历史存证一致),按完结月份筛选后合并为一单一行
  61. *
  62. * @return array<int, array<string, mixed>>
  63. */
  64. protected function buildExportableOrderRows(string $ym): array
  65. {
  66. try {
  67. $query = Db::table('purchase_order');
  68. $query->whereRaw(ProcuremenStatus::sqlPoCompleted('status'));
  69. $dbRows = $query
  70. ->field('id,scydgy_id,CCYDH,CYJMC,CGYMC,pick_time,createtime,dStamp,updatetime')
  71. ->order('id', 'desc')
  72. ->select();
  73. } catch (\Throwable $e) {
  74. $dbRows = [];
  75. }
  76. if (!is_array($dbRows)) {
  77. $dbRows = [];
  78. }
  79. $sidList = [];
  80. foreach ($dbRows as $r) {
  81. if (!is_array($r)) {
  82. continue;
  83. }
  84. $sid = (int)($r['scydgy_id'] ?? 0);
  85. if ($sid > 0) {
  86. $sidList[$sid] = true;
  87. }
  88. }
  89. $amountMap = $this->loadDetailAmountSumByScydgyIds(array_keys($sidList));
  90. $completeTsMap = $this->loadOperLogTimestampMap(array_keys($sidList), ['purchase_confirm', 'mark_complete']);
  91. $pool = [];
  92. foreach ($dbRows as $r) {
  93. if (!is_array($r)) {
  94. continue;
  95. }
  96. $rowYm = $this->resolvePurchaseOrderRowYm($r, $completeTsMap);
  97. if ($rowYm !== $ym) {
  98. continue;
  99. }
  100. $sid = (int)($r['scydgy_id'] ?? 0);
  101. $doneText = $this->formatPurchaseOrderDoneTime($r, $completeTsMap);
  102. $pool[] = [
  103. 'id' => (int)($r['id'] ?? 0),
  104. 'scydgy_id' => $sid,
  105. 'CCYDH' => trim((string)($r['CCYDH'] ?? '')),
  106. 'CYJMC' => trim((string)($r['CYJMC'] ?? '')),
  107. 'CGYMC' => trim((string)($r['CGYMC'] ?? '')),
  108. 'ym' => $rowYm,
  109. 'createtime_text' => $doneText,
  110. 'detail_amount' => (float)($amountMap[$sid] ?? 0),
  111. ];
  112. }
  113. $merged = $this->collapseExportRowsByOrder($pool);
  114. foreach ($merged as &$row) {
  115. $amt = 0.0;
  116. $cnt = 0;
  117. if (!empty($row['_merge_rows']) && is_array($row['_merge_rows'])) {
  118. foreach ($row['_merge_rows'] as $mr) {
  119. if (!is_array($mr)) {
  120. continue;
  121. }
  122. $amt += (float)($mr['detail_amount'] ?? 0);
  123. $cnt++;
  124. if (trim((string)($row['ym'] ?? '')) === '') {
  125. $ymv = trim((string)($mr['ym'] ?? ''));
  126. if ($ymv !== '') {
  127. $row['ym'] = $ymv;
  128. }
  129. }
  130. }
  131. } else {
  132. $amt = (float)($row['detail_amount'] ?? 0);
  133. $cnt = 1;
  134. }
  135. $row['row_count'] = $cnt;
  136. $row['total_amount'] = round($amt, 2);
  137. $row['total_amount_text'] = number_format($amt, 2, '.', ',');
  138. unset($row['_merge_rows'], $row['detail_amount']);
  139. }
  140. unset($row);
  141. return $merged;
  142. }
  143. /**
  144. * 工序行金额合计:优先已选定明细 status=1,否则汇总有效明细上的加工金额
  145. *
  146. * @param int[] $scydgyIds
  147. * @return array<int, float>
  148. */
  149. protected function loadDetailAmountSumByScydgyIds(array $scydgyIds): array
  150. {
  151. $picked = $this->loadPickedAmountSumByScydgyIds($scydgyIds);
  152. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  153. $missing = [];
  154. foreach ($scydgyIds as $sid) {
  155. if ($sid > 0 && !isset($picked[$sid])) {
  156. $missing[] = $sid;
  157. }
  158. }
  159. if ($missing === []) {
  160. return $picked;
  161. }
  162. try {
  163. $rows = Db::table('purchase_order_detail')
  164. ->where('scydgy_id', 'in', $missing)
  165. ->whereRaw(ProcuremenStatus::sqlPodNotVoid('status'))
  166. ->field('scydgy_id,amount')
  167. ->select();
  168. } catch (\Throwable $e) {
  169. $rows = [];
  170. }
  171. if (!is_array($rows)) {
  172. return $picked;
  173. }
  174. foreach ($rows as $r) {
  175. if (!is_array($r)) {
  176. continue;
  177. }
  178. $sid = (int)($r['scydgy_id'] ?? 0);
  179. $am = trim((string)($r['amount'] ?? ''));
  180. if ($sid <= 0 || $am === '' || $am === '0' || $am === '0.00') {
  181. continue;
  182. }
  183. if (!isset($picked[$sid])) {
  184. $picked[$sid] = 0.0;
  185. }
  186. $picked[$sid] += (float)$am;
  187. }
  188. return $picked;
  189. }
  190. /**
  191. * @param int[] $scydgyIds
  192. * @return array<int, float>
  193. */
  194. protected function loadPickedAmountSumByScydgyIds(array $scydgyIds): array
  195. {
  196. $out = [];
  197. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  198. if ($scydgyIds === []) {
  199. return $out;
  200. }
  201. try {
  202. $rows = Db::table('purchase_order_detail')
  203. ->where('scydgy_id', 'in', $scydgyIds)
  204. ->whereIn('status', ProcuremenStatus::podPickedValues())
  205. ->field('scydgy_id,amount')
  206. ->select();
  207. } catch (\Throwable $e) {
  208. $rows = [];
  209. }
  210. if (!is_array($rows)) {
  211. return $out;
  212. }
  213. foreach ($rows as $r) {
  214. if (!is_array($r)) {
  215. continue;
  216. }
  217. $sid = (int)($r['scydgy_id'] ?? 0);
  218. $am = trim((string)($r['amount'] ?? ''));
  219. if ($sid <= 0 || $am === '' || $am === '0' || $am === '0.00') {
  220. continue;
  221. }
  222. if (!isset($out[$sid])) {
  223. $out[$sid] = 0.0;
  224. }
  225. $out[$sid] += (float)$am;
  226. }
  227. return $out;
  228. }
  229. /**
  230. * 归属月份:优先采购确认/完结操作时间,其次 updatetime,再 createtime
  231. *
  232. * @param array<string, mixed> $row
  233. * @param array<int, int> $completeTsMap scydgy_id => unix 完结时间
  234. */
  235. protected function resolvePurchaseOrderRowYm(array $row, array $completeTsMap = []): string
  236. {
  237. $ts = $this->resolvePurchaseOrderCompleteTimestamp($row, $completeTsMap);
  238. if ($ts > 0) {
  239. return date('Y-m', $ts);
  240. }
  241. return '';
  242. }
  243. /**
  244. * @param array<string, mixed> $row
  245. * @param array<int, int> $completeTsMap
  246. */
  247. protected function resolvePurchaseOrderCompleteTimestamp(array $row, array $completeTsMap = []): int
  248. {
  249. $sid = (int)($row['scydgy_id'] ?? 0);
  250. if ($sid > 0 && isset($completeTsMap[$sid]) && (int)$completeTsMap[$sid] > 946684800) {
  251. return (int)$completeTsMap[$sid];
  252. }
  253. foreach (['updatetime', 'createtime', 'dStamp'] as $key) {
  254. $raw = $row[$key] ?? null;
  255. if ($raw === null || $raw === '') {
  256. continue;
  257. }
  258. if (is_numeric($raw) && (int)$raw > 946684800) {
  259. return (int)$raw;
  260. }
  261. $t = trim((string)$raw);
  262. if ($t !== '' && stripos($t, '0000-00-00') !== 0) {
  263. $parsed = (int)strtotime($t);
  264. return $parsed > 946684800 ? $parsed : 0;
  265. }
  266. }
  267. return 0;
  268. }
  269. /**
  270. * @param array<int, string> $actions
  271. * @return array<int, int> scydgy_id => 最近操作 unix 时间
  272. */
  273. protected function loadOperLogTimestampMap(array $scydgyIds, array $actions): array
  274. {
  275. $out = [];
  276. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  277. $actions = array_values(array_filter(array_map('strval', $actions)));
  278. if ($scydgyIds === [] || $actions === []) {
  279. return $out;
  280. }
  281. try {
  282. $logs = Db::table('purchase_order_oper_log')
  283. ->where('scydgy_id', 'in', $scydgyIds)
  284. ->where('action', 'in', $actions)
  285. ->field('scydgy_id,createtime')
  286. ->order('id', 'asc')
  287. ->select();
  288. } catch (\Throwable $e) {
  289. $logs = [];
  290. }
  291. if (!is_array($logs)) {
  292. return $out;
  293. }
  294. foreach ($logs as $log) {
  295. if (!is_array($log)) {
  296. continue;
  297. }
  298. $sid = (int)($log['scydgy_id'] ?? 0);
  299. $ct = (int)($log['createtime'] ?? 0);
  300. if ($sid > 0 && $ct > 946684800) {
  301. $out[$sid] = isset($out[$sid]) ? max($out[$sid], $ct) : $ct;
  302. }
  303. }
  304. return $out;
  305. }
  306. /**
  307. * @param array<string, mixed> $row
  308. * @param array<int, int> $completeTsMap
  309. */
  310. protected function formatPurchaseOrderDoneTime(array $row, array $completeTsMap = []): string
  311. {
  312. $ts = $this->resolvePurchaseOrderCompleteTimestamp($row, $completeTsMap);
  313. if ($ts > 0) {
  314. return date('Y-m-d H:i:s', $ts);
  315. }
  316. return '';
  317. }
  318. /**
  319. * @param array<int, array<string, mixed>> $rows
  320. * @return array<int, array<string, mixed>>
  321. */
  322. protected function collapseExportRowsByOrder(array $rows): array
  323. {
  324. if ($rows === []) {
  325. return [];
  326. }
  327. $groups = [];
  328. foreach ($rows as $row) {
  329. if (!is_array($row)) {
  330. continue;
  331. }
  332. $dh = trim((string)($row['CCYDH'] ?? ''));
  333. $key = $dh !== '' ? $dh : ('_id_' . (int)($row['id'] ?? 0));
  334. if (!isset($groups[$key])) {
  335. $groups[$key] = [];
  336. }
  337. $groups[$key][] = $row;
  338. }
  339. $out = [];
  340. foreach ($groups as $groupRows) {
  341. if ($groupRows === []) {
  342. continue;
  343. }
  344. usort($groupRows, function ($a, $b) {
  345. return ((int)($a['id'] ?? 0)) <=> ((int)($b['id'] ?? 0));
  346. });
  347. $head = $groupRows[0];
  348. $gymcList = [];
  349. foreach ($groupRows as $r) {
  350. $g = trim((string)($r['CGYMC'] ?? ''));
  351. if ($g !== '' && !in_array($g, $gymcList, true)) {
  352. $gymcList[] = $g;
  353. }
  354. }
  355. $merged = $head;
  356. $merged['_merge_rows'] = $groupRows;
  357. if (count($groupRows) > 1) {
  358. $merged['CGYMC'] = implode('、', $gymcList);
  359. }
  360. $latestText = '';
  361. $latestTs = 0;
  362. foreach ($groupRows as $r) {
  363. $t = trim((string)($r['createtime_text'] ?? ''));
  364. $ts = $t !== '' ? (int)strtotime($t) : 0;
  365. if ($ts > $latestTs) {
  366. $latestTs = $ts;
  367. $latestText = $t;
  368. }
  369. }
  370. if ($latestText !== '') {
  371. $merged['createtime_text'] = $latestText;
  372. }
  373. $maxId = (int)($head['id'] ?? 0);
  374. foreach ($groupRows as $r) {
  375. $maxId = max($maxId, (int)($r['id'] ?? 0));
  376. }
  377. $merged['id'] = $maxId;
  378. $out[] = $merged;
  379. }
  380. usort($out, function ($a, $b) {
  381. return ((int)($b['id'] ?? 0)) <=> ((int)($a['id'] ?? 0));
  382. });
  383. return $out;
  384. }
  385. }