UnifiedCostCalculationService.php 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025
  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 = []; // 成本v23_月度成本明细
  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. * 主入口:执行所有成本计算并统一入库
  22. */
  23. public function calculateAndSaveAll(array $param): array
  24. {
  25. Db::startTrans();
  26. try {
  27. $month = $param['month'];
  28. $sysId = $param['sys_id'] ?? '';
  29. // 1. 清空旧数据
  30. $this->clearOldData($month);
  31. // 2. 执行五项计算(注意顺序!)
  32. $this->calculateDirectLabor($param); // 直接人工(第一步,生成monthlyCostDetails)
  33. $this->calculateDirectUtilities($param); // 直接水电(第二步,依赖calculateDirectLabor的结果)
  34. $this->calculateIndirectMaterials($month); // 间接材料分摊
  35. $this->calculateIndirectLabor($month); // 间接人工分摊
  36. $this->calculateApportionedUtilities($param); // 分摊水电
  37. // 3. 统一插入数据
  38. $this->saveAllData($month, $sysId);
  39. Db::commit();
  40. Log::info("成本核算完成", [
  41. 'month' => $month,
  42. '月度成本明细记录数' => count($this->monthlyCostDetails),
  43. '分摊系数记录数' => count($this->allocationFactors)
  44. ]);
  45. return [
  46. 'success' => true,
  47. 'message' => '成本核算完成',
  48. 'stats' => [
  49. 'monthly_cost_details' => count($this->monthlyCostDetails),
  50. 'allocation_factors' => count($this->allocationFactors)
  51. ]
  52. ];
  53. } catch (Exception $e) {
  54. Db::rollback();
  55. Log::error("统一成本核算失败: " . $e->getMessage());
  56. return [
  57. 'success' => false,
  58. 'message' => '成本核算失败: ' . $e->getMessage()
  59. ];
  60. }
  61. }
  62. /**
  63. * 清空旧数据
  64. */
  65. protected function clearOldData(string $month): void
  66. {
  67. // 这里只记录要删除,实际删除操作在最后统一执行
  68. // 所有数据都缓存在内存中,最后统一插入
  69. $this->monthlyCostDetails = [];
  70. $this->allocationFactors = [];
  71. }
  72. /**
  73. * 1. 计算直接人工
  74. */
  75. protected function calculateDirectLabor(array $param): void
  76. {
  77. $month = $param['month'];
  78. $sysId = $param['sys_id'] ?? '';
  79. $list = Db::name('绩效工资汇总')
  80. ->alias('a')
  81. ->join('工单_工艺资料 b', 'a.sczl_gdbh = b.Gy0_gdbh and a.sczl_yjno = b.Gy0_yjno and a.sczl_gxh = b.Gy0_gxh')
  82. ->join('设备_基本资料 c', 'a.sczl_jtbh = c.设备编号', 'LEFT')
  83. ->join('工单_印件资料 d', 'a.sczl_gdbh = d.Yj_Gdbh and a.sczl_yjno = d.yj_Yjno')
  84. ->field('a.sczl_gdbh as 工单编号,a.sczl_yjno as 印件号,a.sczl_gxh as 工序号,sum(a.班组车头产量) as 班组车头产量,b.Gy0_gxmc as 工序名称,
  85. a.sczl_ms as 墨色数,c.使用部门,b.印刷方式,b.版距,b.工价系数,a.sczl_jtbh,d.yj_yjmc as 印件名称,sum(a.车头产量占用机时) as 占用机时,a.sys_rq as 年月,
  86. a.工序难度系数,sum(a.班组换算产量) as 班组换算产量,a.千件工价')
  87. ->where('a.sys_ny', $month)
  88. ->group('a.sczl_gdbh,a.sczl_yjno,a.sczl_gxh,a.sczl_jtbh')
  89. ->select();
  90. $now = date('Y-m-d H:i:s');
  91. foreach ($list as $k => $v) {
  92. // 处理特殊值
  93. $banju = $v['版距'] === '0.0' ? 1000 : $v['版距'];
  94. $moshushu = $v['墨色数'] === '0.00' ? 1 : $v['墨色数'];
  95. if (strpos($v['工序名称'], '切废')) {
  96. $moshushu = 0.2;
  97. }
  98. $chanliang = $v['班组车头产量'] * $v['工序难度系数'] + $v['班组换算产量'];
  99. $renGongFenTan = ($chanliang / 1000) * $v['千件工价'];
  100. $this->monthlyCostDetails[] = [
  101. '车间名称' => $v['使用部门'] ?? '',
  102. 'sys_ny' => $month,
  103. 'sczl_gdbh' => $v['工单编号'],
  104. '印件名称' => $v['印件名称'] ?? '',
  105. 'sczl_yjno' => $v['印件号'],
  106. 'sczl_gxh' => $v['工序号'],
  107. '工序名称' => $v['工序名称'] ?? '',
  108. 'sczl_jtbh' => $v['sczl_jtbh'] ?? '',
  109. '卷张换算系数' => floatval($banju) / 1000,
  110. '占用机时' => floatval($v['占用机时']) ?? 0,
  111. '班组车头产量' => floatval($v['班组车头产量']) ?? 0,
  112. 'sczl_ms' => floatval($moshushu),
  113. '工序难度系数' => floatval($v['工序难度系数']) ?? 1,
  114. '班组换算产量' => floatval($v['班组换算产量']) ?? 0,
  115. '千件工价' => floatval($v['千件工价']) ?? 0,
  116. '计件产量' => floatval($chanliang),
  117. '水电分摊因子' => floatval($v['占用机时']) ?? 0,
  118. '材料分摊因子' => floatval($chanliang),
  119. '人工分摊因子' => floatval($renGongFenTan),
  120. '直接水电' => 0, // 后续计算
  121. '分摊材料' => 0, // 后续计算
  122. '车间人工' => 0, // 后续计算
  123. '部门人工附加' => 0, // 后续计算
  124. '分摊水电' => 0, // 后续计算
  125. '废气处理' => 0, // 后续计算
  126. '锅炉' => 0, // 后续计算
  127. '空压机' => 0, // 后续计算
  128. '热水锅炉' => 0, // 后续计算
  129. '真空鼓风机' => 0, // 后续计算
  130. '中央空调' => 0, // 后续计算
  131. '分摊其他' => 0,
  132. 'Sys_id' => $sysId,
  133. 'Sys_rq' => $now
  134. ];
  135. }
  136. }
  137. /**
  138. * 2. 计算直接水电
  139. */
  140. protected function calculateDirectUtilities(array $param): void
  141. {
  142. $month = $param['month'];
  143. $sysId = $param['sys_id'] ?? '';
  144. // 检查是否已有直接人工数据
  145. if (empty($this->monthlyCostDetails)) {
  146. Log::error("直接水电计算需要先计算直接人工数据");
  147. throw new Exception("请先执行直接人工计算");
  148. }
  149. try {
  150. // 1. 从成本_各月水电气表查询直接水电费用数据
  151. $utilityData = Db::name('成本_各月水电气')
  152. ->where('Sys_ny', $month)
  153. ->where('费用类型', '直接')
  154. ->field('设备编号, 部门名称, 科目名称, 耗电量, 单位电价, 耗气量, 单位气价')
  155. ->select();
  156. if (empty($utilityData)) {
  157. Log::info("{$month}月份未找到直接水电费用数据");
  158. return;
  159. }
  160. Log::info("找到直接水电费用数据条数: " . count($utilityData));
  161. // 2. 计算每个机台的总水电费金额
  162. $machineUtilities = [];
  163. foreach ($utilityData as $item) {
  164. $machineCode = $item['设备编号'] ?? '';
  165. if (empty($machineCode)) {
  166. continue;
  167. }
  168. // 计算电费
  169. $electricityCost = floatval($item['耗电量'] ?? 0) * floatval($item['单位电价'] ?? 0);
  170. // 计算气费
  171. $gasCost = floatval($item['耗气量'] ?? 0) * floatval($item['单位气价'] ?? 0);
  172. // 总费用
  173. $totalCost = round($electricityCost + $gasCost, 2);
  174. if (!isset($machineUtilities[$machineCode])) {
  175. $machineUtilities[$machineCode] = [
  176. '机器编号' => $machineCode,
  177. '部门名称' => $item['部门名称'] ?? '',
  178. '总费用' => 0,
  179. '电费' => 0,
  180. '气费' => 0,
  181. ];
  182. }
  183. $machineUtilities[$machineCode]['总费用'] += $totalCost;
  184. $machineUtilities[$machineCode]['电费'] += $electricityCost;
  185. $machineUtilities[$machineCode]['气费'] += $gasCost;
  186. }
  187. Log::info("机台水电费统计: " . json_encode($machineUtilities, JSON_UNESCAPED_UNICODE));
  188. // 3. 获取本月所有工单的机台运行时间
  189. $workOrderHours = $this->getMachineWorkHours($month);
  190. if (empty($workOrderHours)) {
  191. Log::warning("{$month}月份未找到工单运行时间数据");
  192. return;
  193. }
  194. // 4. 计算每个机台的总运行时间
  195. $machineTotalHours = [];
  196. foreach ($workOrderHours as $workOrder) {
  197. $machineCode = $workOrder['sczl_jtbh'] ?? '';
  198. $hours = floatval($workOrder['占用机时'] ?? 0);
  199. if (empty($machineCode) || $hours <= 0) {
  200. continue;
  201. }
  202. if (!isset($machineTotalHours[$machineCode])) {
  203. $machineTotalHours[$machineCode] = 0;
  204. }
  205. $machineTotalHours[$machineCode] += $hours;
  206. }
  207. // 5. 计算每个工单应分摊的水电费
  208. $allocationResults = $this->allocateUtilitiesToWorkOrders($machineUtilities, $workOrderHours, $machineTotalHours);
  209. // 6. 将分配结果更新到monthlyCostDetails中
  210. $this->updateDirectUtilitiesToCostDetails($allocationResults);
  211. Log::info("直接水电费分配完成,更新工单数: " . count($allocationResults));
  212. } catch (\Exception $e) {
  213. Log::error("计算直接水电费失败: " . $e->getMessage());
  214. throw new Exception("直接水电费计算失败: " . $e->getMessage());
  215. }
  216. }
  217. /**
  218. * 获取机台生产工单的运行时间
  219. * 从已计算的数据中获取,因为成本v23_月度成本明细表中尚未插入数据
  220. */
  221. protected function getMachineWorkHours(string $month): array
  222. {
  223. if (empty($this->monthlyCostDetails)) {
  224. Log::warning("月度成本明细数据为空,无法获取机台运行时间");
  225. return [];
  226. }
  227. $workHours = [];
  228. foreach ($this->monthlyCostDetails as $detail) {
  229. $machineCode = $detail['sczl_jtbh'] ?? '';
  230. $hours = floatval($detail['占用机时'] ?? 0);
  231. if (empty($machineCode) || $hours <= 0) {
  232. continue;
  233. }
  234. $workHours[] = [
  235. 'sczl_gdbh' => $detail['sczl_gdbh'] ?? '',
  236. 'sczl_yjno' => $detail['sczl_yjno'] ?? '',
  237. 'sczl_gxh' => $detail['sczl_gxh'] ?? '',
  238. 'sczl_jtbh' => $machineCode,
  239. '占用机时' => $hours,
  240. '车间名称' => $detail['车间名称'] ?? '',
  241. ];
  242. }
  243. Log::info("从月度成本明细数据中获取机台运行时间,记录数: " . count($workHours));
  244. return $workHours;
  245. }
  246. /**
  247. * 将水电费按照工时比例分配到各个工单
  248. */
  249. protected function allocateUtilitiesToWorkOrders(array $machineUtilities, array $workOrderHours, array $machineTotalHours): array
  250. {
  251. $allocationResults = [];
  252. foreach ($workOrderHours as $workOrder) {
  253. $machineCode = $workOrder['sczl_jtbh'] ?? '';
  254. $hours = floatval($workOrder['占用机时'] ?? 0);
  255. // 如果机台没有水电费数据,跳过
  256. if (empty($machineCode) || !isset($machineUtilities[$machineCode]) || $machineUtilities[$machineCode]['总费用'] <= 0) {
  257. continue;
  258. }
  259. // 如果机台总运行时间为0,跳过
  260. if (!isset($machineTotalHours[$machineCode]) || $machineTotalHours[$machineCode] <= 0) {
  261. Log::warning("机台 {$machineCode} 总运行时间为0,无法分摊");
  262. continue;
  263. }
  264. // 计算该工单应分摊的金额
  265. $allocationRatio = $hours / $machineTotalHours[$machineCode];
  266. $allocatedAmount = round($machineUtilities[$machineCode]['总费用'] * $allocationRatio, 2);
  267. // 构建唯一标识(用于查找对应的monthlyCostDetails记录)
  268. $uniqueKey = $this->getWorkOrderUniqueKey($workOrder);
  269. $allocationResults[$uniqueKey] = [
  270. 'unique_key' => $uniqueKey,
  271. 'sczl_gdbh' => $workOrder['sczl_gdbh'] ?? '',
  272. 'sczl_yjno' => $workOrder['sczl_yjno'] ?? '',
  273. 'sczl_gxh' => $workOrder['sczl_gxh'] ?? '',
  274. 'sczl_jtbh' => $machineCode,
  275. '占用机时' => $hours,
  276. '分摊比例' => $allocationRatio,
  277. '分摊金额' => $allocatedAmount,
  278. '机台总费用' => $machineUtilities[$machineCode]['总费用'],
  279. '机台总工时' => $machineTotalHours[$machineCode],
  280. ];
  281. }
  282. return $allocationResults;
  283. }
  284. /**
  285. * 获取工单唯一标识
  286. */
  287. protected function getWorkOrderUniqueKey(array $workOrder): string
  288. {
  289. return sprintf(
  290. '%s-%s-%s-%s',
  291. $workOrder['sczl_gdbh'] ?? '',
  292. $workOrder['sczl_yjno'] ?? '',
  293. $workOrder['sczl_gxh'] ?? '',
  294. $workOrder['sczl_jtbh'] ?? ''
  295. );
  296. }
  297. /**
  298. * 将分配的水电费更新到monthlyCostDetails中
  299. */
  300. protected function updateDirectUtilitiesToCostDetails(array $allocationResults): void
  301. {
  302. if (empty($allocationResults) || empty($this->monthlyCostDetails)) {
  303. Log::warning("水电费分配结果或成本明细数据为空,无法更新");
  304. return;
  305. }
  306. $updatedCount = 0;
  307. $totalAllocatedAmount = 0;
  308. foreach ($this->monthlyCostDetails as &$costDetail) {
  309. $uniqueKey = $this->getWorkOrderUniqueKey($costDetail);
  310. if (isset($allocationResults[$uniqueKey])) {
  311. $allocatedAmount = $allocationResults[$uniqueKey]['分摊金额'];
  312. $costDetail['直接水电'] = $allocatedAmount;
  313. $totalAllocatedAmount += $allocatedAmount;
  314. $updatedCount++;
  315. }
  316. }
  317. Log::info("已更新直接水电费的工单数量: {$updatedCount}, 分配总金额: {$totalAllocatedAmount}");
  318. }
  319. /**
  320. * 3. 计算间接材料分摊
  321. */
  322. protected function calculateIndirectMaterials(string $month): void
  323. {
  324. if (empty($this->monthlyCostDetails)) {
  325. return;
  326. }
  327. $date = substr($month, 0, 4) . '-' . substr($month, 4, 2);
  328. // 获取分摊材料总金额
  329. $totalMoney = Db::name('材料出库单列表')
  330. ->where([
  331. '出库日期' => ['like', $date . '%'],
  332. '部门' => '印刷成本中心'
  333. ])
  334. ->whereNull('表体生产订单号')
  335. ->field('SUM(金额) as money')
  336. ->find();
  337. if (!$totalMoney || $totalMoney['money'] <= 0) {
  338. return;
  339. }
  340. // 计算总色度数
  341. $totalChroma = 0;
  342. foreach ($this->monthlyCostDetails as $detail) {
  343. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  344. $totalChroma += $chroma;
  345. }
  346. if ($totalChroma <= 0) {
  347. return;
  348. }
  349. // 分摊到每个记录
  350. foreach ($this->monthlyCostDetails as &$detail) {
  351. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  352. $money = round($totalMoney['money'] * ($chroma / $totalChroma), 2);
  353. $detail['分摊材料'] = $money;
  354. }
  355. }
  356. /**
  357. * 4. 计算间接人工分摊
  358. */
  359. protected function calculateIndirectLabor(string $month): void
  360. {
  361. // 获取工资比例
  362. $wageRatio = $this->getWageRatio($month);
  363. if (empty($wageRatio)) {
  364. return;
  365. }
  366. // 获取月度工资数据
  367. $monthWage = Db::name('成本_各月其他费用')
  368. ->where('sys_ny', $month)
  369. ->field('部门人员工资,管理人员工资')
  370. ->find();
  371. if (empty($monthWage)) {
  372. return;
  373. }
  374. // 计算每个车间的分配数据
  375. foreach ($wageRatio as $workshopName => $ratio) {
  376. $chromaData = $this->getChromaDataForWorkshop($workshopName);
  377. if (empty($chromaData['list']) || $chromaData['total'] == 0) {
  378. continue;
  379. }
  380. // 计算两种工资类型的分配
  381. $this->allocateWageToWorkshop($workshopName, $ratio, $monthWage, $chromaData);
  382. }
  383. }
  384. /**
  385. * 获取车间色度数数据
  386. */
  387. protected function getChromaDataForWorkshop(string $workshop): array
  388. {
  389. $data = [
  390. 'total' => 0,
  391. 'list' => []
  392. ];
  393. foreach ($this->monthlyCostDetails as $detail) {
  394. if ($detail['车间名称'] === $workshop) {
  395. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  396. $data['total'] += $chroma;
  397. $data['list'][] = [
  398. 'sczl_gdbh' => $detail['sczl_gdbh'],
  399. 'sczl_yjno' => $detail['sczl_yjno'],
  400. 'sczl_gxh' => $detail['sczl_gxh'],
  401. 'sczl_jtbh' => $detail['sczl_jtbh'],
  402. 'chroma' => $chroma
  403. ];
  404. }
  405. }
  406. return $data;
  407. }
  408. /**
  409. * 分配工资到车间
  410. */
  411. protected function allocateWageToWorkshop(string $workshop, float $ratio, array $monthWage, array $chromaData): void
  412. {
  413. $updateTypes = [
  414. '部门人员工资' => '车间人工',
  415. '管理人员工资' => '部门人工附加'
  416. ];
  417. foreach ($updateTypes as $wageType => $fieldName) {
  418. if (!isset($monthWage[$wageType]) || $monthWage[$wageType] <= 0) {
  419. continue;
  420. }
  421. $money = $ratio * $monthWage[$wageType];
  422. if ($chromaData['total'] <= 0) {
  423. continue;
  424. }
  425. // 找到对应记录并更新
  426. foreach ($this->monthlyCostDetails as &$detail) {
  427. if ($detail['车间名称'] === $workshop) {
  428. $chroma = floatval($detail['班组车头产量']) * floatval($detail['sczl_ms']);
  429. $perAmount = round($chroma / $chromaData['total'] * $money, 2);
  430. if ($fieldName === '车间人工') {
  431. $detail['车间人工'] += $perAmount;
  432. } else {
  433. $detail['部门人工附加'] += $perAmount;
  434. }
  435. }
  436. }
  437. }
  438. }
  439. /**
  440. * 获取工资比例
  441. */
  442. protected function getWageRatio(string $month): array
  443. {
  444. // 从缓存数据中计算
  445. $workshopTotals = [];
  446. foreach ($this->monthlyCostDetails as $detail) {
  447. $workshop = $detail['车间名称'];
  448. $amount = floatval($detail['人工分摊因子']);
  449. if (!isset($workshopTotals[$workshop])) {
  450. $workshopTotals[$workshop] = 0;
  451. }
  452. $workshopTotals[$workshop] += $amount;
  453. }
  454. $total = array_sum($workshopTotals);
  455. if ($total <= 0) {
  456. return [];
  457. }
  458. $ratios = [];
  459. foreach ($workshopTotals as $workshop => $workshopTotal) {
  460. $ratios[$workshop] = round($workshopTotal / $total, 4);
  461. }
  462. return $ratios;
  463. }
  464. /**
  465. * 5. 计算分摊水电
  466. */
  467. protected function calculateApportionedUtilities(array $param): void
  468. {
  469. $month = $param['month'];
  470. $sysId = $param['sys_id'] ?? '';
  471. // 获取分摊水电数据
  472. $utilityData = Db::name('成本_各月水电气')
  473. ->where('Sys_ny', $month)
  474. ->whereLike('费用类型', '%分摊%')
  475. ->select();
  476. if (empty($utilityData)) {
  477. return;
  478. }
  479. // 计算各机台的分摊金额
  480. $machineAllocations = $this->calculateMachineAllocations($utilityData, $month);
  481. // 生成分摊系数记录
  482. $this->generateAllocationFactors($machineAllocations, $month, $sysId);
  483. // 分配到工单
  484. $this->allocateUtilitiesToWorkOrders($machineAllocations, $month);
  485. }
  486. /**
  487. * 计算机台分摊金额
  488. */
  489. protected function calculateMachineAllocations(array $utilityData, string $month): array
  490. {
  491. $allocations = [];
  492. // 先按设备编号分组计算总机时
  493. $machineHours = $this->getMachineHours($month);
  494. // 按科目处理分摊
  495. foreach ($utilityData as $item) {
  496. $subject = $this->simplifySubjectName($item['科目名称']);
  497. $amount = $this->calculateUtilityAmount($item);
  498. if ($amount <= 0) {
  499. continue;
  500. }
  501. // 根据科目类型选择分摊方式
  502. if ($subject === '待分摊总额') {
  503. $this->allocateByFloor($allocations, $amount, $machineHours, $month);
  504. } elseif (in_array($subject, ['锅炉', '热水锅炉'])) {
  505. $this->allocateToRollCoater($allocations, $amount, $machineHours, $month);
  506. } else {
  507. $this->allocateGlobally($allocations, $amount, $machineHours);
  508. }
  509. }
  510. return $allocations;
  511. }
  512. /**
  513. * 获取机台运行时间
  514. */
  515. protected function getMachineHours(string $month): array
  516. {
  517. $hours = [];
  518. foreach ($this->monthlyCostDetails as $detail) {
  519. $machine = $detail['sczl_jtbh'];
  520. $hour = floatval($detail['占用机时']);
  521. if (!isset($hours[$machine])) {
  522. $hours[$machine] = 0;
  523. }
  524. $hours[$machine] += $hour;
  525. }
  526. return $hours;
  527. }
  528. /**
  529. * 按楼层分摊
  530. */
  531. protected function allocateByFloor(array &$allocations, float $amount, array $machineHours, string $month): void
  532. {
  533. // 按楼层分组
  534. $floorHours = ['1' => 0, '2' => 0];
  535. $floorMachines = ['1' => [], '2' => []];
  536. // 先计算每个楼层总机时
  537. foreach ($machineHours as $machine => $hours) {
  538. $floor = $this->getFloorByMachine($machine);
  539. if ($floor && isset($floorHours[$floor])) {
  540. $floorHours[$floor] += $hours;
  541. $floorMachines[$floor][] = $machine;
  542. }
  543. }
  544. // 按楼层比例分摊
  545. $totalFloorHours = array_sum($floorHours);
  546. if ($totalFloorHours <= 0) {
  547. return;
  548. }
  549. foreach ($floorHours as $floor => $hours) {
  550. if ($hours <= 0) continue;
  551. $floorAmount = $amount * ($hours / $totalFloorHours);
  552. // 在楼层内按机台分摊
  553. foreach ($floorMachines[$floor] as $machine) {
  554. if (!isset($allocations[$machine])) {
  555. $allocations[$machine] = [];
  556. }
  557. $machineAmount = round($floorAmount * ($machineHours[$machine] / $hours), 2);
  558. $allocations[$machine]['待分摊总额'] = ($allocations[$machine]['待分摊总额'] ?? 0) + $machineAmount;
  559. }
  560. }
  561. }
  562. /**
  563. * 只分摊到卷凹机组
  564. */
  565. protected function allocateToRollCoater(array &$allocations, float $amount, array $machineHours, string $month): void
  566. {
  567. // 筛选卷凹机组的机台
  568. $rollCoaterMachines = $this->filterRollCoaterMachines(array_keys($machineHours));
  569. $totalHours = 0;
  570. foreach ($rollCoaterMachines as $machine) {
  571. $totalHours += $machineHours[$machine];
  572. }
  573. if ($totalHours <= 0) {
  574. return;
  575. }
  576. foreach ($rollCoaterMachines as $machine) {
  577. if (!isset($allocations[$machine])) {
  578. $allocations[$machine] = [];
  579. }
  580. $machineAmount = round($amount * ($machineHours[$machine] / $totalHours), 2);
  581. $allocations[$machine]['锅炉'] = ($allocations[$machine]['锅炉'] ?? 0) + $machineAmount;
  582. }
  583. }
  584. /**
  585. * 全局分摊
  586. */
  587. protected function allocateGlobally(array &$allocations, float $amount, array $machineHours): void
  588. {
  589. $totalHours = array_sum($machineHours);
  590. if ($totalHours <= 0) {
  591. return;
  592. }
  593. foreach ($machineHours as $machine => $hours) {
  594. if ($hours <= 0) continue;
  595. if (!isset($allocations[$machine])) {
  596. $allocations[$machine] = [];
  597. }
  598. $machineAmount = round($amount * ($hours / $totalHours), 2);
  599. $allocations[$machine][$this->getSubjectKey($amount)] = ($allocations[$machine][$this->getSubjectKey($amount)] ?? 0) + $machineAmount;
  600. }
  601. }
  602. /**
  603. * 根据机台获取楼层
  604. */
  605. protected function getFloorByMachine(string $machine): ?string
  606. {
  607. // 简化实现,实际应从数据库查询
  608. $groups = Db::name('设备_基本资料')
  609. ->where('设备编号', $machine)
  610. ->value('设备编组');
  611. if (!$groups) {
  612. return null;
  613. }
  614. foreach (self::FLOOR_GROUP_MAP as $floor => $groupNames) {
  615. foreach ($groupNames as $groupName) {
  616. if (strpos($groups, $groupName) !== false) {
  617. return $floor;
  618. }
  619. }
  620. }
  621. return null;
  622. }
  623. /**
  624. * 筛选卷凹机组的机台
  625. */
  626. protected function filterRollCoaterMachines(array $machines): array
  627. {
  628. $rollCoater = [];
  629. foreach ($machines as $machine) {
  630. $group = Db::name('设备_基本资料')
  631. ->where('设备编号', $machine)
  632. ->value('设备编组');
  633. if ($group && strpos($group, '03、卷凹机组') !== false) {
  634. $rollCoater[] = $machine;
  635. }
  636. }
  637. return $rollCoater;
  638. }
  639. /**
  640. * 简化科目名称
  641. */
  642. protected function simplifySubjectName(string $subject): string
  643. {
  644. $map = [
  645. '废气处理' => '废气处理',
  646. '锅炉' => '锅炉',
  647. '空压机' => '空压机',
  648. '热水锅炉' => '热水锅炉',
  649. '真空鼓风机' => '真空鼓风机',
  650. '中央空调' => '中央空调',
  651. '待分摊总额' => '待分摊总额',
  652. ];
  653. foreach ($map as $keyword => $simple) {
  654. if (strpos($subject, $keyword) !== false) {
  655. return $simple;
  656. }
  657. }
  658. return $subject;
  659. }
  660. /**
  661. * 计算水电金额
  662. */
  663. protected function calculateUtilityAmount(array $item): float
  664. {
  665. $electricity = floatval($item['耗电量'] ?? 0) * floatval($item['单位电价'] ?? 0);
  666. $gas = floatval($item['耗气量'] ?? 0) * floatval($item['单位气价'] ?? 0);
  667. return round($electricity + $gas, 2);
  668. }
  669. /**
  670. * 获取科目键名
  671. */
  672. protected function getSubjectKey(float $amount): string
  673. {
  674. // 简化的实现
  675. return '分摊其他';
  676. }
  677. /**
  678. * 生成分摊系数记录
  679. */
  680. protected function generateAllocationFactors(array $allocations, string $month, string $sysId): void
  681. {
  682. $now = date('Y-m-d H:i:s');
  683. foreach ($allocations as $machine => $subjects) {
  684. foreach ($subjects as $subject => $amount) {
  685. $this->allocationFactors[] = [
  686. 'Sys_ny' => $month,
  687. '科目名称' => $subject,
  688. '设备编号' => $machine,
  689. '分摊系数' => 1,
  690. '分摊金额' => $amount,
  691. 'Sys_id' => $sysId,
  692. 'Sys_rq' => $now,
  693. ];
  694. }
  695. }
  696. }
  697. /**
  698. * 分配水电到工单
  699. */
  700. protected function allocateUtilitiesToWorkOrders(array $machineAllocations, string $month): void
  701. {
  702. // 计算机台每机时费用
  703. $machineRates = $this->calculateMachineRates($machineAllocations);
  704. // 更新月度成本明细
  705. foreach ($this->monthlyCostDetails as &$detail) {
  706. $machine = $detail['sczl_jtbh'];
  707. $hours = floatval($detail['占用机时']);
  708. if ($hours <= 0 || !isset($machineRates[$machine])) {
  709. continue;
  710. }
  711. // 直接水电费
  712. $detail['直接水电'] = round($hours * 0.69, 2);
  713. // 分摊水电费
  714. foreach ($machineRates[$machine] as $subject => $rate) {
  715. $field = $this->getUtilityFieldName($subject);
  716. $detail[$field] = round($hours * $rate, 2);
  717. }
  718. }
  719. }
  720. /**
  721. * 计算机台每机时费用
  722. */
  723. protected function calculateMachineRates(array $allocations): array
  724. {
  725. $rates = [];
  726. $machineHours = [];
  727. // 先计算机台总机时
  728. foreach ($this->monthlyCostDetails as $detail) {
  729. $machine = $detail['sczl_jtbh'];
  730. $hours = floatval($detail['占用机时']);
  731. if (!isset($machineHours[$machine])) {
  732. $machineHours[$machine] = 0;
  733. }
  734. $machineHours[$machine] += $hours;
  735. }
  736. // 计算每机时费用
  737. foreach ($allocations as $machine => $subjects) {
  738. $totalHours = $machineHours[$machine] ?? 0;
  739. if ($totalHours <= 0) {
  740. continue;
  741. }
  742. $rates[$machine] = [];
  743. foreach ($subjects as $subject => $amount) {
  744. $rates[$machine][$subject] = round($amount / $totalHours, 4);
  745. }
  746. }
  747. return $rates;
  748. }
  749. /**
  750. * 获取水电字段名
  751. */
  752. protected function getUtilityFieldName(string $subject): string
  753. {
  754. $map = [
  755. '待分摊总额' => '分摊水电',
  756. '废气处理' => '废气处理',
  757. '锅炉' => '锅炉',
  758. '空压机' => '空压机',
  759. '热水锅炉' => '热水锅炉',
  760. '真空鼓风机' => '真空鼓风机',
  761. '中央空调' => '中央空调',
  762. ];
  763. return $map[$subject] ?? $subject;
  764. }
  765. /**
  766. * 统一保存所有数据
  767. */
  768. protected function saveAllData(string $month, string $sysId): void
  769. {
  770. $now = date('Y-m-d H:i:s');
  771. // 1. 删除旧数据
  772. Db::name('成本v23_月度成本明细')->where('sys_ny', $month)->delete();
  773. Db::name('成本_各月分摊系数')->where('Sys_ny', $month)->delete();
  774. // 2. 插入前检查数据结构
  775. $this->validateAndFixData();
  776. // 3. 插入新数据(使用分批插入避免单次数据量过大)
  777. $this->insertDataInBatches();
  778. }
  779. /**
  780. * 验证并修复数据
  781. */
  782. protected function validateAndFixData(): void
  783. {
  784. if (empty($this->monthlyCostDetails)) {
  785. return;
  786. }
  787. // 获取表结构
  788. $tableName = '成本v23_月度成本明细';
  789. $columns = Db::query("DESCRIBE `{$tableName}`");
  790. $columnNames = array_column($columns, 'Field');
  791. Log::info("表{$tableName}结构: " . implode(', ', $columnNames));
  792. Log::info("插入数据字段数: " . count(array_keys($this->monthlyCostDetails[0])));
  793. // 检查每条数据的字段数
  794. foreach ($this->monthlyCostDetails as $index => $row) {
  795. $rowCount = count($row);
  796. if ($rowCount !== count($columnNames)) {
  797. Log::error("第{$index}行字段数不匹配: 数据有{$rowCount}个字段,表有" . count($columnNames) . "个字段");
  798. Log::error("数据字段: " . implode(', ', array_keys($row)));
  799. // 修正第58行数据(从0开始计数,第58行是索引57)
  800. if ($index === 57) {
  801. $this->fixRowData($row, $columnNames);
  802. }
  803. }
  804. }
  805. }
  806. /**
  807. * 修复行数据
  808. */
  809. protected function fixRowData(array &$row, array $columnNames): void
  810. {
  811. $fixedRow = [];
  812. foreach ($columnNames as $column) {
  813. $fixedRow[$column] = $row[$column] ?? null;
  814. }
  815. // 替换原数据
  816. $this->monthlyCostDetails[57] = $fixedRow;
  817. Log::info("已修复第58行数据");
  818. }
  819. /**
  820. * 分批插入数据
  821. */
  822. protected function insertDataInBatches(): void
  823. {
  824. // 分批插入月度成本明细
  825. if (!empty($this->monthlyCostDetails)) {
  826. $batchSize = 100;
  827. $total = count($this->monthlyCostDetails);
  828. for ($i = 0; $i < $total; $i += $batchSize) {
  829. $batch = array_slice($this->monthlyCostDetails, $i, $batchSize);
  830. // 构建确保字段顺序正确的SQL
  831. $firstRow = reset($batch);
  832. $fields = array_keys($firstRow);
  833. $fieldStr = '`' . implode('`,`', $fields) . '`';
  834. $values = [];
  835. foreach ($batch as $row) {
  836. $rowValues = [];
  837. foreach ($fields as $field) {
  838. $value = $row[$field] ?? null;
  839. $rowValues[] = is_numeric($value) ? $value : "'" . addslashes($value) . "'";
  840. }
  841. $values[] = '(' . implode(',', $rowValues) . ')';
  842. }
  843. $sql = "INSERT INTO `成本v23_月度成本明细` ({$fieldStr}) VALUES " . implode(',', $values);
  844. try {
  845. Db::execute($sql);
  846. Log::info("成功插入批次 " . ($i/$batchSize + 1));
  847. } catch (\Exception $e) {
  848. Log::error("插入批次失败: " . $e->getMessage());
  849. throw $e;
  850. }
  851. }
  852. }
  853. if (!empty($this->allocationFactors)) {
  854. $sql = Db::name('成本_各月分摊系数')->fetchSql(true)->insertAll($this->allocationFactors);
  855. Db::query($sql);
  856. }
  857. }
  858. }