diff --git a/app/schemas.py b/app/schemas.py index 44be384..54c208f 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -491,6 +491,18 @@ class ReportItemOut(BaseModel): is_continuous_die: bool = False is_multi_person: bool = False remark: str | None = None + over_limit_status: str = "not_checked" + over_limit_type: str | None = None + over_limit_reason: str | None = None + over_limit_checked_at: datetime | None = None + over_limit_check_source: str | None = None + over_limit_batch_no: str | None = None + over_limit_product_gross_weight_kg: float | None = None + over_limit_issued_weight_kg: float | None = None + over_limit_max_reportable_qty: float | None = None + over_limit_previous_good_qty: float | None = None + over_limit_current_cumulative_qty: float | None = None + over_limit_current_report_qty: float | None = None allocations: list[ReportAllocationOut] = Field(default_factory=list) corrections: dict[str, Any] = Field(default_factory=dict) @@ -530,6 +542,8 @@ class ReportOut(BaseModel): submitted_at: datetime is_system_auto_submitted: bool = False auto_submit_reason: str | None = None + has_over_limit: bool = False + over_limit_summary: str | None = None is_multi_person_assistant: bool = False multi_person_source_report_id: int | None = None is_voided: bool = False diff --git a/app/services/serializers.py b/app/services/serializers.py index 25c1dce..b9a5e97 100644 --- a/app/services/serializers.py +++ b/app/services/serializers.py @@ -52,6 +52,10 @@ def temporary_expired(person: Personnel) -> bool: ) +def _optional_float(value) -> float | None: + return None if value is None else as_float(value) + + def attendance_point_out(point: AttendancePoint) -> AttendancePointOut: return AttendancePointOut( name=point.name, @@ -123,7 +127,6 @@ def personnel_summary_out(person: Personnel) -> dict: def product_out(product: Product) -> ProductOut: - optional_float = lambda value: None if value is None else as_float(value) return ProductOut( attendance_point_name=product.attendance_point_name, project_no=product.project_no, @@ -132,10 +135,10 @@ def product_out(product: Product) -> ProductOut: material_code=product.material_code, material_name=product.material_name, supplier=product.supplier, - product_net_weight_kg=optional_float(product.product_net_weight_kg), - product_gross_weight_kg=optional_float(product.product_gross_weight_kg), - scrap_loss_rate=optional_float(product.scrap_loss_rate), - waste_price_yuan_per_kg=optional_float(product.waste_price_yuan_per_kg), + product_net_weight_kg=_optional_float(product.product_net_weight_kg), + product_gross_weight_kg=_optional_float(product.product_gross_weight_kg), + scrap_loss_rate=_optional_float(product.scrap_loss_rate), + waste_price_yuan_per_kg=_optional_float(product.waste_price_yuan_per_kg), device_no=product.device_no, process_name=product.process_name, stamping_method=product.stamping_method, @@ -410,6 +413,18 @@ 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, + over_limit_status=item.over_limit_status or "not_checked", + over_limit_type=item.over_limit_type, + over_limit_reason=item.over_limit_reason, + over_limit_checked_at=item.over_limit_checked_at, + over_limit_check_source=item.over_limit_check_source, + over_limit_batch_no=item.over_limit_batch_no, + over_limit_product_gross_weight_kg=_optional_float(item.over_limit_product_gross_weight_kg), + over_limit_issued_weight_kg=_optional_float(item.over_limit_issued_weight_kg), + over_limit_max_reportable_qty=_optional_float(item.over_limit_max_reportable_qty), + over_limit_previous_good_qty=_optional_float(item.over_limit_previous_good_qty), + over_limit_current_cumulative_qty=_optional_float(item.over_limit_current_cumulative_qty), + over_limit_current_report_qty=_optional_float(item.over_limit_current_report_qty), allocations=[report_allocation_out(row) for row in allocation_rows], corrections=corrections or {}, ) @@ -505,6 +520,8 @@ def report_out(report: ProductionReport, schedule: WorkScheduleConfig | None = N submitted_at=report.submitted_at, is_system_auto_submitted=bool(report.is_system_auto_submitted), auto_submit_reason=report.auto_submit_reason, + has_over_limit=bool(report.has_over_limit), + over_limit_summary=report.over_limit_summary, is_multi_person_assistant=bool(report.is_multi_person_assistant), multi_person_source_report_id=report.multi_person_source_report_id, is_voided=bool(report.is_voided), diff --git a/tests/test_report_over_limit.py b/tests/test_report_over_limit.py index 89cf1f5..9b9bea5 100644 --- a/tests/test_report_over_limit.py +++ b/tests/test_report_over_limit.py @@ -26,6 +26,7 @@ from app.services.report_over_limit import ( parse_process_order, refresh_report_over_limit_snapshot, ) +from app.services.serializers import report_out from app.timezone import now @@ -810,3 +811,56 @@ def test_approve_report_refreshes_over_limit_snapshot_after_correction(monkeypat assert saved.items[0].over_limit_current_report_qty == Decimal("60.00") finally: db.close() + + +def test_report_out_exposes_over_limit_snapshot_fields(): + report = ProductionReport( + id=1, + session_id=1, + attendance_point_name="总厂", + employee_phone="13800000000", + report_date=datetime(2026, 7, 25).date(), + start_at=datetime(2026, 7, 25, 8), + end_at=datetime(2026, 7, 25, 10), + status=ReportStatus.pending, + submitted_at=datetime(2026, 7, 25, 10), + has_over_limit=True, + over_limit_summary="1项超额", + ) + item = ProductionReportItem( + id=1, + report=report, + attendance_point_name="总厂", + device_no="A", + project_no="P1", + product_name="产品A", + raw_material_batch_no="LOT-1", + process_name="1", + good_qty=60, + defect_qty=0, + scrap_qty=0, + standard_beat=1, + over_limit_status=OVER_LIMIT_STATUS_EXCEEDED, + over_limit_type="first_process_material", + over_limit_reason="1序累计报工数量超过本批次材料可支撑数量", + over_limit_checked_at=datetime(2026, 7, 25, 10), + over_limit_check_source="submit", + over_limit_batch_no="LOT-1", + over_limit_product_gross_weight_kg=Decimal("2.0000"), + over_limit_issued_weight_kg=Decimal("100.000000"), + over_limit_max_reportable_qty=Decimal("50.00"), + over_limit_previous_good_qty=None, + over_limit_current_cumulative_qty=Decimal("60.00"), + over_limit_current_report_qty=Decimal("60.00"), + ) + report.items = [item] + report.allocations = [] + report.audit_logs = [] + + output = report_out(report) + + assert output.has_over_limit is True + assert output.over_limit_summary == "1项超额" + assert output.items[0].over_limit_status == OVER_LIMIT_STATUS_EXCEEDED + assert output.items[0].over_limit_reason == "1序累计报工数量超过本批次材料可支撑数量" + assert output.items[0].over_limit_max_reportable_qty == 50