diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 32d8b0d..61a155b 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -264,6 +264,11 @@ def _dashboard_rows_from_allocations(allocations: list) -> list[DashboardRow]: "is_voided": False, "review_remarks": [], "review_remark_keys": set(), + "has_over_limit": False, + "over_limit_exceeded_keys": set(), + "over_limit_uncheckable_keys": set(), + "over_limit_reasons": [], + "over_limit_reason_keys": set(), "correction_pairs": [], "correction_pair_keys": set(), }, @@ -303,6 +308,17 @@ def _dashboard_rows_from_allocations(allocations: list) -> list[DashboardRow]: group["is_multi_person_assistant"] = ( group["is_multi_person_assistant"] or bool(report.is_multi_person_assistant) ) + over_limit_status = str(getattr(item, "over_limit_status", "") or "") + over_limit_reason = str(getattr(item, "over_limit_reason", "") or "").strip() + over_limit_item_key = getattr(item, "id", None) or id(item) + if over_limit_status == "exceeded": + group["has_over_limit"] = True + group["over_limit_exceeded_keys"].add(over_limit_item_key) + elif over_limit_status == "uncheckable": + group["over_limit_uncheckable_keys"].add(over_limit_item_key) + if over_limit_reason and over_limit_reason not in group["over_limit_reason_keys"]: + group["over_limit_reason_keys"].add(over_limit_reason) + group["over_limit_reasons"].append(over_limit_reason) if report.review_remark: review_remark = str(report.review_remark).strip() if review_remark and review_remark not in group["review_remark_keys"]: @@ -333,6 +349,16 @@ def _dashboard_rows_from_allocations(allocations: list) -> list[DashboardRow]: "night": group["shift_night_minutes"], } source_report_dates = sorted(group["source_report_dates"]) + exceeded_count = len(group["over_limit_exceeded_keys"]) + uncheckable_count = len(group["over_limit_uncheckable_keys"]) + if exceeded_count and uncheckable_count: + over_limit_summary = f"{exceeded_count}项超额,{uncheckable_count}项无法校验" + elif exceeded_count: + over_limit_summary = f"{exceeded_count}项超额" + elif uncheckable_count: + over_limit_summary = f"{uncheckable_count}项无法校验" + else: + over_limit_summary = None rows.append( DashboardRow( id=group["id"], @@ -380,6 +406,9 @@ def _dashboard_rows_from_allocations(allocations: list) -> list[DashboardRow]: is_system_auto_submitted=group["is_system_auto_submitted"], is_voided=group["is_voided"], review_remark=";".join(group["review_remarks"]) or None, + has_over_limit=group["has_over_limit"], + over_limit_summary=over_limit_summary, + over_limit_reasons=";".join(group["over_limit_reasons"]) or None, correction_pairs=group["correction_pairs"], ) ) diff --git a/app/schemas.py b/app/schemas.py index 54c208f..57eabb4 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -658,6 +658,9 @@ class DashboardRow(BaseModel): is_system_auto_submitted: bool = False is_voided: bool = False review_remark: str | None = None + has_over_limit: bool = False + over_limit_summary: str | None = None + over_limit_reasons: str | None = None correction_pairs: list[dict[str, Any]] = Field(default_factory=list) diff --git a/app/services/excel_export.py b/app/services/excel_export.py index 6ea21ae..ea9d20b 100644 --- a/app/services/excel_export.py +++ b/app/services/excel_export.py @@ -133,6 +133,9 @@ def export_dashboard_rows(rows: list) -> bytes: "操作人数", "工序单价", "参考工资", + "是否超额", + "超额摘要", + "超额原因", "报工标记", "校正项", "备注", @@ -184,6 +187,9 @@ def export_dashboard_rows(rows: list) -> bytes: row.operator_count, row.process_unit_price_yuan, row.reference_wage, + "是" if getattr(row, "has_over_limit", False) else "否", + getattr(row, "over_limit_summary", None), + getattr(row, "over_limit_reasons", None), "、".join(report_flags), correction_text, getattr(row, "review_remark", None), diff --git a/tests/test_dashboard_allocations.py b/tests/test_dashboard_allocations.py index f1b9d46..2445bf0 100644 --- a/tests/test_dashboard_allocations.py +++ b/tests/test_dashboard_allocations.py @@ -156,3 +156,237 @@ def test_export_dashboard_rows_writes_allocation_and_source_dates_with_warning_c assert sheet.cell(row=2, column=3).value == "2026-07-23、2026-07-25" assert sheet.cell(row=2, column=12).fill.fgColor.rgb == "FFFF6666" assert sheet.cell(row=2, column=15).fill.fgColor.rgb == "FFFF6666" + + +def test_dashboard_rows_include_over_limit_summary_from_reports(): + report = SimpleNamespace( + id=1, + attendance_point_name="总厂", + employee_phone="13800000000", + report_date=date(2026, 7, 25), + has_over_limit=True, + over_limit_summary="1项超额", + review_remark=None, + employee=SimpleNamespace(name="张三", is_temporary=False), + items=[], + audit_logs=[], + session=None, + is_system_auto_submitted=False, + is_voided=False, + is_multi_person_assistant=False, + ) + item = SimpleNamespace( + id=1, + report=report, + report_id=1, + attendance_point_name="总厂", + device_no="A", + project_no="P1", + product_name="产品A", + material_code="M1", + material_name="材料A", + raw_material_batch_no="LOT-1", + process_name="1", + remark=None, + stamping_method="冲压", + operator_count=1, + process_unit_price_yuan=1, + standard_beat=1, + good_qty=60, + defect_qty=0, + scrap_qty=0, + changeover_count=0, + allocated_minutes=60, + over_limit_status="exceeded", + over_limit_reason="1序累计报工数量超过本批次材料可支撑数量", + ) + report.items = [item] + allocation = SimpleNamespace( + report=report, + item=item, + report_id=1, + allocation_date=date(2026, 7, 25), + attendance_point_name="总厂", + employee_phone="13800000000", + good_qty=60, + defect_qty=0, + scrap_qty=0, + effective_minutes=60, + day_minutes=60, + overtime_minutes=0, + night_minutes=0, + reference_wage=60, + changeover_count=0, + ) + + rows = dashboard._dashboard_rows_from_allocations([allocation]) + + assert rows[0].has_over_limit is True + assert rows[0].over_limit_summary == "1项超额" + assert rows[0].over_limit_reasons == "1序累计报工数量超过本批次材料可支撑数量" + + +def test_export_dashboard_rows_writes_over_limit_columns(): + row = SimpleNamespace( + attendance_point_name="总厂", + report_date=date(2026, 7, 24), + source_report_dates=[date(2026, 7, 24)], + source_report_date=None, + employee_name="张三", + employee_phone="13800000000", + worker_type="正式工", + project_no="P1", + product_name="23#", + process_name="1", + effective_minutes=60, + shift_distribution_text="白班1小时", + total_good_qty=50, + total_defect_qty=2, + expected_workload=100, + actual_beat=20, + standard_beat=10, + profile_no="PF-1", + material_code="M-1", + material_name="铝材", + raw_material_batch_no="BATCH-1", + device_no="A-01", + stamping_method="单冲", + changeover_count=1, + operator_count=1, + process_unit_price_yuan=2, + reference_wage=100, + has_over_limit=True, + over_limit_summary="1项超额", + over_limit_reasons="1序累计报工数量超过本批次材料可支撑数量", + is_voided=False, + is_multi_person=False, + is_continuous_die=False, + correction_pairs=[], + review_remark="复核通过", + ) + + workbook = load_workbook(BytesIO(export_dashboard_rows([row]))) + sheet = workbook["报工看板"] + headers = [sheet.cell(row=1, column=index).value for index in range(1, sheet.max_column + 1)] + + assert "是否超额" in headers + assert "超额摘要" in headers + assert "超额原因" in headers + assert sheet.cell(row=2, column=headers.index("是否超额") + 1).value == "是" + assert sheet.cell(row=2, column=headers.index("超额摘要") + 1).value == "1项超额" + assert sheet.cell(row=2, column=headers.index("超额原因") + 1).value == "1序累计报工数量超过本批次材料可支撑数量" + + +def test_dashboard_over_limit_does_not_spread_to_other_items_in_same_report(): + report = SimpleNamespace( + id=1, + attendance_point_name="总厂", + employee_phone="13800000000", + report_date=date(2026, 7, 25), + has_over_limit=True, + over_limit_summary="1项超额", + review_remark=None, + employee=SimpleNamespace(name="张三", is_temporary=False), + items=[], + audit_logs=[], + session=None, + is_system_auto_submitted=False, + is_voided=False, + is_multi_person_assistant=False, + ) + exceeded_item = SimpleNamespace( + id=1, + report=report, + report_id=1, + attendance_point_name="总厂", + device_no="A", + project_no="P1", + product_name="产品A", + material_code="M1", + material_name="材料A", + raw_material_batch_no="LOT-1", + process_name="1", + remark=None, + stamping_method="冲压", + operator_count=1, + process_unit_price_yuan=1, + standard_beat=1, + good_qty=60, + defect_qty=0, + scrap_qty=0, + changeover_count=0, + allocated_minutes=60, + over_limit_status="exceeded", + over_limit_reason="1序累计报工数量超过本批次材料可支撑数量", + ) + normal_item = SimpleNamespace( + id=2, + report=report, + report_id=1, + attendance_point_name="总厂", + device_no="B", + project_no="P1", + product_name="产品A", + material_code="M1", + material_name="材料A", + raw_material_batch_no="LOT-1", + process_name="2", + remark=None, + stamping_method="冲压", + operator_count=1, + process_unit_price_yuan=1, + standard_beat=1, + good_qty=40, + defect_qty=0, + scrap_qty=0, + changeover_count=0, + allocated_minutes=40, + over_limit_status="ok", + over_limit_reason=None, + ) + report.items = [exceeded_item, normal_item] + allocations = [ + SimpleNamespace( + report=report, + item=exceeded_item, + report_id=1, + allocation_date=date(2026, 7, 25), + attendance_point_name="总厂", + employee_phone="13800000000", + good_qty=60, + defect_qty=0, + scrap_qty=0, + effective_minutes=60, + day_minutes=60, + overtime_minutes=0, + night_minutes=0, + reference_wage=60, + changeover_count=0, + ), + SimpleNamespace( + report=report, + item=normal_item, + report_id=1, + allocation_date=date(2026, 7, 25), + attendance_point_name="总厂", + employee_phone="13800000000", + good_qty=40, + defect_qty=0, + scrap_qty=0, + effective_minutes=40, + day_minutes=40, + overtime_minutes=0, + night_minutes=0, + reference_wage=40, + changeover_count=0, + ), + ] + + rows = dashboard._dashboard_rows_from_allocations(allocations) + rows_by_process = {row.process_name: row for row in rows} + + assert rows_by_process["1"].has_over_limit is True + assert rows_by_process["1"].over_limit_reasons == "1序累计报工数量超过本批次材料可支撑数量" + assert rows_by_process["2"].has_over_limit is False + assert rows_by_process["2"].over_limit_summary is None + assert rows_by_process["2"].over_limit_reasons is None