From 63ea6fdcc5f5353c3b2160fd53edcce8e3f9bdaa Mon Sep 17 00:00:00 2001 From: souplearn Date: Sat, 25 Jul 2026 03:13:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AF=B9=E9=BD=90=E6=8A=A5=E5=B7=A5?= =?UTF-8?q?=E5=BD=92=E5=B1=9E=E6=9C=89=E6=95=88=E5=B7=A5=E6=97=B6=E5=88=86?= =?UTF-8?q?=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/metrics.py | 16 +++++++- app/services/report_allocations.py | 20 +--------- tests/test_report_allocations.py | 59 ++++++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/app/services/metrics.py b/app/services/metrics.py index 0a0d678..e19d2b5 100644 --- a/app/services/metrics.py +++ b/app/services/metrics.py @@ -461,9 +461,11 @@ def item_time_ranges( ranges: list[ItemTimeRange] = [] allocated_item_ids: set[int] = set() + unassigned_ranges: list[tuple[datetime, datetime]] = [] for device_no, device_ranges in segment_ranges.items(): device_items = items_by_device.get(device_no, []) if not device_items: + unassigned_ranges.extend(device_ranges) continue for segment_start, segment_end in device_ranges: ranges.extend(_device_item_time_ranges(device_items, segment_start, segment_end)) @@ -471,8 +473,18 @@ def item_time_ranges( allocated_item_ids.add(id(item)) unmatched_items = [item for item in items if id(item) not in allocated_item_ids] - if unmatched_items: - ranges.extend(ItemTimeRange(item=item, start_at=start_at, end_at=end_at) for item in unmatched_items) + if unmatched_items and unassigned_ranges: + allocation_weight = 1 / len(unmatched_items) + for segment_start, segment_end in unassigned_ranges: + ranges.extend( + ItemTimeRange( + item=item, + start_at=segment_start, + end_at=segment_end, + allocation_weight=allocation_weight, + ) + for item in unmatched_items + ) return ranges diff --git a/app/services/report_allocations.py b/app/services/report_allocations.py index f97fc0a..14078c4 100644 --- a/app/services/report_allocations.py +++ b/app/services/report_allocations.py @@ -1,6 +1,6 @@ from collections import defaultdict from dataclasses import dataclass -from datetime import date, datetime, time, timedelta +from datetime import date, datetime, timedelta from sqlalchemy import delete from sqlalchemy.orm import Session @@ -40,10 +40,6 @@ class ReportAllocationDraft: reference_wage: float = 0 -def _midnight(value: date) -> datetime: - return datetime.combine(value, time.min) - - def _add_minutes( rows: dict[date, dict[str, float]], allocation_date: date, @@ -55,18 +51,6 @@ def _add_minutes( rows[allocation_date][bucket] += minutes -def _add_night_minutes( - rows: dict[date, dict[str, float]], - start_at: datetime, - end_at: datetime, - base_date: date, -) -> None: - midnight = _midnight(base_date + timedelta(days=1)) - before_midnight = _overlap_minutes(start_at, end_at, _midnight(base_date), midnight) - after_midnight = _overlap_minutes(start_at, end_at, midnight, midnight + timedelta(days=1)) - _add_minutes(rows, base_date, "night", before_midnight + after_midnight) - - def _range_allocation_minutes( start_at: datetime, end_at: datetime, @@ -107,7 +91,7 @@ def _range_allocation_minutes( ) minutes = max(0.0, minutes_between(overlap_start, overlap_end) - meal_minutes) if bucket == "night": - _add_night_minutes(rows, overlap_start, overlap_end, current_date) + _add_minutes(rows, current_date, "night", minutes) else: _add_minutes(rows, overlap_start.date(), bucket, minutes) diff --git a/tests/test_report_allocations.py b/tests/test_report_allocations.py index 62f59f2..9ebd668 100644 --- a/tests/test_report_allocations.py +++ b/tests/test_report_allocations.py @@ -7,11 +7,13 @@ from sqlalchemy.orm import sessionmaker from app.database import Base from app.models import ProductionReport, ProductionReportAllocation, ProductionReportItem +from app.services.metrics import calculate_report_metrics from app.services.report_allocations import ( allocation_summary_text, build_report_allocation_drafts, refresh_report_allocations, ) +from app.services.work_schedule import WorkScheduleConfig @compiles(BigInteger, "sqlite") @@ -28,12 +30,15 @@ def _item( changeover_count: float = 0, process_unit_price_yuan: float = 0, started_at: datetime | None = None, + device_no: str = "23#", + product_name: str | None = None, + process_name: str | None = "冲压", ): return SimpleNamespace( id=id, - device_no="23#", - product_name="23#", - process_name="冲压", + device_no=device_no, + product_name=product_name or device_no, + process_name=process_name, started_at=started_at, standard_beat=1, standard_workload=0, @@ -168,6 +173,54 @@ def test_items_sharing_same_started_at_split_the_segment_minutes(): ] +def test_device_matched_and_unmatched_items_match_metrics_remaining_minutes(): + start_at = datetime(2026, 7, 25, 8, 0) + end_at = datetime(2026, 7, 25, 11, 0) + matching_item = _item(id=1, good_qty=10, device_no="23#") + unmatched_item = _item(id=2, good_qty=20, device_no="99#") + report = _report(start_at, end_at, [matching_item, unmatched_item], devices=[_device(start_at)]) + + calculate_report_metrics(start_at, end_at, [matching_item, unmatched_item], devices=[_device(start_at)]) + rows = build_report_allocation_drafts(report) + + assert {matching_item.id: matching_item.allocated_minutes, unmatched_item.id: unmatched_item.allocated_minutes} == { + 1: 180, + 2: 0, + } + assert {row.report_item_id: row.effective_minutes for row in rows} == { + 1: 180, + 2: 0, + } + + +def test_night_allocation_uses_meal_deducted_minutes_with_previous_date_rules(): + schedule = WorkScheduleConfig( + day_start="08:00", + day_end="17:20", + lunch_start="11:40", + lunch_end="12:40", + dinner_start="21:00", + dinner_end="21:30", + overtime_start="18:00", + overtime_end="20:00", + night_start="20:00", + night_end="06:00", + ) + start_at = datetime(2026, 7, 25, 20, 0) + end_at = datetime(2026, 7, 25, 22, 0) + item = _item(id=1, good_qty=90) + report = _report(start_at, end_at, item) + + metrics = calculate_report_metrics(start_at, end_at, [item], schedule=schedule) + rows = build_report_allocation_drafts(report, schedule=schedule) + + assert metrics["effective_minutes"] == 90 + assert metrics["shift_night_minutes"] == 90 + assert [(row.allocation_date, row.night_minutes, row.effective_minutes) for row in rows] == [ + (date(2026, 7, 25), 90, 90), + ] + + def test_zero_effective_minutes_falls_back_to_original_report_date(): item = _item(good_qty=5, defect_qty=1, scrap_qty=0.5, changeover_count=2, process_unit_price_yuan=3) report = _report(