fix: 按指标结果校准归属工时

This commit is contained in:
souplearn 2026-07-25 03:41:54 +08:00
parent 891c10ddea
commit 1a8871d3b4
2 changed files with 65 additions and 0 deletions

View File

@ -17,6 +17,7 @@ from app.services.metrics import (
_mold_key, _mold_key,
_overlap_minutes, _overlap_minutes,
_schedule_interval, _schedule_interval,
calculate_report_metrics,
format_hours, format_hours,
item_time_ranges, item_time_ranges,
minutes_between, minutes_between,
@ -129,6 +130,18 @@ def _item_reference_wage(item) -> float:
return as_float(getattr(item, "good_qty", 0)) * as_float(getattr(item, "process_unit_price_yuan", 0)) return as_float(getattr(item, "good_qty", 0)) * as_float(getattr(item, "process_unit_price_yuan", 0))
def _item_target_minutes(report, items: list, schedule: WorkScheduleConfig | None = None) -> dict[int, float]:
calculate_report_metrics(
getattr(report, "start_at"),
getattr(report, "end_at"),
items,
break_minutes=as_float(getattr(report, "break_minutes", 0)),
devices=_report_devices(report),
schedule=schedule,
)
return {id(item): as_float(getattr(item, "allocated_minutes", 0)) for item in items}
def _minutes_total(rows: dict[date, dict[str, float]]) -> float: def _minutes_total(rows: dict[date, dict[str, float]]) -> float:
return sum(sum(minutes.get(key, 0.0) for key in SHIFT_BUCKETS) for minutes in rows.values()) return sum(sum(minutes.get(key, 0.0) for key in SHIFT_BUCKETS) for minutes in rows.values())
@ -180,6 +193,21 @@ def _scale_item_minutes_to_bucket_targets(
minutes[key] = minutes.get(key, 0.0) * scale minutes[key] = minutes.get(key, 0.0) * scale
def _scale_item_minutes_to_item_targets(
minutes_by_item: dict[int, dict[date, dict[str, float]]],
target_minutes_by_item: dict[int, float],
) -> None:
for item_id, rows_by_date in minutes_by_item.items():
raw_total = _minutes_total(rows_by_date)
target_total = target_minutes_by_item.get(item_id, raw_total)
if raw_total <= 0 or abs(raw_total - target_total) <= 0.000001:
continue
scale = target_total / raw_total
for minutes in rows_by_date.values():
for key in SHIFT_BUCKETS:
minutes[key] = minutes.get(key, 0.0) * scale
def _items_by_device(items: list) -> dict[str, list]: def _items_by_device(items: list) -> dict[str, list]:
grouped: dict[str, list] = defaultdict(list) grouped: dict[str, list] = defaultdict(list)
for item in items: for item in items:
@ -305,6 +333,7 @@ def build_report_allocation_drafts(report, schedule: WorkScheduleConfig | None =
minutes_by_item = _device_minutes_by_item(report, items, schedule) minutes_by_item = _device_minutes_by_item(report, items, schedule)
if minutes_by_item is None: if minutes_by_item is None:
minutes_by_item = _range_minutes_by_item(report, items, schedule) minutes_by_item = _range_minutes_by_item(report, items, schedule)
_scale_item_minutes_to_item_targets(minutes_by_item, _item_target_minutes(report, items, schedule))
rows: list[ReportAllocationDraft] = [] rows: list[ReportAllocationDraft] = []
for item in items: for item in items:

View File

@ -297,6 +297,42 @@ def test_matched_meal_split_and_unmatched_device_range_follow_metrics_order():
assert round(sum(row.effective_minutes for row in rows), 2) == metrics["effective_minutes"] assert round(sum(row.effective_minutes for row in rows), 2) == metrics["effective_minutes"]
def test_item_totals_anchor_to_metrics_when_unmatched_remaining_buckets_exceed_item_target():
start_at = datetime(2026, 7, 25, 8, 0)
second_started_at = datetime(2026, 7, 25, 12, 0)
switch_at = datetime(2026, 7, 25, 18, 0)
end_at = datetime(2026, 7, 25, 20, 0)
devices = [
_device(start_at, device_no="23#", sort_order=0),
_device(switch_at, device_no="77#", sort_order=1),
]
first_item = _item(id=1, good_qty=240, device_no="23#", started_at=start_at)
second_item = _item(id=2, good_qty=360, device_no="23#", started_at=second_started_at)
unmatched_item = _item(id=3, good_qty=60, device_no="99#")
items = [first_item, second_item, unmatched_item]
report = _report(start_at, end_at, items, devices=devices)
metrics = calculate_report_metrics(start_at, end_at, items, devices=devices)
rows = build_report_allocation_drafts(report)
assert metrics["effective_minutes"] == 660
assert {item.id: item.allocated_minutes for item in items} == {
1: 240,
2: 360,
3: 60,
}
assert {
item_id: round(sum(row.effective_minutes for row in rows if row.report_item_id == item_id), 2)
for item_id in [1, 2, 3]
} == {
1: 240,
2: 360,
3: 60,
}
assert round(sum(row.effective_minutes for row in rows), 2) == 660
assert all(row.effective_minutes == row.day_minutes + row.overtime_minutes + row.night_minutes for row in rows)
def test_zero_effective_minutes_falls_back_to_original_report_date(): 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) item = _item(good_qty=5, defect_qty=1, scrap_qty=0.5, changeover_count=2, process_unit_price_yuan=3)
report = _report( report = _report(