UnifiedCostCalculationService.php 60 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045
  1. <?php
  2. namespace app\service;
  3. use think\Db;
  4. use think\Exception;
  5. use think\Log;
  6. /**
  7. * 统一成本核算服务类
  8. * 五项计算完成后统一插入数据库
  9. */
  10. class UnifiedCostCalculationService
  11. {
  12. // 存储中间数据的数组
  13. protected $monthlyCostDetails = [];
  14. protected $allocationFactors = [];
  15. // 配置常量
  16. const FLOOR_GROUP_MAP = [
  17. '1' => ['02、胶印机组', '03、卷凹机组', '06、单凹机组', '05、圆切机组', '04、圆烫机组', '10、模切机组', '09、烫金机组'],
  18. '2' => ['01、切纸机组', '11、检品机组', '07、丝印机组', '12、覆膜机组', '08、喷码机组'],
  19. ];
  20. // 科目名称映射
  21. const SUBJECT_MAPPING = [
  22. '废气处理' => '废气处理',
  23. '锅炉' => '锅炉',
  24. '空压机' => '空压机',
  25. '热水锅炉' => '热水锅炉',
  26. '真空鼓风机' => '真空鼓风机',
  27. '中央空调' => '中央空调',
  28. '待分摊总额' => '待分摊总额',
  29. ];
  30. // 字段默认值
  31. const DEFAULT_FIELD_VALUES = [
  32. '直接水电' => 0,
  33. '分摊材料' => 0,
  34. '车间人工' => 0,
  35. '部门人工附加' => 0,
  36. '分摊水电' => 0,
  37. '废气处理' => 0,
  38. '锅炉' => 0,
  39. '空压机' => 0,
  40. '热水锅炉' => 0,
  41. '真空鼓风机' => 0,
  42. '中央空调' => 0,
  43. '分摊其他' => 0,
  44. 'A_小时折旧额' => 0,
  45. '直接材料' => 0,
  46. '考核直接材料' => 0,
  47. '直接折旧' => 0,
  48. '分摊折旧' => 0,
  49. '场地租金' => 0,
  50. '成本合计' => 0,
  51. ];
  52. /** 参与「成本合计」汇总的金额字段(不含考核直接材料,避免与直接材料重复) */
  53. const COST_TOTAL_SUM_FIELDS = [
  54. '直接材料',
  55. '车间人工',
  56. '部门人工附加',
  57. '分摊材料',
  58. '直接水电',
  59. '分摊水电',
  60. '废气处理',
  61. '锅炉',
  62. '热水锅炉',
  63. '空压机',
  64. '真空鼓风机',
  65. '中央空调',
  66. '分摊其他',
  67. '分摊折旧',
  68. '场地租金',
  69. '直接折旧',
  70. 'A_小时折旧额',
  71. ];
  72. // 添加科目名称到数据库字段的映射
  73. const SUBJECT_TO_FIELD_MAP = [
  74. // 真空鼓风机相关
  75. '真空鼓风机' => '真空鼓风机',
  76. '真空鼓风' => '真空鼓风机',
  77. '6#楼真空鼓风' => '真空鼓风机',
  78. '真空风机' => '真空鼓风机',
  79. // 废气处理相关
  80. '废气处理' => '废气处理',
  81. '废气' => '废气处理',
  82. // 锅炉相关
  83. '锅炉' => '锅炉',
  84. '热水锅炉' => '热水锅炉',
  85. // 空压机相关
  86. '空压机' => '空压机',
  87. '空压' => '空压机',
  88. // 中央空调相关
  89. '中央空调' => '中央空调',
  90. '空调' => '中央空调',
  91. // 待分摊总额
  92. '待分摊总额' => '分摊水电',
  93. '分摊总额' => '分摊水电',
  94. ];
  95. // 批次大小
  96. const BATCH_SIZE = 100;
  97. /**
  98. * 主入口:执行所有成本计算并统一入库
  99. */
  100. public function calculateAndSaveAll(array $param): array
  101. {
  102. Db::startTrans();
  103. try {
  104. if (!isset($param['month'])) {
  105. throw new Exception("缺少必要参数: month");
  106. }
  107. $month = $param['month'];
  108. $sysId = $param['sys_id'] ?? uniqid();
  109. // 重置数据数组,确保是空数组
  110. $this->monthlyCostDetails = [];
  111. $this->allocationFactors = [];
  112. // 1. 清空旧数据
  113. $this->clearOldData($month);
  114. // 2. 执行五项计算
  115. $this->calculateDirectLabor($param);
  116. $this->calculateDirectUtilities($param);
  117. $this->calculateMaterialAllocation($month);
  118. $this->calculateIndirectLabor($month);
  119. $this->calculatePrepaidExpenses($month);
  120. $this->calculateApportionedUtilities($param);
  121. $this->calculateDirectDepreciation();
  122. $this->calculateTotalCostSummary();
  123. // 3. 统一插入数据
  124. $this->saveAllData($month, $sysId);
  125. Db::commit();
  126. $this->logSuccess($month);
  127. return $this->buildSuccessResponse($month);
  128. } catch (\Throwable $t) {
  129. Db::rollback();
  130. // 直接记录错误,不通过 logError 方法
  131. try {
  132. error_log("成本核算失败: " . $t->getMessage() . " at " . $t->getFile() . ":" . $t->getLine());
  133. error_log("堆栈跟踪: " . $t->getTraceAsString());
  134. } catch (\Throwable $logError) {
  135. // 忽略日志记录错误
  136. }
  137. return [
  138. 'success' => false,
  139. 'message' => '成本核算失败: ' . $t->getMessage(),
  140. 'error' => $t->getMessage(),
  141. 'file' => $t->getFile(),
  142. 'line' => $t->getLine()
  143. ];
  144. }
  145. }
  146. /**
  147. * 清空旧数据
  148. */
  149. protected function clearOldData(string $month): void
  150. {
  151. // 确保数据数组是有效的数组
  152. $this->monthlyCostDetails = [];
  153. $this->allocationFactors = [];
  154. }
  155. /**
  156. * 1. 计算直接人工
  157. */
  158. protected function calculateDirectLabor(array $param): void
  159. {
  160. try {
  161. $month = $param['month'];
  162. $sysId = $param['sys_id'] ?? '';
  163. $now = date('Y-m-d H:i:s');
  164. $list = Db::name('绩效工资汇总')
  165. ->alias('a')
  166. ->join('工单_工艺资料 b', 'a.sczl_gdbh = b.Gy0_gdbh and a.sczl_yjno = b.Gy0_yjno and a.sczl_gxh = b.Gy0_gxh')
  167. ->join('设备_基本资料 c', 'a.sczl_jtbh = c.设备编号', 'LEFT')
  168. ->join('工单_印件资料 d', 'a.sczl_gdbh = d.Yj_Gdbh and a.sczl_yjno = d.yj_Yjno')
  169. ->field($this->getDirectLaborFields())
  170. ->where('a.sys_ny', $month)
  171. ->group('a.sczl_gdbh,a.sczl_yjno,a.sczl_gxh,a.sczl_jtbh')
  172. ->select();
  173. // 转换为数组(如果返回的是对象)
  174. if (!is_array($list)) {
  175. $list = $list->toArray();
  176. }
  177. foreach ($list as $item) {
  178. // 确保 $item 是数组
  179. $itemArray = is_object($item) ? (array)$item : $item;
  180. if (!is_array($itemArray)) {
  181. continue;
  182. }
  183. $this->monthlyCostDetails[] = $this->buildMonthlyCostDetail($itemArray, $month, $sysId, $now);
  184. }
  185. } catch (\Exception $e) {
  186. throw new Exception("直接人工计算失败: " . $e->getMessage());
  187. }
  188. }
  189. /**
  190. * 获取直接人工查询字段
  191. */
  192. protected function getDirectLaborFields(): string
  193. {
  194. return 'a.sczl_gdbh as 工单编号,a.sczl_yjno as 印件号,a.sczl_gxh as 工序号,
  195. sum(a.班组车头产量) as 班组车头产量,b.Gy0_gxmc as 工序名称,
  196. a.sczl_ms as 墨色数,c.使用部门,b.印刷方式,b.版距,b.工价系数,
  197. a.sczl_jtbh,d.yj_yjmc as 印件名称,sum(a.车头产量占用机时) as 占用机时,
  198. a.sys_rq as 年月,a.工序难度系数,sum(a.班组换算产量) as 班组换算产量,
  199. a.千件工价';
  200. }
  201. /**
  202. * 构建月度成本明细记录
  203. */
  204. protected function buildMonthlyCostDetail(array $data, string $month, string $sysId, string $now): array
  205. {
  206. $banju = $data['版距'] === '0.0' ? 1000 : $data['版距'];
  207. $moshushu = $this->calculateMoshushu($data);
  208. $chanliang = $this->calculateChanliang($data);
  209. $renGongFenTan = $this->calculateRenGongFenTan($chanliang, $data);
  210. return array_merge([
  211. '车间名称' => $data['使用部门'] ?? '',
  212. 'sys_ny' => $month,
  213. 'sczl_gdbh' => $data['工单编号'],
  214. '印件名称' => $data['印件名称'] ?? '',
  215. 'sczl_yjno' => $data['印件号'],
  216. 'sczl_gxh' => $data['工序号'],
  217. '工序名称' => $data['工序名称'] ?? '',
  218. 'sczl_jtbh' => $data['sczl_jtbh'] ?? '',
  219. '卷张换算系数' => floatval($banju) / 1000,
  220. '占用机时' => floatval($data['占用机时']) ?? 0,
  221. '班组车头产量' => floatval($data['班组车头产量']) ?? 0,
  222. 'sczl_ms' => floatval($moshushu),
  223. '工序难度系数' => floatval($data['工序难度系数']) ?? 1,
  224. '班组换算产量' => floatval($data['班组换算产量']) ?? 0,
  225. '千件工价' => floatval($data['千件工价']) ?? 0,
  226. '计件产量' => floatval($chanliang),
  227. '水电分摊因子' => floatval($data['占用机时']) ?? 0,
  228. '材料分摊因子' => floatval($chanliang),
  229. '人工分摊因子' => floatval($renGongFenTan),
  230. 'Sys_id' => $sysId,
  231. 'Sys_rq' => $now
  232. ], self::DEFAULT_FIELD_VALUES);
  233. }
  234. /**
  235. * 计算墨色数
  236. */
  237. protected function calculateMoshushu(array $data): float
  238. {
  239. if (strpos($data['工序名称'] ?? '', '切废') !== false) {
  240. return 0.2;
  241. }
  242. return $data['墨色数'] === '0.00' ? 1.0 : floatval($data['墨色数']);
  243. }
  244. /**
  245. * 计算产量
  246. */
  247. protected function calculateChanliang(array $data): float
  248. {
  249. return ($data['班组车头产量'] * $data['工序难度系数']) + $data['班组换算产量'];
  250. }
  251. /**
  252. * 计算人工分摊
  253. */
  254. protected function calculateRenGongFenTan(float $chanliang, array $data): float
  255. {
  256. return ($chanliang / 1000) * $data['千件工价'];
  257. }
  258. /**
  259. * 2. 计算直接水电
  260. */
  261. protected function calculateDirectUtilities(array $param): void
  262. {
  263. $month = $param['month'];
  264. if (empty($this->monthlyCostDetails)) {
  265. throw new Exception("请先执行直接人工计算");
  266. }
  267. $utilityData = $this->fetchDirectUtilities($month);
  268. if (empty($utilityData)) {
  269. return;
  270. }
  271. $machineUtilities = $this->calculateMachineUtilities($utilityData);
  272. $workOrderHours = $this->getMachineWorkHours();
  273. $machineTotalHours = $this->calculateMachineTotalHours($workOrderHours);
  274. $allocationResults = $this->allocateUtilitiesToWorkOrders(
  275. $machineUtilities,
  276. $workOrderHours,
  277. $machineTotalHours
  278. );
  279. $this->updateDirectUtilitiesToCostDetails($allocationResults);
  280. }
  281. /**
  282. * 获取直接水电数据
  283. */
  284. protected function fetchDirectUtilities(string $month): array
  285. {
  286. return Db::name('成本_各月水电气')
  287. ->where('Sys_ny', $month)
  288. ->where('费用类型', '直接')
  289. ->field('设备编号, 部门名称, 科目名称, 耗电量, 单位电价, 耗气量, 单位气价')
  290. ->select();
  291. }
  292. /**
  293. * 计算机台水电费用
  294. */
  295. protected function calculateMachineUtilities(array $utilityData): array
  296. {
  297. $machineUtilities = [];
  298. foreach ($utilityData as $item) {
  299. $machineCode = $item['设备编号'] ?? '';
  300. if (empty($machineCode)) {
  301. continue;
  302. }
  303. $electricityCost = $this->calculateElectricityCost($item);
  304. $gasCost = $this->calculateGasCost($item);
  305. $totalCost = $electricityCost + $gasCost;
  306. if (!isset($machineUtilities[$machineCode])) {
  307. $machineUtilities[$machineCode] = [
  308. '机器编号' => $machineCode,
  309. '部门名称' => $item['部门名称'] ?? '',
  310. '总费用' => 0,
  311. '电费' => 0,
  312. '气费' => 0,
  313. ];
  314. }
  315. $machineUtilities[$machineCode]['总费用'] += $totalCost;
  316. $machineUtilities[$machineCode]['电费'] += $electricityCost;
  317. $machineUtilities[$machineCode]['气费'] += $gasCost;
  318. }
  319. return $machineUtilities;
  320. }
  321. /**
  322. * 计算电费
  323. */
  324. protected function calculateElectricityCost(array $item): float
  325. {
  326. return round(floatval($item['耗电量'] ?? 0) * floatval($item['单位电价'] ?? 0), 2);
  327. }
  328. /**
  329. * 计算气费
  330. */
  331. protected function calculateGasCost(array $item): float
  332. {
  333. return round(floatval($item['耗气量'] ?? 0) * floatval($item['单位气价'] ?? 0), 2);
  334. }
  335. /**
  336. * 获取机台工作小时数
  337. */
  338. protected function getMachineWorkHours(): array
  339. {
  340. $workHours = [];
  341. foreach ($this->monthlyCostDetails as $detail) {
  342. try {
  343. // 确保 $detail 是数组
  344. $detailArray = is_object($detail) ? (array)$detail : $detail;
  345. if (!is_array($detailArray)) {
  346. continue;
  347. }
  348. $machineCode = $detailArray['sczl_jtbh'] ?? '';
  349. $hours = floatval($detailArray['占用机时'] ?? 0);
  350. if (!empty($machineCode) && $hours > 0) {
  351. $workHours[] = [
  352. 'sczl_gdbh' => $detailArray['sczl_gdbh'] ?? '',
  353. 'sczl_yjno' => $detailArray['sczl_yjno'] ?? '',
  354. 'sczl_gxh' => $detailArray['sczl_gxh'] ?? '',
  355. 'sczl_jtbh' => $machineCode,
  356. '占用机时' => $hours,
  357. '车间名称' => $detailArray['车间名称'] ?? '',
  358. ];
  359. }
  360. } catch (\Exception $e) {
  361. continue;
  362. }
  363. }
  364. return $workHours;
  365. }
  366. /**
  367. * 计算机台总小时数
  368. */
  369. protected function calculateMachineTotalHours(array $workOrderHours): array
  370. {
  371. $machineTotalHours = [];
  372. foreach ($workOrderHours as $workOrder) {
  373. $machineCode = $workOrder['sczl_jtbh'] ?? '';
  374. $hours = floatval($workOrder['占用机时'] ?? 0);
  375. if (!empty($machineCode) && $hours > 0) {
  376. $machineTotalHours[$machineCode] = ($machineTotalHours[$machineCode] ?? 0) + $hours;
  377. }
  378. }
  379. return $machineTotalHours;
  380. }
  381. /**
  382. * 分摊水电到工单
  383. */
  384. protected function allocateUtilitiesToWorkOrders(
  385. array $machineUtilities,
  386. array $workOrderHours,
  387. array $machineTotalHours
  388. ): array {
  389. $allocationResults = [];
  390. $processedCount = 0;
  391. $skippedCount = 0;
  392. foreach ($workOrderHours as $workOrder) {
  393. try {
  394. // 确保 $workOrder 是数组
  395. $orderArray = is_object($workOrder) ? (array)$workOrder : $workOrder;
  396. if (!is_array($orderArray)) {
  397. $skippedCount++;
  398. continue;
  399. }
  400. $machineCode = $orderArray['sczl_jtbh'] ?? '';
  401. $hours = floatval($orderArray['占用机时'] ?? 0);
  402. if (empty($machineCode)) {
  403. $skippedCount++;
  404. continue;
  405. }
  406. if (!isset($machineUtilities[$machineCode])) {
  407. $skippedCount++;
  408. continue;
  409. }
  410. if ($machineUtilities[$machineCode]['总费用'] <= 0) {
  411. $skippedCount++;
  412. continue;
  413. }
  414. if (!isset($machineTotalHours[$machineCode]) || $machineTotalHours[$machineCode] <= 0) {
  415. $skippedCount++;
  416. continue;
  417. }
  418. $allocationRatio = $hours / $machineTotalHours[$machineCode];
  419. $allocatedAmount = round($machineUtilities[$machineCode]['总费用'] * $allocationRatio, 2);
  420. $uniqueKey = $this->getWorkOrderUniqueKey($orderArray);
  421. $allocationResults[$uniqueKey] = [
  422. 'unique_key' => $uniqueKey,
  423. 'sczl_gdbh' => $orderArray['sczl_gdbh'] ?? '',
  424. 'sczl_yjno' => $orderArray['sczl_yjno'] ?? '',
  425. 'sczl_gxh' => $orderArray['sczl_gxh'] ?? '',
  426. 'sczl_jtbh' => $machineCode,
  427. '占用机时' => $hours,
  428. '分摊比例' => $allocationRatio,
  429. '分摊金额' => $allocatedAmount,
  430. '机台总费用' => $machineUtilities[$machineCode]['总费用'],
  431. '机台总工时' => $machineTotalHours[$machineCode],
  432. ];
  433. $processedCount++;
  434. } catch (\Exception $e) {
  435. $skippedCount++;
  436. continue;
  437. }
  438. }
  439. return $allocationResults;
  440. }
  441. /**
  442. * 获取工单唯一键
  443. */
  444. protected function getWorkOrderUniqueKey($workOrder): string
  445. {
  446. // 记录传入参数的类型和内容(用于调试)
  447. $type = gettype($workOrder);
  448. // 如果是对象,转换为数组
  449. if (is_object($workOrder)) {
  450. $workOrder = (array)$workOrder;
  451. }
  452. // 安全地获取所有可能键名
  453. $gdbh = '';
  454. $yjno = '';
  455. $gxh = '';
  456. $jtbh = '';
  457. // 尝试各种可能的键名
  458. if (isset($workOrder['sczl_gdbh'])) {
  459. $gdbh = $workOrder['sczl_gdbh'];
  460. } elseif (isset($workOrder['工单编号'])) {
  461. $gdbh = $workOrder['工单编号'];
  462. }
  463. if (isset($workOrder['sczl_yjno'])) {
  464. $yjno = $workOrder['sczl_yjno'];
  465. } elseif (isset($workOrder['印件号'])) {
  466. $yjno = $workOrder['印件号'];
  467. }
  468. if (isset($workOrder['sczl_gxh'])) {
  469. $gxh = $workOrder['sczl_gxh'];
  470. } elseif (isset($workOrder['工序号'])) {
  471. $gxh = $workOrder['工序号'];
  472. }
  473. if (isset($workOrder['sczl_jtbh'])) {
  474. $jtbh = $workOrder['sczl_jtbh'];
  475. } elseif (isset($workOrder['机器编号'])) {
  476. $jtbh = $workOrder['机器编号'];
  477. }
  478. $key = sprintf('%s-%s-%s-%s', $gdbh, $yjno, $gxh, $jtbh);
  479. return $key;
  480. }
  481. /**
  482. * 更新直接水电到成本明细
  483. */
  484. protected function updateDirectUtilitiesToCostDetails(array $allocationResults): void
  485. {
  486. if (empty($allocationResults) || empty($this->monthlyCostDetails)) {
  487. return;
  488. }
  489. $updatedCount = 0;
  490. $totalAllocatedAmount = 0;
  491. $notFoundCount = 0;
  492. foreach ($this->monthlyCostDetails as &$costDetail) {
  493. try {
  494. // 确保 $costDetail 是数组
  495. $detailArray = is_object($costDetail) ? (array)$costDetail : $costDetail;
  496. $uniqueKey = $this->getWorkOrderUniqueKey($detailArray);
  497. if (isset($allocationResults[$uniqueKey])) {
  498. $allocatedAmount = $allocationResults[$uniqueKey]['分摊金额'] ?? 0;
  499. $costDetail['直接水电'] = $allocatedAmount;
  500. $totalAllocatedAmount += $allocatedAmount;
  501. $updatedCount++;
  502. } else {
  503. $notFoundCount++;
  504. }
  505. } catch (\Exception $e) {
  506. continue;
  507. }
  508. }
  509. }
  510. /**
  511. * 3. 材料出库分摊(材料出库单列表)
  512. */
  513. protected function calculateMaterialAllocation(string $month): void
  514. {
  515. if (empty($this->monthlyCostDetails)) {
  516. return;
  517. }
  518. $datePrefix = substr($month, 0, 4) . '-' . substr($month, 4, 2);
  519. $rows = $this->fetchMaterialOutboundRows($datePrefix);
  520. if (empty($rows)) {
  521. return;
  522. }
  523. $directRows = [];
  524. $plateRows = [];
  525. $repairRows = [];
  526. $otherRows = [];
  527. foreach ($rows as $row) {
  528. $row = is_object($row) ? (array)$row : $row;
  529. $amount = $this->getMaterialLineAmount($row);
  530. if ($amount <= 0) {
  531. continue;
  532. }
  533. if (!$this->isBlankWorkOrderNo($row['表体生产订单号'] ?? null)) {
  534. $directRows[] = $row;
  535. continue;
  536. }
  537. if ($this->isRepairOutboundRow($row)) {
  538. $repairRows[] = $row;
  539. continue;
  540. }
  541. if ($this->isPlateOutboundRow($row)) {
  542. $plateRows[] = $row;
  543. continue;
  544. }
  545. $otherRows[] = $row;
  546. }
  547. foreach ($directRows as $row) {
  548. $this->allocateDirectMaterialRow($row);
  549. }
  550. foreach ($plateRows as $row) {
  551. $this->allocatePlateMaterialRow($row);
  552. }
  553. foreach ($repairRows as $row) {
  554. $this->allocateRepairMaterialRow($row);
  555. }
  556. $this->allocateOtherMaterialRows($otherRows);
  557. }
  558. /**
  559. * 当月印刷成本中心材料出库
  560. */
  561. protected function fetchMaterialOutboundRows(string $datePrefix): array
  562. {
  563. $list = Db::name('材料出库单列表')
  564. ->where('出库日期', 'like', $datePrefix . '%')
  565. ->where('部门', '印刷成本中心')
  566. ->select();
  567. if (!is_array($list)) {
  568. return $list ? $list->toArray() : [];
  569. }
  570. return $list;
  571. }
  572. protected function getMaterialLineAmount(array $row): float
  573. {
  574. if (isset($row['金额']) && floatval($row['金额']) != 0) {
  575. return round(floatval($row['金额']), 2);
  576. }
  577. return round(floatval($row['数量'] ?? 0) * floatval($row['单价'] ?? 0), 2);
  578. }
  579. protected function isBlankWorkOrderNo($orderNo): bool
  580. {
  581. return $orderNo === null || trim((string)$orderNo) === '';
  582. }
  583. protected function getDetailChroma(array $detail): float
  584. {
  585. return floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  586. }
  587. /**
  588. * 有工单号:计入直接材料(数量×单价,优先取金额)
  589. */
  590. protected function allocateDirectMaterialRow(array $row): void
  591. {
  592. $gdbh = trim((string)($row['表体生产订单号'] ?? ''));
  593. $amount = $this->getMaterialLineAmount($row);
  594. if ($gdbh === '' || $amount <= 0) {
  595. return;
  596. }
  597. $this->addMaterialAmountByChromaRatio('直接材料', $amount, function ($detail) use ($gdbh) {
  598. return trim((string)($detail['sczl_gdbh'] ?? '')) === $gdbh;
  599. });
  600. $this->syncAssessedDirectMaterial();
  601. }
  602. /**
  603. * 板材(印版库):仅首次领用时绑定到对应工单直接材料
  604. */
  605. protected function allocatePlateMaterialRow(array $row): void
  606. {
  607. $stockCode = $this->getOutboundStockCode($row);
  608. if ($stockCode === '') {
  609. return;
  610. }
  611. $batch = $this->getOutboundBatchNo($row);
  612. if ($batch === '') {
  613. $batch = Db::name('产品_印版库')
  614. ->where('存货编码', $stockCode)
  615. ->order('UniqID asc')
  616. ->value('供方批号');
  617. $batch = trim((string)$batch);
  618. }
  619. if ($batch === '') {
  620. return;
  621. }
  622. $existsInStock = Db::name('产品_印版库')
  623. ->where('存货编码', $stockCode)
  624. ->where('供方批号', $batch)
  625. ->count();
  626. if (!$existsInStock) {
  627. return;
  628. }
  629. $usageCount = Db::name('工单_印版领用记录')
  630. ->where('Yb_存货编码', $stockCode)
  631. ->where('Yb_供方批号', $batch)
  632. ->count();
  633. if ($usageCount !== 1) {
  634. return;
  635. }
  636. $usage = Db::name('工单_印版领用记录')
  637. ->where('Yb_存货编码', $stockCode)
  638. ->where('Yb_供方批号', $batch)
  639. ->order('Yb_领用日期 asc, UniqID asc')
  640. ->find();
  641. if (empty($usage)) {
  642. return;
  643. }
  644. $gdbh = trim((string)($usage['Yb_工单编号'] ?? ''));
  645. $yjno = trim((string)($usage['Yb_印件号'] ?? ''));
  646. $machine = trim((string)($usage['Yb_领用机台'] ?? ''));
  647. $amount = $this->getMaterialLineAmount($row);
  648. if ($gdbh === '' || $amount <= 0) {
  649. return;
  650. }
  651. $this->addMaterialAmountByChromaRatio('直接材料', $amount, function ($detail) use ($gdbh, $yjno, $machine) {
  652. if (trim((string)($detail['sczl_gdbh'] ?? '')) !== $gdbh) {
  653. return false;
  654. }
  655. if ($yjno !== '' && trim((string)($detail['sczl_yjno'] ?? '')) !== $yjno) {
  656. return false;
  657. }
  658. if ($machine !== '' && trim((string)($detail['sczl_jtbh'] ?? '')) !== $machine) {
  659. return false;
  660. }
  661. return true;
  662. });
  663. $this->syncAssessedDirectMaterial();
  664. }
  665. /**
  666. * 修理领用:有机台则摊到机台工单,否则摊到车间各机台
  667. */
  668. protected function allocateRepairMaterialRow(array $row): void
  669. {
  670. $amount = $this->getMaterialLineAmount($row);
  671. if ($amount <= 0) {
  672. return;
  673. }
  674. $machine = $this->getOutboundMachineCode($row);
  675. if ($machine !== '') {
  676. $this->addMaterialAmountByChromaRatio('分摊材料', $amount, function ($detail) use ($machine) {
  677. return trim((string)($detail['sczl_jtbh'] ?? '')) === $machine;
  678. });
  679. return;
  680. }
  681. $workshop = $this->resolveOutboundWorkshop($row);
  682. if ($workshop === '') {
  683. return;
  684. }
  685. $machineHours = $this->getWorkshopMachineHours($workshop);
  686. if (empty($machineHours)) {
  687. return;
  688. }
  689. $totalHours = array_sum($machineHours);
  690. if ($totalHours <= 0) {
  691. return;
  692. }
  693. foreach ($machineHours as $machineCode => $hours) {
  694. if ($hours <= 0) {
  695. continue;
  696. }
  697. $machineAmount = round($amount * ($hours / $totalHours), 2);
  698. $this->addMaterialAmountByChromaRatio('分摊材料', $machineAmount, function ($detail) use ($machineCode) {
  699. return trim((string)($detail['sczl_jtbh'] ?? '')) === $machineCode;
  700. });
  701. }
  702. }
  703. /**
  704. * 其他无工单材料:按车间内 工序色度/车间总色度 分摊
  705. */
  706. protected function allocateOtherMaterialRows(array $rows): void
  707. {
  708. if (empty($rows)) {
  709. return;
  710. }
  711. $workshopTotals = [];
  712. foreach ($rows as $row) {
  713. $amount = $this->getMaterialLineAmount($row);
  714. if ($amount <= 0) {
  715. continue;
  716. }
  717. $workshop = $this->resolveOutboundWorkshop($row);
  718. if ($workshop === '') {
  719. $workshop = '__ALL__';
  720. }
  721. $workshopTotals[$workshop] = ($workshopTotals[$workshop] ?? 0) + $amount;
  722. }
  723. foreach ($workshopTotals as $workshop => $totalAmount) {
  724. if ($totalAmount <= 0) {
  725. continue;
  726. }
  727. if ($workshop === '__ALL__') {
  728. $this->addMaterialAmountByChromaRatio('分摊材料', $totalAmount, function () {
  729. return true;
  730. });
  731. continue;
  732. }
  733. $this->addMaterialAmountByChromaRatio('分摊材料', $totalAmount, function ($detail) use ($workshop) {
  734. return trim((string)($detail['车间名称'] ?? '')) === $workshop;
  735. });
  736. }
  737. }
  738. protected function addMaterialAmountByChromaRatio(string $targetField, float $amount, callable $detailFilter): void
  739. {
  740. if ($amount <= 0) {
  741. return;
  742. }
  743. $weighted = [];
  744. $totalChroma = 0;
  745. foreach ($this->monthlyCostDetails as $index => $detail) {
  746. if (!$detailFilter($detail)) {
  747. continue;
  748. }
  749. $chroma = $this->getDetailChroma($detail);
  750. if ($chroma <= 0) {
  751. continue;
  752. }
  753. $weighted[] = ['index' => $index, 'chroma' => $chroma];
  754. $totalChroma += $chroma;
  755. }
  756. if ($totalChroma <= 0) {
  757. return;
  758. }
  759. foreach ($weighted as $item) {
  760. $share = round($amount * ($item['chroma'] / $totalChroma), 2);
  761. $this->monthlyCostDetails[$item['index']][$targetField] =
  762. floatval($this->monthlyCostDetails[$item['index']][$targetField] ?? 0) + $share;
  763. }
  764. }
  765. protected function syncAssessedDirectMaterial(): void
  766. {
  767. foreach ($this->monthlyCostDetails as &$detail) {
  768. $detail['考核直接材料'] = floatval($detail['直接材料'] ?? 0);
  769. }
  770. unset($detail);
  771. }
  772. protected function isRepairOutboundRow(array $row): bool
  773. {
  774. $fields = ['出库类别', '收发类别', '业务类型', '交易类别', '领料类型'];
  775. foreach ($fields as $field) {
  776. if (empty($row[$field])) {
  777. continue;
  778. }
  779. if (mb_strpos((string)$row[$field], '修理领用') !== false) {
  780. return true;
  781. }
  782. }
  783. return false;
  784. }
  785. protected function isPlateOutboundRow(array $row): bool
  786. {
  787. $stockCode = $this->getOutboundStockCode($row);
  788. if ($stockCode === '') {
  789. return false;
  790. }
  791. $batch = $this->getOutboundBatchNo($row);
  792. $query = Db::name('产品_印版库')->where('存货编码', $stockCode);
  793. if ($batch !== '') {
  794. $query->where('供方批号', $batch);
  795. }
  796. return $query->count() > 0;
  797. }
  798. protected function getOutboundStockCode(array $row): string
  799. {
  800. foreach (['存货编码', '物料编码', '表体存货编码'] as $field) {
  801. if (!empty($row[$field])) {
  802. return trim((string)$row[$field]);
  803. }
  804. }
  805. return '';
  806. }
  807. protected function getOutboundBatchNo(array $row): string
  808. {
  809. foreach (['供方批号', '批号', '供应商批号', '表体批号'] as $field) {
  810. if (!empty($row[$field])) {
  811. return trim((string)$row[$field]);
  812. }
  813. }
  814. return '';
  815. }
  816. protected function getOutboundMachineCode(array $row): string
  817. {
  818. foreach (['机台编号', '设备编号', '表体机台', '领用机台', 'st_jtbh'] as $field) {
  819. if (!empty($row[$field])) {
  820. return trim((string)$row[$field]);
  821. }
  822. }
  823. return '';
  824. }
  825. protected function resolveOutboundWorkshop(array $row): string
  826. {
  827. foreach (['车间', '车间名称', '领用车间', '使用部门'] as $field) {
  828. if (!empty($row[$field])) {
  829. $name = trim((string)$row[$field]);
  830. if ($name !== '' && $name !== '印刷成本中心') {
  831. return $name;
  832. }
  833. }
  834. }
  835. $machine = $this->getOutboundMachineCode($row);
  836. if ($machine !== '') {
  837. $dept = Db::name('设备_基本资料')
  838. ->where('设备编号', $machine)
  839. ->value('使用部门');
  840. return trim((string)$dept);
  841. }
  842. return '';
  843. }
  844. /**
  845. * @return array<string, float> 机台 => 占用机时合计
  846. */
  847. protected function getWorkshopMachineHours(string $workshop): array
  848. {
  849. $hours = [];
  850. foreach ($this->monthlyCostDetails as $detail) {
  851. if (trim((string)($detail['车间名称'] ?? '')) !== $workshop) {
  852. continue;
  853. }
  854. $machine = trim((string)($detail['sczl_jtbh'] ?? ''));
  855. if ($machine === '') {
  856. continue;
  857. }
  858. $hour = floatval($detail['占用机时'] ?? 0);
  859. if ($hour <= 0) {
  860. continue;
  861. }
  862. $hours[$machine] = ($hours[$machine] ?? 0) + $hour;
  863. }
  864. return $hours;
  865. }
  866. /**
  867. * 4. 计算间接人工分摊
  868. */
  869. protected function calculateIndirectLabor(string $month): void
  870. {
  871. $workshopLaborTotal = $this->getWorkshopLaborTotalFromPerformancePayroll($month);
  872. if ($workshopLaborTotal > 0) {
  873. $this->allocateMonthlyTotalByWorkshopAndChroma(
  874. $workshopLaborTotal,
  875. '车间人工'
  876. );
  877. }
  878. $monthWage = $this->getMonthlyWage($month);
  879. if (!empty($monthWage)) {
  880. $this->allocateMonthlyTotalByWorkshopAndChroma(
  881. floatval($monthWage['管理人员工资'] ?? 0),
  882. '部门人工附加'
  883. );
  884. }
  885. }
  886. /**
  887. * 直接折旧:工单通电工时(占用机时)× 设备 A_小时折旧额
  888. */
  889. protected function calculateDirectDepreciation(): void
  890. {
  891. if (empty($this->monthlyCostDetails)) {
  892. return;
  893. }
  894. $machineCodes = [];
  895. foreach ($this->monthlyCostDetails as $detail) {
  896. $code = trim((string)($detail['sczl_jtbh'] ?? ''));
  897. if ($code !== '') {
  898. $machineCodes[$code] = true;
  899. }
  900. }
  901. if (empty($machineCodes)) {
  902. return;
  903. }
  904. $depreciationRates = Db::name('设备_基本资料')
  905. ->where('设备编号', 'in', array_keys($machineCodes))
  906. ->column('A_小时折旧额', '设备编号');
  907. foreach ($this->monthlyCostDetails as &$detail) {
  908. $machine = trim((string)($detail['sczl_jtbh'] ?? ''));
  909. $hourlyRate = floatval($depreciationRates[$machine] ?? 0);
  910. $powerHours = floatval($detail['占用机时'] ?? 0);
  911. $detail['A_小时折旧额'] = round($hourlyRate, 4);
  912. $detail['直接折旧'] = round($powerHours * $hourlyRate, 2);
  913. }
  914. unset($detail);
  915. }
  916. /**
  917. * 6. 租金、待摊折旧、其他待摊(成本_各月其他费用)
  918. */
  919. protected function calculatePrepaidExpenses(string $month): void
  920. {
  921. if (empty($this->monthlyCostDetails)) {
  922. return;
  923. }
  924. $expenses = Db::name('成本_各月其他费用')
  925. ->where('sys_ny', $month)
  926. ->field('场地租金,待摊折旧,其他')
  927. ->find();
  928. if (empty($expenses)) {
  929. return;
  930. }
  931. $this->allocateMonthlyTotalByWorkshopAndChroma(
  932. floatval($expenses['场地租金'] ?? 0),
  933. '场地租金'
  934. );
  935. $this->allocateMonthlyTotalByWorkshopAndChroma(
  936. floatval($expenses['待摊折旧'] ?? 0),
  937. '分摊折旧'
  938. );
  939. $this->allocateMonthlyTotalByWorkshopAndChroma(
  940. floatval($expenses['其他'] ?? 0),
  941. '分摊其他'
  942. );
  943. }
  944. /**
  945. * 按车间直接人工占比、再按车间内色度数占比分摊月度总额
  946. */
  947. protected function allocateMonthlyTotalByWorkshopAndChroma(float $totalAmount, string $targetField): void
  948. {
  949. if ($totalAmount <= 0) {
  950. return;
  951. }
  952. $wageRatio = $this->getWageRatio();
  953. if (empty($wageRatio)) {
  954. return;
  955. }
  956. foreach ($wageRatio as $workshopName => $ratio) {
  957. $chromaData = $this->getChromaDataForWorkshop($workshopName);
  958. if (empty($chromaData['total'])) {
  959. continue;
  960. }
  961. $workshopAmount = $ratio * $totalAmount;
  962. $this->allocateWageToDetails($workshopName, $workshopAmount, $chromaData, $targetField);
  963. }
  964. }
  965. /**
  966. * 获取工资比例
  967. */
  968. protected function getWageRatio(): array
  969. {
  970. $workshopTotals = [];
  971. foreach ($this->monthlyCostDetails as $detail) {
  972. $workshop = $detail['车间名称'];
  973. $amount = floatval($detail['人工分摊因子']);
  974. $workshopTotals[$workshop] = ($workshopTotals[$workshop] ?? 0) + $amount;
  975. }
  976. $total = array_sum($workshopTotals);
  977. if ($total <= 0) {
  978. return [];
  979. }
  980. $ratios = [];
  981. foreach ($workshopTotals as $workshop => $workshopTotal) {
  982. $ratios[$workshop] = round($workshopTotal / $total, 4);
  983. }
  984. return $ratios;
  985. }
  986. /**
  987. * 获取月度工资数据
  988. */
  989. protected function getMonthlyWage(string $month): array
  990. {
  991. return Db::name('成本_各月其他费用')
  992. ->where('sys_ny', $month)
  993. ->field('部门人员工资,管理人员工资')
  994. ->find() ?: [];
  995. }
  996. /**
  997. * 车间人工总额:绩效工资汇总表计件工资 + 加班工资(与成本人工维护口径一致)
  998. */
  999. protected function getWorkshopLaborTotalFromPerformancePayroll(string $month): float
  1000. {
  1001. $row = Db::name('绩效工资汇总')
  1002. ->where('sys_ny', $month)
  1003. ->field('SUM(个人计件工资) AS piece_wage, SUM(个人加班工资) AS overtime_wage')
  1004. ->find();
  1005. if (empty($row)) {
  1006. return 0.0;
  1007. }
  1008. return floatval($row['piece_wage'] ?? 0) + floatval($row['overtime_wage'] ?? 0);
  1009. }
  1010. /**
  1011. * 获取车间色度数数据
  1012. */
  1013. protected function getChromaDataForWorkshop(string $workshop): array
  1014. {
  1015. $data = ['total' => 0, 'list' => []];
  1016. foreach ($this->monthlyCostDetails as $detail) {
  1017. if ($detail['车间名称'] === $workshop) {
  1018. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  1019. $data['total'] += $chroma;
  1020. $data['list'][] = [
  1021. 'sczl_gdbh' => $detail['sczl_gdbh'],
  1022. 'sczl_yjno' => $detail['sczl_yjno'],
  1023. 'sczl_gxh' => $detail['sczl_gxh'],
  1024. 'sczl_jtbh' => $detail['sczl_jtbh'],
  1025. 'chroma' => $chroma
  1026. ];
  1027. }
  1028. }
  1029. return $data;
  1030. }
  1031. /**
  1032. * 分配工资到明细记录
  1033. */
  1034. protected function allocateWageToDetails(
  1035. string $workshop,
  1036. float $workshopAmount,
  1037. array $chromaData,
  1038. string $fieldName
  1039. ): void {
  1040. foreach ($this->monthlyCostDetails as &$detail) {
  1041. if ($detail['车间名称'] === $workshop) {
  1042. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  1043. $amount = round($chroma / $chromaData['total'] * $workshopAmount, 2);
  1044. $detail[$fieldName] += $amount;
  1045. }
  1046. }
  1047. }
  1048. /**
  1049. * 5. 计算分摊水电
  1050. */
  1051. protected function calculateApportionedUtilities(array $param): void
  1052. {
  1053. try {
  1054. $month = $param['month'];
  1055. $sysId = $param['sys_id'] ?? '';
  1056. $utilityData = $this->fetchApportionedUtilities($month);
  1057. if (empty($utilityData)) {
  1058. return;
  1059. }
  1060. // 记录原始科目名称用于调试
  1061. $originalSubjects = array_unique(array_column($utilityData, '科目名称'));
  1062. $machineAllocations = $this->calculateMachineAllocations($utilityData);
  1063. if (!empty($machineAllocations)) {
  1064. $this->generateAllocationFactors($machineAllocations, $month, $sysId);
  1065. $this->allocateApportionedUtilities($machineAllocations);
  1066. }
  1067. } catch (\Exception $e) {
  1068. throw new Exception("分摊水电计算失败: " . $e->getMessage());
  1069. }
  1070. }
  1071. /**
  1072. * 获取分摊水电数据
  1073. */
  1074. protected function fetchApportionedUtilities(string $month): array
  1075. {
  1076. return Db::name('成本_各月水电气')
  1077. ->where('Sys_ny', $month)
  1078. ->whereLike('费用类型', '%分摊%')
  1079. ->select();
  1080. }
  1081. /**
  1082. * 计算机台分摊金额
  1083. */
  1084. protected function calculateMachineAllocations(array $utilityData): array
  1085. {
  1086. $allocations = [];
  1087. $machineHours = $this->getMachineHours();
  1088. foreach ($utilityData as $item) {
  1089. try {
  1090. $subject = $this->simplifySubjectName($item['科目名称']);
  1091. $amount = $this->calculateUtilityAmount($item);
  1092. if ($amount <= 0) {
  1093. continue;
  1094. }
  1095. $this->allocateBySubject($allocations, $subject, $amount, $machineHours);
  1096. } catch (\Exception $e) {
  1097. continue;
  1098. }
  1099. }
  1100. return $allocations;
  1101. }
  1102. /**
  1103. * 获取机台运行时间
  1104. */
  1105. protected function getMachineHours(): array
  1106. {
  1107. $hours = [];
  1108. foreach ($this->monthlyCostDetails as $detail) {
  1109. $machine = $detail['sczl_jtbh'];
  1110. $hour = floatval($detail['占用机时']);
  1111. if (!empty($machine)) {
  1112. $hours[$machine] = ($hours[$machine] ?? 0) + $hour;
  1113. }
  1114. }
  1115. return $hours;
  1116. }
  1117. /**
  1118. * 按科目分摊
  1119. */
  1120. protected function allocateBySubject(
  1121. array &$allocations,
  1122. string $subject,
  1123. float $amount,
  1124. array $machineHours
  1125. ): void {
  1126. try {
  1127. if ($subject === '待分摊总额') {
  1128. $this->allocateByFloor($allocations, $amount, $machineHours);
  1129. } elseif (in_array($subject, ['锅炉', '热水锅炉'])) {
  1130. $this->allocateToRollCoater($allocations, $subject, $amount, $machineHours);
  1131. } else {
  1132. $this->allocateGlobally($allocations, $subject, $amount, $machineHours);
  1133. }
  1134. } catch (\Exception $e) {
  1135. }
  1136. }
  1137. /**
  1138. * 按楼层分摊
  1139. */
  1140. protected function allocateByFloor(array &$allocations, float $amount, array $machineHours): void
  1141. {
  1142. $floorData = $this->groupMachinesByFloor($machineHours);
  1143. if ($floorData['totalHours'] <= 0) {
  1144. return;
  1145. }
  1146. foreach ($floorData['floors'] as $floor => $data) {
  1147. if ($data['hours'] <= 0) {
  1148. continue;
  1149. }
  1150. $floorAmount = $amount * ($data['hours'] / $floorData['totalHours']);
  1151. $this->allocateToFloorMachines($allocations, $floorAmount, $data['machines'], $machineHours);
  1152. }
  1153. }
  1154. /**
  1155. * 按楼层分组机台
  1156. */
  1157. protected function groupMachinesByFloor(array $machineHours): array
  1158. {
  1159. $floors = ['1' => ['hours' => 0, 'machines' => []], '2' => ['hours' => 0, 'machines' => []]];
  1160. $totalHours = 0;
  1161. foreach ($machineHours as $machine => $hours) {
  1162. $floor = $this->getFloorByMachine($machine);
  1163. if ($floor && isset($floors[$floor])) {
  1164. $floors[$floor]['hours'] += $hours;
  1165. $floors[$floor]['machines'][] = $machine;
  1166. $totalHours += $hours;
  1167. }
  1168. }
  1169. return ['floors' => $floors, 'totalHours' => $totalHours];
  1170. }
  1171. /**
  1172. * 分摊到楼层机台
  1173. */
  1174. protected function allocateToFloorMachines(
  1175. array &$allocations,
  1176. float $floorAmount,
  1177. array $machines,
  1178. array $machineHours
  1179. ): void {
  1180. $floorHours = 0;
  1181. foreach ($machines as $machine) {
  1182. $floorHours += $machineHours[$machine];
  1183. }
  1184. if ($floorHours <= 0) {
  1185. return;
  1186. }
  1187. foreach ($machines as $machine) {
  1188. $machineHoursAmount = $machineHours[$machine];
  1189. if ($machineHoursAmount <= 0) {
  1190. continue;
  1191. }
  1192. $allocations[$machine]['待分摊总额'] =
  1193. ($allocations[$machine]['待分摊总额'] ?? 0) +
  1194. round($floorAmount * ($machineHoursAmount / $floorHours), 2);
  1195. }
  1196. }
  1197. /**
  1198. * 只分摊到卷凹机组
  1199. */
  1200. protected function allocateToRollCoater(
  1201. array &$allocations,
  1202. string $subject,
  1203. float $amount,
  1204. array $machineHours
  1205. ): void {
  1206. $rollCoaterMachines = $this->filterRollCoaterMachines(array_keys($machineHours));
  1207. $totalHours = 0;
  1208. foreach ($rollCoaterMachines as $machine) {
  1209. $totalHours += $machineHours[$machine];
  1210. }
  1211. if ($totalHours <= 0) {
  1212. return;
  1213. }
  1214. foreach ($rollCoaterMachines as $machine) {
  1215. $machineHoursAmount = $machineHours[$machine];
  1216. if ($machineHoursAmount <= 0) {
  1217. continue;
  1218. }
  1219. $allocations[$machine][$subject] =
  1220. ($allocations[$machine][$subject] ?? 0) +
  1221. round($amount * ($machineHoursAmount / $totalHours), 2);
  1222. }
  1223. }
  1224. /**
  1225. * 全局分摊
  1226. */
  1227. protected function allocateGlobally(
  1228. array &$allocations,
  1229. string $subject,
  1230. float $amount,
  1231. array $machineHours
  1232. ): void {
  1233. $totalHours = array_sum($machineHours);
  1234. if ($totalHours <= 0) {
  1235. return;
  1236. }
  1237. foreach ($machineHours as $machine => $hours) {
  1238. try {
  1239. // 确保 $machine 是有效的字符串键名
  1240. if (!is_string($machine) && !is_numeric($machine)) {
  1241. continue;
  1242. }
  1243. $machine = (string)$machine;
  1244. if ($hours <= 0) {
  1245. continue;
  1246. }
  1247. if (!isset($allocations[$machine])) {
  1248. $allocations[$machine] = [];
  1249. }
  1250. $machineAmount = round($amount * ($hours / $totalHours), 2);
  1251. $allocations[$machine][$subject] =
  1252. ($allocations[$machine][$subject] ?? 0) + $machineAmount;
  1253. } catch (\Exception $e) {
  1254. continue;
  1255. }
  1256. }
  1257. }
  1258. /**
  1259. * 根据机台获取楼层
  1260. */
  1261. protected function getFloorByMachine(string $machine): ?string
  1262. {
  1263. $group = Db::name('设备_基本资料')
  1264. ->where('设备编号', $machine)
  1265. ->value('设备编组');
  1266. if (!$group) {
  1267. return null;
  1268. }
  1269. foreach (self::FLOOR_GROUP_MAP as $floor => $groupNames) {
  1270. foreach ($groupNames as $groupName) {
  1271. if (strpos($group, $groupName) !== false) {
  1272. return $floor;
  1273. }
  1274. }
  1275. }
  1276. return null;
  1277. }
  1278. /**
  1279. * 筛选卷凹机组的机台
  1280. */
  1281. protected function filterRollCoaterMachines(array $machines): array
  1282. {
  1283. $rollCoater = [];
  1284. foreach ($machines as $machine) {
  1285. $group = Db::name('设备_基本资料')
  1286. ->where('设备编号', $machine)
  1287. ->value('设备编组');
  1288. if ($group && strpos($group, '03、卷凹机组') !== false) {
  1289. $rollCoater[] = $machine;
  1290. }
  1291. }
  1292. return $rollCoater;
  1293. }
  1294. /**
  1295. * 简化科目名称
  1296. */
  1297. protected function simplifySubjectName(string $subject): string
  1298. {
  1299. // 先尝试完全匹配
  1300. $subject = trim($subject);
  1301. foreach (self::SUBJECT_TO_FIELD_MAP as $keyword => $mappedField) {
  1302. if ($subject === $keyword) {
  1303. return $mappedField;
  1304. }
  1305. }
  1306. // 尝试部分匹配
  1307. foreach (self::SUBJECT_TO_FIELD_MAP as $keyword => $mappedField) {
  1308. if (strpos($subject, $keyword) !== false) {
  1309. return $mappedField;
  1310. }
  1311. }
  1312. return '分摊水电';
  1313. }
  1314. /**
  1315. * 计算水电金额
  1316. */
  1317. protected function calculateUtilityAmount(array $item): float
  1318. {
  1319. $electricity = $this->calculateElectricityCost($item);
  1320. $gas = $this->calculateGasCost($item);
  1321. return $electricity + $gas;
  1322. }
  1323. /**
  1324. * 生成分摊系数记录
  1325. */
  1326. protected function generateAllocationFactors(array $allocations, string $month, string $sysId): void
  1327. {
  1328. $now = date('Y-m-d H:i:s');
  1329. foreach ($allocations as $machine => $subjects) {
  1330. if (!is_string($machine) && !is_numeric($machine)) {
  1331. continue;
  1332. }
  1333. foreach ($subjects as $subject => $amount) {
  1334. if (!is_string($subject)) {
  1335. continue;
  1336. }
  1337. // 使用简化后的科目名称
  1338. $simplifiedSubject = $this->simplifySubjectName($subject);
  1339. $this->allocationFactors[] = [
  1340. 'Sys_ny' => $month,
  1341. '科目名称' => $simplifiedSubject, // 原始科目名称
  1342. '设备编号' => (string)$machine,
  1343. '分摊系数' => 1,
  1344. '分摊金额' => floatval($amount),
  1345. 'Sys_id' => $sysId,
  1346. 'Sys_rq' => $now,
  1347. ];
  1348. }
  1349. }
  1350. }
  1351. /**
  1352. * 分配分摊水电
  1353. */
  1354. protected function allocateApportionedUtilities(array $machineAllocations): void
  1355. {
  1356. $machineRates = $this->calculateMachineRates($machineAllocations);
  1357. foreach ($this->monthlyCostDetails as &$detail) {
  1358. $machine = $detail['sczl_jtbh'];
  1359. $hours = floatval($detail['占用机时']);
  1360. if ($hours <= 0 || !isset($machineRates[$machine])) {
  1361. continue;
  1362. }
  1363. // 直接水电费(保持不变)
  1364. $detail['直接水电'] = round($hours * 0.69, 2);
  1365. // 分摊水电费 - 使用映射后的字段名
  1366. foreach ($machineRates[$machine] as $subject => $rate) {
  1367. $field = $this->getUtilityFieldName($subject);
  1368. if (!isset($detail[$field])) {
  1369. continue;
  1370. }
  1371. $detail[$field] = round($hours * $rate, 2);
  1372. }
  1373. }
  1374. }
  1375. /**
  1376. * 计算机台每机时费用
  1377. */
  1378. protected function calculateMachineRates(array $allocations): array
  1379. {
  1380. $rates = [];
  1381. $machineHours = $this->getMachineHours();
  1382. foreach ($allocations as $machine => $subjects) {
  1383. $totalHours = $machineHours[$machine] ?? 0;
  1384. if ($totalHours <= 0) {
  1385. continue;
  1386. }
  1387. $rates[$machine] = [];
  1388. foreach ($subjects as $subject => $amount) {
  1389. $rates[$machine][$subject] = round($amount / $totalHours, 4);
  1390. }
  1391. }
  1392. return $rates;
  1393. }
  1394. /**
  1395. * 获取水电字段名
  1396. */
  1397. protected function getUtilityFieldName(string $subject): string
  1398. {
  1399. // 使用映射表
  1400. if (isset(self::SUBJECT_TO_FIELD_MAP[$subject])) {
  1401. return self::SUBJECT_TO_FIELD_MAP[$subject];
  1402. }
  1403. // 尝试部分匹配
  1404. foreach (self::SUBJECT_TO_FIELD_MAP as $keyword => $mappedField) {
  1405. if (strpos($subject, $keyword) !== false) {
  1406. return $mappedField;
  1407. }
  1408. }
  1409. // 默认映射到 '分摊水电'
  1410. return '分摊水电';
  1411. }
  1412. /**
  1413. * 汇总每条工序明细的成本合计
  1414. */
  1415. protected function calculateTotalCostSummary(): void
  1416. {
  1417. foreach ($this->monthlyCostDetails as &$detail) {
  1418. $total = 0.0;
  1419. foreach (self::COST_TOTAL_SUM_FIELDS as $field) {
  1420. $total += floatval($detail[$field] ?? 0);
  1421. }
  1422. $detail['成本合计'] = round($total, 2);
  1423. }
  1424. unset($detail);
  1425. }
  1426. /**
  1427. * 统一保存所有数据
  1428. */
  1429. protected function saveAllData(string $month, string $sysId): void
  1430. {
  1431. // 1. 删除旧数据
  1432. $this->deleteOldData($month);
  1433. // 2. 插入前检查数据结构
  1434. $this->validateDataStructure();
  1435. // 3. 插入新数据
  1436. $this->insertAllData();
  1437. }
  1438. /**
  1439. * 删除旧数据
  1440. */
  1441. protected function deleteOldData(string $month): void
  1442. {
  1443. Db::name('成本v23_月度成本明细')->where('sys_ny', $month)->delete();
  1444. Db::name('成本_各月分摊系数')->where('Sys_ny', $month)->delete();
  1445. }
  1446. /**
  1447. * 验证数据结构
  1448. */
  1449. protected function validateDataStructure(): void
  1450. {
  1451. try {
  1452. if (!is_array($this->monthlyCostDetails)) {
  1453. $this->monthlyCostDetails = [];
  1454. return;
  1455. }
  1456. if (empty($this->monthlyCostDetails)) {
  1457. return;
  1458. }
  1459. $tableName = '成本v23_月度成本明细';
  1460. $columns = Db::query("DESCRIBE `{$tableName}`");
  1461. if (empty($columns)) {
  1462. return;
  1463. }
  1464. $columnNames = array_column($columns, 'Field');
  1465. // 验证并修复每条记录
  1466. foreach ($this->monthlyCostDetails as $index => &$row) {
  1467. // 确保行是数组
  1468. if (!is_array($row)) {
  1469. $row = [];
  1470. continue;
  1471. }
  1472. // 检查字段数是否匹配
  1473. $rowCount = count($row);
  1474. $columnCount = count($columnNames);
  1475. if ($rowCount !== $columnCount) {
  1476. $this->fixRowData($row, $columnNames, $index);
  1477. }
  1478. // 确保所有键名都是字符串
  1479. foreach ($row as $key => $value) {
  1480. if (!is_string($key) && !is_int($key)) {
  1481. // 移除非法键名
  1482. unset($row[$key]);
  1483. // 如果有值,尝试保存
  1484. if ($value !== null) {
  1485. $row['invalid_key_' . $index] = $value;
  1486. }
  1487. }
  1488. }
  1489. }
  1490. } catch (\Throwable $t) {
  1491. // 不抛出异常,继续执行
  1492. }
  1493. }
  1494. /**
  1495. * 修复行数据
  1496. */
  1497. protected function fixRowData(array &$row, array $columnNames, int $index): void
  1498. {
  1499. $fixedRow = [];
  1500. foreach ($columnNames as $column) {
  1501. // 确保列名是字符串
  1502. $column = (string)$column;
  1503. // 获取值,如果不存在则设置默认值
  1504. if (array_key_exists($column, $row)) {
  1505. $value = $row[$column];
  1506. } else {
  1507. // 根据列名设置默认值
  1508. if (in_array($column, ['直接水电', '分摊材料', '直接材料', '考核直接材料', '车间人工', '部门人工附加', '分摊水电',
  1509. '废气处理', '锅炉', '空压机', '热水锅炉', '真空鼓风机', '中央空调', '分摊其他',
  1510. '分摊折旧', '场地租金', '直接折旧', 'A_小时折旧额', '成本合计'])) {
  1511. $value = 0;
  1512. } else {
  1513. $value = null;
  1514. }
  1515. }
  1516. $fixedRow[$column] = $value;
  1517. }
  1518. $this->monthlyCostDetails[$index] = $fixedRow;
  1519. }
  1520. /**
  1521. * 插入所有数据
  1522. */
  1523. protected function insertAllData(): void
  1524. {
  1525. $this->insertMonthlyCostDetails();
  1526. $this->insertAllocationFactors();
  1527. }
  1528. /**
  1529. * 插入月度成本明细
  1530. */
  1531. protected function insertMonthlyCostDetails(): void
  1532. {
  1533. if (empty($this->monthlyCostDetails)) {
  1534. return;
  1535. }
  1536. $total = count($this->monthlyCostDetails);
  1537. for ($i = 0; $i < $total; $i += self::BATCH_SIZE) {
  1538. $batch = array_slice($this->monthlyCostDetails, $i, self::BATCH_SIZE);
  1539. $this->insertBatch($batch, '成本v23_月度成本明细', $i);
  1540. }
  1541. }
  1542. /**
  1543. * 插入批次数据
  1544. */
  1545. protected function insertBatch(array $batch, string $tableName, int $startIndex): void
  1546. {
  1547. if (empty($batch)) {
  1548. return;
  1549. }
  1550. $firstRow = reset($batch);
  1551. $fields = array_keys($firstRow);
  1552. // 验证字段名不包含特殊字符
  1553. foreach ($fields as $field) {
  1554. if (preg_match('/[#@$%^&*()+\-=\[\]{}|;:"<>,.?\/]/', $field)) {
  1555. throw new Exception("字段名 '{$field}' 包含非法字符");
  1556. }
  1557. }
  1558. $fieldStr = '`' . implode('`,`', $fields) . '`';
  1559. $values = [];
  1560. foreach ($batch as $rowIndex => $row) {
  1561. $rowValues = [];
  1562. foreach ($fields as $field) {
  1563. $value = $row[$field] ?? null;
  1564. if (is_numeric($value)) {
  1565. $rowValues[] = $value;
  1566. } elseif (is_null($value)) {
  1567. $rowValues[] = 'NULL';
  1568. } else {
  1569. $rowValues[] = "'" . addslashes((string)$value) . "'";
  1570. }
  1571. }
  1572. $values[] = '(' . implode(',', $rowValues) . ')';
  1573. }
  1574. $sql = "INSERT INTO `{$tableName}` ({$fieldStr}) VALUES " . implode(',', $values);
  1575. try {
  1576. $result = Db::execute($sql);
  1577. } catch (\Exception $e) {
  1578. throw $e;
  1579. }
  1580. }
  1581. /**
  1582. * 插入分摊系数
  1583. */
  1584. protected function insertAllocationFactors(): void
  1585. {
  1586. if (!empty($this->allocationFactors)) {
  1587. $total = count($this->allocationFactors);
  1588. for ($i = 0; $i < $total; $i += self::BATCH_SIZE) {
  1589. $batch = array_slice($this->allocationFactors, $i, self::BATCH_SIZE);
  1590. $this->insertBatch($batch, '成本_各月分摊系数', $i);
  1591. }
  1592. }
  1593. }
  1594. /**
  1595. * 记录成功日志
  1596. */
  1597. protected function logSuccess(string $month): void
  1598. {
  1599. try {
  1600. // 确保记录数为整数
  1601. $monthlyCostDetailsCount = is_countable($this->monthlyCostDetails) ? count($this->monthlyCostDetails) : 0;
  1602. $allocationFactorsCount = is_countable($this->allocationFactors) ? count($this->allocationFactors) : 0;
  1603. $logData = [
  1604. 'month' => $month,
  1605. '月度成本明细记录数' => $monthlyCostDetailsCount,
  1606. '分摊系数记录数' => $allocationFactorsCount,
  1607. 'timestamp' => date('Y-m-d H:i:s')
  1608. ];
  1609. // 记录每个字段的数据类型,用于调试
  1610. if (!empty($this->monthlyCostDetails)) {
  1611. $firstRecord = reset($this->monthlyCostDetails);
  1612. $logData['first_record_keys'] = is_array($firstRecord) ? array_keys($firstRecord) : 'invalid';
  1613. $logData['first_record_type'] = gettype($firstRecord);
  1614. }
  1615. } catch (\Throwable $t) {
  1616. }
  1617. }
  1618. /**
  1619. * 构建成功响应
  1620. */
  1621. protected function buildSuccessResponse(string $month): array
  1622. {
  1623. try {
  1624. return [
  1625. 'success' => true,
  1626. 'message' => '成本核算完成',
  1627. 'month' => $month,
  1628. 'stats' => [
  1629. 'monthly_cost_details' => is_countable($this->monthlyCostDetails) ? count($this->monthlyCostDetails) : 0,
  1630. 'allocation_factors' => is_countable($this->allocationFactors) ? count($this->allocationFactors) : 0
  1631. ],
  1632. 'timestamp' => date('Y-m-d H:i:s')
  1633. ];
  1634. } catch (\Throwable $t) {
  1635. // 即使构建响应失败,也返回基本成功信息
  1636. return [
  1637. 'success' => true,
  1638. 'message' => '成本核算完成(统计信息获取失败)',
  1639. 'month' => $month,
  1640. 'error' => $t->getMessage()
  1641. ];
  1642. }
  1643. }
  1644. /**
  1645. * 记录错误日志
  1646. */
  1647. protected function logError(\Throwable $t): void
  1648. {
  1649. try {
  1650. $errorDetails = [
  1651. '时间' => date('Y-m-d H:i:s'),
  1652. '错误类型' => get_class($t),
  1653. '错误信息' => $t->getMessage(),
  1654. '错误代码' => $t->getCode(),
  1655. '文件' => $t->getFile(),
  1656. '行号' => $t->getLine(),
  1657. '堆栈跟踪' => $t->getTraceAsString(),
  1658. '月度成本明细数' => is_countable($this->monthlyCostDetails) ? count($this->monthlyCostDetails) : 'N/A',
  1659. '分摊系数数' => is_countable($this->allocationFactors) ? count($this->allocationFactors) : 'N/A',
  1660. ];
  1661. } catch (\Throwable $logError) {
  1662. }
  1663. }
  1664. /**
  1665. * 构建错误响应
  1666. */
  1667. protected function buildErrorResponse(Exception $e): array
  1668. {
  1669. return [
  1670. 'success' => false,
  1671. 'message' => '成本核算失败: ' . $e->getMessage(),
  1672. 'error' => $e->getMessage()
  1673. ];
  1674. }
  1675. /**
  1676. * 清理字段名中的特殊字符
  1677. */
  1678. protected function sanitizeFieldNames(array &$row): void
  1679. {
  1680. $sanitized = [];
  1681. foreach ($row as $key => $value) {
  1682. // 移除字段名中的特殊字符,只保留字母、数字、下划线和中文字符
  1683. $cleanKey = preg_replace('/[^\w\x{4e00}-\x{9fa5}]/u', '', $key);
  1684. $sanitized[$cleanKey] = $value;
  1685. }
  1686. $row = $sanitized;
  1687. }
  1688. }