fix: 平衡报工归属拆分并约束唯一性
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import ProductionReportAllocation
|
||||
from app.models import ProductionReport, ProductionReportAllocation
|
||||
from app.services.common import as_float, round2
|
||||
from app.services.metrics import (
|
||||
SHIFT_BUCKETS,
|
||||
@@ -429,6 +429,91 @@ def _draft_from_minutes(report, item, allocation_date: date, minutes: dict[str,
|
||||
)
|
||||
|
||||
|
||||
SPLIT_RESIDUAL_FIELDS = (
|
||||
"good_qty",
|
||||
"defect_qty",
|
||||
"scrap_qty",
|
||||
"changeover_count",
|
||||
"reference_wage",
|
||||
)
|
||||
|
||||
|
||||
def _item_split_targets(item) -> dict[str, float]:
|
||||
return {
|
||||
"good_qty": round2(getattr(item, "good_qty", 0)),
|
||||
"defect_qty": round2(getattr(item, "defect_qty", 0)),
|
||||
"scrap_qty": round2(getattr(item, "scrap_qty", 0)),
|
||||
"changeover_count": round2(getattr(item, "changeover_count", 0)),
|
||||
"reference_wage": round2(_item_reference_wage(item)),
|
||||
}
|
||||
|
||||
|
||||
def _balance_item_split_residuals(
|
||||
drafts: list[ReportAllocationDraft],
|
||||
targets: dict[str, float],
|
||||
) -> list[ReportAllocationDraft]:
|
||||
if not drafts:
|
||||
return drafts
|
||||
|
||||
mutable_values = [
|
||||
{field: int(round(as_float(getattr(draft, field)) * 100)) for field in SPLIT_RESIDUAL_FIELDS}
|
||||
for draft in drafts
|
||||
]
|
||||
|
||||
for field in SPLIT_RESIDUAL_FIELDS:
|
||||
target_cents = int(round(round2(targets[field]) * 100))
|
||||
current_cents = sum(values[field] for values in mutable_values)
|
||||
residual = target_cents - current_cents
|
||||
if residual == 0:
|
||||
continue
|
||||
|
||||
if residual > 0:
|
||||
ordered_indexes = sorted(
|
||||
range(len(drafts)),
|
||||
key=lambda index: (
|
||||
-as_float(drafts[index].effective_minutes),
|
||||
drafts[index].allocation_date.isoformat(),
|
||||
index,
|
||||
),
|
||||
)
|
||||
else:
|
||||
ordered_indexes = sorted(
|
||||
range(len(drafts)),
|
||||
key=lambda index: (
|
||||
-mutable_values[index][field],
|
||||
-as_float(drafts[index].effective_minutes),
|
||||
drafts[index].allocation_date.isoformat(),
|
||||
index,
|
||||
),
|
||||
)
|
||||
|
||||
step = 1 if residual > 0 else -1
|
||||
remaining = abs(residual)
|
||||
while remaining > 0:
|
||||
changed = False
|
||||
for index in ordered_indexes:
|
||||
if step < 0 and mutable_values[index][field] <= 0:
|
||||
continue
|
||||
mutable_values[index][field] += step
|
||||
remaining -= 1
|
||||
changed = True
|
||||
if remaining == 0:
|
||||
break
|
||||
if not changed:
|
||||
break
|
||||
|
||||
return [
|
||||
replace(
|
||||
draft,
|
||||
**{
|
||||
field: mutable_values[index][field] / 100
|
||||
for field in SPLIT_RESIDUAL_FIELDS
|
||||
},
|
||||
)
|
||||
for index, draft in enumerate(drafts)
|
||||
]
|
||||
|
||||
|
||||
def build_report_allocation_drafts(report, schedule: WorkScheduleConfig | None = None) -> list[ReportAllocationDraft]:
|
||||
items = list(getattr(report, "items", []) or [])
|
||||
if not items:
|
||||
@@ -452,12 +537,14 @@ def build_report_allocation_drafts(report, schedule: WorkScheduleConfig | None =
|
||||
if total_effective <= 0:
|
||||
rows.append(_fallback_row(report, item))
|
||||
continue
|
||||
item_rows: list[ReportAllocationDraft] = []
|
||||
for allocation_date in sorted(item_minutes):
|
||||
minutes = item_minutes[allocation_date]
|
||||
effective = sum(minutes.get(key, 0.0) for key in SHIFT_BUCKETS)
|
||||
if effective <= 0:
|
||||
continue
|
||||
rows.append(_draft_from_minutes(report, item, allocation_date, minutes, effective / total_effective))
|
||||
item_rows.append(_draft_from_minutes(report, item, allocation_date, minutes, effective / total_effective))
|
||||
rows.extend(_balance_item_split_residuals(item_rows, _item_split_targets(item)))
|
||||
return rows
|
||||
|
||||
|
||||
@@ -489,7 +576,14 @@ 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")))
|
||||
report_id = getattr(report, "id")
|
||||
if isinstance(report_id, int):
|
||||
db.execute(
|
||||
select(ProductionReport.id)
|
||||
.where(ProductionReport.id == report_id)
|
||||
.with_for_update()
|
||||
).scalar_one_or_none()
|
||||
db.execute(delete(ProductionReportAllocation).where(ProductionReportAllocation.report_id == report_id))
|
||||
rows = [
|
||||
ProductionReportAllocation(
|
||||
report_id=draft.report_id,
|
||||
|
||||
Reference in New Issue
Block a user