JhHardwareWRS_BackPoint/app/services/serializers.py
2026-06-24 15:19:14 +08:00

491 lines
19 KiB
Python

from datetime import datetime
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models import (
AttendancePoint,
AuditAction,
Equipment,
PersonRole,
Personnel,
Product,
ProductionReport,
ProductionReportItem,
WorkSession,
WorkSessionDevice,
)
from app.schemas import (
AttendancePointOut,
ClockSessionOut,
EquipmentOption,
EquipmentOut,
PersonnelOut,
ProductOption,
ProductOut,
ReportDeviceSegmentOut,
ReportItemOut,
ReportMetrics,
ReportOut,
)
from app.services.cleaning import is_cleaning_item, is_cleaning_product
from app.services.common import REPORT_STATUS_NAMES, ROLE_NAMES, as_float
from app.services.continuous_die import is_continuous_die_item, is_continuous_die_product
from app.services.display_names import mold_process_display_name
from app.services.metrics import build_result_text, calculate_report_metrics
from app.services.misc_work import is_misc_item, is_misc_product
from app.services.multi_person import is_multi_person_item, is_multi_person_product
from app.services.process_names import export_process_name
from app.services.report_lifecycle import can_edit_report, can_unvoid_report
from app.services.work_schedule import WorkScheduleConfig
from app.timezone import now
def temporary_expired(person: Personnel) -> bool:
return bool(
person.is_temporary
and (person.temporary_expires_at is None or person.temporary_expires_at <= now())
)
def attendance_point_out(point: AttendancePoint) -> AttendancePointOut:
return AttendancePointOut(
name=point.name,
latitude=None if point.latitude is None else float(point.latitude),
longitude=None if point.longitude is None else float(point.longitude),
radius_meters=int(point.radius_meters or 500),
day_start=point.day_start,
day_end=point.day_end,
lunch_start=point.lunch_start,
lunch_end=point.lunch_end,
dinner_start=point.dinner_start,
dinner_end=point.dinner_end,
overtime_start=point.overtime_start,
overtime_end=point.overtime_end,
night_start=point.night_start,
night_end=point.night_end,
remark=point.remark,
is_active=bool(point.is_active),
created_at=point.created_at,
updated_at=point.updated_at,
)
def personnel_out(person: Personnel, role=None) -> PersonnelOut:
selected_role = role if role is not None else getattr(person, "role", None)
roles = sorted([item.role for item in person.roles], key=lambda item: str(item))
points = sorted(
[item.attendance_point for item in getattr(person, "attendance_points", []) if item.attendance_point is not None],
key=lambda item: item.name,
)
return PersonnelOut(
phone=person.phone,
name=person.name,
role=selected_role,
role_name=ROLE_NAMES.get(str(selected_role), str(selected_role)),
roles=roles,
role_names=[ROLE_NAMES.get(str(item), str(item)) for item in roles],
attendance_point_names=[point.name for point in points],
attendance_points=[attendance_point_out(point) for point in points],
is_temporary=bool(person.is_temporary),
temporary_expires_at=person.temporary_expires_at,
temporary_expired=temporary_expired(person),
)
def person_role_out(person_role: PersonRole) -> PersonnelOut:
return personnel_out(person_role.person, person_role.role)
def personnel_summary_out(person: Personnel) -> dict:
roles = sorted([str(item.role) for item in person.roles])
points = sorted(
[item.attendance_point for item in getattr(person, "attendance_points", []) if item.attendance_point is not None],
key=lambda item: item.name,
)
return {
"phone": person.phone,
"name": person.name,
"roles": roles,
"role_names": [ROLE_NAMES.get(role, role) for role in roles],
"role": roles[0] if roles else "",
"role_name": "".join(ROLE_NAMES.get(role, role) for role in roles),
"attendance_point_names": [point.name for point in points],
"attendance_points": [attendance_point_out(point) for point in points],
"is_temporary": bool(person.is_temporary),
"temporary_expires_at": person.temporary_expires_at,
"temporary_expired": temporary_expired(person),
}
def product_out(product: Product) -> ProductOut:
optional_float = lambda value: None if value is None else as_float(value)
return ProductOut(
attendance_point_name=product.attendance_point_name,
project_no=product.project_no,
product_name=product.product_name,
profile_no=product.profile_no,
material_code=product.material_code,
material_name=product.material_name,
supplier=product.supplier,
product_net_weight_kg=optional_float(product.product_net_weight_kg),
product_gross_weight_kg=optional_float(product.product_gross_weight_kg),
scrap_loss_rate=optional_float(product.scrap_loss_rate),
waste_price_yuan_per_kg=optional_float(product.waste_price_yuan_per_kg),
device_no=product.device_no,
process_name=product.process_name,
stamping_method=product.stamping_method,
operator_count=as_float(product.operator_count),
process_unit_price_yuan=as_float(product.process_unit_price_yuan),
standard_beat=as_float(product.standard_beat),
standard_workload=as_float(product.standard_workload),
display_name=f"{product.project_no} / {mold_process_display_name(product.product_name, product.process_name, product.stamping_method)}",
)
def product_option(product: Product) -> ProductOption:
data = product_out(product).model_dump()
data["is_cleaning"] = is_cleaning_product(product)
data["is_misc"] = is_misc_product(product)
data["is_continuous_die"] = is_continuous_die_product(product)
data["is_multi_person"] = is_multi_person_product(product)
return ProductOption(**data)
def equipment_out(equipment: Equipment) -> EquipmentOut:
return EquipmentOut(
attendance_point_name=equipment.attendance_point_name,
device_no=equipment.device_no,
device_type=equipment.device_type or "冲压设备",
remark=equipment.remark,
created_at=equipment.created_at,
updated_at=equipment.updated_at,
)
def equipment_option(equipment: Equipment) -> EquipmentOption:
display_name = equipment.device_no
if equipment.remark:
display_name = f"{equipment.device_no} / {equipment.remark}"
return EquipmentOption(
attendance_point_name=equipment.attendance_point_name,
device_no=equipment.device_no,
device_type=equipment.device_type or "冲压设备",
remark=equipment.remark,
display_name=display_name,
)
def _session_device_stamping_method(
db: Session | None,
session: WorkSession,
device: WorkSessionDevice,
) -> str | None:
if db is None:
return None
point_name = device.attendance_point_name or session.attendance_point_name
product = db.scalar(
select(Product)
.where(
Product.attendance_point_name == point_name,
Product.product_name == device.device_no,
Product.process_name == (device.process_name or ""),
Product.device_no == "",
)
.order_by(Product.project_no.asc())
)
return product.stamping_method if product else None
def session_out(session: WorkSession, db: Session | None = None) -> ClockSessionOut:
return ClockSessionOut(
id=session.id,
attendance_point_name=session.attendance_point_name,
employee_phone=session.employee_phone,
start_at=session.start_at,
end_at=session.end_at,
status=str(session.status),
devices=[
mold_process_display_name(
device.device_no,
device.process_name,
_session_device_stamping_method(db, session, device),
)
for device in session.devices
],
)
def _changed(old_value, new_value) -> bool:
return str(old_value or "") != str(new_value or "")
def _parse_datetime(value) -> datetime | None:
if isinstance(value, datetime):
return value
if not value:
return None
try:
return datetime.fromisoformat(str(value))
except ValueError:
return None
def _datetime_changed(old_value, new_value) -> bool:
old_dt = _parse_datetime(old_value)
new_dt = _parse_datetime(new_value)
if old_dt is None or new_dt is None:
return _changed(old_value, new_value)
if old_dt.replace(second=0, microsecond=0) == new_dt.replace(second=0, microsecond=0):
return False
return old_dt != new_dt
def _numeric_changed(old_value, new_value) -> bool:
return round(as_float(old_value), 4) != round(as_float(new_value), 4)
def _process_name_changed(old_value, new_value) -> bool:
return export_process_name(old_value) != export_process_name(new_value)
def _ordered_session_devices(session: WorkSession | None) -> list[WorkSessionDevice]:
return sorted(
getattr(session, "devices", []) or [],
key=lambda device: (int(device.sort_order or 0), device.scanned_at, int(device.id or 0)),
)
def report_correction_maps(report: ProductionReport) -> tuple[dict, dict[int, dict], dict[int, dict]]:
logs = [
log for log in getattr(report, "audit_logs", [])
if log.action == AuditAction.approve_with_correction and log.before_json
]
if not logs:
return {}, {}, {}
log = sorted(logs, key=lambda item: item.created_at)[-1]
before = log.before_json or {}
report_corrections = {}
if _datetime_changed(before.get("start_at"), report.start_at.isoformat()):
report_corrections["start_at"] = before.get("start_at")
if _datetime_changed(before.get("end_at"), report.end_at.isoformat()):
report_corrections["end_at"] = before.get("end_at")
if _changed(before.get("review_remark"), report.review_remark):
report_corrections["review_remark"] = before.get("review_remark")
before_items = {
item.get("id"): item
for item in before.get("items", [])
if item.get("id") is not None
}
item_corrections: dict[int, dict] = {}
text_fields = [
"device_no",
"project_no",
"product_name",
"raw_material_batch_no",
"process_name",
"stamping_method",
"remark",
]
numeric_fields = ["process_unit_price_yuan", "changeover_count", "good_qty", "defect_qty", "scrap_qty"]
for item in report.items:
old = before_items.get(item.id)
if not old:
continue
changes = {}
for field in text_fields:
if field == "process_name":
if _process_name_changed(old.get(field), getattr(item, field)):
changes[field] = export_process_name(old.get(field))
elif _changed(old.get(field), getattr(item, field)):
changes[field] = old.get(field)
for field in numeric_fields:
if _numeric_changed(old.get(field), getattr(item, field)):
changes[field] = as_float(old.get(field))
if changes:
item_corrections[item.id] = changes
before_devices = {
device.get("id"): device
for device in before.get("devices", [])
if device.get("id") is not None
}
device_corrections: dict[int, dict] = {}
for device in _ordered_session_devices(report.session if report.session else None):
old = before_devices.get(device.id)
if old and _datetime_changed(old.get("scanned_at"), device.scanned_at.isoformat()):
device_corrections[device.id] = {"scanned_at": old.get("scanned_at")}
return report_corrections, item_corrections, device_corrections
def report_device_segment_out(
device: WorkSessionDevice,
corrections: dict | None = None,
stamping_method: str | None = None,
) -> ReportDeviceSegmentOut:
process_name = device.process_name or ""
display_name = mold_process_display_name(device.device_no, process_name, stamping_method)
return ReportDeviceSegmentOut(
id=device.id,
attendance_point_name=device.attendance_point_name,
device_no=device.device_no,
mold_name=device.device_no,
process_name=process_name,
stamping_method=stamping_method,
display_name=display_name,
scanned_at=device.scanned_at,
released_at=device.released_at,
sort_order=int(device.sort_order or 0),
corrections=corrections or {},
)
def report_item_out(item: ProductionReportItem, corrections: dict | None = None) -> ReportItemOut:
return ReportItemOut(
id=item.id,
attendance_point_name=item.attendance_point_name,
device_no=item.device_no,
project_no=item.project_no,
product_name=item.product_name,
material_code=item.material_code,
material_name=item.material_name,
raw_material_batch_no=item.raw_material_batch_no,
process_name=item.process_name,
stamping_method=item.stamping_method,
operator_count=as_float(item.operator_count),
process_unit_price_yuan=as_float(item.process_unit_price_yuan),
changeover_count=as_float(item.changeover_count),
standard_beat=as_float(item.standard_beat),
standard_workload=as_float(item.standard_workload),
good_qty=as_float(item.good_qty),
defect_qty=as_float(item.defect_qty),
scrap_qty=as_float(item.scrap_qty),
allocated_minutes=as_float(item.allocated_minutes),
started_at=item.started_at,
is_misc=is_misc_item(item),
is_continuous_die=is_continuous_die_item(item),
is_multi_person=is_multi_person_item(item),
remark=item.remark,
corrections=corrections or {},
)
def _report_is_cleaning(report: ProductionReport) -> bool:
return bool(report.items) and all(is_cleaning_item(item) for item in report.items)
def _report_is_misc_only(report: ProductionReport) -> bool:
return bool(report.items) and all(is_misc_item(item) for item in report.items)
def _cleaning_report_metrics(report: ProductionReport) -> dict:
total_good_qty = sum(as_float(item.good_qty) for item in report.items)
total_output_qty = sum(
as_float(item.good_qty) + as_float(item.defect_qty) + as_float(item.scrap_qty)
for item in report.items
)
return {
"duration_minutes": 0,
"effective_minutes": 0,
"shift_distribution_text": "清洗报工不计工时",
"shift_day_minutes": 0,
"shift_overtime_minutes": 0,
"shift_night_minutes": 0,
"shift_other_minutes": 0,
"shift_meal_break_minutes": 0,
"total_good_qty": total_good_qty,
"total_output_qty": total_output_qty,
"actual_beat": 0,
"standard_beat": 0,
"expected_workload": 0,
"pace_rate": 0,
"workload_rate": 0,
}
def report_out(report: ProductionReport, schedule: WorkScheduleConfig | None = None) -> ReportOut:
is_cleaning_report = _report_is_cleaning(report)
calculated_metrics = (
_cleaning_report_metrics(report)
if is_cleaning_report
else calculate_report_metrics(
report.start_at,
report.end_at,
list(report.items),
devices=report.session.devices if report.session else None,
schedule=schedule,
)
)
metrics = {
"effective_minutes": as_float(calculated_metrics["effective_minutes"]),
"shift_distribution_text": calculated_metrics["shift_distribution_text"],
"shift_day_minutes": as_float(calculated_metrics["shift_day_minutes"]),
"shift_overtime_minutes": as_float(calculated_metrics["shift_overtime_minutes"]),
"shift_night_minutes": as_float(calculated_metrics["shift_night_minutes"]),
"shift_other_minutes": as_float(calculated_metrics["shift_other_minutes"]),
"shift_meal_break_minutes": as_float(calculated_metrics["shift_meal_break_minutes"]),
"total_good_qty": as_float(calculated_metrics["total_good_qty"]),
"total_output_qty": as_float(calculated_metrics["total_output_qty"]),
"actual_beat": as_float(calculated_metrics["actual_beat"]),
"standard_beat": as_float(calculated_metrics["standard_beat"]),
"expected_workload": as_float(calculated_metrics["expected_workload"]),
"pace_rate": as_float(calculated_metrics["pace_rate"]),
"workload_rate": as_float(calculated_metrics["workload_rate"]),
}
report_corrections, item_corrections, device_corrections = report_correction_maps(report)
is_modified = bool(report_corrections or item_corrections or device_corrections)
stamping_by_mold = {
(item.product_name, item.process_name or ""): item.stamping_method
for item in report.items
}
return ReportOut(
id=report.id,
attendance_point_name=report.attendance_point_name,
session_id=report.session_id,
employee_phone=report.employee_phone,
employee_name=report.employee.name if report.employee else "",
report_date=report.report_date,
start_at=report.start_at,
end_at=report.end_at,
duration_minutes=as_float(calculated_metrics["duration_minutes"]),
break_minutes=as_float(report.break_minutes),
status=report.status,
status_name=REPORT_STATUS_NAMES.get(str(report.status), str(report.status)),
reviewer_phone=report.reviewer_phone,
reviewer_name=report.reviewer.name if report.reviewer else None,
reviewed_at=report.reviewed_at,
reject_reason=report.reject_reason,
review_remark=report.review_remark,
submitted_at=report.submitted_at,
is_system_auto_submitted=bool(report.is_system_auto_submitted),
auto_submit_reason=report.auto_submit_reason,
is_multi_person_assistant=bool(report.is_multi_person_assistant),
multi_person_source_report_id=report.multi_person_source_report_id,
is_voided=bool(report.is_voided),
voided_at=report.voided_at,
voided_by=report.voided_by,
voided_by_name=report.voider.name if report.voider else None,
unvoid_deadline_at=report.unvoid_deadline_at,
can_void=not bool(report.is_voided),
can_unvoid=can_unvoid_report(report),
can_edit=can_edit_report(report),
is_modified=is_modified,
device_segments=[
report_device_segment_out(
device,
device_corrections.get(device.id, {}),
stamping_by_mold.get((device.device_no, device.process_name or "")),
)
for device in _ordered_session_devices(report.session if report.session else None)
],
items=[report_item_out(item, item_corrections.get(item.id, {})) for item in report.items],
metrics=ReportMetrics(**metrics),
result_text=(
"清洗报工已提交,等待管理员审核"
if is_cleaning_report
else ("处理杂活已提交,等待管理员审核" if _report_is_misc_only(report) else build_result_text(metrics))
),
corrections=report_corrections,
)