diff --git a/app/routers/reports.py b/app/routers/reports.py index f912255..aa3dc03 100644 --- a/app/routers/reports.py +++ b/app/routers/reports.py @@ -74,7 +74,8 @@ def _cleaning_device_nos(item: CleaningReportSubmitItem) -> list[str]: def _report_query(): return select(ProductionReport).options( - selectinload(ProductionReport.items), + selectinload(ProductionReport.allocations), + selectinload(ProductionReport.items).selectinload(ProductionReportItem.allocations), selectinload(ProductionReport.audit_logs), selectinload(ProductionReport.employee), selectinload(ProductionReport.reviewer), diff --git a/app/routers/reviews.py b/app/routers/reviews.py index 4d8a55a..0a615e5 100644 --- a/app/routers/reviews.py +++ b/app/routers/reviews.py @@ -117,7 +117,8 @@ def _validate_normal_report_devices(db: Session, report: ProductionReport) -> No def _report_query(): return select(ProductionReport).options( - selectinload(ProductionReport.items), + selectinload(ProductionReport.allocations), + selectinload(ProductionReport.items).selectinload(ProductionReportItem.allocations), selectinload(ProductionReport.audit_logs), selectinload(ProductionReport.employee), selectinload(ProductionReport.reviewer), diff --git a/app/schemas.py b/app/schemas.py index 9ee1590..44be384 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -453,6 +453,19 @@ class ReportMetrics(BaseModel): workload_rate: float +class ReportAllocationOut(BaseModel): + allocation_date: date + day_minutes: float = 0 + overtime_minutes: float = 0 + night_minutes: float = 0 + effective_minutes: float = 0 + good_qty: float = 0 + defect_qty: float = 0 + scrap_qty: float = 0 + changeover_count: float = 0 + reference_wage: float = 0 + + class ReportItemOut(BaseModel): id: int | None = None attendance_point_name: str = "" @@ -478,6 +491,7 @@ class ReportItemOut(BaseModel): is_continuous_die: bool = False is_multi_person: bool = False remark: str | None = None + allocations: list[ReportAllocationOut] = Field(default_factory=list) corrections: dict[str, Any] = Field(default_factory=dict) @@ -531,6 +545,7 @@ class ReportOut(BaseModel): items: list[ReportItemOut] metrics: ReportMetrics result_text: str + allocation_summary_text: str = "" corrections: dict[str, Any] = Field(default_factory=dict) @@ -587,6 +602,8 @@ class DashboardRow(BaseModel): id: str attendance_point_name: str = "" report_date: date + source_report_date: date | None = None + source_report_dates: list[date] = Field(default_factory=list) employee_phone: str employee_name: str project_no: str diff --git a/app/services/serializers.py b/app/services/serializers.py index 7b19a28..57dbf50 100644 --- a/app/services/serializers.py +++ b/app/services/serializers.py @@ -23,6 +23,7 @@ from app.schemas import ( PersonnelOut, ProductOption, ProductOut, + ReportAllocationOut, ReportDeviceSegmentOut, ReportItemOut, ReportMetrics, @@ -36,6 +37,7 @@ from app.services.metrics import build_result_text, calculate_report_metrics from app.services.misc_work import is_misc_item, is_misc_product from app.services.multi_person import is_multi_person_item, is_multi_person_product from app.services.process_names import export_process_name +from app.services.report_allocations import allocation_summary_text from app.services.report_lifecycle import can_edit_report, can_unvoid_report from app.services.work_schedule import WorkScheduleConfig from app.timezone import now @@ -341,6 +343,21 @@ def report_device_segment_out( ) +def report_allocation_out(row) -> ReportAllocationOut: + return ReportAllocationOut( + allocation_date=row.allocation_date, + day_minutes=as_float(row.day_minutes), + overtime_minutes=as_float(row.overtime_minutes), + night_minutes=as_float(row.night_minutes), + effective_minutes=as_float(row.effective_minutes), + good_qty=as_float(row.good_qty), + defect_qty=as_float(row.defect_qty), + scrap_qty=as_float(row.scrap_qty), + changeover_count=as_float(row.changeover_count), + reference_wage=as_float(row.reference_wage), + ) + + def report_item_out(item: ProductionReportItem, corrections: dict | None = None) -> ReportItemOut: return ReportItemOut( id=item.id, @@ -367,6 +384,7 @@ def report_item_out(item: ProductionReportItem, corrections: dict | None = None) is_continuous_die=is_continuous_die_item(item), is_multi_person=is_multi_person_item(item), remark=item.remark, + allocations=[report_allocation_out(row) for row in getattr(item, "allocations", []) or []], corrections=corrections or {}, ) @@ -486,5 +504,6 @@ def report_out(report: ProductionReport, schedule: WorkScheduleConfig | None = N if is_cleaning_report else ("处理杂活已提交,等待管理员审核" if _report_is_misc_only(report) else build_result_text(metrics)) ), + allocation_summary_text=allocation_summary_text(list(getattr(report, "allocations", []) or [])), corrections=report_corrections, ) diff --git a/tests/test_report_allocations.py b/tests/test_report_allocations.py index 39ffa1b..917d9a3 100644 --- a/tests/test_report_allocations.py +++ b/tests/test_report_allocations.py @@ -1,4 +1,5 @@ from datetime import date, datetime +from decimal import Decimal from types import SimpleNamespace from sqlalchemy import BigInteger, create_engine, select @@ -9,6 +10,7 @@ from app.database import Base from app.models import ( AttendancePoint, Equipment, + Personnel, Product, ProductionReport, ProductionReportAllocation, @@ -38,6 +40,7 @@ from app.services.report_allocations import ( build_report_allocation_drafts, refresh_report_allocations, ) +from app.services.serializers import report_out from app.services.work_schedule import WorkScheduleConfig @@ -453,6 +456,131 @@ def test_allocation_summary_text_groups_dates_and_shift_kinds(): assert allocation_summary_text(rows) == "2026-07-24 夜班5.83小时;2026-07-25 白班3小时、加班2小时" +def test_report_out_exposes_allocation_summary_and_item_allocations(): + report = ProductionReport( + id=1, + session_id=1, + attendance_point_name="总厂", + employee_phone="13800000000", + report_date=date(2026, 7, 25), + start_at=datetime(2026, 7, 25, 8, 0), + end_at=datetime(2026, 7, 25, 18, 30), + break_minutes=60, + status=ReportStatus.pending, + submitted_at=datetime(2026, 7, 25, 18, 31), + ) + report.employee = Personnel(phone="13800000000", name="张三") + report.session = WorkSession( + id=1, + employee_phone="13800000000", + attendance_point_name="总厂", + start_at=datetime(2026, 7, 25, 8, 0), + status=SessionStatus.submitted, + ) + report.session.devices = [] + item = ProductionReportItem( + id=1, + report_id=1, + attendance_point_name="总厂", + device_no="23#", + project_no="P1", + product_name="23#", + process_name="冲压", + process_unit_price_yuan=Decimal("2.50"), + good_qty=Decimal("100.50"), + defect_qty=Decimal("2.25"), + scrap_qty=Decimal("1.25"), + changeover_count=Decimal("1.50"), + standard_beat=Decimal("3.00"), + standard_workload=Decimal("0.00"), + ) + allocation = ProductionReportAllocation( + id=1, + report_id=1, + report_item_id=1, + attendance_point_name="总厂", + employee_phone="13800000000", + allocation_date=date(2026, 7, 25), + day_minutes=Decimal("120.50"), + overtime_minutes=Decimal("30.25"), + night_minutes=Decimal("0.00"), + effective_minutes=Decimal("150.75"), + good_qty=Decimal("100.50"), + defect_qty=Decimal("2.25"), + scrap_qty=Decimal("1.25"), + changeover_count=Decimal("1.50"), + reference_wage=Decimal("251.25"), + ) + item.allocations = [allocation] + report.items = [item] + report.allocations = [allocation] + report.audit_logs = [] + + output = report_out(report) + + assert output.allocation_summary_text == "2026-07-25 白班2.01小时、加班0.5小时" + assert len(output.items[0].allocations) == 1 + item_allocation = output.items[0].allocations[0] + assert item_allocation.allocation_date == date(2026, 7, 25) + assert item_allocation.day_minutes == 120.5 + assert item_allocation.overtime_minutes == 30.25 + assert item_allocation.effective_minutes == 150.75 + assert item_allocation.good_qty == 100.5 + assert item_allocation.defect_qty == 2.25 + assert item_allocation.scrap_qty == 1.25 + assert item_allocation.changeover_count == 1.5 + assert item_allocation.reference_wage == 251.25 + + +def test_report_out_handles_empty_allocations(): + report = ProductionReport( + id=1, + session_id=1, + attendance_point_name="总厂", + employee_phone="13800000000", + report_date=date(2026, 7, 25), + start_at=datetime(2026, 7, 25, 8, 0), + end_at=datetime(2026, 7, 25, 9, 0), + break_minutes=0, + status=ReportStatus.pending, + submitted_at=datetime(2026, 7, 25, 9, 1), + ) + report.employee = Personnel(phone="13800000000", name="张三") + report.session = WorkSession( + id=1, + employee_phone="13800000000", + attendance_point_name="总厂", + start_at=datetime(2026, 7, 25, 8, 0), + status=SessionStatus.submitted, + ) + report.session.devices = [] + item = ProductionReportItem( + id=1, + report_id=1, + attendance_point_name="总厂", + device_no="23#", + project_no="P1", + product_name="23#", + process_name="冲压", + process_unit_price_yuan=0, + good_qty=0, + defect_qty=0, + scrap_qty=0, + changeover_count=0, + standard_beat=1, + standard_workload=0, + ) + item.allocations = [] + report.items = [item] + report.allocations = [] + report.audit_logs = [] + + output = report_out(report) + + assert output.allocation_summary_text == "" + assert output.items[0].allocations == [] + + def test_refresh_report_allocations_replaces_existing_rows_and_flushes(): engine = create_engine("sqlite+pysqlite:///:memory:", future=True) Base.metadata.create_all(engine)