work_record.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. """Work Record API routes."""
  2. from flask import request
  3. from flask_restx import Namespace, Resource, fields
  4. from app.services.work_record_service import WorkRecordService
  5. from app.utils.auth_decorator import require_auth
  6. work_record_ns = Namespace('work-records', description='工作记录管理接口')
  7. # API models for Swagger documentation
  8. work_record_model = work_record_ns.model('WorkRecord', {
  9. 'id': fields.Integer(readonly=True, description='记录ID'),
  10. 'person_id': fields.Integer(required=True, description='人员ID'),
  11. 'person_name': fields.String(readonly=True, description='人员姓名'),
  12. 'item_id': fields.Integer(required=True, description='物品ID'),
  13. 'item_name': fields.String(readonly=True, description='物品名称'),
  14. 'unit_price': fields.Float(readonly=True, description='单价'),
  15. 'supplier_id': fields.Integer(readonly=True, description='供应商ID'),
  16. 'supplier_name': fields.String(readonly=True, description='供应商名称'),
  17. 'work_date': fields.String(required=True, description='工作日期 (YYYY-MM-DD)'),
  18. 'quantity': fields.Integer(required=True, description='数量'),
  19. 'total_price': fields.Float(readonly=True, description='总价'),
  20. 'is_settled': fields.Boolean(readonly=True, description='结算状态'),
  21. 'created_at': fields.String(readonly=True, description='创建时间'),
  22. 'updated_at': fields.String(readonly=True, description='更新时间')
  23. })
  24. work_record_input = work_record_ns.model('WorkRecordInput', {
  25. 'person_id': fields.Integer(required=True, description='人员ID'),
  26. 'item_id': fields.Integer(required=True, description='物品ID'),
  27. 'work_date': fields.String(required=True, description='工作日期 (YYYY-MM-DD)'),
  28. 'quantity': fields.Integer(required=True, description='数量')
  29. })
  30. work_record_update = work_record_ns.model('WorkRecordUpdate', {
  31. 'id': fields.Integer(required=True, description='记录ID'),
  32. 'person_id': fields.Integer(description='人员ID'),
  33. 'item_id': fields.Integer(description='物品ID'),
  34. 'work_date': fields.String(description='工作日期 (YYYY-MM-DD)'),
  35. 'quantity': fields.Integer(description='数量')
  36. })
  37. work_record_delete = work_record_ns.model('WorkRecordDelete', {
  38. 'id': fields.Integer(required=True, description='记录ID')
  39. })
  40. # Settlement models
  41. batch_settlement_input = work_record_ns.model('BatchSettlementInput', {
  42. 'person_id': fields.Integer(description='人员ID(可选)'),
  43. 'year': fields.Integer(required=True, description='年份'),
  44. 'month': fields.Integer(required=True, description='月份 (1-12)'),
  45. 'supplier_id': fields.Integer(description='供应商ID(可选)'),
  46. 'is_settled': fields.Boolean(required=True, description='结算状态')
  47. })
  48. batch_settlement_response = work_record_ns.model('BatchSettlementResponse', {
  49. 'updated_count': fields.Integer(description='更新的记录数'),
  50. 'total_matched': fields.Integer(description='匹配的总记录数')
  51. })
  52. # Response models
  53. success_response = work_record_ns.model('SuccessResponse', {
  54. 'success': fields.Boolean(description='操作是否成功'),
  55. 'data': fields.Raw(description='返回数据'),
  56. 'message': fields.String(description='消息')
  57. })
  58. error_response = work_record_ns.model('ErrorResponse', {
  59. 'success': fields.Boolean(description='操作是否成功'),
  60. 'error': fields.String(description='错误信息'),
  61. 'code': fields.String(description='错误代码')
  62. })
  63. # Daily summary models
  64. daily_summary_item = work_record_ns.model('DailySummaryItem', {
  65. 'item_name': fields.String(description='物品名称'),
  66. 'supplier_name': fields.String(description='供应商名称'),
  67. 'unit_price': fields.Float(description='单价'),
  68. 'quantity': fields.Integer(description='数量'),
  69. 'total_price': fields.Float(description='总价')
  70. })
  71. daily_summary_person = work_record_ns.model('DailySummaryPerson', {
  72. 'person_id': fields.Integer(description='人员ID'),
  73. 'person_name': fields.String(description='人员姓名'),
  74. 'total_items': fields.Integer(description='总数量'),
  75. 'total_value': fields.Float(description='总金额'),
  76. 'items': fields.List(fields.Nested(daily_summary_item), description='物品明细')
  77. })
  78. daily_summary_response = work_record_ns.model('DailySummaryResponse', {
  79. 'date': fields.String(description='日期'),
  80. 'summary': fields.List(fields.Nested(daily_summary_person), description='人员汇总'),
  81. 'grand_total_items': fields.Integer(description='总数量'),
  82. 'grand_total_value': fields.Float(description='总金额')
  83. })
  84. # Monthly summary models
  85. monthly_top_performer = work_record_ns.model('MonthlyTopPerformer', {
  86. 'person_id': fields.Integer(description='人员ID'),
  87. 'person_name': fields.String(description='人员姓名'),
  88. 'earnings': fields.Float(description='收入')
  89. })
  90. monthly_item_breakdown = work_record_ns.model('MonthlyItemBreakdown', {
  91. 'item_id': fields.Integer(description='物品ID'),
  92. 'item_name': fields.String(description='物品名称'),
  93. 'supplier_name': fields.String(description='供应商名称'),
  94. 'quantity': fields.Integer(description='数量'),
  95. 'earnings': fields.Float(description='收入')
  96. })
  97. monthly_supplier_breakdown = work_record_ns.model('MonthlySupplierBreakdown', {
  98. 'person_id': fields.Integer(description='人员ID'),
  99. 'person_name': fields.String(description='人员姓名'),
  100. 'supplier_name': fields.String(description='供应商名称'),
  101. 'earnings': fields.Float(description='收入')
  102. })
  103. monthly_summary_response = work_record_ns.model('MonthlySummaryResponse', {
  104. 'year': fields.Integer(description='年份'),
  105. 'month': fields.Integer(description='月份'),
  106. 'total_records': fields.Integer(description='总记录数'),
  107. 'total_earnings': fields.Float(description='总收入'),
  108. 'top_performers': fields.List(fields.Nested(monthly_top_performer), description='业绩排名'),
  109. 'item_breakdown': fields.List(fields.Nested(monthly_item_breakdown), description='物品收入明细'),
  110. 'supplier_breakdown': fields.List(fields.Nested(monthly_supplier_breakdown), description='人员按供应商收入明细')
  111. })
  112. # Yearly summary models
  113. yearly_summary_person = work_record_ns.model('YearlySummaryPerson', {
  114. 'person_id': fields.Integer(description='人员ID'),
  115. 'person_name': fields.String(description='人员姓名'),
  116. 'monthly_earnings': fields.List(fields.Float, description='12个月的收入数组'),
  117. 'yearly_total': fields.Float(description='年度总收入'),
  118. 'settled_total': fields.Float(description='已结算总收入'),
  119. 'unsettled_total': fields.Float(description='未结算总收入')
  120. })
  121. yearly_summary_response = work_record_ns.model('YearlySummaryResponse', {
  122. 'year': fields.Integer(description='年份'),
  123. 'persons': fields.List(fields.Nested(yearly_summary_person), description='人员汇总列表'),
  124. 'monthly_totals': fields.List(fields.Float, description='每月所有人的合计'),
  125. 'grand_total': fields.Float(description='年度总计'),
  126. 'settled_grand_total': fields.Float(description='已结算年度总计'),
  127. 'unsettled_grand_total': fields.Float(description='未结算年度总计')
  128. })
  129. @work_record_ns.route('')
  130. class WorkRecordList(Resource):
  131. """Resource for listing work records with filters."""
  132. @work_record_ns.doc('list_work_records')
  133. @work_record_ns.param('person_id', '按人员ID筛选', type=int)
  134. @work_record_ns.param('date', '按具体日期筛选 (YYYY-MM-DD)', type=str)
  135. @work_record_ns.param('year', '按年份筛选 (如 2024)', type=int)
  136. @work_record_ns.param('month', '按月份筛选 (1-12)', type=int)
  137. @work_record_ns.param('start_date', '开始日期 (YYYY-MM-DD)', type=str)
  138. @work_record_ns.param('end_date', '结束日期 (YYYY-MM-DD)', type=str)
  139. @work_record_ns.param('is_settled', '按结算状态筛选 (true/false)', type=str)
  140. @work_record_ns.response(200, 'Success', success_response)
  141. @require_auth
  142. def get(self):
  143. """获取工作记录列表(支持筛选)"""
  144. person_id = request.args.get('person_id', type=int)
  145. date = request.args.get('date')
  146. year = request.args.get('year', type=int)
  147. month = request.args.get('month', type=int)
  148. start_date = request.args.get('start_date')
  149. end_date = request.args.get('end_date')
  150. # Parse is_settled parameter
  151. is_settled_param = request.args.get('is_settled')
  152. is_settled = None
  153. if is_settled_param is not None:
  154. is_settled = is_settled_param.lower() == 'true'
  155. # 如果指定了具体日期,使用 start_date 和 end_date 来筛选同一天
  156. if date:
  157. start_date = date
  158. end_date = date
  159. work_records = WorkRecordService.get_all(
  160. person_id=person_id,
  161. start_date=start_date,
  162. end_date=end_date,
  163. year=year,
  164. month=month,
  165. is_settled=is_settled
  166. )
  167. return {
  168. 'success': True,
  169. 'data': work_records,
  170. 'message': 'Work records retrieved successfully'
  171. }, 200
  172. @work_record_ns.route('/<int:id>')
  173. @work_record_ns.param('id', '记录ID')
  174. class WorkRecordDetail(Resource):
  175. """Resource for getting a single work record."""
  176. @work_record_ns.doc('get_work_record')
  177. @work_record_ns.response(200, 'Success', success_response)
  178. @work_record_ns.response(404, 'Work record not found', error_response)
  179. @require_auth
  180. def get(self, id):
  181. """根据ID获取工作记录"""
  182. work_record, error = WorkRecordService.get_by_id(id)
  183. if error:
  184. return {
  185. 'success': False,
  186. 'error': error,
  187. 'code': 'NOT_FOUND'
  188. }, 404
  189. return {
  190. 'success': True,
  191. 'data': work_record,
  192. 'message': 'Work record retrieved successfully'
  193. }, 200
  194. @work_record_ns.route('/create')
  195. class WorkRecordCreate(Resource):
  196. """Resource for creating a work record."""
  197. @work_record_ns.doc('create_work_record')
  198. @work_record_ns.expect(work_record_input)
  199. @work_record_ns.response(200, 'Success', success_response)
  200. @work_record_ns.response(400, 'Validation error', error_response)
  201. @work_record_ns.response(404, 'Reference not found', error_response)
  202. @require_auth
  203. def post(self):
  204. """创建新工作记录"""
  205. data = work_record_ns.payload
  206. person_id = data.get('person_id')
  207. item_id = data.get('item_id')
  208. work_date = data.get('work_date')
  209. quantity = data.get('quantity')
  210. work_record, error = WorkRecordService.create(
  211. person_id=person_id,
  212. item_id=item_id,
  213. work_date=work_date,
  214. quantity=quantity
  215. )
  216. if error:
  217. if 'not found' in error.lower():
  218. return {
  219. 'success': False,
  220. 'error': error,
  221. 'code': 'REFERENCE_ERROR'
  222. }, 404
  223. return {
  224. 'success': False,
  225. 'error': error,
  226. 'code': 'VALIDATION_ERROR'
  227. }, 400
  228. return {
  229. 'success': True,
  230. 'data': work_record,
  231. 'message': 'Work record created successfully'
  232. }, 200
  233. @work_record_ns.route('/update')
  234. class WorkRecordUpdate(Resource):
  235. """Resource for updating a work record."""
  236. @work_record_ns.doc('update_work_record')
  237. @work_record_ns.expect(work_record_update)
  238. @work_record_ns.response(200, 'Success', success_response)
  239. @work_record_ns.response(400, 'Validation error', error_response)
  240. @work_record_ns.response(404, 'Not found', error_response)
  241. @require_auth
  242. def post(self):
  243. """更新工作记录"""
  244. data = work_record_ns.payload
  245. work_record_id = data.get('id')
  246. if not work_record_id:
  247. return {
  248. 'success': False,
  249. 'error': 'Work record ID is required',
  250. 'code': 'VALIDATION_ERROR'
  251. }, 400
  252. work_record, error = WorkRecordService.update(
  253. work_record_id=work_record_id,
  254. person_id=data.get('person_id'),
  255. item_id=data.get('item_id'),
  256. work_date=data.get('work_date'),
  257. quantity=data.get('quantity')
  258. )
  259. if error:
  260. if 'not found' in error.lower():
  261. return {
  262. 'success': False,
  263. 'error': error,
  264. 'code': 'NOT_FOUND'
  265. }, 404
  266. return {
  267. 'success': False,
  268. 'error': error,
  269. 'code': 'VALIDATION_ERROR'
  270. }, 400
  271. return {
  272. 'success': True,
  273. 'data': work_record,
  274. 'message': 'Work record updated successfully'
  275. }, 200
  276. @work_record_ns.route('/delete')
  277. class WorkRecordDelete(Resource):
  278. """Resource for deleting a work record."""
  279. @work_record_ns.doc('delete_work_record')
  280. @work_record_ns.expect(work_record_delete)
  281. @work_record_ns.response(200, 'Success', success_response)
  282. @work_record_ns.response(404, 'Work record not found', error_response)
  283. @require_auth
  284. def post(self):
  285. """删除工作记录"""
  286. data = work_record_ns.payload
  287. work_record_id = data.get('id')
  288. if not work_record_id:
  289. return {
  290. 'success': False,
  291. 'error': 'Work record ID is required',
  292. 'code': 'VALIDATION_ERROR'
  293. }, 400
  294. success, error = WorkRecordService.delete(work_record_id)
  295. if error:
  296. return {
  297. 'success': False,
  298. 'error': error,
  299. 'code': 'NOT_FOUND'
  300. }, 404
  301. return {
  302. 'success': True,
  303. 'data': None,
  304. 'message': 'Work record deleted successfully'
  305. }, 200
  306. @work_record_ns.route('/daily-summary')
  307. class WorkRecordDailySummary(Resource):
  308. """Resource for getting daily summary."""
  309. @work_record_ns.doc('get_daily_summary')
  310. @work_record_ns.param('date', '日期 (YYYY-MM-DD)', required=True, type=str)
  311. @work_record_ns.param('person_id', '按人员ID筛选', type=int)
  312. @work_record_ns.response(200, 'Success')
  313. @work_record_ns.response(400, 'Validation error', error_response)
  314. @require_auth
  315. def get(self):
  316. """获取每日工作汇总"""
  317. work_date = request.args.get('date')
  318. person_id = request.args.get('person_id', type=int)
  319. if not work_date:
  320. return {
  321. 'success': False,
  322. 'error': 'Date parameter is required',
  323. 'code': 'VALIDATION_ERROR'
  324. }, 400
  325. try:
  326. summary = WorkRecordService.get_daily_summary(
  327. work_date=work_date,
  328. person_id=person_id
  329. )
  330. return {
  331. 'success': True,
  332. 'data': summary,
  333. 'message': 'Daily summary retrieved successfully'
  334. }, 200
  335. except ValueError as e:
  336. return {
  337. 'success': False,
  338. 'error': str(e),
  339. 'code': 'VALIDATION_ERROR'
  340. }, 400
  341. @work_record_ns.route('/monthly-summary')
  342. class WorkRecordMonthlySummary(Resource):
  343. """Resource for getting monthly summary."""
  344. @work_record_ns.doc('get_monthly_summary')
  345. @work_record_ns.param('year', '年份 (如 2024)', required=True, type=int)
  346. @work_record_ns.param('month', '月份 (1-12)', required=True, type=int)
  347. @work_record_ns.response(200, 'Success')
  348. @work_record_ns.response(400, 'Validation error', error_response)
  349. @require_auth
  350. def get(self):
  351. """获取月度工作汇总"""
  352. year = request.args.get('year', type=int)
  353. month = request.args.get('month', type=int)
  354. if not year or not month:
  355. return {
  356. 'success': False,
  357. 'error': 'Year and month parameters are required',
  358. 'code': 'VALIDATION_ERROR'
  359. }, 400
  360. if month < 1 or month > 12:
  361. return {
  362. 'success': False,
  363. 'error': 'Month must be between 1 and 12',
  364. 'code': 'VALIDATION_ERROR'
  365. }, 400
  366. try:
  367. summary = WorkRecordService.get_monthly_summary(
  368. year=year,
  369. month=month
  370. )
  371. return {
  372. 'success': True,
  373. 'data': summary,
  374. 'message': 'Monthly summary retrieved successfully'
  375. }, 200
  376. except ValueError as e:
  377. return {
  378. 'success': False,
  379. 'error': str(e),
  380. 'code': 'VALIDATION_ERROR'
  381. }, 400
  382. @work_record_ns.route('/yearly-summary')
  383. class WorkRecordYearlySummary(Resource):
  384. """Resource for getting yearly summary with monthly breakdown."""
  385. @work_record_ns.doc('get_yearly_summary')
  386. @work_record_ns.param('year', '年份 (如 2024)', required=True, type=int)
  387. @work_record_ns.response(200, 'Success')
  388. @work_record_ns.response(400, 'Validation error', error_response)
  389. @require_auth
  390. def get(self):
  391. """获取年度工作汇总(按月份分解)"""
  392. year = request.args.get('year', type=int)
  393. if year is None:
  394. return {
  395. 'success': False,
  396. 'error': '缺少年份参数',
  397. 'code': 'VALIDATION_ERROR'
  398. }, 400
  399. if not isinstance(year, int) or year < 1900 or year > 9999:
  400. return {
  401. 'success': False,
  402. 'error': '年份无效,必须在 1900 到 9999 之间',
  403. 'code': 'VALIDATION_ERROR'
  404. }, 400
  405. try:
  406. summary = WorkRecordService.get_yearly_summary(year=year)
  407. return {
  408. 'success': True,
  409. 'data': summary,
  410. 'message': 'Yearly summary retrieved successfully'
  411. }, 200
  412. except Exception as e:
  413. return {
  414. 'success': False,
  415. 'error': str(e),
  416. 'code': 'INTERNAL_ERROR'
  417. }, 500
  418. @work_record_ns.route('/<int:id>/settlement')
  419. @work_record_ns.param('id', '记录ID')
  420. class WorkRecordSettlement(Resource):
  421. """Resource for toggling settlement status of a single work record."""
  422. @work_record_ns.doc('toggle_settlement')
  423. @work_record_ns.response(200, 'Success', success_response)
  424. @work_record_ns.response(404, 'Work record not found', error_response)
  425. @require_auth
  426. def put(self, id):
  427. """切换单条工作记录的结算状态"""
  428. work_record, error = WorkRecordService.toggle_settlement(id)
  429. if error:
  430. return {
  431. 'success': False,
  432. 'error': error,
  433. 'code': 'NOT_FOUND'
  434. }, 404
  435. return {
  436. 'success': True,
  437. 'data': work_record,
  438. 'message': 'Settlement status toggled successfully'
  439. }, 200
  440. @work_record_ns.route('/batch-settlement')
  441. class WorkRecordBatchSettlement(Resource):
  442. """Resource for batch updating settlement status."""
  443. @work_record_ns.doc('batch_settlement')
  444. @work_record_ns.expect(batch_settlement_input)
  445. @work_record_ns.response(200, 'Success', success_response)
  446. @work_record_ns.response(400, 'Validation error', error_response)
  447. @require_auth
  448. def post(self):
  449. """批量更新工作记录的结算状态"""
  450. data = work_record_ns.payload
  451. year = data.get('year')
  452. month = data.get('month')
  453. is_settled = data.get('is_settled')
  454. person_id = data.get('person_id')
  455. supplier_id = data.get('supplier_id')
  456. # Handle "none" string for items without supplier
  457. if supplier_id == 'none':
  458. pass # Keep as string "none"
  459. elif supplier_id is not None:
  460. # Convert to int if it's a valid number
  461. try:
  462. supplier_id = int(supplier_id)
  463. except (ValueError, TypeError):
  464. supplier_id = None
  465. # Validate required fields
  466. if year is None or month is None:
  467. return {
  468. 'success': False,
  469. 'error': '年份和月份为必填参数',
  470. 'code': 'VALIDATION_ERROR'
  471. }, 400
  472. if is_settled is None:
  473. return {
  474. 'success': False,
  475. 'error': '结算状态为必填参数',
  476. 'code': 'VALIDATION_ERROR'
  477. }, 400
  478. if month < 1 or month > 12:
  479. return {
  480. 'success': False,
  481. 'error': '月份必须在 1 到 12 之间',
  482. 'code': 'VALIDATION_ERROR'
  483. }, 400
  484. result, error = WorkRecordService.batch_update_settlement(
  485. year=year,
  486. month=month,
  487. is_settled=is_settled,
  488. person_id=person_id,
  489. supplier_id=supplier_id
  490. )
  491. if error:
  492. return {
  493. 'success': False,
  494. 'error': error,
  495. 'code': 'VALIDATION_ERROR'
  496. }, 400
  497. return {
  498. 'success': True,
  499. 'data': result,
  500. 'message': f'成功更新 {result["updated_count"]} 条记录的结算状态'
  501. }, 200