work_record.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  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('supplier_id', '按供应商ID筛选,使用 "none" 筛选无供应商的记录', type=str)
  154. @work_record_ns.param('page', '页码,从1开始', type=int, default=1)
  155. @work_record_ns.param('page_size', '每页数量,默认20,最大100', type=int, default=20)
  156. @work_record_ns.response(200, 'Success', paginated_response)
  157. @require_auth
  158. def get(self):
  159. """获取工作记录列表(支持筛选和分页)"""
  160. person_id = request.args.get('person_id', type=int)
  161. date = request.args.get('date')
  162. year = request.args.get('year', type=int)
  163. month = request.args.get('month', type=int)
  164. start_date = request.args.get('start_date')
  165. end_date = request.args.get('end_date')
  166. page = request.args.get('page', 1, type=int)
  167. page_size = request.args.get('page_size', 20, type=int)
  168. # Parse is_settled parameter
  169. is_settled_param = request.args.get('is_settled')
  170. is_settled = None
  171. if is_settled_param is not None:
  172. is_settled = is_settled_param.lower() == 'true'
  173. # Parse supplier_id parameter
  174. supplier_id_param = request.args.get('supplier_id')
  175. supplier_id = None
  176. if supplier_id_param is not None:
  177. if supplier_id_param.lower() == 'none':
  178. supplier_id = 'none'
  179. else:
  180. try:
  181. supplier_id = int(supplier_id_param)
  182. except (ValueError, TypeError):
  183. pass
  184. # 如果指定了具体日期,使用 start_date 和 end_date 来筛选同一天
  185. if date:
  186. start_date = date
  187. end_date = date
  188. result = WorkRecordService.get_all(
  189. person_id=person_id,
  190. start_date=start_date,
  191. end_date=end_date,
  192. year=year,
  193. month=month,
  194. is_settled=is_settled,
  195. supplier_id=supplier_id,
  196. page=page,
  197. page_size=page_size
  198. )
  199. return {
  200. 'success': True,
  201. 'data': result['data'],
  202. 'pagination': {
  203. 'total': result['total'],
  204. 'page': result['page'],
  205. 'page_size': result['page_size'],
  206. 'total_pages': result['total_pages']
  207. },
  208. 'message': 'Work records retrieved successfully'
  209. }, 200
  210. @work_record_ns.route('/<int:id>')
  211. @work_record_ns.param('id', '记录ID')
  212. class WorkRecordDetail(Resource):
  213. """Resource for getting a single work record."""
  214. @work_record_ns.doc('get_work_record')
  215. @work_record_ns.response(200, 'Success', success_response)
  216. @work_record_ns.response(404, 'Work record not found', error_response)
  217. @require_auth
  218. def get(self, id):
  219. """根据ID获取工作记录"""
  220. work_record, error = WorkRecordService.get_by_id(id)
  221. if error:
  222. return {
  223. 'success': False,
  224. 'error': error,
  225. 'code': 'NOT_FOUND'
  226. }, 404
  227. return {
  228. 'success': True,
  229. 'data': work_record,
  230. 'message': 'Work record retrieved successfully'
  231. }, 200
  232. @work_record_ns.route('/create')
  233. class WorkRecordCreate(Resource):
  234. """Resource for creating a work record."""
  235. @work_record_ns.doc('create_work_record')
  236. @work_record_ns.expect(work_record_input)
  237. @work_record_ns.response(200, 'Success', success_response)
  238. @work_record_ns.response(400, 'Validation error', error_response)
  239. @work_record_ns.response(404, 'Reference not found', error_response)
  240. @require_auth
  241. def post(self):
  242. """创建新工作记录"""
  243. data = work_record_ns.payload
  244. person_id = data.get('person_id')
  245. item_id = data.get('item_id')
  246. work_date = data.get('work_date')
  247. quantity = data.get('quantity')
  248. work_record, error = WorkRecordService.create(
  249. person_id=person_id,
  250. item_id=item_id,
  251. work_date=work_date,
  252. quantity=quantity
  253. )
  254. if error:
  255. if 'not found' in error.lower():
  256. return {
  257. 'success': False,
  258. 'error': error,
  259. 'code': 'REFERENCE_ERROR'
  260. }, 404
  261. return {
  262. 'success': False,
  263. 'error': error,
  264. 'code': 'VALIDATION_ERROR'
  265. }, 400
  266. return {
  267. 'success': True,
  268. 'data': work_record,
  269. 'message': 'Work record created successfully'
  270. }, 200
  271. @work_record_ns.route('/update')
  272. class WorkRecordUpdate(Resource):
  273. """Resource for updating a work record."""
  274. @work_record_ns.doc('update_work_record')
  275. @work_record_ns.expect(work_record_update)
  276. @work_record_ns.response(200, 'Success', success_response)
  277. @work_record_ns.response(400, 'Validation error', error_response)
  278. @work_record_ns.response(404, 'Not found', error_response)
  279. @require_auth
  280. def post(self):
  281. """更新工作记录"""
  282. data = work_record_ns.payload
  283. work_record_id = data.get('id')
  284. if not work_record_id:
  285. return {
  286. 'success': False,
  287. 'error': 'Work record ID is required',
  288. 'code': 'VALIDATION_ERROR'
  289. }, 400
  290. work_record, error = WorkRecordService.update(
  291. work_record_id=work_record_id,
  292. person_id=data.get('person_id'),
  293. item_id=data.get('item_id'),
  294. work_date=data.get('work_date'),
  295. quantity=data.get('quantity')
  296. )
  297. if error:
  298. if 'not found' in error.lower():
  299. return {
  300. 'success': False,
  301. 'error': error,
  302. 'code': 'NOT_FOUND'
  303. }, 404
  304. return {
  305. 'success': False,
  306. 'error': error,
  307. 'code': 'VALIDATION_ERROR'
  308. }, 400
  309. return {
  310. 'success': True,
  311. 'data': work_record,
  312. 'message': 'Work record updated successfully'
  313. }, 200
  314. @work_record_ns.route('/delete')
  315. class WorkRecordDelete(Resource):
  316. """Resource for deleting a work record."""
  317. @work_record_ns.doc('delete_work_record')
  318. @work_record_ns.expect(work_record_delete)
  319. @work_record_ns.response(200, 'Success', success_response)
  320. @work_record_ns.response(404, 'Work record not found', error_response)
  321. @require_auth
  322. def post(self):
  323. """删除工作记录"""
  324. data = work_record_ns.payload
  325. work_record_id = data.get('id')
  326. if not work_record_id:
  327. return {
  328. 'success': False,
  329. 'error': 'Work record ID is required',
  330. 'code': 'VALIDATION_ERROR'
  331. }, 400
  332. success, error = WorkRecordService.delete(work_record_id)
  333. if error:
  334. return {
  335. 'success': False,
  336. 'error': error,
  337. 'code': 'NOT_FOUND'
  338. }, 404
  339. return {
  340. 'success': True,
  341. 'data': None,
  342. 'message': 'Work record deleted successfully'
  343. }, 200
  344. @work_record_ns.route('/daily-summary')
  345. class WorkRecordDailySummary(Resource):
  346. """Resource for getting daily summary."""
  347. @work_record_ns.doc('get_daily_summary')
  348. @work_record_ns.param('date', '日期 (YYYY-MM-DD)', required=True, type=str)
  349. @work_record_ns.param('person_id', '按人员ID筛选', type=int)
  350. @work_record_ns.response(200, 'Success')
  351. @work_record_ns.response(400, 'Validation error', error_response)
  352. @require_auth
  353. def get(self):
  354. """获取每日工作汇总"""
  355. work_date = request.args.get('date')
  356. person_id = request.args.get('person_id', type=int)
  357. if not work_date:
  358. return {
  359. 'success': False,
  360. 'error': 'Date parameter is required',
  361. 'code': 'VALIDATION_ERROR'
  362. }, 400
  363. try:
  364. summary = WorkRecordService.get_daily_summary(
  365. work_date=work_date,
  366. person_id=person_id
  367. )
  368. return {
  369. 'success': True,
  370. 'data': summary,
  371. 'message': 'Daily summary retrieved successfully'
  372. }, 200
  373. except ValueError as e:
  374. return {
  375. 'success': False,
  376. 'error': str(e),
  377. 'code': 'VALIDATION_ERROR'
  378. }, 400
  379. @work_record_ns.route('/monthly-summary')
  380. class WorkRecordMonthlySummary(Resource):
  381. """Resource for getting monthly summary."""
  382. @work_record_ns.doc('get_monthly_summary')
  383. @work_record_ns.param('year', '年份 (如 2024)', required=True, type=int)
  384. @work_record_ns.param('month', '月份 (1-12)', required=True, type=int)
  385. @work_record_ns.response(200, 'Success')
  386. @work_record_ns.response(400, 'Validation error', error_response)
  387. @require_auth
  388. def get(self):
  389. """获取月度工作汇总"""
  390. year = request.args.get('year', type=int)
  391. month = request.args.get('month', type=int)
  392. if not year or not month:
  393. return {
  394. 'success': False,
  395. 'error': 'Year and month parameters are required',
  396. 'code': 'VALIDATION_ERROR'
  397. }, 400
  398. if month < 1 or month > 12:
  399. return {
  400. 'success': False,
  401. 'error': 'Month must be between 1 and 12',
  402. 'code': 'VALIDATION_ERROR'
  403. }, 400
  404. try:
  405. summary = WorkRecordService.get_monthly_summary(
  406. year=year,
  407. month=month
  408. )
  409. return {
  410. 'success': True,
  411. 'data': summary,
  412. 'message': 'Monthly summary retrieved successfully'
  413. }, 200
  414. except ValueError as e:
  415. return {
  416. 'success': False,
  417. 'error': str(e),
  418. 'code': 'VALIDATION_ERROR'
  419. }, 400
  420. @work_record_ns.route('/yearly-summary')
  421. class WorkRecordYearlySummary(Resource):
  422. """Resource for getting yearly summary with monthly breakdown."""
  423. @work_record_ns.doc('get_yearly_summary')
  424. @work_record_ns.param('year', '年份 (如 2024)', required=True, type=int)
  425. @work_record_ns.response(200, 'Success')
  426. @work_record_ns.response(400, 'Validation error', error_response)
  427. @require_auth
  428. def get(self):
  429. """获取年度工作汇总(按月份分解)"""
  430. year = request.args.get('year', type=int)
  431. if year is None:
  432. return {
  433. 'success': False,
  434. 'error': '缺少年份参数',
  435. 'code': 'VALIDATION_ERROR'
  436. }, 400
  437. if not isinstance(year, int) or year < 1900 or year > 9999:
  438. return {
  439. 'success': False,
  440. 'error': '年份无效,必须在 1900 到 9999 之间',
  441. 'code': 'VALIDATION_ERROR'
  442. }, 400
  443. try:
  444. summary = WorkRecordService.get_yearly_summary(year=year)
  445. return {
  446. 'success': True,
  447. 'data': summary,
  448. 'message': 'Yearly summary retrieved successfully'
  449. }, 200
  450. except Exception as e:
  451. return {
  452. 'success': False,
  453. 'error': str(e),
  454. 'code': 'INTERNAL_ERROR'
  455. }, 500
  456. @work_record_ns.route('/<int:id>/settlement')
  457. @work_record_ns.param('id', '记录ID')
  458. class WorkRecordSettlement(Resource):
  459. """Resource for toggling settlement status of a single work record."""
  460. @work_record_ns.doc('toggle_settlement')
  461. @work_record_ns.response(200, 'Success', success_response)
  462. @work_record_ns.response(404, 'Work record not found', error_response)
  463. @require_auth
  464. def put(self, id):
  465. """切换单条工作记录的结算状态"""
  466. work_record, error = WorkRecordService.toggle_settlement(id)
  467. if error:
  468. return {
  469. 'success': False,
  470. 'error': error,
  471. 'code': 'NOT_FOUND'
  472. }, 404
  473. return {
  474. 'success': True,
  475. 'data': work_record,
  476. 'message': 'Settlement status toggled successfully'
  477. }, 200
  478. @work_record_ns.route('/batch-settlement')
  479. class WorkRecordBatchSettlement(Resource):
  480. """Resource for batch updating settlement status."""
  481. @work_record_ns.doc('batch_settlement')
  482. @work_record_ns.expect(batch_settlement_input)
  483. @work_record_ns.response(200, 'Success', success_response)
  484. @work_record_ns.response(400, 'Validation error', error_response)
  485. @require_auth
  486. def post(self):
  487. """批量更新工作记录的结算状态"""
  488. data = work_record_ns.payload
  489. year = data.get('year')
  490. month = data.get('month')
  491. is_settled = data.get('is_settled')
  492. person_id = data.get('person_id')
  493. supplier_id = data.get('supplier_id')
  494. # Handle "none" string for items without supplier
  495. if supplier_id == 'none':
  496. pass # Keep as string "none"
  497. elif supplier_id is not None:
  498. # Convert to int if it's a valid number
  499. try:
  500. supplier_id = int(supplier_id)
  501. except (ValueError, TypeError):
  502. supplier_id = None
  503. # Validate required fields
  504. if year is None or month is None:
  505. return {
  506. 'success': False,
  507. 'error': '年份和月份为必填参数',
  508. 'code': 'VALIDATION_ERROR'
  509. }, 400
  510. if is_settled is None:
  511. return {
  512. 'success': False,
  513. 'error': '结算状态为必填参数',
  514. 'code': 'VALIDATION_ERROR'
  515. }, 400
  516. if month < 1 or month > 12:
  517. return {
  518. 'success': False,
  519. 'error': '月份必须在 1 到 12 之间',
  520. 'code': 'VALIDATION_ERROR'
  521. }, 400
  522. result, error = WorkRecordService.batch_update_settlement(
  523. year=year,
  524. month=month,
  525. is_settled=is_settled,
  526. person_id=person_id,
  527. supplier_id=supplier_id
  528. )
  529. if error:
  530. return {
  531. 'success': False,
  532. 'error': error,
  533. 'code': 'VALIDATION_ERROR'
  534. }, 400
  535. return {
  536. 'success': True,
  537. 'data': result,
  538. 'message': f'成功更新 {result["updated_count"]} 条记录的结算状态'
  539. }, 200