"""日期报表查询与响应回归,仅读取生成的查询,不连接数据库。""" import unittest from datetime import date from unittest.mock import AsyncMock, patch from common.vo import PageModel from module_business.dao.report_dao import ReportDao from module_business.domain.test_batch_scope import TestBatchScope from module_business.entity.vo.report_vo import ( ConsumptionReportModel, ConsumptionReportQueryModel, PalletInventoryReportModel, PalletInventoryReportQueryModel, PalletTraceReportModel, PalletTraceReportQueryModel, ReturnReportModel, ReturnReportQueryModel, ) from module_business.service.report_service import ReportService from tests.module_business.test_report_service import make_user, report_row from tests.module_business.test_report_tunnel import joins, sql REPORTS = ( ('pallet_inventory', PalletInventoryReportModel, PalletInventoryReportQueryModel, 'active_filling'), ('consumption_report', ConsumptionReportModel, ConsumptionReportQueryModel, 'biz_filling_record'), ('return_report', ReturnReportModel, ReturnReportQueryModel, 'return_filling'), ('pallet_trace', PalletTraceReportModel, PalletTraceReportQueryModel, 'trace_filling'), ) class TestReportDates(unittest.IsolatedAsyncioTestCase): async def statement(self, name, query, batch_id=None): page = PageModel(rows=[], total=0, pageNum=1, pageSize=10, hasNext=False) with patch.object(ReportDao, '_paginate', AsyncMock(return_value=page)) as paginate: await getattr(ReportDao, f'get_{name}_list')( None, query, TestBatchScope(batch_id, 'TEST' if batch_id else 'FORMAL', None, False), ) return paginate.await_args.args[1] async def test_dates_are_selected_from_the_referenced_filling_not_operation_time(self): for name, _, query_type, alias in REPORTS: with self.subTest(report=name): statement = await self.statement(name, query_type()) for field in ('production_date', 'expiry_date'): self.assertIn(field, statement.selected_columns.keys()) self.assertEqual(f'{alias}.{field}', sql(statement.selected_columns[field].element)) if name != 'pallet_inventory': self.assertNotIn('biz_pallet_inventory', sql(statement)) expected_join = { 'pallet_inventory': 'active_filling.filling_id = biz_pallet_inventory.active_filling_id', 'consumption_report': 'biz_filling_record.filling_id = biz_usage_record.active_filling_id', 'return_report': 'return_filling.filling_id = biz_return_item.active_filling_id_snapshot', }.get(name) if expected_join: self.assertIn(expected_join, sql(statement)) async def test_trace_resolves_original_filling_by_event_source_and_same_pallet(self): statement = await self.statement('pallet_trace', PalletTraceReportQueryModel()) self.assertIn('production_date', statement.selected_columns.keys()) query = sql(statement) for source, reference in ( ('FILLING', 'biz_pallet_event.source_id'), ('USAGE', 'trace_usage.active_filling_id'), ('RETURN', 'trace_return_item.active_filling_id_snapshot'), ('SHIPMENT', 'trace_shipment_item.active_filling_id'), ): self.assertIn(f"WHEN (biz_pallet_event.source_type = '{source}') THEN {reference}", query) for alias, id_field in (('trace_usage', 'usage_id'), ('trace_shipment_item', 'shipment_id')): self.assertIn(f'{alias}.{id_field} = biz_pallet_event.source_id', query) self.assertIn(f'{alias}.pallet_id = biz_pallet_event.pallet_id', query) self.assertIn('trace_filling.pallet_id = biz_pallet_event.pallet_id', query) self.assertNotIn('biz_pallet_inventory', query) async def test_tunnel_entry_resolves_prior_filling_event_not_later_refill(self): statement = await self.statement('pallet_trace', PalletTraceReportQueryModel()) query = sql(statement) self.assertIn('SELECT prior_filling_event.source_id', query) self.assertIn('prior_filling_event.pallet_id = biz_pallet_event.pallet_id', query) self.assertIn('prior_filling_event.event_seq < biz_pallet_event.event_seq', query) self.assertIn("prior_filling_event.event_type = 'FILLING_CONFIRMED'", query) self.assertIn('ORDER BY prior_filling_event.event_seq DESC', query) self.assertIn('LIMIT 1', query) async def test_trace_date_joins_are_optional_and_batch_isolated(self): for batch_id, predicate in ((None, 'IS NULL'), (41, '= 41')): with self.subTest(batch_id=batch_id): statement = await self.statement('pallet_trace', PalletTraceReportQueryModel(), batch_id) all_joins = {join.right.name: join for join in joins(statement)} for alias, key in ( ('trace_usage', 'usage_id'), ('trace_shipment_item', 'shipment_item_id'), ('trace_filling', 'filling_id'), ): self.assertIn(alias, all_joins) self.assertTrue(all_joins[alias].isouter) self.assertIn(f'({alias}.{key} IS NULL OR {alias}.test_batch_id {predicate})', sql(statement)) self.assertIn(f'prior_filling_event.test_batch_id {predicate}', sql(statement)) async def test_response_preserves_original_date_and_null_as_iso_calendar_dates(self): for name, model_type, query_type, _ in REPORTS: for production, expiry, expected in ( (date(2025, 12, 31), date(2026, 12, 31), ('2025-12-31', '2026-12-31')), (None, None, (None, None)), ): with self.subTest(report=name, production=production): row = dict(report_row(model_type), production_date=production, expiry_date=expiry) page = PageModel(rows=[row], total=1, pageNum=1, pageSize=10, hasNext=False) with patch.object(ReportDao, f'get_{name}_list', AsyncMock(return_value=page)): result = await getattr(ReportService, f'get_{name}_list_services')( None, query_type(), make_user(1), TestBatchScope.formal(), ) actual = result.rows[0].model_dump(mode='json', by_alias=True) self.assertIn('productionDate', actual) self.assertIn('expiryDate', actual) self.assertEqual(expected, (actual['productionDate'], actual['expiryDate'])) if __name__ == '__main__': unittest.main()