UnifiedCostCalculationService.php 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
  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 BATCH_SIZE = 100;
  54. /**
  55. * 主入口:执行所有成本计算并统一入库
  56. */
  57. public function calculateAndSaveAll(array $param): array
  58. {
  59. Db::startTrans();
  60. try {
  61. $month = $param['month'];
  62. $sysId = $param['sys_id'] ?? '';
  63. // 1. 清空旧数据
  64. $this->clearOldData($month);
  65. // 2. 执行五项计算
  66. $this->calculateDirectLabor($param);
  67. $this->calculateDirectUtilities($param);
  68. $this->calculateIndirectMaterials($month);
  69. $this->calculateIndirectLabor($month);
  70. $this->calculateApportionedUtilities($param);
  71. // 3. 统一插入数据
  72. $this->saveAllData($month, $sysId);
  73. Db::commit();
  74. $this->logSuccess($month);
  75. return $this->buildSuccessResponse($month);
  76. } catch (Exception $e) {
  77. Db::rollback();
  78. $this->logError($e);
  79. return $this->buildErrorResponse($e);
  80. }
  81. }
  82. /**
  83. * 清空旧数据
  84. */
  85. protected function clearOldData(string $month): void
  86. {
  87. $this->monthlyCostDetails = [];
  88. $this->allocationFactors = [];
  89. }
  90. /**
  91. * 1. 计算直接人工
  92. */
  93. protected function calculateDirectLabor(array $param): void
  94. {
  95. $month = $param['month'];
  96. $sysId = $param['sys_id'] ?? '';
  97. $now = date('Y-m-d H:i:s');
  98. $list = Db::name('绩效工资汇总')
  99. ->alias('a')
  100. ->join('工单_工艺资料 b', 'a.sczl_gdbh = b.Gy0_gdbh and a.sczl_yjno = b.Gy0_yjno and a.sczl_gxh = b.Gy0_gxh')
  101. ->join('设备_基本资料 c', 'a.sczl_jtbh = c.设备编号', 'LEFT')
  102. ->join('工单_印件资料 d', 'a.sczl_gdbh = d.Yj_Gdbh and a.sczl_yjno = d.yj_Yjno')
  103. ->field($this->getDirectLaborFields())
  104. ->where('a.sys_ny', $month)
  105. ->group('a.sczl_gdbh,a.sczl_yjno,a.sczl_gxh,a.sczl_jtbh')
  106. ->select();
  107. foreach ($list as $item) {
  108. $this->monthlyCostDetails[] = $this->buildMonthlyCostDetail($item, $month, $sysId, $now);
  109. }
  110. Log::info("直接人工计算完成,记录数: " . count($list));
  111. }
  112. /**
  113. * 获取直接人工查询字段
  114. */
  115. protected function getDirectLaborFields(): string
  116. {
  117. return 'a.sczl_gdbh as 工单编号,a.sczl_yjno as 印件号,a.sczl_gxh as 工序号,
  118. sum(a.班组车头产量) as 班组车头产量,b.Gy0_gxmc as 工序名称,
  119. a.sczl_ms as 墨色数,c.使用部门,b.印刷方式,b.版距,b.工价系数,
  120. a.sczl_jtbh,d.yj_yjmc as 印件名称,sum(a.车头产量占用机时) as 占用机时,
  121. a.sys_rq as 年月,a.工序难度系数,sum(a.班组换算产量) as 班组换算产量,
  122. a.千件工价';
  123. }
  124. /**
  125. * 构建月度成本明细记录
  126. */
  127. protected function buildMonthlyCostDetail(array $data, string $month, string $sysId, string $now): array
  128. {
  129. $banju = $data['版距'] === '0.0' ? 1000 : $data['版距'];
  130. $moshushu = $this->calculateMoshushu($data);
  131. $chanliang = $this->calculateChanliang($data);
  132. $renGongFenTan = $this->calculateRenGongFenTan($chanliang, $data);
  133. return array_merge([
  134. '车间名称' => $data['使用部门'] ?? '',
  135. 'sys_ny' => $month,
  136. 'sczl_gdbh' => $data['工单编号'],
  137. '印件名称' => $data['印件名称'] ?? '',
  138. 'sczl_yjno' => $data['印件号'],
  139. 'sczl_gxh' => $data['工序号'],
  140. '工序名称' => $data['工序名称'] ?? '',
  141. 'sczl_jtbh' => $data['sczl_jtbh'] ?? '',
  142. '卷张换算系数' => floatval($banju) / 1000,
  143. '占用机时' => floatval($data['占用机时']) ?? 0,
  144. '班组车头产量' => floatval($data['班组车头产量']) ?? 0,
  145. 'sczl_ms' => floatval($moshushu),
  146. '工序难度系数' => floatval($data['工序难度系数']) ?? 1,
  147. '班组换算产量' => floatval($data['班组换算产量']) ?? 0,
  148. '千件工价' => floatval($data['千件工价']) ?? 0,
  149. '计件产量' => floatval($chanliang),
  150. '水电分摊因子' => floatval($data['占用机时']) ?? 0,
  151. '材料分摊因子' => floatval($chanliang),
  152. '人工分摊因子' => floatval($renGongFenTan),
  153. 'Sys_id' => $sysId,
  154. 'Sys_rq' => $now
  155. ], self::DEFAULT_FIELD_VALUES);
  156. }
  157. /**
  158. * 计算墨色数
  159. */
  160. protected function calculateMoshushu(array $data): float
  161. {
  162. if (strpos($data['工序名称'] ?? '', '切废') !== false) {
  163. return 0.2;
  164. }
  165. return $data['墨色数'] === '0.00' ? 1.0 : floatval($data['墨色数']);
  166. }
  167. /**
  168. * 计算产量
  169. */
  170. protected function calculateChanliang(array $data): float
  171. {
  172. return ($data['班组车头产量'] * $data['工序难度系数']) + $data['班组换算产量'];
  173. }
  174. /**
  175. * 计算人工分摊
  176. */
  177. protected function calculateRenGongFenTan(float $chanliang, array $data): float
  178. {
  179. return ($chanliang / 1000) * $data['千件工价'];
  180. }
  181. /**
  182. * 2. 计算直接水电
  183. */
  184. protected function calculateDirectUtilities(array $param): void
  185. {
  186. $month = $param['month'];
  187. if (empty($this->monthlyCostDetails)) {
  188. throw new Exception("请先执行直接人工计算");
  189. }
  190. $utilityData = $this->fetchDirectUtilities($month);
  191. if (empty($utilityData)) {
  192. Log::info("{$month}月份未找到直接水电费用数据");
  193. return;
  194. }
  195. $machineUtilities = $this->calculateMachineUtilities($utilityData);
  196. $workOrderHours = $this->getMachineWorkHours();
  197. $machineTotalHours = $this->calculateMachineTotalHours($workOrderHours);
  198. $allocationResults = $this->allocateUtilitiesToWorkOrders(
  199. $machineUtilities,
  200. $workOrderHours,
  201. $machineTotalHours
  202. );
  203. $this->updateDirectUtilitiesToCostDetails($allocationResults);
  204. }
  205. /**
  206. * 获取直接水电数据
  207. */
  208. protected function fetchDirectUtilities(string $month): array
  209. {
  210. return Db::name('成本_各月水电气')
  211. ->where('Sys_ny', $month)
  212. ->where('费用类型', '直接')
  213. ->field('设备编号, 部门名称, 科目名称, 耗电量, 单位电价, 耗气量, 单位气价')
  214. ->select();
  215. }
  216. /**
  217. * 计算机台水电费用
  218. */
  219. protected function calculateMachineUtilities(array $utilityData): array
  220. {
  221. $machineUtilities = [];
  222. foreach ($utilityData as $item) {
  223. $machineCode = $item['设备编号'] ?? '';
  224. if (empty($machineCode)) {
  225. continue;
  226. }
  227. $electricityCost = $this->calculateElectricityCost($item);
  228. $gasCost = $this->calculateGasCost($item);
  229. $totalCost = $electricityCost + $gasCost;
  230. if (!isset($machineUtilities[$machineCode])) {
  231. $machineUtilities[$machineCode] = [
  232. '机器编号' => $machineCode,
  233. '部门名称' => $item['部门名称'] ?? '',
  234. '总费用' => 0,
  235. '电费' => 0,
  236. '气费' => 0,
  237. ];
  238. }
  239. $machineUtilities[$machineCode]['总费用'] += $totalCost;
  240. $machineUtilities[$machineCode]['电费'] += $electricityCost;
  241. $machineUtilities[$machineCode]['气费'] += $gasCost;
  242. }
  243. return $machineUtilities;
  244. }
  245. /**
  246. * 计算电费
  247. */
  248. protected function calculateElectricityCost(array $item): float
  249. {
  250. return round(floatval($item['耗电量'] ?? 0) * floatval($item['单位电价'] ?? 0), 2);
  251. }
  252. /**
  253. * 计算气费
  254. */
  255. protected function calculateGasCost(array $item): float
  256. {
  257. return round(floatval($item['耗气量'] ?? 0) * floatval($item['单位气价'] ?? 0), 2);
  258. }
  259. /**
  260. * 获取机台工作小时数
  261. */
  262. protected function getMachineWorkHours(): array
  263. {
  264. $workHours = [];
  265. foreach ($this->monthlyCostDetails as $detail) {
  266. $machineCode = $detail['sczl_jtbh'] ?? '';
  267. $hours = floatval($detail['占用机时'] ?? 0);
  268. if (!empty($machineCode) && $hours > 0) {
  269. $workHours[] = [
  270. 'sczl_gdbh' => $detail['sczl_gdbh'] ?? '',
  271. 'sczl_yjno' => $detail['sczl_yjno'] ?? '',
  272. 'sczl_gxh' => $detail['sczl_gxh'] ?? '',
  273. 'sczl_jtbh' => $machineCode,
  274. '占用机时' => $hours,
  275. '车间名称' => $detail['车间名称'] ?? '',
  276. ];
  277. }
  278. }
  279. return $workHours;
  280. }
  281. /**
  282. * 计算机台总小时数
  283. */
  284. protected function calculateMachineTotalHours(array $workOrderHours): array
  285. {
  286. $machineTotalHours = [];
  287. foreach ($workOrderHours as $workOrder) {
  288. $machineCode = $workOrder['sczl_jtbh'] ?? '';
  289. $hours = floatval($workOrder['占用机时'] ?? 0);
  290. if (!empty($machineCode) && $hours > 0) {
  291. $machineTotalHours[$machineCode] = ($machineTotalHours[$machineCode] ?? 0) + $hours;
  292. }
  293. }
  294. return $machineTotalHours;
  295. }
  296. /**
  297. * 分摊水电到工单
  298. */
  299. protected function allocateUtilitiesToWorkOrders(
  300. array $machineUtilities,
  301. array $workOrderHours,
  302. array $machineTotalHours
  303. ): array {
  304. $allocationResults = [];
  305. foreach ($workOrderHours as $workOrder) {
  306. $machineCode = $workOrder['sczl_jtbh'] ?? '';
  307. $hours = floatval($workOrder['占用机时'] ?? 0);
  308. if (empty($machineCode) ||
  309. !isset($machineUtilities[$machineCode]) ||
  310. $machineUtilities[$machineCode]['总费用'] <= 0 ||
  311. !isset($machineTotalHours[$machineCode]) ||
  312. $machineTotalHours[$machineCode] <= 0) {
  313. continue;
  314. }
  315. $allocationRatio = $hours / $machineTotalHours[$machineCode];
  316. $allocatedAmount = round($machineUtilities[$machineCode]['总费用'] * $allocationRatio, 2);
  317. $uniqueKey = $this->getWorkOrderUniqueKey($workOrder);
  318. $allocationResults[$uniqueKey] = [
  319. 'unique_key' => $uniqueKey,
  320. 'sczl_gdbh' => $workOrder['sczl_gdbh'] ?? '',
  321. 'sczl_yjno' => $workOrder['sczl_yjno'] ?? '',
  322. 'sczl_gxh' => $workOrder['sczl_gxh'] ?? '',
  323. 'sczl_jtbh' => $machineCode,
  324. '占用机时' => $hours,
  325. '分摊比例' => $allocationRatio,
  326. '分摊金额' => $allocatedAmount,
  327. '机台总费用' => $machineUtilities[$machineCode]['总费用'],
  328. '机台总工时' => $machineTotalHours[$machineCode],
  329. ];
  330. }
  331. return $allocationResults;
  332. }
  333. /**
  334. * 获取工单唯一键
  335. */
  336. protected function getWorkOrderUniqueKey(array $workOrder): string
  337. {
  338. return implode('-', [
  339. $workOrder['sczl_gdbh'] ?? '',
  340. $workOrder['sczl_yjno'] ?? '',
  341. $workOrder['sczl_gxh'] ?? '',
  342. $workOrder['sczl_jtbh'] ?? ''
  343. ]);
  344. }
  345. /**
  346. * 更新直接水电到成本明细
  347. */
  348. protected function updateDirectUtilitiesToCostDetails(array $allocationResults): void
  349. {
  350. if (empty($allocationResults)) {
  351. return;
  352. }
  353. $updatedCount = 0;
  354. $totalAllocatedAmount = 0;
  355. foreach ($this->monthlyCostDetails as &$costDetail) {
  356. $uniqueKey = $this->getWorkOrderUniqueKey($costDetail);
  357. if (isset($allocationResults[$uniqueKey])) {
  358. $allocatedAmount = $allocationResults[$uniqueKey]['分摊金额'];
  359. $costDetail['直接水电'] = $allocatedAmount;
  360. $totalAllocatedAmount += $allocatedAmount;
  361. $updatedCount++;
  362. }
  363. }
  364. Log::info("已更新直接水电费的工单数量: {$updatedCount}, 分配总金额: {$totalAllocatedAmount}");
  365. }
  366. /**
  367. * 3. 计算间接材料分摊
  368. */
  369. protected function calculateIndirectMaterials(string $month): void
  370. {
  371. if (empty($this->monthlyCostDetails)) {
  372. return;
  373. }
  374. $date = substr($month, 0, 4) . '-' . substr($month, 4, 2);
  375. $totalMoney = $this->getIndirectMaterialTotal($date);
  376. if (!$totalMoney || $totalMoney <= 0) {
  377. return;
  378. }
  379. $totalChroma = $this->calculateTotalChroma();
  380. if ($totalChroma <= 0) {
  381. return;
  382. }
  383. $this->allocateIndirectMaterials($totalMoney, $totalChroma);
  384. }
  385. /**
  386. * 获取间接材料总额
  387. */
  388. protected function getIndirectMaterialTotal(string $date): float
  389. {
  390. $result = Db::name('材料出库单列表')
  391. ->where([
  392. '出库日期' => ['like', $date . '%'],
  393. '部门' => '印刷成本中心'
  394. ])
  395. ->whereNull('表体生产订单号')
  396. ->field('SUM(金额) as money')
  397. ->find();
  398. return $result ? floatval($result['money']) : 0;
  399. }
  400. /**
  401. * 计算总色度数
  402. */
  403. protected function calculateTotalChroma(): float
  404. {
  405. $totalChroma = 0;
  406. foreach ($this->monthlyCostDetails as $detail) {
  407. $totalChroma += floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  408. }
  409. return $totalChroma;
  410. }
  411. /**
  412. * 分摊间接材料
  413. */
  414. protected function allocateIndirectMaterials(float $totalMoney, float $totalChroma): void
  415. {
  416. foreach ($this->monthlyCostDetails as &$detail) {
  417. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  418. $money = round($totalMoney * ($chroma / $totalChroma), 2);
  419. $detail['分摊材料'] = $money;
  420. }
  421. }
  422. /**
  423. * 4. 计算间接人工分摊
  424. */
  425. protected function calculateIndirectLabor(string $month): void
  426. {
  427. $wageRatio = $this->getWageRatio();
  428. if (empty($wageRatio)) {
  429. return;
  430. }
  431. $monthWage = $this->getMonthlyWage($month);
  432. if (empty($monthWage)) {
  433. return;
  434. }
  435. $this->allocateIndirectLabor($wageRatio, $monthWage);
  436. }
  437. /**
  438. * 获取工资比例
  439. */
  440. protected function getWageRatio(): array
  441. {
  442. $workshopTotals = [];
  443. foreach ($this->monthlyCostDetails as $detail) {
  444. $workshop = $detail['车间名称'];
  445. $amount = floatval($detail['人工分摊因子']);
  446. $workshopTotals[$workshop] = ($workshopTotals[$workshop] ?? 0) + $amount;
  447. }
  448. $total = array_sum($workshopTotals);
  449. if ($total <= 0) {
  450. return [];
  451. }
  452. $ratios = [];
  453. foreach ($workshopTotals as $workshop => $workshopTotal) {
  454. $ratios[$workshop] = round($workshopTotal / $total, 4);
  455. }
  456. return $ratios;
  457. }
  458. /**
  459. * 获取月度工资数据
  460. */
  461. protected function getMonthlyWage(string $month): array
  462. {
  463. return Db::name('成本_各月其他费用')
  464. ->where('sys_ny', $month)
  465. ->field('部门人员工资,管理人员工资')
  466. ->find() ?: [];
  467. }
  468. /**
  469. * 分摊间接人工
  470. */
  471. protected function allocateIndirectLabor(array $wageRatio, array $monthWage): void
  472. {
  473. foreach ($wageRatio as $workshopName => $ratio) {
  474. $chromaData = $this->getChromaDataForWorkshop($workshopName);
  475. if (empty($chromaData['list']) || $chromaData['total'] == 0) {
  476. continue;
  477. }
  478. $this->allocateWageToWorkshop($workshopName, $ratio, $monthWage, $chromaData);
  479. }
  480. }
  481. /**
  482. * 获取车间色度数数据
  483. */
  484. protected function getChromaDataForWorkshop(string $workshop): array
  485. {
  486. $data = ['total' => 0, 'list' => []];
  487. foreach ($this->monthlyCostDetails as $detail) {
  488. if ($detail['车间名称'] === $workshop) {
  489. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  490. $data['total'] += $chroma;
  491. $data['list'][] = [
  492. 'sczl_gdbh' => $detail['sczl_gdbh'],
  493. 'sczl_yjno' => $detail['sczl_yjno'],
  494. 'sczl_gxh' => $detail['sczl_gxh'],
  495. 'sczl_jtbh' => $detail['sczl_jtbh'],
  496. 'chroma' => $chroma
  497. ];
  498. }
  499. }
  500. return $data;
  501. }
  502. /**
  503. * 分配工资到车间
  504. */
  505. protected function allocateWageToWorkshop(
  506. string $workshop,
  507. float $ratio,
  508. array $monthWage,
  509. array $chromaData
  510. ): void {
  511. $wageTypes = [
  512. '部门人员工资' => '车间人工',
  513. '管理人员工资' => '部门人工附加'
  514. ];
  515. foreach ($wageTypes as $wageType => $fieldName) {
  516. if (empty($monthWage[$wageType]) || $monthWage[$wageType] <= 0) {
  517. continue;
  518. }
  519. $workshopAmount = $ratio * $monthWage[$wageType];
  520. if ($chromaData['total'] <= 0) {
  521. continue;
  522. }
  523. $this->allocateWageToDetails($workshop, $workshopAmount, $chromaData, $fieldName);
  524. }
  525. }
  526. /**
  527. * 分配工资到明细记录
  528. */
  529. protected function allocateWageToDetails(
  530. string $workshop,
  531. float $workshopAmount,
  532. array $chromaData,
  533. string $fieldName
  534. ): void {
  535. foreach ($this->monthlyCostDetails as &$detail) {
  536. if ($detail['车间名称'] === $workshop) {
  537. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  538. $amount = round($chroma / $chromaData['total'] * $workshopAmount, 2);
  539. $detail[$fieldName] += $amount;
  540. }
  541. }
  542. }
  543. /**
  544. * 5. 计算分摊水电
  545. */
  546. protected function calculateApportionedUtilities(array $param): void
  547. {
  548. $month = $param['month'];
  549. $sysId = $param['sys_id'] ?? '';
  550. $utilityData = $this->fetchApportionedUtilities($month);
  551. if (empty($utilityData)) {
  552. return;
  553. }
  554. $machineAllocations = $this->calculateMachineAllocations($utilityData);
  555. $this->generateAllocationFactors($machineAllocations, $month, $sysId);
  556. $this->allocateApportionedUtilities($machineAllocations);
  557. }
  558. /**
  559. * 获取分摊水电数据
  560. */
  561. protected function fetchApportionedUtilities(string $month): array
  562. {
  563. return Db::name('成本_各月水电气')
  564. ->where('Sys_ny', $month)
  565. ->whereLike('费用类型', '%分摊%')
  566. ->select();
  567. }
  568. /**
  569. * 计算机台分摊金额
  570. */
  571. protected function calculateMachineAllocations(array $utilityData): array
  572. {
  573. $allocations = [];
  574. $machineHours = $this->getMachineHours();
  575. foreach ($utilityData as $item) {
  576. $subject = $this->simplifySubjectName($item['科目名称']);
  577. $amount = $this->calculateUtilityAmount($item);
  578. if ($amount <= 0) {
  579. continue;
  580. }
  581. $this->allocateBySubject($allocations, $subject, $amount, $machineHours);
  582. }
  583. return $allocations;
  584. }
  585. /**
  586. * 获取机台运行时间
  587. */
  588. protected function getMachineHours(): array
  589. {
  590. $hours = [];
  591. foreach ($this->monthlyCostDetails as $detail) {
  592. $machine = $detail['sczl_jtbh'];
  593. $hour = floatval($detail['占用机时']);
  594. if (!empty($machine)) {
  595. $hours[$machine] = ($hours[$machine] ?? 0) + $hour;
  596. }
  597. }
  598. return $hours;
  599. }
  600. /**
  601. * 按科目分摊
  602. */
  603. protected function allocateBySubject(
  604. array &$allocations,
  605. string $subject,
  606. float $amount,
  607. array $machineHours
  608. ): void {
  609. if ($subject === '待分摊总额') {
  610. $this->allocateByFloor($allocations, $amount, $machineHours);
  611. } elseif (in_array($subject, ['锅炉', '热水锅炉'])) {
  612. $this->allocateToRollCoater($allocations, $subject, $amount, $machineHours);
  613. } else {
  614. $this->allocateGlobally($allocations, $subject, $amount, $machineHours);
  615. }
  616. }
  617. /**
  618. * 按楼层分摊
  619. */
  620. protected function allocateByFloor(array &$allocations, float $amount, array $machineHours): void
  621. {
  622. $floorData = $this->groupMachinesByFloor($machineHours);
  623. if ($floorData['totalHours'] <= 0) {
  624. return;
  625. }
  626. foreach ($floorData['floors'] as $floor => $data) {
  627. if ($data['hours'] <= 0) {
  628. continue;
  629. }
  630. $floorAmount = $amount * ($data['hours'] / $floorData['totalHours']);
  631. $this->allocateToFloorMachines($allocations, $floorAmount, $data['machines'], $machineHours);
  632. }
  633. }
  634. /**
  635. * 按楼层分组机台
  636. */
  637. protected function groupMachinesByFloor(array $machineHours): array
  638. {
  639. $floors = ['1' => ['hours' => 0, 'machines' => []], '2' => ['hours' => 0, 'machines' => []]];
  640. $totalHours = 0;
  641. foreach ($machineHours as $machine => $hours) {
  642. $floor = $this->getFloorByMachine($machine);
  643. if ($floor && isset($floors[$floor])) {
  644. $floors[$floor]['hours'] += $hours;
  645. $floors[$floor]['machines'][] = $machine;
  646. $totalHours += $hours;
  647. }
  648. }
  649. return ['floors' => $floors, 'totalHours' => $totalHours];
  650. }
  651. /**
  652. * 分摊到楼层机台
  653. */
  654. protected function allocateToFloorMachines(
  655. array &$allocations,
  656. float $floorAmount,
  657. array $machines,
  658. array $machineHours
  659. ): void {
  660. $floorHours = 0;
  661. foreach ($machines as $machine) {
  662. $floorHours += $machineHours[$machine];
  663. }
  664. if ($floorHours <= 0) {
  665. return;
  666. }
  667. foreach ($machines as $machine) {
  668. $machineHoursAmount = $machineHours[$machine];
  669. if ($machineHoursAmount <= 0) {
  670. continue;
  671. }
  672. $allocations[$machine]['待分摊总额'] =
  673. ($allocations[$machine]['待分摊总额'] ?? 0) +
  674. round($floorAmount * ($machineHoursAmount / $floorHours), 2);
  675. }
  676. }
  677. /**
  678. * 只分摊到卷凹机组
  679. */
  680. protected function allocateToRollCoater(
  681. array &$allocations,
  682. string $subject,
  683. float $amount,
  684. array $machineHours
  685. ): void {
  686. $rollCoaterMachines = $this->filterRollCoaterMachines(array_keys($machineHours));
  687. $totalHours = 0;
  688. foreach ($rollCoaterMachines as $machine) {
  689. $totalHours += $machineHours[$machine];
  690. }
  691. if ($totalHours <= 0) {
  692. return;
  693. }
  694. foreach ($rollCoaterMachines as $machine) {
  695. $machineHoursAmount = $machineHours[$machine];
  696. if ($machineHoursAmount <= 0) {
  697. continue;
  698. }
  699. $allocations[$machine][$subject] =
  700. ($allocations[$machine][$subject] ?? 0) +
  701. round($amount * ($machineHoursAmount / $totalHours), 2);
  702. }
  703. }
  704. /**
  705. * 全局分摊
  706. */
  707. protected function allocateGlobally(
  708. array &$allocations,
  709. string $subject,
  710. float $amount,
  711. array $machineHours
  712. ): void {
  713. $totalHours = array_sum($machineHours);
  714. if ($totalHours <= 0) {
  715. return;
  716. }
  717. foreach ($machineHours as $machine => $hours) {
  718. if ($hours <= 0) {
  719. continue;
  720. }
  721. $allocations[$machine][$subject] =
  722. ($allocations[$machine][$subject] ?? 0) +
  723. round($amount * ($hours / $totalHours), 2);
  724. }
  725. }
  726. /**
  727. * 根据机台获取楼层
  728. */
  729. protected function getFloorByMachine(string $machine): ?string
  730. {
  731. $group = Db::name('设备_基本资料')
  732. ->where('设备编号', $machine)
  733. ->value('设备编组');
  734. if (!$group) {
  735. return null;
  736. }
  737. foreach (self::FLOOR_GROUP_MAP as $floor => $groupNames) {
  738. foreach ($groupNames as $groupName) {
  739. if (strpos($group, $groupName) !== false) {
  740. return $floor;
  741. }
  742. }
  743. }
  744. return null;
  745. }
  746. /**
  747. * 筛选卷凹机组的机台
  748. */
  749. protected function filterRollCoaterMachines(array $machines): array
  750. {
  751. $rollCoater = [];
  752. foreach ($machines as $machine) {
  753. $group = Db::name('设备_基本资料')
  754. ->where('设备编号', $machine)
  755. ->value('设备编组');
  756. if ($group && strpos($group, '03、卷凹机组') !== false) {
  757. $rollCoater[] = $machine;
  758. }
  759. }
  760. return $rollCoater;
  761. }
  762. /**
  763. * 简化科目名称
  764. */
  765. protected function simplifySubjectName(string $subject): string
  766. {
  767. foreach (self::SUBJECT_MAPPING as $keyword => $simple) {
  768. if (strpos($subject, $keyword) !== false) {
  769. return $simple;
  770. }
  771. }
  772. return $subject;
  773. }
  774. /**
  775. * 计算水电金额
  776. */
  777. protected function calculateUtilityAmount(array $item): float
  778. {
  779. $electricity = $this->calculateElectricityCost($item);
  780. $gas = $this->calculateGasCost($item);
  781. return $electricity + $gas;
  782. }
  783. /**
  784. * 生成分摊系数记录
  785. */
  786. protected function generateAllocationFactors(array $allocations, string $month, string $sysId): void
  787. {
  788. $now = date('Y-m-d H:i:s');
  789. foreach ($allocations as $machine => $subjects) {
  790. foreach ($subjects as $subject => $amount) {
  791. $this->allocationFactors[] = [
  792. 'Sys_ny' => $month,
  793. '科目名称' => $subject,
  794. '设备编号' => $machine,
  795. '分摊系数' => 1,
  796. '分摊金额' => $amount,
  797. 'Sys_id' => $sysId,
  798. 'Sys_rq' => $now,
  799. ];
  800. }
  801. }
  802. }
  803. /**
  804. * 分配分摊水电
  805. */
  806. protected function allocateApportionedUtilities(array $machineAllocations): void
  807. {
  808. $machineRates = $this->calculateMachineRates($machineAllocations);
  809. foreach ($this->monthlyCostDetails as &$detail) {
  810. $machine = $detail['sczl_jtbh'];
  811. $hours = floatval($detail['占用机时']);
  812. if ($hours <= 0 || !isset($machineRates[$machine])) {
  813. continue;
  814. }
  815. $detail['直接水电'] = round($hours * 0.69, 2);
  816. foreach ($machineRates[$machine] as $subject => $rate) {
  817. $field = $this->getUtilityFieldName($subject);
  818. $detail[$field] = round($hours * $rate, 2);
  819. }
  820. }
  821. }
  822. /**
  823. * 计算机台每机时费用
  824. */
  825. protected function calculateMachineRates(array $allocations): array
  826. {
  827. $rates = [];
  828. $machineHours = $this->getMachineHours();
  829. foreach ($allocations as $machine => $subjects) {
  830. $totalHours = $machineHours[$machine] ?? 0;
  831. if ($totalHours <= 0) {
  832. continue;
  833. }
  834. $rates[$machine] = [];
  835. foreach ($subjects as $subject => $amount) {
  836. $rates[$machine][$subject] = round($amount / $totalHours, 4);
  837. }
  838. }
  839. return $rates;
  840. }
  841. /**
  842. * 获取水电字段名
  843. */
  844. protected function getUtilityFieldName(string $subject): string
  845. {
  846. $map = [
  847. '待分摊总额' => '分摊水电',
  848. '废气处理' => '废气处理',
  849. '锅炉' => '锅炉',
  850. '空压机' => '空压机',
  851. '热水锅炉' => '热水锅炉',
  852. '真空鼓风机' => '真空鼓风机',
  853. '中央空调' => '中央空调',
  854. ];
  855. return $map[$subject] ?? $subject;
  856. }
  857. /**
  858. * 统一保存所有数据
  859. */
  860. protected function saveAllData(string $month, string $sysId): void
  861. {
  862. // 1. 删除旧数据
  863. $this->deleteOldData($month);
  864. // 2. 插入前检查数据结构
  865. $this->validateDataStructure();
  866. // 3. 插入新数据
  867. $this->insertAllData();
  868. }
  869. /**
  870. * 删除旧数据
  871. */
  872. protected function deleteOldData(string $month): void
  873. {
  874. Db::name('成本v23_月度成本明细')->where('sys_ny', $month)->delete();
  875. Db::name('成本_各月分摊系数')->where('Sys_ny', $month)->delete();
  876. }
  877. /**
  878. * 验证数据结构
  879. */
  880. protected function validateDataStructure(): void
  881. {
  882. if (empty($this->monthlyCostDetails)) {
  883. return;
  884. }
  885. $tableName = '成本v23_月度成本明细';
  886. $columns = Db::query("DESCRIBE `{$tableName}`");
  887. $columnNames = array_column($columns, 'Field');
  888. foreach ($this->monthlyCostDetails as $index => &$row) {
  889. if (count($row) !== count($columnNames)) {
  890. $this->fixRowData($row, $columnNames, $index);
  891. }
  892. }
  893. }
  894. /**
  895. * 修复行数据
  896. */
  897. protected function fixRowData(array &$row, array $columnNames, int $index): void
  898. {
  899. $fixedRow = [];
  900. foreach ($columnNames as $column) {
  901. $fixedRow[$column] = $row[$column] ?? 0;
  902. }
  903. $this->monthlyCostDetails[$index] = $fixedRow;
  904. Log::info("已修复第" . ($index + 1) . "行数据");
  905. }
  906. /**
  907. * 插入所有数据
  908. */
  909. protected function insertAllData(): void
  910. {
  911. $this->insertMonthlyCostDetails();
  912. $this->insertAllocationFactors();
  913. }
  914. /**
  915. * 插入月度成本明细
  916. */
  917. protected function insertMonthlyCostDetails(): void
  918. {
  919. if (empty($this->monthlyCostDetails)) {
  920. return;
  921. }
  922. $total = count($this->monthlyCostDetails);
  923. for ($i = 0; $i < $total; $i += self::BATCH_SIZE) {
  924. $batch = array_slice($this->monthlyCostDetails, $i, self::BATCH_SIZE);
  925. $this->insertBatch($batch, '成本v23_月度成本明细', $i);
  926. }
  927. }
  928. /**
  929. * 插入批次数据
  930. */
  931. protected function insertBatch(array $batch, string $tableName, int $startIndex): void
  932. {
  933. $firstRow = reset($batch);
  934. $fields = array_keys($firstRow);
  935. $fieldStr = '`' . implode('`,`', $fields) . '`';
  936. $values = [];
  937. foreach ($batch as $row) {
  938. $rowValues = [];
  939. foreach ($fields as $field) {
  940. $value = $row[$field] ?? null;
  941. $rowValues[] = is_numeric($value) ? $value : "'" . addslashes($value) . "'";
  942. }
  943. $values[] = '(' . implode(',', $rowValues) . ')';
  944. }
  945. $sql = "INSERT INTO `{$tableName}` ({$fieldStr}) VALUES " . implode(',', $values);
  946. try {
  947. Db::execute($sql);
  948. Log::info("成功插入批次 " . (($startIndex / self::BATCH_SIZE) + 1));
  949. } catch (\Exception $e) {
  950. Log::error("插入批次失败: " . $e->getMessage());
  951. throw $e;
  952. }
  953. }
  954. /**
  955. * 插入分摊系数
  956. */
  957. protected function insertAllocationFactors(): void
  958. {
  959. if (!empty($this->allocationFactors)) {
  960. $total = count($this->allocationFactors);
  961. for ($i = 0; $i < $total; $i += self::BATCH_SIZE) {
  962. $batch = array_slice($this->allocationFactors, $i, self::BATCH_SIZE);
  963. $this->insertBatch($batch, '成本_各月分摊系数', $i);
  964. }
  965. }
  966. }
  967. /**
  968. * 记录成功日志
  969. */
  970. protected function logSuccess(string $month): void
  971. {
  972. Log::info("成本核算完成", [
  973. 'month' => $month,
  974. '月度成本明细记录数' => count($this->monthlyCostDetails),
  975. '分摊系数记录数' => count($this->allocationFactors)
  976. ]);
  977. }
  978. /**
  979. * 构建成功响应
  980. */
  981. protected function buildSuccessResponse(string $month): array
  982. {
  983. return [
  984. 'success' => true,
  985. 'message' => '成本核算完成',
  986. 'month' => $month,
  987. 'stats' => [
  988. 'monthly_cost_details' => count($this->monthlyCostDetails),
  989. 'allocation_factors' => count($this->allocationFactors)
  990. ]
  991. ];
  992. }
  993. /**
  994. * 记录错误日志
  995. */
  996. protected function logError(Exception $e): void
  997. {
  998. Log::error("统一成本核算失败", [
  999. 'message' => $e->getMessage(),
  1000. 'trace' => $e->getTraceAsString()
  1001. ]);
  1002. }
  1003. /**
  1004. * 构建错误响应
  1005. */
  1006. protected function buildErrorResponse(Exception $e): array
  1007. {
  1008. return [
  1009. 'success' => false,
  1010. 'message' => '成本核算失败: ' . $e->getMessage(),
  1011. 'error' => $e->getMessage()
  1012. ];
  1013. }
  1014. }