UnifiedCostCalculationService.php 38 KB

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