From 5918b5ddde4459f0bc37887946cff10cff332d8b Mon Sep 17 00:00:00 2001 From: souplearn Date: Sat, 25 Jul 2026 04:05:05 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=9D=E7=95=99=E6=9C=AA=E5=A1=AB?= =?UTF-8?q?=E6=8A=A5=E8=AE=BE=E5=A4=87=E6=AE=B5=E5=BD=92=E5=B1=9E=E5=B7=A5?= =?UTF-8?q?=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/report_allocations.py | 21 ++++++++++++++++++++ tests/test_report_allocations.py | 31 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/app/services/report_allocations.py b/app/services/report_allocations.py index 370d938..3c2d3a0 100644 --- a/app/services/report_allocations.py +++ b/app/services/report_allocations.py @@ -184,6 +184,25 @@ def _item_bucket_totals(minutes_by_item: dict[int, dict[date, dict[str, float]]] return totals +def _allocate_unassigned_minutes_to_existing_items( + minutes_by_item: dict[int, dict[date, dict[str, float]]], + unassigned_minutes: dict[date, dict[str, float]], +) -> None: + unassigned_total = _minutes_total(unassigned_minutes) + if unassigned_total <= 0 or not minutes_by_item: + return + + raw_item_totals = { + item_id: _minutes_total(rows_by_date) + for item_id, rows_by_date in minutes_by_item.items() + } + raw_total = sum(raw_item_totals.values()) + item_count = len(minutes_by_item) + for item_id, rows_by_date in minutes_by_item.items(): + weight = raw_item_totals[item_id] / raw_total if raw_total > 0 else 1 / item_count + _add_date_bucket_minutes(rows_by_date, unassigned_minutes, scale=weight) + + def _scale_item_minutes_to_bucket_targets( minutes_by_item: dict[int, dict[date, dict[str, float]]], target_buckets: dict[str, float], @@ -352,6 +371,7 @@ def _device_minutes_by_item(report, items: list, schedule: WorkScheduleConfig | row[key] += minutes.get(key, 0.0) * (remaining / raw_unassigned) / len(unmatched_items) return minutes_by_item + _allocate_unassigned_minutes_to_existing_items(minutes_by_item, unassigned_minutes) _scale_item_minutes_to_bucket_targets(minutes_by_item, _bucket_totals(target_minutes)) return minutes_by_item @@ -468,6 +488,7 @@ def refresh_report_allocations( *, commit: bool = False, ) -> list[ProductionReportAllocation]: + """Rebuild allocation rows with metrics semantics, updating item allocation-derived fields.""" db.execute(delete(ProductionReportAllocation).where(ProductionReportAllocation.report_id == getattr(report, "id"))) rows = [ ProductionReportAllocation( diff --git a/tests/test_report_allocations.py b/tests/test_report_allocations.py index f3a31d6..af977cd 100644 --- a/tests/test_report_allocations.py +++ b/tests/test_report_allocations.py @@ -214,6 +214,37 @@ def test_device_matched_and_unmatched_items_match_metrics_remaining_minutes(): } +def test_unassigned_device_segment_is_allocated_when_all_items_already_matched(): + start_at = datetime(2026, 7, 25, 0, 0) + switch_at = datetime(2026, 7, 25, 8, 0) + end_at = datetime(2026, 7, 25, 10, 0) + devices = [ + _device(start_at, device_no="23#", sort_order=0), + _device(switch_at, device_no="99#", sort_order=1), + ] + item = _item(id=1, good_qty=600, device_no="23#", started_at=start_at) + report = _report(start_at, end_at, item, devices=devices) + + metrics = calculate_report_metrics(start_at, end_at, [item], devices=devices) + rows = build_report_allocation_drafts(report) + + assert metrics["effective_minutes"] == 600 + assert metrics["shift_day_minutes"] == 120 + assert metrics["shift_overtime_minutes"] == 120 + assert metrics["shift_night_minutes"] == 360 + assert round(sum(row.effective_minutes for row in rows), 2) == metrics["effective_minutes"] + assert { + "day": round(sum(row.day_minutes for row in rows), 2), + "overtime": round(sum(row.overtime_minutes for row in rows), 2), + "night": round(sum(row.night_minutes for row in rows), 2), + } == { + "day": metrics["shift_day_minutes"], + "overtime": metrics["shift_overtime_minutes"], + "night": metrics["shift_night_minutes"], + } + assert all(row.effective_minutes == round(row.day_minutes + row.overtime_minutes + row.night_minutes, 2) for row in rows) + + def test_night_allocation_uses_meal_deducted_minutes_with_previous_date_rules(): schedule = WorkScheduleConfig( day_start="08:00",