test_export.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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 row
  103. data_rows = list(summary_sheet.iter_rows(min_row=2, values_only=True))
  104. assert len(data_rows) >= 2 # At least 2 persons
  105. # Last row should be total
  106. last_row = data_rows[-1]
  107. assert last_row[0] == '合计'
  108. # Check settlement status column values (except total row)
  109. for row in data_rows[:-1]:
  110. assert row[3] in ['已结算', '未结算']
  111. def test_monthly_export_missing_year(self, client, sample_data, auth_headers):
  112. """Test monthly export with missing year parameter."""
  113. response = client.get('/api/export/monthly?month=1', headers=auth_headers)
  114. assert response.status_code == 400
  115. data = response.get_json()
  116. assert data['success'] is False
  117. assert 'Year' in data['error']
  118. def test_monthly_export_missing_month(self, client, sample_data, auth_headers):
  119. """Test monthly export with missing month parameter."""
  120. response = client.get('/api/export/monthly?year=2024', headers=auth_headers)
  121. assert response.status_code == 400
  122. data = response.get_json()
  123. assert data['success'] is False
  124. assert 'Month' in data['error']
  125. def test_monthly_export_invalid_month(self, client, sample_data, auth_headers):
  126. """Test monthly export with invalid month."""
  127. response = client.get('/api/export/monthly?year=2024&month=13', headers=auth_headers)
  128. assert response.status_code == 400
  129. data = response.get_json()
  130. assert data['success'] is False
  131. def test_monthly_export_empty_month(self, client, sample_data, auth_headers):
  132. """Test monthly export for month with no records."""
  133. response = client.get('/api/export/monthly?year=2024&month=12', headers=auth_headers)
  134. assert response.status_code == 200
  135. wb = load_workbook(BytesIO(response.data))
  136. detail_sheet = wb.active
  137. # Should have headers but no data rows
  138. data_rows = list(detail_sheet.iter_rows(min_row=2, values_only=True))
  139. assert len(data_rows) == 0
  140. def test_yearly_export_success(self, client, sample_data, auth_headers):
  141. """Test successful yearly export."""
  142. response = client.get('/api/export/yearly?year=2024', headers=auth_headers)
  143. assert response.status_code == 200
  144. assert response.content_type == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  145. # Verify Excel content
  146. wb = load_workbook(BytesIO(response.data))
  147. assert len(wb.sheetnames) == 2
  148. assert '2024年明细' in wb.sheetnames
  149. assert '年度汇总' in wb.sheetnames
  150. def test_yearly_export_detail_sheet(self, client, sample_data, auth_headers):
  151. """Test yearly export detail sheet content."""
  152. response = client.get('/api/export/yearly?year=2024', headers=auth_headers)
  153. wb = load_workbook(BytesIO(response.data))
  154. detail_sheet = wb['2024年明细']
  155. # Check headers (now includes supplier and settlement status columns)
  156. headers = [cell.value for cell in detail_sheet[1]]
  157. assert headers == ['人员', '日期', '供应商', '物品', '单价', '数量', '总价', '结算状态']
  158. # Check data rows (5 records total in 2024)
  159. data_rows = list(detail_sheet.iter_rows(min_row=2, values_only=True))
  160. assert len(data_rows) == 5
  161. # Check settlement status column values
  162. for row in data_rows:
  163. assert row[7] in ['已结算', '未结算']
  164. def test_yearly_export_summary_sheet(self, client, sample_data, auth_headers):
  165. """Test yearly export summary sheet with monthly breakdown."""
  166. response = client.get('/api/export/yearly?year=2024', headers=auth_headers)
  167. wb = load_workbook(BytesIO(response.data))
  168. summary_sheet = wb['年度汇总']
  169. # Check headers: 人员, 1月-12月, 年度合计, 已结算, 未结算
  170. headers = [cell.value for cell in summary_sheet[1]]
  171. assert headers[0] == '人员'
  172. assert headers[1] == '1月'
  173. assert headers[12] == '12月'
  174. assert headers[13] == '年度合计'
  175. assert headers[14] == '已结算'
  176. assert headers[15] == '未结算'
  177. # Check that summary has person rows plus total row
  178. data_rows = list(summary_sheet.iter_rows(min_row=2, values_only=True))
  179. assert len(data_rows) >= 2 # At least 2 persons
  180. # Last row should be total
  181. last_row = data_rows[-1]
  182. assert last_row[0] == '合计'
  183. # Check that settled + unsettled = yearly total for each person row
  184. for row in data_rows:
  185. yearly_total = row[13]
  186. settled = row[14]
  187. unsettled = row[15]
  188. if yearly_total is not None and settled is not None and unsettled is not None:
  189. assert abs(yearly_total - (settled + unsettled)) < 0.01
  190. def test_yearly_export_missing_year(self, client, sample_data, auth_headers):
  191. """Test yearly export with missing year parameter."""
  192. response = client.get('/api/export/yearly', headers=auth_headers)
  193. assert response.status_code == 400
  194. data = response.get_json()
  195. assert data['success'] is False
  196. assert 'Year' in data['error']
  197. def test_yearly_export_empty_year(self, client, sample_data, auth_headers):
  198. """Test yearly export for year with no records."""
  199. response = client.get('/api/export/yearly?year=2020', headers=auth_headers)
  200. assert response.status_code == 200
  201. wb = load_workbook(BytesIO(response.data))
  202. detail_sheet = wb.active
  203. # Should have headers but no data rows
  204. data_rows = list(detail_sheet.iter_rows(min_row=2, values_only=True))
  205. assert len(data_rows) == 0
  206. def test_export_total_price_calculation(self, client, sample_data, auth_headers):
  207. """Test that total_price is correctly calculated in export."""
  208. response = client.get('/api/export/monthly?year=2024&month=1', headers=auth_headers)
  209. wb = load_workbook(BytesIO(response.data))
  210. detail_sheet = wb['2024年1月明细']
  211. # Check each row's total_price = unit_price * quantity
  212. # Column indices: 0=人员, 1=日期, 2=供应商, 3=物品, 4=单价, 5=数量, 6=总价
  213. for row in detail_sheet.iter_rows(min_row=2, values_only=True):
  214. if row[0] is not None: # Skip empty rows
  215. unit_price = row[4]
  216. quantity = row[5]
  217. total_price = row[6]
  218. assert abs(total_price - (unit_price * quantity)) < 0.01
  219. def test_unauthorized_access(self, client, app):
  220. """Test that export endpoints require authentication."""
  221. with app.app_context():
  222. db.create_all()
  223. response = client.get('/api/export/monthly?year=2024&month=1')
  224. assert response.status_code == 401
  225. data = response.get_json()
  226. assert data['success'] is False
  227. assert data['code'] == 'UNAUTHORIZED'