JhHardwareWRS_BackPoint/app/services/report_allocations.py
2026-07-25 03:25:42 +08:00

285 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from collections import defaultdict
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from sqlalchemy import delete
from sqlalchemy.orm import Session
from app.models import ProductionReportAllocation
from app.services.common import as_float, round2
from app.services.metrics import (
SHIFT_BUCKETS,
SHIFT_LABELS,
_blank_overtime_intervals,
_device_segment_ranges,
_iter_shift_base_dates,
_overlap_minutes,
_schedule_interval,
format_hours,
item_time_ranges,
minutes_between,
)
from app.services.work_schedule import DEFAULT_WORK_SCHEDULE_CONFIG, WorkScheduleConfig
@dataclass(frozen=True)
class ReportAllocationDraft:
report_id: int | None
report_item_id: int | None
attendance_point_name: str
employee_phone: str
allocation_date: date
day_minutes: float = 0
overtime_minutes: float = 0
night_minutes: float = 0
effective_minutes: float = 0
good_qty: float = 0
defect_qty: float = 0
scrap_qty: float = 0
changeover_count: float = 0
reference_wage: float = 0
def _add_minutes(
rows: dict[date, dict[str, float]],
allocation_date: date,
bucket: str,
minutes: float,
) -> None:
if minutes <= 0:
return
rows[allocation_date][bucket] += minutes
def _range_allocation_minutes(
start_at: datetime,
end_at: datetime,
schedule: WorkScheduleConfig | None = None,
) -> dict[date, dict[str, float]]:
active_schedule = schedule or DEFAULT_WORK_SCHEDULE_CONFIG
rows: dict[date, dict[str, float]] = defaultdict(lambda: {key: 0.0 for key in SHIFT_BUCKETS})
if end_at <= start_at:
return rows
for current_date in _iter_shift_base_dates(start_at, end_at):
meal_intervals = [
_schedule_interval(current_date, active_schedule.lunch_start, active_schedule.lunch_end),
_schedule_interval(current_date, active_schedule.dinner_start, active_schedule.dinner_end),
]
shift_intervals = {
"day": [_schedule_interval(current_date, active_schedule.day_start, active_schedule.day_end)],
"overtime": [
_schedule_interval(current_date, active_schedule.overtime_start, active_schedule.overtime_end)
],
"night": [_schedule_interval(current_date, active_schedule.night_start, active_schedule.night_end)],
}
for bucket, intervals in shift_intervals.items():
for interval_start, interval_end in intervals:
overlap_start = max(start_at, interval_start)
overlap_end = min(end_at, interval_end)
if overlap_end <= overlap_start:
continue
meal_minutes = sum(
_overlap_minutes(overlap_start, overlap_end, meal_start, meal_end)
for meal_start, meal_end in meal_intervals
if (
start_at <= meal_start
and end_at >= meal_end
and end_at > meal_end + timedelta(minutes=5)
)
)
minutes = max(0.0, minutes_between(overlap_start, overlap_end) - meal_minutes)
if bucket == "night":
_add_minutes(rows, current_date, "night", minutes)
else:
_add_minutes(rows, overlap_start.date(), bucket, minutes)
for blank_start, blank_end in _blank_overtime_intervals(current_date, active_schedule):
overlap_start = max(start_at, blank_start)
overlap_end = min(end_at, blank_end)
if overlap_end <= overlap_start:
continue
meal_minutes = sum(
_overlap_minutes(overlap_start, overlap_end, meal_start, meal_end)
for meal_start, meal_end in meal_intervals
if (
start_at <= meal_start
and end_at >= meal_end
and end_at > meal_end + timedelta(minutes=5)
)
)
minutes = max(0.0, minutes_between(overlap_start, overlap_end) - meal_minutes)
_add_minutes(rows, overlap_start.date(), "overtime", minutes)
return rows
def _report_devices(report) -> list | None:
session = getattr(report, "session", None)
devices = getattr(session, "devices", None)
if devices is not None:
return list(devices)
devices = getattr(report, "devices", None)
return list(devices) if devices is not None else None
def _item_reference_wage(item) -> float:
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:
return ReportAllocationDraft(
report_id=getattr(report, "id", None),
report_item_id=getattr(item, "id", None),
attendance_point_name=str(getattr(report, "attendance_point_name", "") or ""),
employee_phone=str(getattr(report, "employee_phone", "") or ""),
allocation_date=getattr(report, "report_date"),
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 _draft_from_minutes(report, item, allocation_date: date, minutes: dict[str, float], ratio: float) -> ReportAllocationDraft:
reference_wage = _item_reference_wage(item)
day_minutes = round2(minutes.get("day", 0))
overtime_minutes = round2(minutes.get("overtime", 0))
night_minutes = round2(minutes.get("night", 0))
return ReportAllocationDraft(
report_id=getattr(report, "id", None),
report_item_id=getattr(item, "id", None),
attendance_point_name=str(getattr(report, "attendance_point_name", "") or ""),
employee_phone=str(getattr(report, "employee_phone", "") or ""),
allocation_date=allocation_date,
day_minutes=day_minutes,
overtime_minutes=overtime_minutes,
night_minutes=night_minutes,
effective_minutes=round2(day_minutes + overtime_minutes + night_minutes),
good_qty=round2(as_float(getattr(item, "good_qty", 0)) * ratio),
defect_qty=round2(as_float(getattr(item, "defect_qty", 0)) * ratio),
scrap_qty=round2(as_float(getattr(item, "scrap_qty", 0)) * ratio),
changeover_count=round2(as_float(getattr(item, "changeover_count", 0)) * ratio),
reference_wage=round2(reference_wage * ratio),
)
def build_report_allocation_drafts(report, schedule: WorkScheduleConfig | None = None) -> list[ReportAllocationDraft]:
items = list(getattr(report, "items", []) or [])
if not items:
return []
ranges = item_time_ranges(getattr(report, "start_at"), getattr(report, "end_at"), items, _report_devices(report))
minutes_by_item: dict[int, dict[date, dict[str, float]]] = {}
for item_range in ranges:
item_key = id(item_range.item)
target = minutes_by_item.setdefault(item_key, defaultdict(lambda: {key: 0.0 for key in SHIFT_BUCKETS}))
for allocation_date, minutes in _range_allocation_minutes(item_range.start_at, item_range.end_at, schedule).items():
for key in SHIFT_BUCKETS:
target[allocation_date][key] += minutes.get(key, 0.0) * as_float(
getattr(item_range, "allocation_weight", 1.0)
)
_scale_minutes_to_target(minutes_by_item, _target_effective_minutes(report, schedule))
rows: list[ReportAllocationDraft] = []
for item in items:
item_minutes = minutes_by_item.get(id(item), {})
total_effective = sum(sum(minutes.get(key, 0.0) for key in SHIFT_BUCKETS) for minutes in item_minutes.values())
if total_effective <= 0:
rows.append(_fallback_row(report, item))
continue
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))
return rows
def allocation_summary_text(rows) -> str:
grouped: dict[date, dict[str, float]] = defaultdict(lambda: {key: 0.0 for key in SHIFT_BUCKETS})
for row in rows:
allocation_date = getattr(row, "allocation_date")
grouped[allocation_date]["day"] += as_float(getattr(row, "day_minutes", 0))
grouped[allocation_date]["overtime"] += as_float(getattr(row, "overtime_minutes", 0))
grouped[allocation_date]["night"] += as_float(getattr(row, "night_minutes", 0))
parts: list[str] = []
for allocation_date in sorted(grouped):
shift_text = "".join(
f"{SHIFT_LABELS[key]}{format_hours(grouped[allocation_date].get(key, 0))}小时"
for key in SHIFT_BUCKETS
if round2(grouped[allocation_date].get(key, 0)) > 0
)
if shift_text:
parts.append(f"{allocation_date.isoformat()} {shift_text}")
return "".join(parts)
def refresh_report_allocations(
db: Session,
report,
schedule: WorkScheduleConfig | None = None,
*,
commit: bool = False,
) -> list[ProductionReportAllocation]:
db.execute(delete(ProductionReportAllocation).where(ProductionReportAllocation.report_id == getattr(report, "id")))
rows = [
ProductionReportAllocation(
report_id=draft.report_id,
report_item_id=draft.report_item_id,
attendance_point_name=draft.attendance_point_name,
employee_phone=draft.employee_phone,
allocation_date=draft.allocation_date,
day_minutes=draft.day_minutes,
overtime_minutes=draft.overtime_minutes,
night_minutes=draft.night_minutes,
effective_minutes=draft.effective_minutes,
good_qty=draft.good_qty,
defect_qty=draft.defect_qty,
scrap_qty=draft.scrap_qty,
changeover_count=draft.changeover_count,
reference_wage=draft.reference_wage,
)
for draft in build_report_allocation_drafts(report, schedule)
]
db.add_all(rows)
db.flush()
if commit:
db.commit()
return rows