work_record.py 23 KB

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