fix: 保留未填报设备段归属工时
This commit is contained in:
parent
2084f834e3
commit
5918b5ddde
@ -184,6 +184,25 @@ def _item_bucket_totals(minutes_by_item: dict[int, dict[date, dict[str, float]]]
|
|||||||
return totals
|
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(
|
def _scale_item_minutes_to_bucket_targets(
|
||||||
minutes_by_item: dict[int, dict[date, dict[str, float]]],
|
minutes_by_item: dict[int, dict[date, dict[str, float]]],
|
||||||
target_buckets: 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)
|
row[key] += minutes.get(key, 0.0) * (remaining / raw_unassigned) / len(unmatched_items)
|
||||||
return minutes_by_item
|
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))
|
_scale_item_minutes_to_bucket_targets(minutes_by_item, _bucket_totals(target_minutes))
|
||||||
return minutes_by_item
|
return minutes_by_item
|
||||||
|
|
||||||
@ -468,6 +488,7 @@ def refresh_report_allocations(
|
|||||||
*,
|
*,
|
||||||
commit: bool = False,
|
commit: bool = False,
|
||||||
) -> list[ProductionReportAllocation]:
|
) -> list[ProductionReportAllocation]:
|
||||||
|
"""Rebuild allocation rows with metrics semantics, updating item allocation-derived fields."""
|
||||||
db.execute(delete(ProductionReportAllocation).where(ProductionReportAllocation.report_id == getattr(report, "id")))
|
db.execute(delete(ProductionReportAllocation).where(ProductionReportAllocation.report_id == getattr(report, "id")))
|
||||||
rows = [
|
rows = [
|
||||||
ProductionReportAllocation(
|
ProductionReportAllocation(
|
||||||
|
|||||||
@ -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():
|
def test_night_allocation_uses_meal_deducted_minutes_with_previous_date_rules():
|
||||||
schedule = WorkScheduleConfig(
|
schedule = WorkScheduleConfig(
|
||||||
day_start="08:00",
|
day_start="08:00",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user