Procuremen.php 95 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568
  1. <?php
  2. namespace app\admin\controller;
  3. use app\common\controller\Backend;
  4. use app\common\library\AliyunOss;
  5. use PHPMailer\PHPMailer\PHPMailer;
  6. use think\Config;
  7. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  8. use PhpOffice\PhpSpreadsheet\Style\Alignment;
  9. use PhpOffice\PhpSpreadsheet\Style\Border;
  10. use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
  11. use think\Db;
  12. use think\Log;
  13. class Procuremen extends Backend
  14. {
  15. /**
  16. * 顶部快速搜索(多列 OR LIKE);字段须为真实列名且带别名 a/b,列不宜过多以免 SQL 异常
  17. */
  18. protected $searchFields = 'a.ID,b.CCYDH,b.CYJMC,a.CDXMC,a.cGzzxMc';
  19. /**
  20. * Procuremen模型对象
  21. * @var \app\admin\model\Procuremen
  22. */
  23. protected $model = null;
  24. /**
  25. * @var array
  26. */
  27. protected $noNeedRight = ['review', 'reviewCompanies', 'outward_detail', 'export_month_outward', 'snapshotToProcure'];
  28. public function _initialize()
  29. {
  30. parent::_initialize();
  31. $this->model = new \app\admin\model\Procuremen;
  32. }
  33. /**
  34. * 手机端外发明细首页直达链接(登录后打开对应明细,免搜索)
  35. * 配置 application/extra/mproc.php:mobile_base_url、mobile_index_path
  36. *
  37. * @param int $purchaseOrderDetailId purchase_order_detail 主键
  38. */
  39. protected function buildMprocMobileOrderUrl($purchaseOrderDetailId)
  40. {
  41. static $mprocCfgLoaded = false;
  42. if (!$mprocCfgLoaded) {
  43. $mprocCfgLoaded = true;
  44. if (is_file(APP_PATH . 'extra/mproc.php')) {
  45. Config::load(APP_PATH . 'extra/mproc.php', 'mproc');
  46. }
  47. }
  48. $eid = (int)$purchaseOrderDetailId;
  49. $base = $this->resolveMprocMobilePublicBaseUrl();
  50. if ($base === '') {
  51. throw new \Exception('请在 application/extra/mproc.php 中配置外网可访问的正式域名');
  52. }
  53. $path = trim((string)Config::get('mproc.mobile_index_path'));
  54. if ($path === '') {
  55. $path = '/index.php/index/index/index';
  56. } else {
  57. $path = '/' . ltrim($path, '/');
  58. }
  59. $query = ['main_tab' => 'orders'];
  60. if ($eid > 0) {
  61. $query['focus_eid'] = $eid;
  62. }
  63. return $base . $path . '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
  64. }
  65. /**
  66. * 邮件/短信外链:须为带点的公网域名
  67. */
  68. protected function procuremenOutboundBaseUrlLooksValid($baseUrl)
  69. {
  70. $u = trim((string)$baseUrl);
  71. if ($u === '' || !preg_match('#^https?://#i', $u)) {
  72. return false;
  73. }
  74. $host = parse_url($u, PHP_URL_HOST);
  75. if (!is_string($host) || $host === '') {
  76. return false;
  77. }
  78. if (strcasecmp($host, 'localhost') === 0) {
  79. return false;
  80. }
  81. if (preg_match('/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host)) {
  82. return false;
  83. }
  84. // 无点主机名(xh、erp 等)在小米/企业邮安全跳转中常报 Invalid url
  85. if (strpos($host, '.') === false) {
  86. return false;
  87. }
  88. return true;
  89. }
  90. /**
  91. * 解析用于外发邮件/短信的手机端站点根(https://域名 无末尾斜杠)
  92. */
  93. protected function resolveMprocMobilePublicBaseUrl()
  94. {
  95. $candidates = [];
  96. $candidates[] = trim((string)Config::get('mproc.mobile_base_url'));
  97. $candidates[] = trim((string)Config::get('site.indexurl'));
  98. $cdn = trim((string)Config::get('site.cdnurl'));
  99. if ($cdn !== '' && preg_match('#^https?://#i', $cdn)) {
  100. $candidates[] = rtrim($cdn, '/');
  101. }
  102. $reqBase = rtrim($this->request->scheme() . '://' . $this->request->host(), '/');
  103. $candidates[] = $reqBase;
  104. foreach ($candidates as $c) {
  105. if ($c === '') {
  106. continue;
  107. }
  108. $c = rtrim($c, '/');
  109. if ($this->procuremenOutboundBaseUrlLooksValid($c)) {
  110. return $c;
  111. }
  112. }
  113. Log::write('外发邮件手机链接:未配置有效 mproc.mobile_base_url,且当前访问主机不适合作为外链,请在 application/extra/mproc.php 设置 https 正式域名', 'warning');
  114. return '';
  115. }
  116. /**
  117. * 排除 mcyd.dputrecord 空串、零日期等非法值,避免 MySQL 1525(DATE_FORMAT/BETWEEN 遇 '' 报错)
  118. */
  119. protected function whereMcydDputrecordValid($query, $alias = 'a')
  120. {
  121. $c = $alias . '.dStamp';
  122. // 先转成 CHAR 再判断,避免 DATETIME 列里存 '' / 0000-00-00 时参与日期函数报错
  123. return $query->whereRaw(
  124. $c . ' IS NOT NULL'
  125. . ' AND TRIM(CAST(' . $c . ' AS CHAR(32))) <> \'\''
  126. . ' AND TRIM(CAST(' . $c . ' AS CHAR(32))) NOT LIKE \'0000-00-00%\''
  127. . ' AND TRIM(CAST(' . $c . ' AS CHAR(32))) REGEXP \'^[12][0-9]{3}-[01][0-9]-[0-3][0-9]\''
  128. );
  129. }
  130. /**
  131. * 左侧菜单:仅「近 12 个自然月(含当月)」
  132. */
  133. protected function getIndexSidebarYearMonths()
  134. {
  135. $ymSet = [];
  136. $anchor = strtotime(date('Y-m-01'));
  137. for ($i = 0; $i < 12; $i++) {
  138. $ym = date('Y-m', strtotime("-{$i} month", $anchor));
  139. $ymSet[$ym] = true;
  140. }
  141. $windowKeys = array_keys($ymSet);
  142. $minYm = min($windowKeys);
  143. $maxYm = max($windowKeys);
  144. $rows = [];
  145. try {
  146. $query = Db::table('scydgy')
  147. ->alias('a')
  148. ->join('mcyd b', 'b.ICYDID = a.ICYDID AND b.iStatus >= 10', 'inner')
  149. ->where([
  150. 'a.bwjg' => 1,
  151. 'a.iEndBz' => 0,
  152. 'a.iType' => 0,
  153. 'a.iStatus' => 10,
  154. ]);
  155. $this->whereMcydDputrecordValid($query, 'a');
  156. $rows = $query->field("DATE_FORMAT(a.dputrecord, '%Y-%m') as ym")
  157. ->group("DATE_FORMAT(a.dputrecord, '%Y-%m')")
  158. ->orderRaw("DATE_FORMAT(a.dputrecord, '%Y-%m') DESC")
  159. ->select();
  160. } catch (\Exception $e) {
  161. $rows = [];
  162. }
  163. foreach ($rows as $r) {
  164. $ym = isset($r['ym']) ? trim((string)$r['ym']) : '';
  165. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  166. continue;
  167. }
  168. if (strcmp($ym, $minYm) >= 0 && strcmp($ym, $maxYm) <= 0) {
  169. $ymSet[$ym] = true;
  170. }
  171. }
  172. $ymList = array_keys($ymSet);
  173. rsort($ymList, SORT_STRING);
  174. $byYear = [];
  175. foreach ($ymList as $ym) {
  176. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  177. continue;
  178. }
  179. $y = substr($ym, 0, 4);
  180. $m = (int)substr($ym, 5, 2);
  181. if (!isset($byYear[$y])) {
  182. $byYear[$y] = [];
  183. }
  184. $byYear[$y][] = ['ym' => $ym, 'label' => $m . '月'];
  185. }
  186. krsort($byYear, SORT_NUMERIC);
  187. foreach ($byYear as $y => $items) {
  188. usort($byYear[$y], function ($a, $b) {
  189. return strcmp($b['ym'], $a['ym']);
  190. });
  191. }
  192. $sidebar = [];
  193. foreach ($byYear as $y => $months) {
  194. $sidebar[] = ['year' => $y, 'months' => $months];
  195. }
  196. return $sidebar;
  197. }
  198. /**
  199. * 列表页
  200. */
  201. public function index(){
  202. $this->request->filter(['strip_tags', 'trim']);
  203. if (!$this->request->isAjax()) {
  204. $this->view->assign('defaultYm', date('Y-m'));
  205. $this->view->assign('sidebarYearMonths', $this->getIndexSidebarYearMonths());
  206. return $this->view->fetch();
  207. }
  208. if ($this->request->request('keyField')) {
  209. return $this->selectpage();
  210. }
  211. list(, $sort, $order, $offset, $limit) = $this->buildparams();
  212. $limit = max(10, min(500, (int)$limit));
  213. $ym = $this->request->request('ym', date('Y-m'));
  214. $ym = is_string($ym) ? trim($ym) : '';
  215. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  216. $ym = date('Y-m');
  217. }
  218. $monthStart = $ym . '-01 00:00:00';
  219. $monthEnd = date('Y-m-t 23:59:59', strtotime($monthStart));
  220. $search = trim((string)$this->request->get('search', ''));
  221. $hasActiveSearch = ($search !== '');
  222. if (!$hasActiveSearch) {
  223. $filterStr = $this->request->get('filter', '');
  224. if ($filterStr !== '' && $filterStr !== '[]' && $filterStr !== '{}') {
  225. $arr = json_decode($filterStr, true);
  226. if (is_array($arr) && count($arr) > 0) {
  227. foreach ($arr as $v) {
  228. if (is_array($v)) {
  229. if (array_filter($v, function ($x) {
  230. return $x !== '' && $x !== null;
  231. })) {
  232. $hasActiveSearch = true;
  233. break;
  234. }
  235. } elseif ($v !== '' && $v !== null) {
  236. $hasActiveSearch = true;
  237. break;
  238. }
  239. }
  240. }
  241. }
  242. }
  243. $applyMonthRange = !$hasActiveSearch;
  244. $filterArr = (array)json_decode($this->request->get('filter', ''), true);
  245. $opArr = (array)json_decode($this->request->get('op', ''), true);
  246. /* wff_tab:all=未发;pending=已下发;picked=已选中(主表 status=1 且存在明细 status=1);done=已完结 */
  247. $wffTab = trim((string)$this->request->request('wff_tab', 'all'));
  248. if (!in_array($wffTab, ['all', 'pending', 'picked', 'done'], true)) {
  249. $wffTab = 'all';
  250. }
  251. try {
  252. $issuedSet = $this->loadIssuedScydgySet();
  253. if (in_array($wffTab, ['pending', 'done', 'picked'], true)) {
  254. $wantStatus = $wffTab === 'pending' ? 0 : 1;
  255. $pool = [];
  256. $dbRows = [];
  257. try {
  258. if ($wantStatus === 0) {
  259. $dbRows = Db::table('purchase_order')
  260. ->where('status', $wantStatus)
  261. ->select();
  262. } else {
  263. $dbRows = Db::table('purchase_order')
  264. ->where('status', $wantStatus)
  265. ->select();
  266. }
  267. } catch (\Throwable $e) {
  268. $dbRows = [];
  269. }
  270. if (!is_array($dbRows)) {
  271. $dbRows = [];
  272. }
  273. if ($wffTab === 'picked') {
  274. $pickedSidSet = $this->loadScydgyIdsWithPickedSupplierDetail();
  275. $dbRows = array_values(array_filter($dbRows, function ($dbRow) use ($pickedSidSet) {
  276. if (!is_array($dbRow)) {
  277. return false;
  278. }
  279. $sid = (int)($dbRow['scydgy_id'] ?? 0);
  280. if ($sid <= 0 && isset($dbRow['SCYDGY_ID'])) {
  281. $sid = (int)$dbRow['SCYDGY_ID'];
  282. }
  283. return $sid > 0 && isset($pickedSidSet[$sid]);
  284. }));
  285. }
  286. $dStampMap = [];
  287. $sidList = [];
  288. foreach ($dbRows as $tmpRow) {
  289. if (is_array($tmpRow) && isset($tmpRow['scydgy_id'])) {
  290. $sid = (int)$tmpRow['scydgy_id'];
  291. if ($sid > 0) {
  292. $sidList[$sid] = true;
  293. }
  294. }
  295. }
  296. if ($sidList !== []) {
  297. try {
  298. $dStampMap = Db::table('scydgy')->where('ID', 'in', array_keys($sidList))->column('dStamp', 'ID');
  299. } catch (\Throwable $e) {
  300. $dStampMap = [];
  301. }
  302. }
  303. foreach ($dbRows as $dbRow) {
  304. $rj = null;
  305. if (!empty($dbRow['row_json'])) {
  306. $decoded = json_decode($dbRow['row_json'], true);
  307. if (is_array($decoded)) {
  308. $rj = $decoded;
  309. }
  310. }
  311. if ($rj !== null && isset($rj['ID'])) {
  312. $rid = (int)$rj['ID'];
  313. $dsFix = isset($rj['dStamp']) ? trim((string)$rj['dStamp']) : '';
  314. if (($dsFix === '' || stripos($dsFix, '0000-00-00') === 0)
  315. && isset($dStampMap[$rid]) && trim((string)$dStampMap[$rid]) !== '') {
  316. $rj['dStamp'] = $dStampMap[$rid];
  317. }
  318. if (array_key_exists('id', $dbRow)) {
  319. $rj['purchase_order_id'] = (int)$dbRow['id'];
  320. }
  321. $pool[] = $rj;
  322. continue;
  323. }
  324. $r = [];
  325. foreach ($dbRow as $k => $v) {
  326. $lk = strtolower((string)$k);
  327. if ($lk === 'id' || $lk === 'row_json') {
  328. continue;
  329. }
  330. $r[$k] = $v;
  331. }
  332. if (isset($dbRow['scydgy_id'])) {
  333. $r['ID'] = (int)$dbRow['scydgy_id'];
  334. $sid = $r['ID'];
  335. $dsOut = '';
  336. if (isset($dStampMap[$sid])) {
  337. $t = trim((string)$dStampMap[$sid]);
  338. if ($t !== '' && stripos($t, '0000-00-00') !== 0) {
  339. $dsOut = $dStampMap[$sid];
  340. }
  341. }
  342. if ($dsOut === '' && !empty($dbRow['createtime'])) {
  343. $ct = $dbRow['createtime'];
  344. if (is_numeric($ct) && (int)$ct > 946684800) {
  345. $dsOut = date('Y-m-d H:i:s', (int)$ct);
  346. } elseif (is_string($ct) && trim($ct) !== '') {
  347. $tc = trim($ct);
  348. if ($tc !== '' && stripos($tc, '0000-00-00') !== 0) {
  349. $dsOut = $tc;
  350. }
  351. }
  352. }
  353. if ($dsOut !== '') {
  354. $r['dStamp'] = $dsOut;
  355. }
  356. }
  357. if (array_key_exists('id', $dbRow)) {
  358. $r['purchase_order_id'] = (int)$dbRow['id'];
  359. }
  360. $pool[] = $r;
  361. }
  362. } else {
  363. $redis = redis();
  364. $raw = $redis->get('procuremen_redis');
  365. if ($raw === false || $raw === '') {
  366. return json([
  367. 'total' => 0,
  368. 'rows' => [],
  369. 'msg' => '暂无缓存数据',
  370. ]);
  371. }
  372. $decoded = json_decode($raw, true);
  373. $pool = (is_array($decoded) && isset($decoded['data']) && is_array($decoded['data']))
  374. ? $decoded['data'] : [];
  375. // 未发:主表已有且 status 为 0/1 才从列表隐藏;status 为空(NULL)仍可在未发展示
  376. try {
  377. $poIds = Db::table('purchase_order')
  378. ->whereRaw('(`status` = 0 OR `status` = 1)')
  379. ->column('scydgy_id');
  380. } catch (\Throwable $e) {
  381. $poIds = [];
  382. }
  383. if ($pool !== [] && is_array($poIds) && $poIds !== []) {
  384. $poSet = [];
  385. foreach ($poIds as $pid) {
  386. $k = (int)$pid;
  387. if ($k > 0) {
  388. $poSet[$k] = true;
  389. }
  390. }
  391. if ($poSet !== []) {
  392. $pool = array_values(array_filter($pool, function ($r) use ($poSet) {
  393. if (!is_array($r)) {
  394. return false;
  395. }
  396. $id = (int)($r['ID'] ?? 0);
  397. if ($id <= 0) {
  398. return true;
  399. }
  400. return !isset($poSet[$id]);
  401. }));
  402. }
  403. }
  404. }
  405. $filtered = $this->filterProcuremenIndexPool(
  406. $pool,
  407. $monthStart,
  408. $monthEnd,
  409. $applyMonthRange,
  410. $search,
  411. $filterArr,
  412. $opArr
  413. );
  414. $sortField = 'ID';
  415. if (is_string($sort) && $sort !== '') {
  416. $parts = explode(',', $sort);
  417. $sortField = preg_replace('/^[ab]\./i', '', trim($parts[0]));
  418. if ($sortField === '') {
  419. $sortField = 'ID';
  420. }
  421. }
  422. $ord = strtoupper((string)$order) === 'ASC' ? 1 : -1;
  423. $nFiltered = count($filtered);
  424. if ($nFiltered > 1) {
  425. // 默认按主键 ID 排序时,用 array_multisort 比 usort+闭包略省开销
  426. if ($sortField === 'ID') {
  427. $sortKeys = [];
  428. foreach ($filtered as $i => $row) {
  429. $sortKeys[$i] = (int)($row['ID'] ?? 0);
  430. }
  431. $dir = $ord === 1 ? SORT_ASC : SORT_DESC;
  432. array_multisort($sortKeys, $dir, SORT_NUMERIC, $filtered);
  433. } else {
  434. usort($filtered, function ($a, $b) use ($sortField, $ord) {
  435. $va = $a[$sortField] ?? null;
  436. $vb = $b[$sortField] ?? null;
  437. $sa = (string)$va;
  438. $sb = (string)$vb;
  439. if ($sa === $sb) {
  440. return 0;
  441. }
  442. return strcmp($sa, $sb) * $ord;
  443. });
  444. }
  445. }
  446. $offset = max(0, (int)$offset);
  447. $limit = max(1, (int)$limit);
  448. $rows = array_slice($filtered, $offset, $limit);
  449. foreach ($rows as &$rw) {
  450. if (!is_array($rw)) {
  451. continue;
  452. }
  453. $rid = (int)($rw['ID'] ?? 0);
  454. if ($wffTab === 'pending' || $wffTab === 'done' || $wffTab === 'picked') {
  455. $rw['_iss_out'] = true;
  456. } else {
  457. $rw['_iss_out'] = $rid > 0 && isset($issuedSet[$rid]);
  458. }
  459. }
  460. unset($rw);
  461. if ($wffTab === 'pending' && count($rows) > 0) {
  462. $idList = [];
  463. foreach ($rows as $rw) {
  464. if (!is_array($rw)) {
  465. continue;
  466. }
  467. $id = (int)($rw['ID'] ?? 0);
  468. if ($id > 0) {
  469. $idList[$id] = true;
  470. }
  471. }
  472. $sumMap = $this->OrderDetailSummary(array_keys($idList));
  473. foreach ($rows as &$rw) {
  474. if (!is_array($rw)) {
  475. continue;
  476. }
  477. $rid = (int)($rw['ID'] ?? 0);
  478. $s = isset($sumMap[$rid]) ? $sumMap[$rid] : ['cnt' => 0, 'amt' => 0, 'deliv' => 0];
  479. $rw['po_detail_count'] = $s['cnt'];
  480. $rw['po_amount_fill_cnt'] = $s['amt'];
  481. $rw['po_delivery_fill_cnt'] = $s['deliv'];
  482. }
  483. unset($rw);
  484. }
  485. if ($wffTab === 'picked' && count($rows) > 0) {
  486. $idList = [];
  487. foreach ($rows as $rw) {
  488. if (!is_array($rw)) {
  489. continue;
  490. }
  491. $id = (int)($rw['ID'] ?? 0);
  492. if ($id > 0) {
  493. $idList[$id] = true;
  494. }
  495. }
  496. $nameMap = $this->loadPickedSupplierCompanyByScydgyIds(array_keys($idList));
  497. foreach ($rows as &$rw) {
  498. if (!is_array($rw)) {
  499. continue;
  500. }
  501. $rid = (int)($rw['ID'] ?? 0);
  502. $rw['picked_supplier_name'] = $nameMap[$rid] ?? '';
  503. }
  504. unset($rw);
  505. }
  506. if ($wffTab === 'all' && count($rows) > 0) {
  507. $this->mergePurchaseOrder($rows);
  508. }
  509. return json([
  510. 'total' => count($filtered),
  511. 'rows' => $rows,
  512. ]);
  513. } catch (\Throwable $e) {
  514. return json([
  515. 'total' => 0,
  516. 'rows' => [],
  517. 'msg' => $e->getMessage(),
  518. ]);
  519. }
  520. }
  521. /**
  522. * 已外发判定:主表 purchase_order.scydgy_id ∪ 明细 purchase_order_detail.scydgy_id(与工序列表行 ID 对应)
  523. *
  524. * @return array<int, true> 列表行 ID => true
  525. */
  526. protected function loadIssuedScydgySet()
  527. {
  528. $issuedSet = [];
  529. foreach (['purchase_order', 'purchase_order_detail'] as $tbl) {
  530. try {
  531. $issuedList = Db::table($tbl)->group('scydgy_id')->column('scydgy_id');
  532. if (!is_array($issuedList)) {
  533. continue;
  534. }
  535. foreach ($issuedList as $ids) {
  536. $n = (int)$ids;
  537. if ($n > 0) {
  538. $issuedSet[$n] = true;
  539. }
  540. }
  541. } catch (\Throwable $e) {
  542. }
  543. }
  544. return $issuedSet;
  545. }
  546. /**
  547. * 采购确认已选定供应商:明细 purchase_order_detail.status = 1 的工序行 scydgy_id 集合
  548. *
  549. * @return array<int, true>
  550. */
  551. protected function loadScydgyIdsWithPickedSupplierDetail(): array
  552. {
  553. $set = [];
  554. try {
  555. $rows = Db::table('purchase_order_detail')->where('status', 1)->field('scydgy_id')->select();
  556. } catch (\Throwable $e) {
  557. $rows = [];
  558. }
  559. if (!is_array($rows)) {
  560. return $set;
  561. }
  562. foreach ($rows as $r) {
  563. if (!is_array($r)) {
  564. continue;
  565. }
  566. $sid = (int)($r['scydgy_id'] ?? $r['SCYDGY_ID'] ?? 0);
  567. if ($sid > 0) {
  568. $set[$sid] = true;
  569. }
  570. }
  571. return $set;
  572. }
  573. /**
  574. * 每个工序行对应已选中供应商名称(取 status=1 的首条明细 company_name)
  575. *
  576. * @param int[] $scydgyIds
  577. * @return array<int, string> scydgy_id => company_name
  578. */
  579. protected function loadPickedSupplierCompanyByScydgyIds(array $scydgyIds): array
  580. {
  581. $out = [];
  582. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  583. if ($scydgyIds === []) {
  584. return $out;
  585. }
  586. try {
  587. $rows = Db::table('purchase_order_detail')
  588. ->where('scydgy_id', 'in', $scydgyIds)
  589. ->where('status', 1)
  590. ->field('scydgy_id,company_name')
  591. ->order('id', 'asc')
  592. ->select();
  593. } catch (\Throwable $e) {
  594. try {
  595. $rows = Db::table('purchase_order_detail')
  596. ->where('scydgy_id', 'in', $scydgyIds)
  597. ->where('status', 1)
  598. ->field('scydgy_id,company_name')
  599. ->order('ID', 'asc')
  600. ->select();
  601. } catch (\Throwable $e2) {
  602. $rows = [];
  603. }
  604. }
  605. if (!is_array($rows)) {
  606. return $out;
  607. }
  608. foreach ($rows as $r) {
  609. if (!is_array($r)) {
  610. continue;
  611. }
  612. $sid = (int)($r['scydgy_id'] ?? $r['SCYDGY_ID'] ?? 0);
  613. if ($sid <= 0 || isset($out[$sid])) {
  614. continue;
  615. }
  616. $name = trim((string)($r['company_name'] ?? ''));
  617. $out[$sid] = $name;
  618. }
  619. return $out;
  620. }
  621. /**
  622. * 已下发列表汇总:按工序行 ID 统计 purchase_order_detail 条数、已填金额条数、已填货期条数
  623. *
  624. * @param int[] $scydgyIds
  625. * @return array<int, array{cnt:int, amt:int, deliv:int}>
  626. */
  627. protected function OrderDetailSummary(array $scydgyIds)
  628. {
  629. $out = [];
  630. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  631. if ($scydgyIds === []) {
  632. return $out;
  633. }
  634. $list = [];
  635. try {
  636. $list = Db::table('purchase_order_detail')
  637. ->where('scydgy_id', 'in', $scydgyIds)
  638. ->field('id,scydgy_id,amount,delivery')
  639. ->select();
  640. } catch (\Throwable $e) {
  641. $list = [];
  642. }
  643. if (!is_array($list)) {
  644. return $out;
  645. }
  646. foreach ($list as $r) {
  647. if (!is_array($r)) {
  648. continue;
  649. }
  650. $ids = (int)($r['scydgy_id'] ?? 0);
  651. if ($ids <= 0) {
  652. continue;
  653. }
  654. if (!isset($out[$ids])) {
  655. $out[$ids] = ['cnt' => 0, 'amt' => 0, 'deliv' => 0];
  656. }
  657. $out[$ids]['cnt']++;
  658. $am = $r['amount'] ?? null;
  659. if ($am !== null && $am !== '') {
  660. if (!(is_string($am) && trim($am) === '')) {
  661. $out[$ids]['amt']++;
  662. }
  663. }
  664. $dv = $r['delivery'] ?? null;
  665. if ($dv !== null && trim((string)$dv) !== '') {
  666. $out[$ids]['deliv']++;
  667. }
  668. }
  669. return $out;
  670. }
  671. /**
  672. * 列表筛选:月份、dStamp 合法性、快速搜索、Bootstrap Table filter(与原先 Redis 分支一致)
  673. *
  674. * @return array
  675. */
  676. protected function filterProcuremenIndexPool(array $pool, $monthStart, $monthEnd, $applyMonthRange, $search, array $filterArr, array $opArr)
  677. {
  678. $strContains = function ($haystack, $needle) {
  679. if ($needle === '') {
  680. return false;
  681. }
  682. $haystack = (string)$haystack;
  683. if (function_exists('mb_stripos')) {
  684. return mb_stripos($haystack, $needle, 0, 'UTF-8') !== false;
  685. }
  686. return stripos($haystack, $needle) !== false;
  687. };
  688. $stripAlias = function ($f) {
  689. return preg_replace('/^[ab]\./i', '', (string)$f);
  690. };
  691. $searchColNames = [];
  692. foreach (explode(',', $this->searchFields) as $colRaw) {
  693. $c = $stripAlias(trim($colRaw));
  694. if ($c !== '') {
  695. $searchColNames[] = $c;
  696. }
  697. }
  698. $filtered = [];
  699. foreach ($pool as $r) {
  700. if (!is_array($r)) {
  701. continue;
  702. }
  703. $ds = isset($r['dStamp']) ? trim((string)$r['dStamp']) : '';
  704. if ($ds === '' || stripos($ds, '0000-00-00') === 0
  705. || !preg_match('/^[12]\d{3}-\d{1,2}-\d{1,2}/', $ds)) {
  706. continue;
  707. }
  708. if ($applyMonthRange) {
  709. if (strcmp($ds, $monthStart) < 0 || strcmp($ds, $monthEnd) > 0) {
  710. continue;
  711. }
  712. }
  713. if ($search !== '') {
  714. $hitSearch = false;
  715. foreach ($searchColNames as $c) {
  716. $cell = isset($r[$c]) ? (string)$r[$c] : '';
  717. if ($cell !== '' && $strContains($cell, $search)) {
  718. $hitSearch = true;
  719. break;
  720. }
  721. }
  722. if (!$hitSearch) {
  723. continue;
  724. }
  725. }
  726. foreach ($filterArr as $fk => $fv) {
  727. if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', (string)$fk)) {
  728. continue;
  729. }
  730. if (is_array($fv)) {
  731. continue;
  732. }
  733. if ($fv === '' && $fv !== '0' && $fv !== 0) {
  734. continue;
  735. }
  736. $sym = strtoupper(trim((string)($opArr[$fk] ?? '=')));
  737. $sym = str_replace(['LIKE %...%', 'NOT LIKE %...%'], ['LIKE', 'NOT LIKE'], $sym);
  738. $field = $stripAlias($fk);
  739. $cell = array_key_exists($field, $r) ? $r[$field] : null;
  740. $cellStr = trim((string)$cell);
  741. switch ($sym) {
  742. case '=':
  743. if ((string)$cell !== (string)$fv) {
  744. continue 3;
  745. }
  746. break;
  747. case '<>':
  748. if ((string)$cell === (string)$fv) {
  749. continue 3;
  750. }
  751. break;
  752. case 'LIKE':
  753. case 'NOT LIKE':
  754. $needle = trim((string)$fv);
  755. $hit = ($needle !== '' && $strContains($cellStr, $needle));
  756. if ($sym === 'LIKE' && !$hit) {
  757. continue 3;
  758. }
  759. if ($sym === 'NOT LIKE' && $hit) {
  760. continue 3;
  761. }
  762. break;
  763. case '>':
  764. case '>=':
  765. case '<':
  766. case '<=':
  767. if (!is_numeric($cell) && $cellStr === '') {
  768. continue 3;
  769. }
  770. $cv = is_numeric($cell) ? (float)$cell : (float)$cellStr;
  771. $vv = (float)$fv;
  772. if ($sym === '>' && !($cv > $vv)) {
  773. continue 3;
  774. }
  775. if ($sym === '>=' && !($cv >= $vv)) {
  776. continue 3;
  777. }
  778. if ($sym === '<' && !($cv < $vv)) {
  779. continue 3;
  780. }
  781. if ($sym === '<=' && !($cv <= $vv)) {
  782. continue 3;
  783. }
  784. break;
  785. case 'BETWEEN':
  786. case 'NOT BETWEEN':
  787. case 'BETWEEN TIME':
  788. case 'NOT BETWEEN TIME':
  789. $rawR = is_array($fv) ? implode(',', $fv) : (string)$fv;
  790. $rawR = str_replace(' - ', ',', $rawR);
  791. $arr = array_slice(array_map('trim', explode(',', $rawR)), 0, 2);
  792. if (count($arr) < 2 || $arr[0] === '' || $arr[1] === '') {
  793. break;
  794. }
  795. $lo = $arr[0];
  796. $hi = $arr[1];
  797. $in = ($cellStr >= $lo && $cellStr <= $hi);
  798. $isNot = (strpos($sym, 'NOT') !== false);
  799. if ($isNot ? $in : !$in) {
  800. continue 3;
  801. }
  802. break;
  803. case 'NULL':
  804. case 'IS NULL':
  805. if ($cell !== null && $cell !== '') {
  806. continue 3;
  807. }
  808. break;
  809. case 'NOT NULL':
  810. case 'IS NOT NULL':
  811. if ($cell === null || $cell === '') {
  812. continue 3;
  813. }
  814. break;
  815. default:
  816. break;
  817. }
  818. }
  819. $filtered[] = $r;
  820. }
  821. return $filtered;
  822. }
  823. /**
  824. * 审核弹窗提交
  825. * 查询订单 purchase_order 数据查有无记录,无则插、有则改(全部写在本方法内)
  826. */
  827. public function snapshotToProcure()
  828. {
  829. if (!$this->request->isPost() || !$this->request->isAjax()) {
  830. $this->error(__('Invalid parameters'));
  831. }
  832. $row = json_decode($this->request->post('row_json', ''), true);
  833. if (!is_array($row)) {
  834. $this->error(__('Invalid parameters'));
  835. }
  836. try {
  837. $ids = (int)($row['ID'] ?? $row['id'] ?? 0);
  838. if ($ids <= 0) {
  839. throw new \Exception('无效的行主键 ID');
  840. }
  841. $exists = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
  842. $data = [
  843. 'scydgy_id' => $ids,
  844. 'CCYDH' => $row['CCYDH'] ?? null,
  845. 'CYJMC' => $row['CYJMC'] ?? null,
  846. 'CDXMC' => $row['CDXMC'] ?? null,
  847. 'CGYBH' => $row['CGYBH'] ?? null,
  848. 'CGYMC' => $row['CGYMC'] ?? null,
  849. 'CDW' => $row['CDW'] ?? null,
  850. 'NGZL' => $row['NGZL'] ?? null,
  851. 'CDF' => $row['CDF'] ?? null,
  852. 'cGzzxMc' => $row['cGzzxMc'] ?? null,
  853. 'MBZ' => $row['MBZ'] ?? null,
  854. 'bwjg' => $row['bwjg'] ?? null,
  855. 'iStatus' => $row['iStatus'] ?? null,
  856. 'dStamp' => $row['dStamp'] ?? null,
  857. 'dputrecord' => $row['dputrecord'] ?? null,
  858. 'cywyxm' => $row['cywyxm'] ?? null,
  859. 'This_quantity' => $row['This_quantity'] ?? $row['this_quantity'] ?? null,
  860. 'ceilingPrice' => $row['ceilingPrice'] ?? $row['ceiling_price'] ?? null,
  861. ];
  862. if ($exists) {
  863. $upd = $data;
  864. unset($upd['scydgy_id'], $upd['status']);
  865. Db::table('purchase_order')->where('scydgy_id', $ids)->update($upd);
  866. } else {
  867. // 新增:不写 status(空/走库默认);仅写创建时间
  868. $data['createtime'] = date('Y-m-d H:i:s');
  869. Db::table('purchase_order')->insert($data);
  870. }
  871. } catch (\Throwable $e) {
  872. $this->error($e->getMessage());
  873. }
  874. // 操作记录仅在「审核提交」时写入(review POST + addOrderLog),此处同步不写日志,避免与下发记录重复
  875. $this->success('操作成功');
  876. }
  877. /**
  878. * 未发列表
  879. * 「完结」或「仅保存本次数量/最高限价」:有则改、无则插
  880. * POST finish=1(默认):并置 status=1;finish=0:更新时不改 status;新增不写 status。
  881. */
  882. public function completeDirectly()
  883. {
  884. if (!$this->request->isPost() || !$this->request->isAjax()) {
  885. $this->error(__('参数错误'));
  886. }
  887. $row = json_decode($this->request->post('row_json', ''), true);
  888. if (!is_array($row)) {
  889. $this->error(__('Invalid parameters'));
  890. }
  891. $finishRaw = $this->request->post('finish', '1');
  892. $asComplete = ($finishRaw === '1' || $finishRaw === 1 || $finishRaw === true);
  893. try {
  894. // 获取工序行 ID,对应 purchase_order.scydgy_id;有则改、无则插
  895. $ids = (int)($row['ID'] ?? $row['id'] ?? 0);
  896. if ($ids <= 0) {
  897. throw new \Exception('无效的行主键 ID');
  898. }
  899. $exists = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
  900. $data = [
  901. 'scydgy_id' => $ids,
  902. 'CCYDH' => $row['CCYDH'] ?? null,
  903. 'CYJMC' => $row['CYJMC'] ?? null,
  904. 'CDXMC' => $row['CDXMC'] ?? null,
  905. 'CGYBH' => $row['CGYBH'] ?? null,
  906. 'CGYMC' => $row['CGYMC'] ?? null,
  907. 'CDW' => $row['CDW'] ?? null,
  908. 'NGZL' => $row['NGZL'] ?? null,
  909. 'CDF' => $row['CDF'] ?? null,
  910. 'cGzzxMc' => $row['cGzzxMc'] ?? null,
  911. 'MBZ' => $row['MBZ'] ?? null,
  912. 'bwjg' => $row['bwjg'] ?? null,
  913. 'iStatus' => $row['iStatus'] ?? null,
  914. 'dStamp' => $row['dStamp'] ?? null,
  915. 'dputrecord' => $row['dputrecord'] ?? null,
  916. 'cywyxm' => $row['cywyxm'] ?? null,
  917. 'This_quantity' => $row['This_quantity'] ?? $row['this_quantity'] ?? null,
  918. 'ceilingPrice' => $row['ceilingPrice'] ?? $row['ceiling_price'] ?? null,
  919. ];
  920. if ($asComplete) {
  921. $data['status'] = 1;
  922. }
  923. if ($exists) {
  924. $upd = $data;
  925. unset($upd['scydgy_id']);
  926. if (!$asComplete) {
  927. unset($upd['status']);
  928. }
  929. Db::table('purchase_order')->where('scydgy_id', $ids)->update($upd);
  930. } else {
  931. // 新增:不写 status;完结时 $data 已含 status=1
  932. $data['createtime'] = date('Y-m-d H:i:s');
  933. Db::table('purchase_order')->insert($data);
  934. }
  935. } catch (\Throwable $e) {
  936. $this->error($e->getMessage());
  937. }
  938. $poIdLog = null;
  939. try {
  940. $rpo = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
  941. if (is_array($rpo)) {
  942. $tid = (int)($rpo['id'] ?? $rpo['ID'] ?? 0);
  943. $poIdLog = $tid > 0 ? $tid : null;
  944. }
  945. } catch (\Throwable $e) {
  946. $poIdLog = null;
  947. }
  948. $q = isset($data['This_quantity']) ? trim((string)$data['This_quantity']) : '';
  949. $p = isset($data['ceilingPrice']) ? trim((string)$data['ceilingPrice']) : '';
  950. if ($asComplete) {
  951. $this->addOrderLog(
  952. $ids,
  953. 'mark_complete',
  954. '点击「完结」,已标记为已完结;本次数量「' . ($q !== '' ? $q : '') . '」,最高限价「' . ($p !== '' ? $p : '') . '」',
  955. $poIdLog
  956. );
  957. } else {
  958. $this->addOrderLog(
  959. $ids,
  960. 'save_qty_price',
  961. '保存本次数量、最高限价:本次数量「' . ($q !== '' ? $q : '') . '」,最高限价「' . ($p !== '' ? $p : '') . '」',
  962. $poIdLog
  963. );
  964. }
  965. $this->success("操作成功");
  966. }
  967. /**
  968. * 未发列表:从 purchase_order 合并已填的本次数量、最高限价(若表中有对应列)
  969. *
  970. * @param array<int, array> $rows 引用传递当前页行
  971. */
  972. protected function mergePurchaseOrder(array &$rows)
  973. {
  974. if ($rows === []) {
  975. return;
  976. }
  977. $ids = [];
  978. foreach ($rows as $rw) {
  979. if (!is_array($rw)) {
  980. continue;
  981. }
  982. $id = (int)($rw['ID'] ?? 0);
  983. if ($id > 0) {
  984. $ids[$id] = true;
  985. }
  986. }
  987. $idList = array_keys($ids);
  988. if ($idList === []) {
  989. return;
  990. }
  991. try {
  992. $list = Db::table('purchase_order')
  993. ->where('scydgy_id', 'in', $idList)
  994. ->field('scydgy_id,This_quantity,ceilingPrice')
  995. ->select();
  996. } catch (\Throwable $e) {
  997. return;
  998. }
  999. if (!is_array($list)) {
  1000. return;
  1001. }
  1002. $byId = [];
  1003. foreach ($list as $dbRow) {
  1004. if (!is_array($dbRow) || !isset($dbRow['scydgy_id'])) {
  1005. continue;
  1006. }
  1007. $byId[(int)$dbRow['scydgy_id']] = $dbRow;
  1008. }
  1009. foreach ($rows as &$rw) {
  1010. if (!is_array($rw)) {
  1011. continue;
  1012. }
  1013. $sid = (int)($rw['ID'] ?? 0);
  1014. if ($sid <= 0 || !isset($byId[$sid])) {
  1015. continue;
  1016. }
  1017. $db = $byId[$sid];
  1018. if (array_key_exists('This_quantity', $db) && $db['This_quantity'] !== null && $db['This_quantity'] !== '') {
  1019. $rw['This_quantity'] = $db['This_quantity'];
  1020. }
  1021. if (array_key_exists('ceilingPrice', $db) && $db['ceilingPrice'] !== null && $db['ceilingPrice'] !== '') {
  1022. $rw['ceilingPrice'] = $db['ceilingPrice'];
  1023. } elseif (array_key_exists('ceiling_price', $db) && $db['ceiling_price'] !== null && $db['ceiling_price'] !== '') {
  1024. $rw['ceilingPrice'] = $db['ceiling_price'];
  1025. }
  1026. }
  1027. unset($rw);
  1028. }
  1029. /**
  1030. * 采购确认:明细选中 status=1、未选 status=0;同时将 purchase_order 主表 status 置为 1
  1031. */
  1032. public function purchaseConfirmPick()
  1033. {
  1034. if (!$this->request->isPost() || !$this->request->isAjax()) {
  1035. $this->error(__('Invalid parameters'));
  1036. }
  1037. $scydgyId = trim((string)$this->request->post('scydgy_id', ''));
  1038. $sid = (int)$scydgyId;
  1039. if ($sid <= 0) {
  1040. $this->error('参数无效');
  1041. }
  1042. $parseIdList = function ($raw) {
  1043. if (is_array($raw)) {
  1044. $arr = $raw;
  1045. } else {
  1046. $decoded = json_decode((string)$raw, true);
  1047. $arr = is_array($decoded) ? $decoded : [];
  1048. }
  1049. return array_values(array_unique(array_filter(array_map('intval', $arr))));
  1050. };
  1051. $selectedRaw = $this->request->post('selected_ids', null);
  1052. if ($selectedRaw === null || $selectedRaw === '') {
  1053. $legacy = (int)$this->request->post('selected_id', 0);
  1054. $selectedIds = $legacy > 0 ? [$legacy] : [];
  1055. } else {
  1056. $selectedIds = $parseIdList($selectedRaw);
  1057. }
  1058. $unselectedIds = $parseIdList($this->request->post('unselected_ids', '[]'));
  1059. if ($selectedIds === []) {
  1060. $this->error('请至少勾选一条明细');
  1061. }
  1062. $inter = array_intersect($selectedIds, $unselectedIds);
  1063. if ($inter !== []) {
  1064. $this->error('选中与未选中 ID 不能重复');
  1065. }
  1066. $allIds = $this->purchaseOrderDetail($sid);
  1067. if ($allIds === []) {
  1068. $this->error('未找到该工序行的下发明细');
  1069. }
  1070. foreach ($selectedIds as $id) {
  1071. if (!in_array($id, $allIds, true)) {
  1072. $this->error('选中 ID 不属于当前工序行');
  1073. }
  1074. }
  1075. foreach ($unselectedIds as $id) {
  1076. if (!in_array($id, $allIds, true)) {
  1077. $this->error('未选中 ID 不属于当前工序行');
  1078. }
  1079. }
  1080. $union = array_values(array_unique(array_merge($selectedIds, $unselectedIds)));
  1081. sort($union, SORT_NUMERIC);
  1082. $expect = $allIds;
  1083. sort($expect, SORT_NUMERIC);
  1084. if ($union !== $expect) {
  1085. $this->error('请提交当前工序下全部明细 ID(选中 + 未选中)');
  1086. }
  1087. $purchaseOrderId = (int)$this->request->post('purchase_order_id', 0);
  1088. Db::startTrans();
  1089. try {
  1090. Db::table('purchase_order_detail')->where('scydgy_id', $sid)->update(['status' => 0]);
  1091. $aff = 0;
  1092. try {
  1093. $aff = (int)Db::table('purchase_order_detail')->where('scydgy_id', $sid)->where('id', 'in', $selectedIds)->update(['status' => 1]);
  1094. } catch (\Throwable $e) {
  1095. $aff = (int)Db::table('purchase_order_detail')->where('scydgy_id', $sid)->where('ID', 'in', $selectedIds)->update(['status' => 1]);
  1096. }
  1097. if ($aff < 1) {
  1098. throw new \Exception('更新选中状态失败');
  1099. }
  1100. if ($purchaseOrderId > 0) {
  1101. $poAff = (int)Db::table('purchase_order')
  1102. ->where('id', $purchaseOrderId)
  1103. ->where('scydgy_id', $sid)
  1104. ->update(['status' => 1]);
  1105. if ($poAff < 1) {
  1106. throw new \Exception('主表订单不存在或与工序行不匹配');
  1107. }
  1108. } else {
  1109. $poAff = (int)Db::table('purchase_order')->where('scydgy_id', $sid)->update(['status' => 1]);
  1110. if ($poAff < 1) {
  1111. throw new \Exception('未找到');
  1112. }
  1113. }
  1114. Db::commit();
  1115. } catch (\Throwable $e) {
  1116. Db::rollback();
  1117. $this->error($e->getMessage());
  1118. }
  1119. $poIdFinal = $purchaseOrderId;
  1120. if ($poIdFinal <= 0) {
  1121. try {
  1122. $rpo = Db::table('purchase_order')->where('scydgy_id', $sid)->find();
  1123. if (is_array($rpo)) {
  1124. $poIdFinal = (int)($rpo['id'] ?? $rpo['ID'] ?? 0);
  1125. }
  1126. } catch (\Throwable $e) {
  1127. $poIdFinal = 0;
  1128. }
  1129. }
  1130. $pickNames = [];
  1131. foreach ($selectedIds as $pid) {
  1132. $dr = $this->purchaseOrderDetai($sid, $pid);
  1133. $nm = trim((string)($dr['company_name'] ?? ''));
  1134. $pickNames[] = $nm !== '' ? $nm : ('明细#' . $pid);
  1135. }
  1136. $this->addOrderLog(
  1137. $sid,
  1138. 'purchase_confirm',
  1139. '采购确认:已选中供应商「' . implode('、', $pickNames) . '」;',
  1140. $poIdFinal > 0 ? $poIdFinal : null
  1141. );
  1142. $ccydh = '';
  1143. $cyjmc = '';
  1144. try {
  1145. $po = Db::table('purchase_order')->where('scydgy_id', $sid)->find();
  1146. if (is_array($po)) {
  1147. $ccydh = trim((string)($po['CCYDH'] ?? ''));
  1148. $cyjmc = trim((string)($po['CYJMC'] ?? ''));
  1149. }
  1150. } catch (\Throwable $e) {
  1151. }
  1152. if ($ccydh === '' || $cyjmc === '') {
  1153. $any = $this->purchaseOrderDetai($sid, $selectedIds[0] ?? 0);
  1154. if (is_array($any)) {
  1155. if ($ccydh === '') {
  1156. $ccydh = trim((string)($any['CCYDH'] ?? ''));
  1157. }
  1158. if ($cyjmc === '') {
  1159. $cyjmc = trim((string)($any['CYJMC'] ?? ''));
  1160. }
  1161. }
  1162. }
  1163. $sendSmsSafe = function ($phone, $content) {
  1164. $phone = trim((string)$phone);
  1165. if ($phone === '') {
  1166. return;
  1167. }
  1168. try {
  1169. $this->smsbao($phone, $content);
  1170. } catch (\Throwable $e) {
  1171. Log::write('采购确认短信失败 phone=' . $phone . ' ' . $e->getMessage(), 'error');
  1172. }
  1173. };
  1174. //批量发送手机短信:采购确认通过
  1175. foreach ($selectedIds as $pid) {
  1176. $dr = $this->purchaseOrderDetai($sid, $pid);
  1177. if (!is_array($dr)) {
  1178. continue;
  1179. }
  1180. $cname = trim((string)($dr['company_name'] ?? ''));
  1181. $sms = "您好,{$cname}:\n\n"
  1182. . "您参与的外发加工订单采购确认结果:已通过。\n"
  1183. . "订单号:{$ccydh}\n"
  1184. . "印件名称:{$cyjmc}\n\n";
  1185. $sendSmsSafe((string)($dr['phone'] ?? ''), $sms);
  1186. }
  1187. //批量发送手机短信:采购确认未通过
  1188. foreach ($unselectedIds as $pid) {
  1189. $dr = $this->purchaseOrderDetai($sid, $pid);
  1190. if (!is_array($dr)) {
  1191. continue;
  1192. }
  1193. $cname = trim((string)($dr['company_name'] ?? ''));
  1194. $sms = "您好,{$cname}:\n\n"
  1195. . "您参与的外发加工订单采购确认结果:未通过。\n"
  1196. . "订单号:{$ccydh}\n"
  1197. . "印件名称:{$cyjmc}\n\n";
  1198. $sendSmsSafe((string)($dr['phone'] ?? ''), $sms);
  1199. }
  1200. $line = sprintf(
  1201. 'purchaseConfirmPick scydgy_id=%s purchase_order_id=%d selected_ids=%s unselected_ids=%s',
  1202. $scydgyId,
  1203. $purchaseOrderId,
  1204. json_encode($selectedIds, JSON_UNESCAPED_UNICODE),
  1205. json_encode($unselectedIds, JSON_UNESCAPED_UNICODE)
  1206. );
  1207. Log::write($line, 'notice');
  1208. $pdfPublicPath = '';
  1209. try {
  1210. $pdfPublicPath = (string)$this->savePurchaseConfirmDetailPdf($sid, $poIdFinal);
  1211. } catch (\Throwable $e) {
  1212. Log::write('采购确认PDF异常: ' . $e->getMessage(), 'error');
  1213. }
  1214. $this->success('操作成功', '', [
  1215. 'scydgy_id' => $scydgyId,
  1216. 'purchase_order_id' => $purchaseOrderId,
  1217. 'selected_ids' => $selectedIds,
  1218. 'unselected_ids' => $unselectedIds,
  1219. 'purchase_confirm_pdf' => $pdfPublicPath,
  1220. ]);
  1221. }
  1222. /**
  1223. * @return int[]
  1224. */
  1225. protected function purchaseOrderDetail(int $scydgyId): array
  1226. {
  1227. if ($scydgyId <= 0) {
  1228. return [];
  1229. }
  1230. try {
  1231. $list = Db::table('purchase_order_detail')->where('scydgy_id', $scydgyId)->select();
  1232. } catch (\Throwable $e) {
  1233. return [];
  1234. }
  1235. if (!is_array($list)) {
  1236. return [];
  1237. }
  1238. $ids = [];
  1239. foreach ($list as $r) {
  1240. if (!is_array($r)) {
  1241. continue;
  1242. }
  1243. $pk = (int)($r['id'] ?? $r['ID'] ?? 0);
  1244. if ($pk > 0) {
  1245. $ids[] = $pk;
  1246. }
  1247. }
  1248. return array_values(array_unique($ids));
  1249. }
  1250. /**
  1251. * 获取当前登录用户信息 [id, 展示名]
  1252. */
  1253. protected function GetUseName(): array
  1254. {
  1255. $id = 0;
  1256. $name = '';
  1257. try {
  1258. if ($this->auth && $this->auth->isLogin()) {
  1259. $u = $this->auth->getUserInfo();
  1260. if (is_array($u)) {
  1261. $id = (int)($u['id'] ?? 0);
  1262. $name = trim((string)($u['nickname'] ?? ''));
  1263. if ($name === '') {
  1264. $name = trim((string)($u['username'] ?? ''));
  1265. }
  1266. }
  1267. }
  1268. } catch (\Throwable $e) {
  1269. }
  1270. if ($name === '') {
  1271. $name = '未知用户';
  1272. }
  1273. return [$id, $name];
  1274. }
  1275. /**
  1276. * 外发采购操作日志(表未建时仅写 runtime 日志,不中断业务)
  1277. */
  1278. protected function addOrderLog(int $scydgyId, string $action, string $content, ?int $purchaseOrderId = null): void
  1279. {
  1280. if ($scydgyId <= 0 || $content === '') {
  1281. return;
  1282. }
  1283. list($adminId, $adminName) = $this->GetUseName();
  1284. $cut = function ($s, $max) {
  1285. if (function_exists('mb_substr')) {
  1286. return mb_substr($s, 0, $max, 'UTF-8');
  1287. }
  1288. return strlen($s) <= $max ? $s : substr($s, 0, $max);
  1289. };
  1290. try {
  1291. Db::table('purchase_order_oper_log')->insert([
  1292. 'scydgy_id' => $scydgyId,
  1293. 'purchase_order_id' => $purchaseOrderId,
  1294. 'admin_id' => $adminId,
  1295. 'admin_name' => $cut($adminName, 64),
  1296. 'action' => $cut((string)$action, 64),
  1297. 'content' => $cut($content, 1000),
  1298. // 表结构 createtime 为 int Unix 时间戳;日期字符串会导致插入失败或时间为 0,详情「操作记录」为空
  1299. 'createtime' => time(),
  1300. ]);
  1301. } catch (\Throwable $e) {
  1302. Log::write('procuremen addOrderLog: ' . $e->getMessage(), 'error');
  1303. }
  1304. }
  1305. /**
  1306. * 按主键取一条
  1307. */
  1308. protected function purchaseOrderDetai(int $scydgyId, int $pk): array
  1309. {
  1310. if ($scydgyId <= 0 || $pk <= 0) {
  1311. return [];
  1312. }
  1313. try {
  1314. $one = Db::table('purchase_order_detail')->where('scydgy_id', $scydgyId)->where('id', $pk)->find();
  1315. if (is_array($one)) {
  1316. return $one;
  1317. }
  1318. } catch (\Throwable $e) {
  1319. }
  1320. try {
  1321. $one = Db::table('purchase_order_detail')->where('scydgy_id', $scydgyId)->where('ID', $pk)->find();
  1322. if (is_array($one)) {
  1323. return $one;
  1324. }
  1325. } catch (\Throwable $e) {
  1326. }
  1327. return [];
  1328. }
  1329. /**
  1330. * 主表/明细时间字段统一为可读字符串(用于详情进度)
  1331. */
  1332. protected function formatProcuremenDetailTime($v): string
  1333. {
  1334. if ($v === null || $v === '') {
  1335. return '';
  1336. }
  1337. if (is_numeric($v) && (int)$v > 946684800) {
  1338. return date('Y-m-d H:i:s', (int)$v);
  1339. }
  1340. $s = trim((string)$v);
  1341. if ($s !== '' && stripos($s, '0000-00-00') !== 0) {
  1342. return $s;
  1343. }
  1344. return '';
  1345. }
  1346. /**
  1347. * 交货日期仅展示 YYYY-MM-DD(详情表、列表展示用)
  1348. */
  1349. protected function formatDeliveryYmd($v): string
  1350. {
  1351. if ($v === null || $v === '') {
  1352. return '';
  1353. }
  1354. $s = trim((string)$v);
  1355. if ($s === '' || preg_match('/^0000-00-00/i', $s)) {
  1356. return '';
  1357. }
  1358. if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) {
  1359. return $m[1];
  1360. }
  1361. $ts = strtotime($s);
  1362. return ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : $s;
  1363. }
  1364. /**
  1365. * 供应商已接单:金额与交货日期均已填写(与列表汇总逻辑一致)
  1366. */
  1367. protected function detailRowSupplierAccepted(array $r): bool
  1368. {
  1369. $am = $r['amount'] ?? null;
  1370. $okAmt = $am !== null && $am !== '' && !(is_string($am) && trim($am) === '');
  1371. $dv = isset($r['delivery']) ? trim((string)$r['delivery']) : '';
  1372. $okDel = $dv !== '' && !preg_match('/^0000-00-00/i', $dv);
  1373. return $okAmt && $okDel;
  1374. }
  1375. /**
  1376. * 详情弹层:进度步骤 + 订单摘要 + 下发明细(已下发 / 已完结均可用)
  1377. *
  1378. * @param array $main purchase_order 一行
  1379. * @param array $details purchase_order_detail 多行(已预处理 createtime_text)
  1380. * @return array{steps: array, orderSummary: array, orderSummaryGrid: array, detailRows: array}
  1381. */
  1382. protected function buildProcuremenDetailsViewData(array $main, array $details): array
  1383. {
  1384. $issueCnt = count($details);
  1385. $acceptCnt = 0;
  1386. $pickedName = '';
  1387. $pickedTime = '';
  1388. foreach ($details as $dr) {
  1389. if (!is_array($dr)) {
  1390. continue;
  1391. }
  1392. if ($this->detailRowSupplierAccepted($dr)) {
  1393. $acceptCnt++;
  1394. }
  1395. if ((int)($dr['status'] ?? 0) === 1) {
  1396. if ($pickedName === '') {
  1397. $pickedName = trim((string)($dr['company_name'] ?? ''));
  1398. }
  1399. if ($pickedTime === '') {
  1400. $pickedTime = trim((string)($dr['createtime_text'] ?? ''));
  1401. if ($pickedTime === '') {
  1402. $pickedTime = $this->formatProcuremenDetailTime($dr['createtime'] ?? null);
  1403. }
  1404. }
  1405. }
  1406. }
  1407. $hasMain = $main !== [];
  1408. $poTime = $this->formatProcuremenDetailTime($main['createtime'] ?? null);
  1409. $mainStatus = isset($main['status']) ? (int)$main['status'] : 0;
  1410. $supplierTime = '';
  1411. if ($acceptCnt > 0) {
  1412. foreach ($details as $dr) {
  1413. if (!is_array($dr) || !$this->detailRowSupplierAccepted($dr)) {
  1414. continue;
  1415. }
  1416. $t = $this->formatProcuremenDetailTime($dr['updatetime'] ?? $dr['createtime'] ?? null);
  1417. if ($t !== '' && ($supplierTime === '' || strcmp($t, $supplierTime) < 0)) {
  1418. $supplierTime = $t;
  1419. }
  1420. }
  1421. }
  1422. $doneTime = '';
  1423. if ($mainStatus === 1) {
  1424. $doneTime = $this->formatProcuremenDetailTime($main['updatetime'] ?? null);
  1425. if ($doneTime === '') {
  1426. foreach ($details as $dr) {
  1427. if (!is_array($dr)) {
  1428. continue;
  1429. }
  1430. $t = $this->formatProcuremenDetailTime($dr['updatetime'] ?? $dr['createtime'] ?? null);
  1431. if ($t !== '' && ($doneTime === '' || strcmp($t, $doneTime) > 0)) {
  1432. $doneTime = $t;
  1433. }
  1434. }
  1435. }
  1436. if ($doneTime === '') {
  1437. $doneTime = $poTime;
  1438. }
  1439. }
  1440. $step1Done = $hasMain;
  1441. $step2Done = $hasMain;
  1442. $step3Done = $acceptCnt > 0;
  1443. $step4Done = false;
  1444. foreach ($details as $dr) {
  1445. if (is_array($dr) && (int)($dr['status'] ?? 0) === 1) {
  1446. $step4Done = true;
  1447. break;
  1448. }
  1449. }
  1450. $step5Done = $mainStatus === 1;
  1451. // 主表已完结:中间环节按业务视为已全部完成(不显示灰色「未到达」)
  1452. if ($mainStatus === 1) {
  1453. $step3Done = true;
  1454. $step4Done = true;
  1455. if ($supplierTime === '') {
  1456. $fillT = $doneTime !== '' ? $doneTime : $poTime;
  1457. if ($fillT !== '') {
  1458. $supplierTime = $fillT;
  1459. }
  1460. }
  1461. if ($pickedTime === '') {
  1462. $fillT = $doneTime !== '' ? $doneTime : $poTime;
  1463. if ($fillT !== '') {
  1464. $pickedTime = $fillT;
  1465. }
  1466. }
  1467. }
  1468. $steps = [
  1469. [
  1470. 'title' => '未发',
  1471. 'subtitle' => '',
  1472. 'time' => $step1Done ? '—' : '',
  1473. 'done' => $step1Done,
  1474. ],
  1475. [
  1476. 'title' => '已发未结束',
  1477. 'subtitle' => '',
  1478. 'time' => $step2Done ? $poTime : '',
  1479. 'done' => $step2Done,
  1480. ],
  1481. [
  1482. 'title' => '供应商接单',
  1483. 'subtitle' => '下发 ' . $issueCnt . ' / 接单 ' . $acceptCnt,
  1484. 'time' => $step3Done ? $supplierTime : '',
  1485. 'done' => $step3Done,
  1486. ],
  1487. [
  1488. 'title' => '采购确认',
  1489. 'subtitle' => $pickedName !== '' ? ('选中供应商:' . $pickedName) : '',
  1490. 'time' => $step4Done ? ($pickedTime !== '' ? $pickedTime : '') : '',
  1491. 'done' => $step4Done,
  1492. ],
  1493. [
  1494. 'title' => '已完结',
  1495. 'subtitle' => '',
  1496. 'time' => $step5Done ? $doneTime : '',
  1497. 'done' => $step5Done,
  1498. ],
  1499. ];
  1500. $currentIdx = count($steps) - 1;
  1501. foreach ($steps as $i => $s) {
  1502. if (!$s['done']) {
  1503. $currentIdx = $i;
  1504. break;
  1505. }
  1506. }
  1507. $nSteps = count($steps);
  1508. foreach ($steps as $idx => &$s) {
  1509. $s['current'] = ($idx === $currentIdx);
  1510. if ($idx === 0) {
  1511. $s['pdf_left_bg'] = '';
  1512. } else {
  1513. $s['pdf_left_bg'] = !empty($steps[$idx - 1]['done']) ? '#1890ff' : '#e0e0e0';
  1514. }
  1515. if ($idx >= $nSteps - 1) {
  1516. $s['pdf_right_bg'] = '';
  1517. } else {
  1518. $s['pdf_right_bg'] = !empty($s['done']) ? '#1890ff' : '#e0e0e0';
  1519. }
  1520. }
  1521. unset($s);
  1522. $labelMap = [
  1523. 'CCYDH' => '订单号',
  1524. 'CYJMC' => '印件名称',
  1525. 'CDXMC' => '订单名称',
  1526. 'CGYMC' => '工序名称',
  1527. 'CGYBH' => '工序编号',
  1528. 'CDW' => '单位',
  1529. 'NGZL' => '工作量',
  1530. 'CDF' => '单价',
  1531. 'cGzzxMc' => '工作中心',
  1532. 'MBZ' => '备注',
  1533. 'This_quantity' => '本次数量',
  1534. 'ceilingPrice' => '最高限价',
  1535. ];
  1536. $orderSummary = [];
  1537. foreach ($labelMap as $key => $lab) {
  1538. if (!array_key_exists($key, $main)) {
  1539. continue;
  1540. }
  1541. $val = $main[$key];
  1542. if ($val === null) {
  1543. $val = '';
  1544. } elseif (!is_scalar($val)) {
  1545. $val = json_encode($val, JSON_UNESCAPED_UNICODE);
  1546. } else {
  1547. $val = (string)$val;
  1548. }
  1549. $orderSummary[] = ['label' => $lab, 'value' => $val];
  1550. }
  1551. $orderSummaryGrid = [];
  1552. $n = count($orderSummary);
  1553. for ($i = 0; $i < $n; $i += 2) {
  1554. $left = $orderSummary[$i];
  1555. $hasRight = ($i + 1) < $n;
  1556. $orderSummaryGrid[] = [
  1557. 'l1' => $left['label'],
  1558. 'v1' => $left['value'],
  1559. 'l2' => $hasRight ? $orderSummary[$i + 1]['label'] : '',
  1560. 'v2' => $hasRight ? $orderSummary[$i + 1]['value'] : '',
  1561. ];
  1562. }
  1563. return [
  1564. 'steps' => $steps,
  1565. 'orderSummary' => $orderSummary,
  1566. 'orderSummaryGrid' => $orderSummaryGrid,
  1567. 'detailRows' => $details,
  1568. ];
  1569. }
  1570. /**
  1571. * 加载并 assign 外发采购「详情」弹窗所需变量(与 {@see details()} 页面一致,供模板 / PDF 共用)。
  1572. *
  1573. * @return array{ok: bool, ccydh: string} ok 表示存在主表或至少一条下发明细(否则 PDF 无可写内容)
  1574. */
  1575. protected function prepareProcuremenDetailsView(string $ids): array
  1576. {
  1577. $main = [];
  1578. try {
  1579. $one = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
  1580. $main = is_array($one) ? $one : [];
  1581. } catch (\Throwable $e) {
  1582. $main = [];
  1583. }
  1584. $details = [];
  1585. try {
  1586. $details = Db::table('purchase_order_detail')->where('scydgy_id', $ids)->order('id', 'asc')->select();
  1587. } catch (\Throwable $e) {
  1588. $details = [];
  1589. }
  1590. if (!is_array($details)) {
  1591. $details = [];
  1592. }
  1593. foreach ($details as &$r) {
  1594. if (is_array($r) && isset($r['ID']) && !isset($r['id'])) {
  1595. $r['id'] = $r['ID'];
  1596. }
  1597. if (isset($r['createtime'])) {
  1598. if (is_numeric($r['createtime']) && (int)$r['createtime'] > 946684800) {
  1599. $r['createtime_text'] = date('Y-m-d H:i:s', (int)$r['createtime']);
  1600. } else {
  1601. $r['createtime_text'] = (string)$r['createtime'];
  1602. }
  1603. } else {
  1604. $r['createtime_text'] = '';
  1605. }
  1606. $r['delivery_ymd'] = $this->formatDeliveryYmd($r['delivery'] ?? null);
  1607. }
  1608. unset($r);
  1609. $operLogs = [];
  1610. try {
  1611. $operLogs = Db::table('purchase_order_oper_log')->where('scydgy_id', $ids)->order('id', 'asc')->select();
  1612. } catch (\Throwable $e) {
  1613. $operLogs = [];
  1614. }
  1615. if (!is_array($operLogs)) {
  1616. $operLogs = [];
  1617. }
  1618. foreach ($operLogs as &$lg) {
  1619. if (!is_array($lg)) {
  1620. continue;
  1621. }
  1622. $ct = isset($lg['createtime']) ? (int)$lg['createtime'] : 0;
  1623. $lg['createtime_text'] = $ct > 946684800 ? date('Y-m-d H:i:s', $ct) : '';
  1624. }
  1625. unset($lg);
  1626. $bundle = $this->buildProcuremenDetailsViewData($main, $details);
  1627. $ccydh = isset($main['CCYDH']) ? trim((string)$main['CCYDH']) : '';
  1628. if ($ccydh === '') {
  1629. foreach ($details as $r) {
  1630. if (!is_array($r)) {
  1631. continue;
  1632. }
  1633. $ccydh = trim((string)($r['CCYDH'] ?? ''));
  1634. if ($ccydh !== '') {
  1635. break;
  1636. }
  1637. }
  1638. }
  1639. $this->view->assign('ccydh', $ccydh);
  1640. $this->view->assign('steps', $bundle['steps']);
  1641. $this->view->assign('orderSummary', $bundle['orderSummary']);
  1642. $this->view->assign('orderSummaryGrid', $bundle['orderSummaryGrid']);
  1643. $this->view->assign('detailRows', $bundle['detailRows']);
  1644. $this->view->assign('operLogs', $operLogs);
  1645. return [
  1646. 'ok' => $main !== [] || $details !== [],
  1647. 'ccydh' => $ccydh,
  1648. ];
  1649. }
  1650. /**
  1651. * 详情弹层:状态进度 + 订单信息 + 下发明细(列表「已下发」「已完结」均可打开)
  1652. */
  1653. public function details()
  1654. {
  1655. $ids = $this->request->param('ids', $this->request->param('id', ''));
  1656. if (is_array($ids)) {
  1657. $ids = isset($ids[0]) ? $ids[0] : '';
  1658. }
  1659. $ids = trim((string)$ids);
  1660. if ($ids === '') {
  1661. $this->error(__('Invalid parameters'));
  1662. }
  1663. $this->prepareProcuremenDetailsView($ids);
  1664. /* 弹层内不套 default 布局,避免出现「控制台 / Control panel」整块标题区 */
  1665. $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false;
  1666. $this->view->engine->layout(false);
  1667. try {
  1668. return $this->view->fetch('procuremen/details_dialog_shell');
  1669. } finally {
  1670. if ($restoreLayout) {
  1671. $this->view->engine->layout($restoreLayout);
  1672. }
  1673. }
  1674. }
  1675. /**
  1676. * 审核弹窗提交:GET 打开弹窗;POST 保存下发(主表 + 明细 + 操作日志)
  1677. */
  1678. public function review()
  1679. {
  1680. if ($this->request->isPost()) {
  1681. $rowJson = $this->request->post('row_json', '');
  1682. $companiesJson = $this->request->post('companies_json', '[]');
  1683. $row = json_decode($rowJson, true);
  1684. if (!is_array($row)) {
  1685. $this->error(__('Invalid parameters'));
  1686. }
  1687. $companies = json_decode($companiesJson, true);
  1688. if (!is_array($companies) || count($companies) === 0) {
  1689. $this->error('请至少选择一个下发单位');
  1690. }
  1691. $toDb = function ($value) {
  1692. if ($value === null) {
  1693. return null;
  1694. }
  1695. if (is_scalar($value)) {
  1696. return $value;
  1697. }
  1698. return json_encode($value, JSON_UNESCAPED_UNICODE);
  1699. };
  1700. Db::startTrans();
  1701. try {
  1702. $ids = (int)($row['ID'] ?? $row['id'] ?? 0);
  1703. if ($ids <= 0) {
  1704. throw new \Exception('无效的行主键 ID');
  1705. }
  1706. $exists = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
  1707. $data = [
  1708. 'scydgy_id' => $ids,
  1709. 'CCYDH' => $row['CCYDH'] ?? null,
  1710. 'CYJMC' => $row['CYJMC'] ?? null,
  1711. 'CDXMC' => $row['CDXMC'] ?? null,
  1712. 'CGYBH' => $row['CGYBH'] ?? null,
  1713. 'CGYMC' => $row['CGYMC'] ?? null,
  1714. 'CDW' => $row['CDW'] ?? null,
  1715. 'NGZL' => $row['NGZL'] ?? null,
  1716. 'CDF' => $row['CDF'] ?? null,
  1717. 'cGzzxMc' => $row['cGzzxMc'] ?? null,
  1718. 'MBZ' => $row['MBZ'] ?? null,
  1719. 'bwjg' => $row['bwjg'] ?? null,
  1720. 'iStatus' => $row['iStatus'] ?? null,
  1721. 'dStamp' => $row['dStamp'] ?? null,
  1722. 'dputrecord' => $row['dputrecord'] ?? null,
  1723. 'cywyxm' => $row['cywyxm'] ?? null,
  1724. 'This_quantity' => $row['This_quantity'] ?? $row['this_quantity'] ?? null,
  1725. 'ceilingPrice' => $row['ceilingPrice'] ?? $row['ceiling_price'] ?? null,
  1726. 'status' => 0,
  1727. ];
  1728. if ($exists) {
  1729. $upd = $data;
  1730. unset($upd['scydgy_id']);
  1731. Db::table('purchase_order')->where('scydgy_id', $ids)->update($upd);
  1732. } else {
  1733. $data['createtime'] = date('Y-m-d H:i:s');
  1734. Db::table('purchase_order')->insert($data);
  1735. }
  1736. $row['scydgy_id'] = (int)($row['ID'] ?? $row['id'] ?? 0);
  1737. unset($row['ID'], $row['id'], $row['_iss_out'], $row['ids'], $row['purchase_order_id']);
  1738. foreach ($companies as $c) {
  1739. if (!is_array($c)) {
  1740. continue;
  1741. }
  1742. $one = [
  1743. 'scydgy_id' => $toDb($row['scydgy_id']),
  1744. 'CCYDH' => $toDb($row['CCYDH'] ?? null),
  1745. 'CYJMC' => $toDb($row['CYJMC'] ?? null),
  1746. 'company_name' => isset($c['name']) ? (string)$c['name'] : null,
  1747. 'email' => isset($c['email']) ? (string)$c['email'] : null,
  1748. 'phone' => isset($c['phone']) ? (string)$c['phone'] : null,
  1749. 'createtime' => date('Y-m-d H:i:s'),
  1750. 'status' => 0,
  1751. 'status_name' => "未提交",
  1752. ];
  1753. // 先校验邮箱与手机号,再写入明细(写入后根据主键生成手机端直达链接 focus_eid)
  1754. $toEmail = isset($c['email']) ? trim((string)$c['email']) : '';
  1755. $companyName = isset($c['name']) ? trim((string)$c['name']) : '外协单位';
  1756. if ($toEmail === '' || !filter_var($toEmail, FILTER_VALIDATE_EMAIL)) {
  1757. throw new \Exception($companyName . ' 未填写有效邮箱,无法发送邮件,未写入数据');
  1758. }
  1759. $phone = isset($c['phone']) ? trim((string)$c['phone']) : '';
  1760. if ($phone === '') {
  1761. $cname = isset($c['name']) ? trim((string)$c['name']) : '';
  1762. throw new \Exception(($cname !== '' ? $cname : '外协单位') . ' 未填写手机号,无法发送短信,未写入数据');
  1763. }
  1764. Db::table('purchase_order_detail')->insert($one);
  1765. $detailId = (int)Db::getLastInsID();
  1766. $mprocUrl = $this->buildMprocMobileOrderUrl($detailId);
  1767. $mprocUrlEsc = htmlspecialchars($mprocUrl, ENT_QUOTES, 'UTF-8');
  1768. // 发送邮箱
  1769. $mailConfig = Config::get('Mailer');
  1770. if (!is_array($mailConfig) || empty($mailConfig['host']) || empty($mailConfig['addr'])) {
  1771. throw new \Exception('邮件未配置');
  1772. }
  1773. $mail = new PHPMailer(true);
  1774. $mail->isSMTP();
  1775. $mail->Host = $mailConfig['host'];
  1776. $mail->SMTPAuth = true;
  1777. $mail->Username = $mailConfig['addr'];
  1778. $mail->Password = $mailConfig['pass'];
  1779. $mail->SMTPSecure = $mailConfig['security'];
  1780. $mail->Port = $mailConfig['port'];
  1781. $mail->CharSet = $mailConfig['charset'];
  1782. $mail->setFrom($mailConfig['addr'], $mailConfig['name']);
  1783. $mail->addAddress($toEmail, $companyName);
  1784. $mail->isHTML(true);
  1785. $mail->Subject = '【外发加工订单通知】';
  1786. $mail->Body = "
  1787. 您好,{$companyName}:<br><br>
  1788. 您有新的外发加工订单待处理:<br>
  1789. 订单号:{$row['CCYDH']}<br>
  1790. 印件名称:{$row['CYJMC']}<br><br>
  1791. 请点击下面链接,登录后可直接打开本条订单(无需再搜索):<br>
  1792. <a href=\"{$mprocUrlEsc}\">{$mprocUrlEsc}</a><br><br>
  1793. 登录后可在手机端填写金额与交货日期。<br><br>
  1794. 请及时查收并处理,谢谢!
  1795. ";
  1796. $mail->send();
  1797. $ccydh = isset($row['CCYDH']) ? (string)$row['CCYDH'] : '';
  1798. $cyjmc = isset($row['CYJMC']) ? (string)$row['CYJMC'] : '';
  1799. $smsContent = "您好,{$companyName}:\n\n"
  1800. . "您有新的外发加工订单待处理:\n"
  1801. . "订单号:{$ccydh}\n"
  1802. . "印件名称:{$cyjmc}\n\n"
  1803. . "手机打开链接(登录后直达本条):\n"
  1804. . $mprocUrl . "\n\n"
  1805. . '请及时查收并处理,谢谢!';
  1806. $this->smsbao($phone, $smsContent);
  1807. }
  1808. Db::commit();
  1809. } catch (\Throwable $e) {
  1810. Db::rollback();
  1811. $this->error($e->getMessage());
  1812. }
  1813. $poIdLog = null;
  1814. try {
  1815. $rpo = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
  1816. if (is_array($rpo)) {
  1817. $tid = (int)($rpo['id'] ?? $rpo['ID'] ?? 0);
  1818. $poIdLog = $tid > 0 ? $tid : null;
  1819. }
  1820. } catch (\Throwable $e) {
  1821. $poIdLog = null;
  1822. }
  1823. $names = [];
  1824. foreach ($companies as $c) {
  1825. if (!is_array($c)) {
  1826. continue;
  1827. }
  1828. $nm = trim((string)($c['name'] ?? $c['company_name'] ?? ''));
  1829. if ($nm !== '') {
  1830. $names[] = $nm;
  1831. }
  1832. }
  1833. $n = count($names);
  1834. $nameStr = $n ? implode('、', $names) : '(未查询到单位名称)';
  1835. $this->addOrderLog(
  1836. $ids,
  1837. 'review_issue',
  1838. '审核共选择 ' . $n . ' 家单位:' . $nameStr,
  1839. $poIdLog
  1840. );
  1841. $this->success('操作成功');
  1842. }
  1843. return $this->view->fetch();
  1844. }
  1845. /**
  1846. * 审核弹窗获取公司列表
  1847. */
  1848. public function reviewCompanies()
  1849. {
  1850. if (!$this->request->isAjax()) {
  1851. $this->error(__('Invalid parameters'));
  1852. }
  1853. $list = [];
  1854. try {
  1855. $rows = Db::table('customer')->order('id', 'desc')->select();
  1856. } catch (\Throwable $e) {
  1857. $this->success('', '', []);
  1858. return;
  1859. }
  1860. if (!is_array($rows)) {
  1861. $this->success('', '', []);
  1862. return;
  1863. }
  1864. $detailColCandidates = [
  1865. 'detail', 'mingxi', 'remark', 'memo', 'notes', 'description',
  1866. 'company_detail', 'company_desc', 'address',
  1867. ];
  1868. foreach ($rows as $row) {
  1869. if (!is_array($row)) {
  1870. continue;
  1871. }
  1872. $norm = [];
  1873. foreach ($row as $k => $v) {
  1874. $norm[is_string($k) ? strtolower($k) : $k] = $v;
  1875. }
  1876. $row = $norm;
  1877. $id = isset($row['id']) ? (string)$row['id'] : '';
  1878. $companyName = '';
  1879. foreach (['company_name', 'name'] as $nk) {
  1880. if (!empty($row[$nk])) {
  1881. $companyName = trim((string)$row[$nk]);
  1882. break;
  1883. }
  1884. }
  1885. $username = '';
  1886. foreach (['username', 'contact', 'linkman', 'contacts'] as $uk) {
  1887. if (isset($row[$uk]) && trim((string)$row[$uk]) !== '') {
  1888. $username = trim((string)$row[$uk]);
  1889. break;
  1890. }
  1891. }
  1892. $email = isset($row['email']) ? trim((string)$row['email']) : '';
  1893. $phone = isset($row['phone']) ? trim((string)$row['phone']) : '';
  1894. $category = '';
  1895. foreach (['company_type', 'category', 'type_name'] as $ck) {
  1896. if (isset($row[$ck]) && trim((string)$row[$ck]) !== '') {
  1897. $category = trim((string)$row[$ck]);
  1898. break;
  1899. }
  1900. }
  1901. $detail = '';
  1902. foreach ($detailColCandidates as $dk) {
  1903. if (!isset($row[$dk])) {
  1904. continue;
  1905. }
  1906. $dv = trim((string)$row[$dk]);
  1907. if ($dv !== '') {
  1908. $detail = $dv;
  1909. break;
  1910. }
  1911. }
  1912. $list[] = [
  1913. 'id' => $id,
  1914. 'name' => $companyName,
  1915. 'company_name' => $companyName,
  1916. 'username' => $username,
  1917. 'email' => $email,
  1918. 'phone' => $phone,
  1919. 'category' => $category,
  1920. 'company_type' => $category,
  1921. 'detail' => $detail,
  1922. ];
  1923. }
  1924. $this->success('', '', $list);
  1925. }
  1926. /**
  1927. * 采购确认-下发明细弹窗:
  1928. * 查 purchase_order_detail;ids 为工序行 scydgy.ID,对应明细 scydgy_id
  1929. */
  1930. public function outward_detail()
  1931. {
  1932. $ids = $this->request->param('ids', $this->request->param('id', ''));
  1933. if (is_array($ids)) {
  1934. $ids = isset($ids[0]) ? $ids[0] : '';
  1935. }
  1936. $ids = trim((string)$ids);
  1937. if ($ids === '') {
  1938. $this->error(__('Invalid parameters'));
  1939. }
  1940. $wffTab = trim((string)$this->request->param('wff_tab', 'all'));
  1941. if (!in_array($wffTab, ['all', 'pending', 'picked', 'done'], true)) {
  1942. $wffTab = 'all';
  1943. }
  1944. $headTitle = $wffTab === 'pending' ? '采购确认(明细)' : '下发明细';
  1945. $rows = [];
  1946. try {
  1947. $rows = Db::table('purchase_order_detail')->where('scydgy_id', $ids)->order('id', 'desc')->select();
  1948. } catch (\Throwable $e) {
  1949. $rows = [];
  1950. }
  1951. foreach ($rows as &$r) {
  1952. if (is_array($r) && isset($r['ID']) && !isset($r['id'])) {
  1953. $r['id'] = $r['ID'];
  1954. }
  1955. if (isset($r['createtime'])) {
  1956. if (is_numeric($r['createtime']) && (int)$r['createtime'] > 946684800) {
  1957. $r['createtime_text'] = date('Y-m-d H:i:s', (int)$r['createtime']);
  1958. } else {
  1959. $r['createtime_text'] = (string)$r['createtime'];
  1960. }
  1961. } else {
  1962. $r['createtime_text'] = '';
  1963. }
  1964. }
  1965. unset($r);
  1966. $purchaseOrderId = 0;
  1967. try {
  1968. $po = Db::table('purchase_order')->where('scydgy_id', $ids)->find();
  1969. if (is_array($po)) {
  1970. $purchaseOrderId = (int)($po['id'] ?? $po['ID'] ?? 0);
  1971. }
  1972. } catch (\Throwable $e) {
  1973. $purchaseOrderId = 0;
  1974. }
  1975. $this->view->assign('rows', $rows ?: []);
  1976. $this->view->assign('rowCount', count($rows ?: []));
  1977. $this->view->assign('scydgyId', $ids);
  1978. $this->view->assign('purchaseOrderId', $purchaseOrderId);
  1979. $this->view->assign('headTitle', $headTitle);
  1980. $this->view->assign('showPurchaseConfirm', $wffTab === 'pending' ? 1 : 0);
  1981. $this->view->assign('detailColspan', $wffTab === 'pending' ? 10 : 9);
  1982. /* 采购确认(pending)需在 iframe 内加载 require-backend,以便勾选与提交;其它 tab 仅只读表格,用轻量壳避免整站 JS/CSS 二次初始化导致弹窗极慢 */
  1983. if ($wffTab === 'pending') {
  1984. return $this->view->fetch();
  1985. }
  1986. $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false;
  1987. $this->view->engine->layout(false);
  1988. try {
  1989. $bodyHtml = $this->view->fetch('procuremen/outward_detail');
  1990. $this->view->assign('dialogBody', $bodyHtml);
  1991. return $this->view->fetch('procuremen/outward_detail_lite_shell');
  1992. } finally {
  1993. if ($restoreLayout) {
  1994. $this->view->engine->layout($restoreLayout);
  1995. }
  1996. }
  1997. }
  1998. /**
  1999. * 月份外发明细导出
  2000. */
  2001. public function export_month_outward()
  2002. {
  2003. $this->request->filter(['strip_tags', 'trim']);
  2004. $ym = trim((string)$this->request->get('ym', date('Y-m')));
  2005. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  2006. $ym = date('Y-m');
  2007. }
  2008. $monthStart = $ym . '-01 00:00:00';
  2009. $monthEnd = date('Y-m-t 23:59:59', strtotime($monthStart));
  2010. $unixStart = (int)strtotime($monthStart);
  2011. $unixEnd = (int)strtotime($monthEnd);
  2012. $rows = [];
  2013. try {
  2014. $rows = Db::table('purchase_order_detail')
  2015. ->whereRaw(
  2016. '(TRIM(CAST(createtime AS CHAR(64))) LIKE ?)'
  2017. . ' OR ((createtime REGEXP \'^[0-9]+$\') AND CAST(createtime AS UNSIGNED) BETWEEN ? AND ?)',
  2018. [$ym . '%', $unixStart, $unixEnd]
  2019. )
  2020. ->order('CCYDH', 'asc')
  2021. ->order('company_name', 'asc')
  2022. ->order('id', 'asc')
  2023. ->select();
  2024. } catch (\Throwable $e) {
  2025. $rows = [];
  2026. }
  2027. $groups = [];
  2028. foreach ($rows as $r) {
  2029. if (!is_array($r)) {
  2030. continue;
  2031. }
  2032. $k = (string)($r['CCYDH'] ?? '') . "\x1f" . (string)($r['company_name'] ?? '');
  2033. if (!isset($groups[$k])) {
  2034. $groups[$k] = [];
  2035. }
  2036. $groups[$k][] = $r;
  2037. }
  2038. ksort($groups, SORT_STRING);
  2039. $spreadsheet = new Spreadsheet();
  2040. $sheet = $spreadsheet->getActiveSheet();
  2041. $sheet->setTitle('外发明细');
  2042. $mon = (int)substr($ym, 5, 2);
  2043. $sheet->mergeCells('A1:H1');
  2044. $sheet->setCellValue('A1', $mon . '月外发明细');
  2045. $sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
  2046. $sheet->getStyle('A1')->getAlignment()
  2047. ->setHorizontal(Alignment::HORIZONTAL_CENTER)
  2048. ->setVertical(Alignment::VERTICAL_CENTER);
  2049. $headers = ['序号', '传票号', '传票名称', '外加工单位', '订法', '客户名称', '工序', '加工金额'];
  2050. $col = 'A';
  2051. foreach ($headers as $h) {
  2052. $sheet->setCellValue($col . '2', $h);
  2053. $col++;
  2054. }
  2055. $sheet->getStyle('A2:H2')->getFont()->setBold(true);
  2056. $sheet->getStyle('A2:H2')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
  2057. $rowNum = 3;
  2058. $sumSubtotalCounts = 0;
  2059. $grandAmount = 0.0;
  2060. if (empty($groups)) {
  2061. $sheet->mergeCells('A3:H3');
  2062. $sheet->setCellValue('A3', '(当月暂无外发明细)');
  2063. $sheet->getStyle('A3')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
  2064. $rowNum = 4;
  2065. } else {
  2066. foreach ($groups as $items) {
  2067. if (empty($items)) {
  2068. continue;
  2069. }
  2070. $groupLineCount = count($items);
  2071. $subAmount = 0.0;
  2072. $i = 0;
  2073. foreach ($items as $r) {
  2074. $i++;
  2075. $amt = $this->procuremenExportAmount($r);
  2076. $subAmount += $amt;
  2077. $sheet->setCellValue('A' . $rowNum, $i);
  2078. $sheet->setCellValue('B' . $rowNum, (string)($r['CCYDH'] ?? ''));
  2079. $sheet->setCellValue('C' . $rowNum, (string)($r['CYJMC'] ?? ''));
  2080. $sheet->setCellValue('D' . $rowNum, (string)($r['company_name'] ?? ''));
  2081. $sheet->setCellValue('E' . $rowNum, (string)($r['CDF'] ?? ''));
  2082. $sheet->setCellValue('F' . $rowNum, (string)($r['cGzzxMc'] ?? ''));
  2083. $sheet->setCellValue('G' . $rowNum, $this->procuremenExportGxText($r));
  2084. $sheet->setCellValue('H' . $rowNum, $amt);
  2085. $sheet->getStyle('H' . $rowNum)->getNumberFormat()->setFormatCode('"¥"#,##0.00');
  2086. $rowNum++;
  2087. }
  2088. $sumSubtotalCounts += $groupLineCount;
  2089. $grandAmount += $subAmount;
  2090. $sheet->setCellValue('A' . $rowNum, $groupLineCount);
  2091. $sheet->mergeCells('G' . $rowNum . ':H' . $rowNum);
  2092. $sheet->setCellValue('G' . $rowNum, '¥ ' . number_format($subAmount, 2, '.', ','));
  2093. $sheet->getStyle('G' . $rowNum)->getAlignment()
  2094. ->setHorizontal(Alignment::HORIZONTAL_RIGHT)
  2095. ->setVertical(Alignment::VERTICAL_CENTER);
  2096. $rowNum++;
  2097. }
  2098. }
  2099. $sheet->setCellValue('A' . $rowNum, '总合');
  2100. $sheet->setCellValue('B' . $rowNum, $sumSubtotalCounts);
  2101. $sheet->mergeCells('G' . $rowNum . ':H' . $rowNum);
  2102. $sheet->setCellValue('G' . $rowNum, '¥ ' . number_format($grandAmount, 2, '.', ','));
  2103. $sheet->getStyle('A' . $rowNum . ':H' . $rowNum)->getFont()->setBold(true);
  2104. $sheet->getStyle('G' . $rowNum)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
  2105. $lastRow = $rowNum;
  2106. $sheet->getStyle('A1:H' . $lastRow)->applyFromArray([
  2107. 'borders' => [
  2108. 'allBorders' => [
  2109. 'borderStyle' => Border::BORDER_THIN,
  2110. 'color' => ['rgb' => '000000'],
  2111. ],
  2112. ],
  2113. ]);
  2114. $sheet->getStyle('A3:H' . $lastRow)->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
  2115. // 长文案列:加宽 + 自动换行,避免导出后被截断
  2116. if ($lastRow >= 3) {
  2117. $sheet->getStyle('C3:C' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
  2118. $sheet->getStyle('D3:D' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
  2119. $sheet->getStyle('F3:F' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
  2120. $sheet->getStyle('G3:G' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
  2121. }
  2122. $sheet->getColumnDimension('A')->setWidth(7);
  2123. $sheet->getColumnDimension('B')->setWidth(16);
  2124. $sheet->getColumnDimension('C')->setWidth(52);
  2125. $sheet->getColumnDimension('D')->setWidth(32);
  2126. $sheet->getColumnDimension('E')->setWidth(12);
  2127. $sheet->getColumnDimension('F')->setWidth(44);
  2128. $sheet->getColumnDimension('G')->setWidth(26);
  2129. $sheet->getColumnDimension('H')->setWidth(13);
  2130. $sheet->getRowDimension(1)->setRowHeight(28);
  2131. $fileBase = '外发明细_' . str_replace('-', '', $ym);
  2132. $filename = $fileBase . '.xlsx';
  2133. if (ob_get_length()) {
  2134. ob_end_clean();
  2135. }
  2136. $asciiName = 'outward_' . str_replace('-', '', $ym) . '.xlsx';
  2137. header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  2138. header('Content-Disposition: attachment;filename="' . $asciiName . '"; filename*=UTF-8\'\'' . rawurlencode($filename));
  2139. header('Cache-Control: max-age=0');
  2140. $writer = new Xlsx($spreadsheet);
  2141. $writer->save('php://output');
  2142. $spreadsheet->disconnectWorksheets();
  2143. unset($spreadsheet);
  2144. exit;
  2145. }
  2146. /**
  2147. * 导出用:工序列文案
  2148. */
  2149. protected function procuremenExportGxText(array $r)
  2150. {
  2151. $a = trim((string)($r['CDXMC'] ?? ''));
  2152. $b = trim((string)($r['CGYMC'] ?? ''));
  2153. if ($a !== '' && $b !== '') {
  2154. return $a . ':' . $b;
  2155. }
  2156. return $a !== '' ? $a : $b;
  2157. }
  2158. /**
  2159. * 导出用:加工金额(表中有 amount/jje/jgje 等则读取,否则 0)
  2160. */
  2161. protected function procuremenExportAmount(array $r)
  2162. {
  2163. foreach (['amount', 'jje', 'jgje', 'processing_amount'] as $k) {
  2164. if (!array_key_exists($k, $r)) {
  2165. continue;
  2166. }
  2167. $v = $r[$k];
  2168. if ($v === null || $v === '') {
  2169. continue;
  2170. }
  2171. if (is_numeric($v)) {
  2172. return (float)$v;
  2173. }
  2174. $v = preg_replace('/[^\d\.\-]/', '', (string)$v);
  2175. if ($v !== '' && is_numeric($v)) {
  2176. return (float)$v;
  2177. }
  2178. }
  2179. return 0.0;
  2180. }
  2181. /**
  2182. * 订单号等用于 PDF 文件名片段(去掉路径非法字符)
  2183. */
  2184. protected function sanitizePurchaseConfirmPdfOrderKey(string $ccydh): string
  2185. {
  2186. $s = trim($ccydh);
  2187. if ($s === '') {
  2188. return 'ORDER';
  2189. }
  2190. $s = preg_replace('@[\\\\/:*?"<>|\\s]+@u', '_', $s);
  2191. $s = trim($s, '._-');
  2192. if ($s === '') {
  2193. return 'ORDER';
  2194. }
  2195. if (function_exists('mb_substr')) {
  2196. return mb_substr($s, 0, 80, 'UTF-8');
  2197. }
  2198. return strlen($s) <= 80 ? $s : substr($s, 0, 80);
  2199. }
  2200. /**
  2201. * 采购确认 PDF 相对路径(与 OSS 对象键一致,无前导斜杠):
  2202. * xinhua/年/月/日/scydgy_id/订单号_scydgy_id.pdf
  2203. *
  2204. * @return array{objectKey: string, webPath: string}
  2205. */
  2206. protected function buildPurchaseConfirmPdfPaths(int $scydgyId, string $ccydhRaw): array
  2207. {
  2208. $sid = (int)$scydgyId;
  2209. $safeOrder = $this->sanitizePurchaseConfirmPdfOrderKey($ccydhRaw);
  2210. $y = date('Y');
  2211. $m = date('m');
  2212. $d = date('d');
  2213. $basename = $safeOrder . '_' . $sid . '.pdf';
  2214. $objectKey = 'xinhua/' . $y . '/' . $m . '/' . $d . '/' . $sid . '/' . $basename;
  2215. return [
  2216. 'objectKey' => $objectKey,
  2217. 'webPath' => '/' . $objectKey,
  2218. ];
  2219. }
  2220. /**
  2221. * 采购确认成功后:用与「详情」弹窗相同的模板片段渲染 HTML,再存为 PDF(改 details_fragment 后 PDF 同步变化)。
  2222. * 优先上传至阿里云 OSS(application/config.php 的 oss 节点);失败或未配置时回退到 public 下与 objectKey 相同目录结构。
  2223. * 成功后将相对路径写入 purchase_order.pdf_url(形如 /xinhua/年/月/日/scydgy_id/订单号_scydgy_id.pdf)。
  2224. *
  2225. * @return string OSS 返回 https 完整 URL;本地回退为以 / 开头的 Web 路径;失败返回空串
  2226. */
  2227. protected function savePurchaseConfirmDetailPdf(int $scydgyId, int $purchaseOrderId): string
  2228. {
  2229. if ($scydgyId <= 0) {
  2230. return '';
  2231. }
  2232. $ids = trim((string)$scydgyId);
  2233. $prep = $this->prepareProcuremenDetailsView($ids);
  2234. if (!$prep['ok']) {
  2235. return '';
  2236. }
  2237. $ccydh = $prep['ccydh'];
  2238. $paths = $this->buildPurchaseConfirmPdfPaths((int)$scydgyId, $ccydh);
  2239. $objectKey = $paths['objectKey'];
  2240. $webPath = $paths['webPath'];
  2241. $meta = sprintf('工序行ID %s | 主表订单ID %d | PDF生成时间 %s', $ids, (int)$purchaseOrderId, date('Y-m-d H:i:s'));
  2242. $this->view->assign([
  2243. 'pdf_export' => 1,
  2244. 'pdfMetaLine' => $meta,
  2245. ]);
  2246. // 关闭后台 layout,避免 default 布局里的「控制台」面包屑等被打进 PDF
  2247. $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false;
  2248. $this->view->engine->layout(false);
  2249. try {
  2250. $html = $this->view->fetch('procuremen/details_pdf_shell');
  2251. } catch (\Throwable $e) {
  2252. if ($restoreLayout) {
  2253. $this->view->engine->layout($restoreLayout);
  2254. }
  2255. Log::write('采购确认PDF模板渲染失败: ' . $e->getMessage(), 'error');
  2256. $this->view->assign(['pdf_export' => '', 'pdfMetaLine' => '']);
  2257. return '';
  2258. }
  2259. if ($restoreLayout) {
  2260. $this->view->engine->layout($restoreLayout);
  2261. }
  2262. $this->view->assign(['pdf_export' => '', 'pdfMetaLine' => '']);
  2263. $tempDir = ROOT_PATH . 'runtime' . DIRECTORY_SEPARATOR . 'mpdf_tmp';
  2264. if (!is_dir($tempDir)) {
  2265. @mkdir($tempDir, 0755, true);
  2266. }
  2267. $tempPdf = $tempDir . DIRECTORY_SEPARATOR . uniqid('pc_pdf_', true) . '.pdf';
  2268. try {
  2269. $mpdf = new \Mpdf\Mpdf([
  2270. 'mode' => 'utf-8',
  2271. 'format' => 'A4',
  2272. 'margin_left' => 12,
  2273. 'margin_right' => 12,
  2274. 'margin_top' => 14,
  2275. 'margin_bottom' => 14,
  2276. 'tempDir' => $tempDir,
  2277. 'autoScriptToLang' => true,
  2278. 'autoLangToFont' => true,
  2279. ]);
  2280. // 与当前站点 HTTP_HOST 不同的占位 base,使 basepathIsLocal=false,避免 mPDF CssManager
  2281. // 在 parse_url 得到有 scheme 无 host 时对 $tr['host'] 触发「Undefined index: host」(日志已复现)。
  2282. $mpdf->SetBasePath('http://127.0.0.1/');
  2283. $mpdf->WriteHTML($html);
  2284. $mpdf->Output($tempPdf, \Mpdf\Output\Destination::FILE);
  2285. } catch (\Throwable $e) {
  2286. Log::write('采购确认PDF写入失败: ' . $e->getMessage(), 'error');
  2287. if (is_file($tempPdf)) {
  2288. @unlink($tempPdf);
  2289. }
  2290. return '';
  2291. }
  2292. $ossUrl = AliyunOss::uploadLocalFile($tempPdf, $objectKey);
  2293. if ($ossUrl !== '') {
  2294. @unlink($tempPdf);
  2295. $this->persistPurchaseOrderPdfUrl((int)$scydgyId, $webPath);
  2296. return $ossUrl;
  2297. }
  2298. $pi = pathinfo($objectKey);
  2299. $dirRel = isset($pi['dirname']) ? (string)$pi['dirname'] : 'xinhua';
  2300. $baseFile = isset($pi['basename']) ? (string)$pi['basename'] : '';
  2301. if ($baseFile === '') {
  2302. @unlink($tempPdf);
  2303. return '';
  2304. }
  2305. $dir = ROOT_PATH . 'public' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $dirRel);
  2306. if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
  2307. Log::write('采购确认PDF目录创建失败: ' . $dir, 'error');
  2308. @unlink($tempPdf);
  2309. return '';
  2310. }
  2311. $fullPath = $dir . DIRECTORY_SEPARATOR . $baseFile;
  2312. if (!@copy($tempPdf, $fullPath)) {
  2313. Log::write('采购确认PDF复制到本地失败: ' . $fullPath, 'error');
  2314. @unlink($tempPdf);
  2315. return '';
  2316. }
  2317. @unlink($tempPdf);
  2318. $this->persistPurchaseOrderPdfUrl((int)$scydgyId, $webPath);
  2319. return $webPath;
  2320. }
  2321. /**
  2322. * 采购确认 PDF 生成成功后写入 purchase_order.pdf_url(列不存在时仅记日志)
  2323. */
  2324. protected function persistPurchaseOrderPdfUrl(int $scydgyId, string $webPath): void
  2325. {
  2326. if ($scydgyId <= 0 || $webPath === '') {
  2327. return;
  2328. }
  2329. try {
  2330. Db::table('purchase_order')->where('scydgy_id', $scydgyId)->update(['pdf_url' => $webPath]);
  2331. } catch (\Throwable $e) {
  2332. Log::write('purchase_order.pdf_url 更新失败 scydgy_id=' . $scydgyId . ' ' . $e->getMessage(), 'notice');
  2333. }
  2334. }
  2335. /**
  2336. * 短信宝发送;失败抛异常(供事务回滚,避免「已入库但未通知」与「通知失败仍入库」)
  2337. * @throws \Exception
  2338. */
  2339. protected function smsbao($phone, $content)
  2340. {
  2341. $statusStr = [
  2342. '0' => '短信发送成功',
  2343. '-1' => '参数不全',
  2344. '-2' => '服务器空间不支持,请确认支持curl或者fsocket,联系您的空间商解决或者更换空间!',
  2345. '30' => '密码错误',
  2346. '40' => '账号不存在',
  2347. '41' => '余额不足',
  2348. '42' => '帐户已过期',
  2349. '43' => 'IP地址限制',
  2350. '50' => '内容含有敏感词',
  2351. ];
  2352. $smsapi = 'http://api.smsbao.com/';
  2353. $user = 'zhuwei123';
  2354. $pass = md5('1d1e605c101e4c1f8a156c6d7b19f126');
  2355. $sendurl = $smsapi . 'sms?u=' . $user . '&p=' . $pass . '&m=' . $phone . '&c=' . urlencode($content);
  2356. $result = @file_get_contents($sendurl);
  2357. if ($result === false) {
  2358. \think\Log::record('smsbao 请求失败 phone=' . $phone, 'error');
  2359. throw new \Exception('短信接口请求失败,请检查网络或稍后再试(未写入数据)');
  2360. }
  2361. $result = trim((string)$result);
  2362. if ($result !== '0') {
  2363. $msg = isset($statusStr[$result]) ? $statusStr[$result] : ('返回码 ' . $result);
  2364. \think\Log::record('smsbao 发送失败 phone=' . $phone . ' code=' . $result . ' ' . $msg, 'error');
  2365. throw new \Exception('短信发送失败:' . $msg . '(' . $phone . '),未写入数据');
  2366. }
  2367. }
  2368. }