fix: 归一化报工归属工时分配
This commit is contained in:
parent
65fd7f85ff
commit
fbbea51d5c
@ -11,6 +11,7 @@ from app.services.metrics import (
|
|||||||
SHIFT_BUCKETS,
|
SHIFT_BUCKETS,
|
||||||
SHIFT_LABELS,
|
SHIFT_LABELS,
|
||||||
_blank_overtime_intervals,
|
_blank_overtime_intervals,
|
||||||
|
_device_segment_ranges,
|
||||||
_iter_shift_base_dates,
|
_iter_shift_base_dates,
|
||||||
_overlap_minutes,
|
_overlap_minutes,
|
||||||
_schedule_interval,
|
_schedule_interval,
|
||||||
@ -126,6 +127,38 @@ 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 _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())
|
||||||
|
|
||||||
|
|
||||||
|
def _target_effective_minutes(report, schedule: WorkScheduleConfig | None = None) -> float:
|
||||||
|
start_at = getattr(report, "start_at")
|
||||||
|
end_at = getattr(report, "end_at")
|
||||||
|
segment_ranges = _device_segment_ranges(start_at, end_at, _report_devices(report))
|
||||||
|
if segment_ranges:
|
||||||
|
return sum(
|
||||||
|
_minutes_total(_range_allocation_minutes(segment_start, segment_end, schedule))
|
||||||
|
for ranges in segment_ranges.values()
|
||||||
|
for segment_start, segment_end in ranges
|
||||||
|
)
|
||||||
|
return _minutes_total(_range_allocation_minutes(start_at, end_at, schedule))
|
||||||
|
|
||||||
|
|
||||||
|
def _scale_minutes_to_target(
|
||||||
|
minutes_by_item: dict[int, dict[date, dict[str, float]]],
|
||||||
|
target_effective_minutes: float,
|
||||||
|
) -> None:
|
||||||
|
raw_total = sum(_minutes_total(item_minutes) for item_minutes in minutes_by_item.values())
|
||||||
|
if raw_total <= 0 or abs(raw_total - target_effective_minutes) <= 0.000001:
|
||||||
|
return
|
||||||
|
|
||||||
|
scale = target_effective_minutes / raw_total
|
||||||
|
for item_minutes in minutes_by_item.values():
|
||||||
|
for minutes in item_minutes.values():
|
||||||
|
for key in SHIFT_BUCKETS:
|
||||||
|
minutes[key] = minutes.get(key, 0.0) * scale
|
||||||
|
|
||||||
|
|
||||||
def _fallback_row(report, item) -> ReportAllocationDraft:
|
def _fallback_row(report, item) -> ReportAllocationDraft:
|
||||||
return ReportAllocationDraft(
|
return ReportAllocationDraft(
|
||||||
report_id=getattr(report, "id", None),
|
report_id=getattr(report, "id", None),
|
||||||
@ -179,6 +212,7 @@ def build_report_allocation_drafts(report, schedule: WorkScheduleConfig | None =
|
|||||||
target[allocation_date][key] += minutes.get(key, 0.0) * as_float(
|
target[allocation_date][key] += minutes.get(key, 0.0) * as_float(
|
||||||
getattr(item_range, "allocation_weight", 1.0)
|
getattr(item_range, "allocation_weight", 1.0)
|
||||||
)
|
)
|
||||||
|
_scale_minutes_to_target(minutes_by_item, _target_effective_minutes(report, schedule))
|
||||||
|
|
||||||
rows: list[ReportAllocationDraft] = []
|
rows: list[ReportAllocationDraft] = []
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|||||||
@ -238,6 +238,30 @@ def test_blank_overtime_meal_deduction_is_clipped_like_metrics():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_same_device_item_split_inside_meal_is_rescaled_to_metrics_effective_minutes():
|
||||||
|
start_at = datetime(2026, 7, 25, 8, 0)
|
||||||
|
second_started_at = datetime(2026, 7, 25, 12, 0)
|
||||||
|
end_at = datetime(2026, 7, 25, 13, 0)
|
||||||
|
first_item = _item(id=1, good_qty=192, started_at=start_at)
|
||||||
|
second_item = _item(id=2, good_qty=48, started_at=second_started_at)
|
||||||
|
report = _report(start_at, end_at, [first_item, second_item], devices=[_device(start_at)])
|
||||||
|
|
||||||
|
metrics = calculate_report_metrics(start_at, end_at, [first_item, second_item], devices=[_device(start_at)])
|
||||||
|
rows = build_report_allocation_drafts(report)
|
||||||
|
|
||||||
|
assert metrics["effective_minutes"] == 240
|
||||||
|
assert metrics["shift_day_minutes"] == 240
|
||||||
|
assert {first_item.id: first_item.allocated_minutes, second_item.id: second_item.allocated_minutes} == {
|
||||||
|
1: 192,
|
||||||
|
2: 48,
|
||||||
|
}
|
||||||
|
assert [(row.report_item_id, row.effective_minutes, row.day_minutes) for row in rows] == [
|
||||||
|
(1, 192, 192),
|
||||||
|
(2, 48, 48),
|
||||||
|
]
|
||||||
|
assert sum(row.effective_minutes for row in rows) == 240
|
||||||
|
|
||||||
|
|
||||||
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(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user