From 00dd4f93b5760513394e556e9214be322c9c042f Mon Sep 17 00:00:00 2001 From: souplearn Date: Sat, 25 Jul 2026 21:23:58 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=A1=E6=A0=B8=E6=9C=AA=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E6=97=B6=E4=BF=9D=E7=95=99=E8=B6=85=E9=A2=9D=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E5=BF=AB=E7=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/reviews.py | 27 +++++++++-- tests/test_report_over_limit.py | 79 +++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/app/routers/reviews.py b/app/routers/reviews.py index 725fce3..dca51ae 100644 --- a/app/routers/reviews.py +++ b/app/routers/reviews.py @@ -116,6 +116,14 @@ def _validate_normal_report_devices(db: Session, report: ProductionReport) -> No raise HTTPException(status_code=400, detail=f"设备 {device_no} 不是该考勤点的冲压设备") +def _missing_over_limit_snapshot(report: ProductionReport) -> bool: + return any( + item.over_limit_checked_at is None and not item.over_limit_check_source + for item in report.items + if not is_cleaning_item(item) + ) + + def _report_query(): return select(ProductionReport).options( selectinload(ProductionReport.allocations), @@ -398,6 +406,7 @@ def approve_report( schedule = get_work_schedule_config(db, report.attendance_point_name) item_map = {item.id: item for item in report.items} has_correction = False + over_limit_input_changed = False next_start_at = _effective_review_time(payload.start_at, report.start_at) next_end_at = _effective_review_time(payload.end_at, report.end_at) @@ -443,11 +452,14 @@ def approve_report( if correction.raw_material_batch_no is not None else item.raw_material_batch_no ) - if ( - next_device_no != item.device_no - or next_project_no != item.project_no + product_identity_changed = ( + next_project_no != item.project_no or next_product_name != item.product_name or next_process_name != item.process_name + ) + if ( + next_device_no != item.device_no + or product_identity_changed ): product = db.scalar( select(Product).where( @@ -481,10 +493,13 @@ def approve_report( item.standard_beat = as_float(product.standard_beat) item.standard_workload = 0 has_correction = True + if product_identity_changed: + over_limit_input_changed = True if next_raw_material_batch_no != item.raw_material_batch_no: item.raw_material_batch_no = next_raw_material_batch_no has_correction = True + over_limit_input_changed = True if correction.changeover_count is not None: next_changeover_count = as_float(correction.changeover_count) @@ -499,12 +514,15 @@ def approve_report( if correction.good_qty is not None and as_float(correction.good_qty) != as_float(item.good_qty): item.good_qty = correction.good_qty has_correction = True + over_limit_input_changed = True if correction.defect_qty is not None and as_float(correction.defect_qty) != as_float(item.defect_qty): item.defect_qty = correction.defect_qty has_correction = True + over_limit_input_changed = True if correction.scrap_qty is not None and as_float(correction.scrap_qty) != as_float(item.scrap_qty): item.scrap_qty = correction.scrap_qty has_correction = True + over_limit_input_changed = True if item_is_misc and correction.remark is not None: next_remark = str(correction.remark or "").strip() or None if next_remark != item.remark: @@ -533,7 +551,8 @@ def approve_report( _apply_metrics(report, schedule) refresh_report_allocations(db, report, schedule=schedule, commit=False) reviewed_at = now() - refresh_report_over_limit_snapshot(db, report, source=CHECK_SOURCE_REVIEW, checked_at=reviewed_at) + if not is_cleaning and (over_limit_input_changed or _missing_over_limit_snapshot(report)): + refresh_report_over_limit_snapshot(db, report, source=CHECK_SOURCE_REVIEW, checked_at=reviewed_at) was_pending = report.status == ReportStatus.pending report.status = ReportStatus.approved report.reviewer_phone = user.phone diff --git a/tests/test_report_over_limit.py b/tests/test_report_over_limit.py index 9b9bea5..2f5eb1f 100644 --- a/tests/test_report_over_limit.py +++ b/tests/test_report_over_limit.py @@ -813,6 +813,85 @@ def test_approve_report_refreshes_over_limit_snapshot_after_correction(monkeypat db.close() +def test_approve_report_without_over_limit_input_changes_preserves_submit_snapshot(monkeypatch): + db = _sqlite_db() + try: + user = SimpleNamespace(phone="13900000000", role=Role.admin) + session = _numeric_session(status=SessionStatus.submitted) + report = ProductionReport( + attendance_point_name="总厂", + session=session, + employee_phone="13800000000", + report_date=date(2026, 7, 25), + start_at=session.start_at, + end_at=session.end_at, + duration_minutes=120, + effective_minutes=120, + total_good_qty=60, + total_output_qty=60, + actual_beat=120, + standard_beat=1, + expected_workload=0, + pace_rate=0, + workload_rate=0, + status=ReportStatus.pending, + submitted_at=session.end_at, + has_over_limit=True, + over_limit_summary="1项超额", + items=[ + ProductionReportItem( + attendance_point_name="总厂", + device_no="E1", + project_no="P1", + product_name="产品A", + raw_material_batch_no="LOT-1", + process_name="1", + standard_beat=1, + standard_workload=0, + good_qty=60, + defect_qty=0, + scrap_qty=0, + over_limit_status=OVER_LIMIT_STATUS_EXCEEDED, + over_limit_type="first_process_material", + over_limit_reason="提交时已超额", + 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_current_cumulative_qty=Decimal("60.00"), + over_limit_current_report_qty=Decimal("60.00"), + ) + ], + ) + db.add_all([_press_equipment(), _numeric_product(), report]) + db.commit() + monkeypatch.setattr(reviews_router, "accessible_point_names", lambda _db, _user: ["总厂"]) + monkeypatch.setattr(reviews_router, "report_out", lambda report, schedule=None: report) + monkeypatch.setattr( + "app.services.report_over_limit.read_batch_issued_weight_kg", + lambda db, batch_no, project_no, product_name: Decimal("200"), + ) + + saved = reviews_router.approve_report( + report.id, + ApproveReportRequest(), + user=user, + db=db, + ) + + assert saved.has_over_limit is True + assert saved.over_limit_summary == "1项超额" + assert saved.items[0].over_limit_status == OVER_LIMIT_STATUS_EXCEEDED + assert saved.items[0].over_limit_check_source == "submit" + assert saved.items[0].over_limit_reason == "提交时已超额" + assert saved.items[0].over_limit_issued_weight_kg == Decimal("100.000000") + assert saved.items[0].over_limit_max_reportable_qty == Decimal("50.00") + finally: + db.close() + + def test_report_out_exposes_over_limit_snapshot_fields(): report = ProductionReport( id=1,