test_export.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. """Tests for Export API endpoints."""
  2. import pytest
  3. from datetime import date
  4. from io import BytesIO
  5. from openpyxl import load_workbook
  6. from flask_bcrypt import Bcrypt
  7. from app import create_app, db
  8. from app.models.person import Person
  9. from app.models.item import Item
  10. from app.models.work_record import WorkRecord
  11. from app.models.admin import Admin
  12. from app.services.auth_service import AuthService
  13. bcrypt = Bcrypt()
  14. @pytest.fixture
  15. def app():
  16. """Create application for testing."""
  17. app = create_app('testing')
  18. with app.app_context():
  19. db.create_all()
  20. yield app
  21. db.session.remove()
  22. db.drop_all()
  23. @pytest.fixture
  24. def client(app):
  25. """Create test client."""
  26. return app.test_client()
  27. @pytest.fixture
  28. def auth_headers(app):
  29. """Create auth headers for testing."""
  30. with app.app_context():
  31. password_hash = bcrypt.generate_password_hash('testpassword').decode('utf-8')
  32. admin = Admin(username='exporttestadmin', password_hash=password_hash)
  33. db.session.add(admin)
  34. db.session.commit()
  35. token = AuthService.generate_token(admin)
  36. return {'Authorization': f'Bearer {token}'}
  37. @pytest.fixture
  38. def sample_data(app, auth_headers):
  39. """Create sample data for export tests."""
  40. with app.app_context():
  41. # Create persons
  42. person1 = Person(name='张三')
  43. person2 = Person(name='李四')
  44. db.session.add_all([person1, person2])
  45. db.session.commit()
  46. # Create items
  47. item1 = Item(name='物品A', unit_price=10.50)
  48. item2 = Item(name='物品B', unit_price=20.75)
  49. db.session.add_all([item1, item2])
  50. db.session.commit()
  51. # Create work records for January 2024
  52. records = [
  53. WorkRecord(person_id=person1.id, item_id=item1.id, work_date=date(2024, 1, 5), quantity=5),
  54. WorkRecord(person_id=person1.id, item_id=item2.id, work_date=date(2024, 1, 10), quantity=3),
  55. WorkRecord(person_id=person2.id, item_id=item1.id, work_date=date(2024, 1, 15), quantity=8),
  56. # February 2024
  57. WorkRecord(person_id=person1.id, item_id=item1.id, work_date=date(2024, 2, 5), quantity=4),
  58. WorkRecord(person_id=person2.id, item_id=item2.id, work_date=date(2024, 2, 10), quantity=6),
  59. ]
  60. db.session.add_all(records)
  61. db.session.commit()
  62. return {
  63. 'person1_id': person1.id,
  64. 'person2_id': person2.id,
  65. 'item1_id': item1.id,
  66. 'item2_id': item2.id
  67. }
  68. class TestExportAPI:
  69. """Test cases for Export API endpoints."""
  70. def test_monthly_export_success(self, client, sample_data, auth_headers):
  71. """Test successful monthly export."""
  72. response = client.get('/api/export/monthly?year=2024&month=1', headers=auth_headers)
  73. assert response.status_code == 200
  74. assert response.content_type == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  75. # Verify Excel content
  76. wb = load_workbook(BytesIO(response.data))
  77. assert len(wb.sheetnames) == 2
  78. assert '2024年1月明细' in wb.sheetnames
  79. assert '月度汇总' in wb.sheetnames
  80. def test_monthly_export_detail_sheet(self, client, sample_data, auth_headers):
  81. """Test monthly export detail sheet content."""
  82. response = client.get('/api/export/monthly?year=2024&month=1', headers=auth_headers)
  83. wb = load_workbook(BytesIO(response.data))
  84. detail_sheet = wb['2024年1月明细']
  85. # Check headers (now includes supplier and settlement status columns)
  86. headers = [cell.value for cell in detail_sheet[1]]
  87. assert headers == ['人员', '日期', '供应商', '物品', '单价', '数量', '总价', '结算状态']
  88. # Check data rows (3 records in January)
  89. data_rows = list(detail_sheet.iter_rows(min_row=2, values_only=True))
  90. assert len(data_rows) == 3
  91. # Check settlement status column values
  92. for row in data_rows:
  93. assert row[7] in ['已结算', '未结算']
  94. def test_monthly_export_summary_sheet(self, client, sample_data, auth_headers):
  95. """Test monthly export summary sheet content."""
  96. response = client.get('/api/export/monthly?year=2024&month=1', headers=auth_headers)
  97. wb = load_workbook(BytesIO(response.data))
  98. summary_sheet = wb['月度汇总']
  99. # Check headers (now includes supplier and settlement status columns)
  100. headers = [cell.value for cell in summary_sheet[1]]
  101. assert headers == ['人员', '供应商', '总金额', '结算状态']
  102. # Check that summary has person+supplier+settlement rows plus total rows (合计, 已结算, 未结算)
  103. data_rows = list(summary_sheet.iter_rows(min_row=2, values_only=True))
  104. assert len(data_rows) >= 5 # At least 2 persons + 3 total rows (合计, 已结算, 未结算)
  105. # Last 3 rows should be total rows: 合计, 已结算, 未结算
  106. assert data_rows[-3][0] == '合计'
  107. assert data_rows[-2][0] == '已结算'
  108. assert data_rows[-1][0] == '未结算'
  109. # Check settlement status column values (except total rows)
  110. for row in data_rows[:-3]:
  111. assert row[3] in ['已结算', '未结算']
  112. def test_monthly_export_missing_year(self, client, sample_data, auth_headers):
  113. """Test monthly export with missing year parameter."""
  114. response = client.get('/api/export/monthly?month=1', headers=auth_headers)
  115. assert response.status_code == 400
  116. data = response.get_json()
  117. assert data['success'] is False
  118. assert 'Year' in data['error']
  119. def test_monthly_export_missing_month(self, client, sample_data, auth_headers):
  120. """Test monthly export with missing month parameter."""
  121. response = client.get('/api/export/monthly?year=2024', headers=auth_headers)
  122. assert response.status_code == 400
  123. data = response.get_json()
  124. assert data['success'] is False
  125. assert 'Month' in data['error']
  126. def test_monthly_export_invalid_month(self, client, sample_data, auth_headers):
  127. """Test monthly export with invalid month."""
  128. response = client.get('/api/export/monthly?year=2024&month=13', headers=auth_headers)
  129. assert response.status_code == 400
  130. data = response.get_json()
  131. assert data['success'] is False
  132. def test_monthly_export_empty_month(self, client, sample_data, auth_headers):
  133. """Test monthly export for month with no records."""
  134. response = client.get('/api/export/monthly?year=2024&month=12', headers=auth_headers)
  135. assert response.status_code == 200
  136. wb = load_workbook(BytesIO(response.data))
  137. detail_sheet = wb.active
  138. # Should have headers but no data rows
  139. data_rows = list(detail_sheet.iter_rows(min_row=2, values_only=True))
  140. assert len(data_rows) == 0
  141. def test_yearly_export_success(self, client, sample_data, auth_headers):
  142. """Test successful yearly export."""
  143. response = client.get('/api/export/yearly?year=2024', headers=auth_headers)
  144. assert response.status_code == 200
  145. assert response.content_type == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  146. # Verify Excel content
  147. wb = load_workbook(BytesIO(response.data))
  148. assert len(wb.sheetnames) == 2
  149. assert '2024年明细' in wb.sheetnames
  150. assert '年度汇总' in wb.sheetnames
  151. def test_yearly_export_detail_sheet(self, client, sample_data, auth_headers):
  152. """Test yearly export detail sheet content."""
  153. response = client.get('/api/export/yearly?year=2024', headers=auth_headers)
  154. wb = load_workbook(BytesIO(response.data))
  155. detail_sheet = wb['2024年明细']
  156. # Check headers (now includes supplier and settlement status columns)
  157. headers = [cell.value for cell in detail_sheet[1]]
  158. assert headers == ['人员', '日期', '供应商', '物品', '单价', '数量', '总价', '结算状态']
  159. # Check data rows (5 records total in 2024)
  160. data_rows = list(detail_sheet.iter_rows(min_row=2, values_only=True))
  161. assert len(data_rows) == 5
  162. # Check settlement status column values
  163. for row in data_rows:
  164. assert row[7] in ['已结算', '未结算']
  165. def test_yearly_export_summary_sheet(self, client, sample_data, auth_headers):
  166. """Test yearly export summary sheet with monthly breakdown."""
  167. response = client.get('/api/export/yearly?year=2024', headers=auth_headers)
  168. wb = load_workbook(BytesIO(response.data))
  169. summary_sheet = wb['年度汇总']
  170. # Check headers: 人员, 1月-12月, 年度合计, 已结算, 未结算
  171. headers = [cell.value for cell in summary_sheet[1]]
  172. assert headers[0] == '人员'
  173. assert headers[1] == '1月'
  174. assert headers[12] == '12月'
  175. assert headers[13] == '年度合计'
  176. assert headers[14] == '已结算'
  177. assert headers[15] == '未结算'
  178. # Check that summary has person rows plus total row
  179. data_rows = list(summary_sheet.iter_rows(min_row=2, values_only=True))
  180. assert len(data_rows) >= 2 # At least 2 persons
  181. # Last row should be total
  182. last_row = data_rows[-1]
  183. assert last_row[0] == '合计'
  184. # Check that settled + unsettled = yearly total for each person row
  185. for row in data_rows:
  186. yearly_total = row[13]
  187. settled = row[14]
  188. unsettled = row[15]
  189. if yearly_total is not None and settled is not None and unsettled is not None:
  190. assert abs(yearly_total - (settled + unsettled)) < 0.01
  191. def test_yearly_export_missing_year(self, client, sample_data, auth_headers):
  192. """Test yearly export with missing year parameter."""
  193. response = client.get('/api/export/yearly', headers=auth_headers)
  194. assert response.status_code == 400
  195. data = response.get_json()
  196. assert data['success'] is False
  197. assert 'Year' in data['error']
  198. def test_yearly_export_empty_year(self, client, sample_data, auth_headers):
  199. """Test yearly export for year with no records."""
  200. response = client.get('/api/export/yearly?year=2020', headers=auth_headers)
  201. assert response.status_code == 200
  202. wb = load_workbook(BytesIO(response.data))
  203. detail_sheet = wb.active
  204. # Should have headers but no data rows
  205. data_rows = list(detail_sheet.iter_rows(min_row=2, values_only=True))
  206. assert len(data_rows) == 0
  207. def test_export_total_price_calculation(self, client, sample_data, auth_headers):
  208. """Test that total_price is correctly calculated in export."""
  209. response = client.get('/api/export/monthly?year=2024&month=1', headers=auth_headers)
  210. wb = load_workbook(BytesIO(response.data))
  211. detail_sheet = wb['2024年1月明细']
  212. # Check each row's total_price = unit_price * quantity
  213. # Column indices: 0=人员, 1=日期, 2=供应商, 3=物品, 4=单价, 5=数量, 6=总价
  214. for row in detail_sheet.iter_rows(min_row=2, values_only=True):
  215. if row[0] is not None: # Skip empty rows
  216. unit_price = row[4]
  217. quantity = row[5]
  218. total_price = row[6]
  219. assert abs(total_price - (unit_price * quantity)) < 0.01
  220. def test_unauthorized_access(self, client, app):
  221. """Test that export endpoints require authentication."""
  222. with app.app_context():
  223. db.create_all()
  224. response = client.get('/api/export/monthly?year=2024&month=1')
  225. assert response.status_code == 401
  226. data = response.get_json()
  227. assert data['success'] is False
  228. assert data['code'] == 'UNAUTHORIZED'