project_quota.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. from typing import Optional
  2. import tools.utils as utils
  3. from core.dtos import ProjectQuotaDto
  4. from core.models import ProjectQuotaModel
  5. from stores import ProjectQuotaStore, ChapterStore, QuotaInputStore
  6. from core.log.log_record import LogRecordHelper
  7. from core.enum import OperationModule, OperationType, SendStatusEnum
  8. import executor
  9. class ProjectQuotaService:
  10. def __init__(self):
  11. self.store = ProjectQuotaStore()
  12. self.quota_input_store = QuotaInputStore()
  13. self.chapter_store = ChapterStore()
  14. self._logger = utils.get_logger()
  15. self._quota_sequence_num = {}
  16. def get_quotas_paginated(
  17. self,
  18. budget_id: int,
  19. project_id: str,
  20. item_code: str,
  21. page: int = 1,
  22. page_size: int = 10,
  23. keyword: Optional[str] = None,
  24. send_status: Optional[int] = None,
  25. ):
  26. """获取项目定额列表
  27. Args:
  28. budget_id: 概算序号
  29. project_id: 项目编号
  30. item_code: 条目编号
  31. page: 页码
  32. page_size: 每页数量
  33. keyword: 关键字
  34. send_status: 推送状态
  35. Returns:
  36. dict: 包含总数和定额列表的字典
  37. """
  38. try:
  39. data = self.store.get_quotas_paginated(
  40. budget_id=budget_id,
  41. project_id=project_id,
  42. item_code=item_code,
  43. page=page,
  44. page_size=page_size,
  45. keyword=keyword,
  46. send_status=send_status,
  47. )
  48. return [
  49. ProjectQuotaDto.from_model(quota).to_dict()
  50. for quota in data.get("data", [])
  51. ], data.get("total", 0)
  52. except Exception as e:
  53. # 记录错误日志
  54. print(f"获取项目定额列表失败: {str(e)}")
  55. raise
  56. def get_quotas_by_task_paginated(
  57. self,
  58. task_id: int,
  59. budget_id,
  60. project_id,
  61. item_code,
  62. page: int = 1,
  63. page_size: int = 10,
  64. keyword: Optional[str] = None,
  65. send_status: Optional[int] = None,
  66. ):
  67. try:
  68. data = self.store.get_quotas_by_task_paginated(
  69. task_id=task_id,
  70. budget_id=budget_id,
  71. project_id=project_id,
  72. item_code=item_code,
  73. page=page,
  74. page_size=page_size,
  75. send_status=send_status,
  76. keyword=keyword,
  77. )
  78. return [
  79. ProjectQuotaDto.from_model(quota).to_dict()
  80. for quota in data.get("data", [])
  81. ], data.get("total", 0)
  82. except Exception as e:
  83. self._logger.error(f"获取任务项目定额列表失败: {str(e)}")
  84. raise
  85. def get_quota_dto(self, quota_id: int):
  86. try:
  87. return self.store.get_quota_dto(quota_id)
  88. except Exception as e:
  89. self._logger.error(f"获取定额条目DTO失败: {str(e)}")
  90. raise
  91. def get_quota(self, quota_id: int) -> Optional[ProjectQuotaModel]:
  92. """获取单个项目定额
  93. Args:
  94. quota_id: 定额ID
  95. Returns:
  96. Optional[ProjectQuotaDto]: 项目定额DTO对象
  97. """
  98. try:
  99. return self.store.get_quota(quota_id)
  100. except Exception as e:
  101. self._logger.error(f"获取定额条目失败: {str(e)}")
  102. raise
  103. def save_quota(self, quota_dto: ProjectQuotaDto) -> Optional[ProjectQuotaDto]:
  104. """保存定额"""
  105. try:
  106. # 业务验证
  107. if quota_dto.id == 0:
  108. quota_dto = self.create_quota(quota_dto)
  109. else:
  110. quota_dto = self.update_quota(quota_dto)
  111. if quota_dto.send_status != SendStatusEnum.NEW.value:
  112. self.update_send_status(quota_dto.id, SendStatusEnum.CHANGE.value)
  113. return quota_dto
  114. except Exception as e:
  115. self._logger.error(f"保存定额条目失败: {str(e)}")
  116. raise
  117. def create_quota(self, quota_dto: ProjectQuotaDto) -> ProjectQuotaDto:
  118. """创建项目定额
  119. Args:
  120. quota_dto: 定额DTO对象
  121. Returns:
  122. ProjectQuotaDto: 创建后的定额DTO对象
  123. """
  124. try:
  125. # 业务验证
  126. if not quota_dto.project_id or not quota_dto.budget_id:
  127. raise ValueError("项目编号和概算序号不能为空")
  128. LogRecordHelper.log_success(
  129. OperationType.CREATE,
  130. OperationModule.QUOTA,
  131. f"新增定额条目: {quota_dto.entry_name}",
  132. utils.to_str(quota_dto.to_dict()),
  133. )
  134. return self.store.create_quota(quota_dto)
  135. except Exception as e:
  136. self._logger.error(f"创建项目定额失败: {str(e)}")
  137. LogRecordHelper.log_fail(
  138. OperationType.CREATE,
  139. OperationModule.QUOTA,
  140. f"新增定额条目失败: {str(e)}",
  141. utils.to_str(quota_dto.to_dict()),
  142. )
  143. raise
  144. def update_quota(self, quota_dto: ProjectQuotaDto) -> Optional[ProjectQuotaDto]:
  145. """更新项目定额
  146. Args:
  147. quota_dto: 定额DTO对象
  148. Returns:
  149. Optional[ProjectQuotaDto]: 更新后的定额DTO对象
  150. """
  151. try:
  152. # 业务验证
  153. if not quota_dto.id:
  154. raise ValueError("定额ID不能为空")
  155. quota = self.get_quota_dto(quota_dto.id)
  156. log_data = utils.to_str(quota.to_dict())
  157. if quota:
  158. quota_dto.id = quota.id
  159. quota_dto.project_id = quota.project_id
  160. quota_dto.budget_id = quota.budget_id
  161. quota_dto.item_id = quota.item_id
  162. quota_dto.item_code = quota.item_code
  163. quota_dto.quota_id = quota.quota_id
  164. quota_dto.ex_file = quota.ex_file
  165. quota_dto.ex_row = quota.ex_row
  166. quota_dto.ex_cell = quota.ex_cell
  167. quota_dto.ex_unit = quota.ex_unit
  168. quota_dto.ex_amount = quota.ex_amount
  169. quota_dto.note = quota.note
  170. quota_dto.send_status = (
  171. SendStatusEnum.CHANGE.value
  172. if quota.send_status == SendStatusEnum.SUCCESS.value
  173. else quota.send_status
  174. )
  175. quota_dto.send_time = quota.send_time
  176. quota_dto.send_error = ""
  177. # self.update_process_status(quota_dto.id,4)
  178. LogRecordHelper.log_success(
  179. OperationType.UPDATE,
  180. OperationModule.QUOTA,
  181. f"修改定额条目: {quota_dto.entry_name}",
  182. log_data,
  183. utils.to_str(quota_dto.to_dict()),
  184. )
  185. return self.store.update_quota(quota_dto)
  186. except Exception as e:
  187. self._logger.error(f"更新项目定额失败: {str(e)}")
  188. LogRecordHelper.log_fail(
  189. OperationType.UPDATE,
  190. OperationModule.QUOTA,
  191. f"修改定额条目失败: {str(e)}",
  192. utils.to_str(quota_dto.to_dict()),
  193. )
  194. raise
  195. def edit_by_key(self, id: int, key: str, value: str):
  196. try:
  197. # 业务验证
  198. # dict_data = {"id": id, key: value}
  199. # quota_dto = ProjectQuotaDto(**dict_data)
  200. quota_dto = self.get_quota_dto(id)
  201. if not quota_dto:
  202. raise ValueError("未查询到定额条目")
  203. log_data = utils.to_str(quota_dto.to_dict())
  204. setattr(quota_dto, key, value)
  205. if quota_dto.is_change == 0 and key == "entry_name":
  206. quota_dto.is_change = 0
  207. else:
  208. quota_dto.is_change = 1
  209. quota_dto.send_status = (
  210. SendStatusEnum.CHANGE.value
  211. if quota_dto.send_status == SendStatusEnum.SUCCESS.value
  212. else quota_dto.send_status
  213. )
  214. # self.update_process_status(quota_dto.id,4)
  215. LogRecordHelper.log_success(
  216. OperationType.UPDATE,
  217. OperationModule.QUOTA,
  218. f"修改定额条目[{key}]: {value}",
  219. log_data,
  220. utils.to_str({"id": id, key: value}),
  221. )
  222. self.store.update_quota(quota_dto)
  223. return None
  224. except Exception as e:
  225. self._logger.error(f"更新项目定额失败[{key}]: {value}, {str(e)}")
  226. LogRecordHelper.log_fail(
  227. OperationType.UPDATE,
  228. OperationModule.QUOTA,
  229. f"修改定额条目失败[{key}]: {value}, {str(e)}",
  230. utils.to_str({"id": id, key: value}),
  231. )
  232. raise
  233. def edit_quota_code_batch(self, ids: str, quota_code: str):
  234. try:
  235. if not ids:
  236. return "请选择要变更的章节"
  237. ids = ids.split(",")
  238. msg = ""
  239. for id in ids:
  240. try:
  241. quota_dto = self.get_quota_dto(int(id))
  242. if not quota_dto:
  243. msg += f"{id}不存在;"
  244. continue
  245. quota_dto.quota_code = quota_code
  246. quota_dto.is_change = 1
  247. quota_dto.send_status = (
  248. SendStatusEnum.CHANGE.value
  249. if quota_dto.send_status == SendStatusEnum.SUCCESS.value
  250. else quota_dto.send_status
  251. )
  252. self.store.update_quota(quota_dto)
  253. except Exception as e:
  254. msg += f"{id}: {str(e)};"
  255. LogRecordHelper.log_fail(
  256. OperationType.UPDATE,
  257. OperationModule.QUOTA,
  258. f"批量修改定额: {ids}",
  259. utils.to_str({"ids": ids, "quota_code": quota_code}),
  260. )
  261. return None if msg == "" else msg
  262. except Exception as e:
  263. msg = f"批量修改定额失败: {str(e)}"
  264. self._logger.error(msg)
  265. LogRecordHelper.log_fail(
  266. OperationType.UPDATE,
  267. OperationModule.QUOTA,
  268. f"批量修改定额失败: {str(e)}",
  269. utils.to_str({"ids": ids, "quota_code": quota_code}),
  270. )
  271. return msg
  272. def delete_quota(self, quota_id: int) -> bool:
  273. """删除项目定额
  274. Args:
  275. quota_id: 定额ID
  276. Returns:
  277. bool: 删除是否成功
  278. """
  279. try:
  280. flag = self.store.delete_quota(quota_id)
  281. LogRecordHelper.log_success(
  282. OperationType.DELETE,
  283. OperationModule.QUOTA,
  284. f"删除定额条目: {quota_id}",
  285. f"{quota_id}",
  286. )
  287. return flag
  288. except Exception as e:
  289. self._logger.error(f"删除项目定额失败: {str(e)}")
  290. LogRecordHelper.log_fail(
  291. OperationType.DELETE,
  292. OperationModule.QUOTA,
  293. f"删除定额条目失败: {quota_id}",
  294. f"{quota_id}",
  295. )
  296. raise
  297. def save_change_chapter(self, project_id, ids, new_id):
  298. try:
  299. if not ids:
  300. return "请选择要变更的章节"
  301. chapter = self.chapter_store.get_chapter_dto(project_id, new_id)
  302. if not chapter:
  303. return "章节不存在"
  304. ids_arr = ids.split(",")
  305. err = ""
  306. for id_str in ids_arr:
  307. try:
  308. self.store.update_quota_chapter(
  309. int(id_str), chapter.item_id, chapter.item_code
  310. )
  311. except Exception as e:
  312. err += str(e) + ";"
  313. continue
  314. if err:
  315. LogRecordHelper.log_fail(
  316. OperationType.UPDATE,
  317. OperationModule.QUOTA,
  318. f"修改定额条目章节失败: {err}",
  319. ids,
  320. )
  321. return err
  322. LogRecordHelper.log_success(
  323. OperationType.UPDATE,
  324. OperationModule.QUOTA,
  325. f"修改定额条目章节",
  326. ids,
  327. )
  328. return None
  329. except Exception as e:
  330. msg = f"章节变更失败: {str(e)}"
  331. self._logger.error(msg)
  332. LogRecordHelper.log_fail(
  333. OperationType.UPDATE,
  334. OperationModule.QUOTA,
  335. f"修改定额条目章节失败: {str(e)}",
  336. ids,
  337. )
  338. return msg
  339. def update_send_status(self, quota_id: int, status: int, err: str = None) -> bool:
  340. """更新推送状态
  341. Args:
  342. quota_id: 定额ID
  343. status: 状态值
  344. err: 错误信息
  345. Returns:
  346. bool: 更新是否成功
  347. """
  348. try:
  349. return self.store.update_send_status(quota_id, status, err)
  350. except Exception as e:
  351. self._logger.error(f"更新项目定额推送状态失败: {str(e)}")
  352. raise
  353. # def start_process(self, quota_id: int) -> Optional[str]:
  354. # """启动处理"""
  355. # quota = self.get_quota_dto(quota_id)
  356. # if quota:
  357. # self.update_process_status(quota_id, 1)
  358. # thread = threading.Thread(target=self._process_quota, args=(quota,))
  359. # thread.start()
  360. # else:
  361. # return "定额条目没有查询到"
  362. #
  363. # def _process_quota(self, quota: ProjectQuotaDto):
  364. # try:
  365. # msg = executor.process_quota(quota)
  366. # if not msg:
  367. # self.start_send(quota.id)
  368. # except Exception as e:
  369. # self._logger.error(f"处理定额条目失败: {str(e)}")
  370. # self.update_process_status(quota.id, 3, str(e))
  371. # raise
  372. def start_send_by_ids(self, ids: str, is_cover: bool = False):
  373. try:
  374. id_list = ids.split(",")
  375. err = ""
  376. self._quota_sequence_num = {}
  377. for _id in id_list:
  378. msg = self.start_send(int(_id), is_cover, False)
  379. if msg:
  380. err += f"{msg}[{_id}],"
  381. if err:
  382. LogRecordHelper.log_fail(
  383. OperationType.SEND,
  384. OperationModule.QUOTA,
  385. f"批量推送定额条目,部分失败: {err}",
  386. ids,
  387. )
  388. LogRecordHelper.log_success(
  389. OperationType.SEND,
  390. OperationModule.QUOTA,
  391. f"批量推送定额条目",
  392. ids,
  393. )
  394. return err
  395. except Exception as e:
  396. LogRecordHelper.log_fail(
  397. OperationType.SEND,
  398. OperationModule.QUOTA,
  399. f"批量推送定额条目失败: {str(e)}",
  400. ids,
  401. )
  402. self._logger.error(f"批量启动定额条目推送失败: {str(e)}")
  403. raise
  404. def start_send_by_task_id(self, task_id):
  405. try:
  406. quota_list = self.store.get_quotas_by_task_id(task_id)
  407. err = ""
  408. self._quota_sequence_num = {}
  409. for quota in quota_list:
  410. msg = self.start_send(
  411. quota.id, True if quota.quota_id else False, False
  412. )
  413. if msg:
  414. err += f"{msg}[{quota.id}],"
  415. if err:
  416. LogRecordHelper.log_fail(
  417. OperationType.SEND,
  418. OperationModule.QUOTA,
  419. f"推送全部定额条目失败",
  420. f"任务ID:{task_id}",
  421. )
  422. LogRecordHelper.log_success(
  423. OperationType.SEND,
  424. OperationModule.QUOTA,
  425. f"推送全部定额条目",
  426. f"任务ID:{task_id}",
  427. )
  428. return err
  429. except Exception as e:
  430. LogRecordHelper.log_fail(
  431. OperationType.SEND,
  432. OperationModule.QUOTA,
  433. f"推送全部定额条目失败: {str(e)}",
  434. f"任务ID:{task_id}",
  435. )
  436. self._logger.error(f"推送全部定额条目失败: {str(e)}")
  437. raise
  438. def start_send(
  439. self,
  440. _id: int,
  441. is_cover: bool = False,
  442. is_log: bool = True,
  443. clear_num: bool = False,
  444. ) -> Optional[str]:
  445. """启动推送"""
  446. if clear_num:
  447. self._quota_sequence_num = {}
  448. quota = self.get_quota_dto(_id)
  449. if quota:
  450. self.update_send_status(_id, SendStatusEnum.SUCCESS.value)
  451. if not is_cover:
  452. quota.quota_id = 0
  453. quota.sequence_number = (
  454. self._get_quota_sequence_num(
  455. quota.project_id, quota.budget_id, quota.item_id
  456. )
  457. if not is_cover or quota.is_change == 2
  458. else quota.sequence_number
  459. )
  460. # thread = threading.Thread(target=self._send_quota, args=(quota,))
  461. # thread.start()
  462. if is_log:
  463. LogRecordHelper.log_success(
  464. OperationType.SEND,
  465. OperationModule.QUOTA,
  466. f"推送定额条目",
  467. f"ID:{_id},是否覆盖:{is_cover}",
  468. )
  469. if self._send_quota(quota):
  470. return None
  471. else:
  472. return f"{_id}推送失败;"
  473. else:
  474. return "定额条目没有查询到"
  475. def _get_quota_sequence_num(self, project_id, budget_id, item_id):
  476. num = self._quota_sequence_num.get(f"{project_id}_{budget_id}_{item_id}")
  477. if num:
  478. num += 1
  479. else:
  480. num = self.quota_input_store.query_quota_sequence_number(
  481. project_id, budget_id, item_id
  482. )
  483. self._quota_sequence_num[f"{project_id}_{budget_id}_{item_id}"] = num
  484. return num
  485. def _send_quota(self, quota: ProjectQuotaDto):
  486. try:
  487. executor.send_quota(quota)
  488. return True
  489. except Exception as e:
  490. self._logger.error(f"推送定额条目失败: {str(e)}")
  491. self.update_send_status(quota.id, 3, str(e))
  492. return False