feat: 经理看板按归属日期汇总
This commit is contained in:
parent
bb9ce5601c
commit
b4fd3388c3
@ -12,6 +12,7 @@ from app.models import (
|
|||||||
Personnel,
|
Personnel,
|
||||||
Product,
|
Product,
|
||||||
ProductionReport,
|
ProductionReport,
|
||||||
|
ProductionReportAllocation,
|
||||||
ProductionReportItem,
|
ProductionReportItem,
|
||||||
ReportStatus,
|
ReportStatus,
|
||||||
Role,
|
Role,
|
||||||
@ -22,12 +23,11 @@ from app.services.cleaning import is_cleaning_item, is_cleaning_product
|
|||||||
from app.services.common import as_float, round1, round2
|
from app.services.common import as_float, round1, round2
|
||||||
from app.services.continuous_die import is_continuous_die_item, is_continuous_die_product
|
from app.services.continuous_die import is_continuous_die_item, is_continuous_die_product
|
||||||
from app.services.excel_export import export_dashboard_rows
|
from app.services.excel_export import export_dashboard_rows
|
||||||
from app.services.metrics import calculate_report_metrics, shift_distribution_text
|
from app.services.metrics import shift_distribution_text
|
||||||
from app.services.misc_work import is_misc_item, is_misc_product
|
from app.services.misc_work import is_misc_item, is_misc_product
|
||||||
from app.services.multi_person import is_multi_person_item
|
from app.services.multi_person import is_multi_person_item
|
||||||
from app.services.report_lifecycle import purge_expired_voided_reports
|
from app.services.report_lifecycle import purge_expired_voided_reports
|
||||||
from app.services.serializers import report_correction_maps
|
from app.services.serializers import report_correction_maps
|
||||||
from app.services.work_schedule import get_work_schedule_config
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
||||||
|
|
||||||
@ -123,141 +123,194 @@ def _dashboard_rows(
|
|||||||
include_voided: bool = False,
|
include_voided: bool = False,
|
||||||
) -> list[DashboardRow]:
|
) -> list[DashboardRow]:
|
||||||
query = (
|
query = (
|
||||||
select(ProductionReport)
|
select(ProductionReportAllocation)
|
||||||
|
.join(ProductionReportAllocation.report)
|
||||||
|
.join(ProductionReportAllocation.item)
|
||||||
.options(
|
.options(
|
||||||
selectinload(ProductionReport.employee),
|
selectinload(ProductionReportAllocation.report).selectinload(ProductionReport.employee),
|
||||||
selectinload(ProductionReport.items),
|
selectinload(ProductionReportAllocation.report).selectinload(ProductionReport.items),
|
||||||
selectinload(ProductionReport.audit_logs),
|
selectinload(ProductionReportAllocation.report).selectinload(
|
||||||
selectinload(ProductionReport.session).selectinload(WorkSession.devices),
|
ProductionReport.audit_logs
|
||||||
|
),
|
||||||
|
selectinload(ProductionReportAllocation.report)
|
||||||
|
.selectinload(ProductionReport.session)
|
||||||
|
.selectinload(WorkSession.devices),
|
||||||
|
selectinload(ProductionReportAllocation.item),
|
||||||
)
|
)
|
||||||
.where(ProductionReport.status == ReportStatus.approved)
|
.where(ProductionReport.status == ReportStatus.approved)
|
||||||
)
|
)
|
||||||
if not include_voided:
|
if not include_voided:
|
||||||
query = query.where(ProductionReport.is_voided.is_(False))
|
query = query.where(ProductionReport.is_voided.is_(False))
|
||||||
if start_date:
|
if start_date:
|
||||||
query = query.where(ProductionReport.report_date >= start_date)
|
query = query.where(ProductionReportAllocation.allocation_date >= start_date)
|
||||||
if end_date:
|
if end_date:
|
||||||
query = query.where(ProductionReport.report_date <= end_date)
|
query = query.where(ProductionReportAllocation.allocation_date <= end_date)
|
||||||
reports = db.scalars(query).all()
|
allocations = db.scalars(query).all()
|
||||||
|
|
||||||
groups: dict[tuple, dict] = {}
|
|
||||||
product_cache: dict[tuple[str, str, str, str, str], Product | None] = {}
|
product_cache: dict[tuple[str, str, str, str, str], Product | None] = {}
|
||||||
schedule_cache = {}
|
for allocation in allocations:
|
||||||
for report in reports:
|
item = allocation.item
|
||||||
if report.attendance_point_name not in schedule_cache:
|
if item is not None:
|
||||||
schedule_cache[report.attendance_point_name] = get_work_schedule_config(db, report.attendance_point_name)
|
setattr(item, "_dashboard_product", _product_for_item(db, product_cache, item))
|
||||||
schedule = schedule_cache[report.attendance_point_name]
|
return _dashboard_rows_from_allocations(allocations)
|
||||||
report_corrections, item_corrections, _device_corrections = report_correction_maps(report)
|
|
||||||
report_metrics = calculate_report_metrics(
|
|
||||||
report.start_at,
|
def _dashboard_rows_from_allocations(allocations: list) -> list[DashboardRow]:
|
||||||
report.end_at,
|
groups: dict[tuple, dict] = {}
|
||||||
list(report.items),
|
correction_cache: dict[int, tuple[dict, dict[int, dict], dict[int, dict]]] = {}
|
||||||
devices=report.session.devices if report.session else None,
|
for allocation in allocations:
|
||||||
schedule=schedule,
|
report = getattr(allocation, "report", None)
|
||||||
)
|
item = getattr(allocation, "item", None)
|
||||||
item_count = max(1, len(report.items))
|
if report is None or item is None:
|
||||||
fallback_minutes = as_float(report.effective_minutes) / item_count
|
continue
|
||||||
|
report_id = getattr(report, "id", None)
|
||||||
|
report_key = report_id if report_id is not None else id(report)
|
||||||
|
if report_key not in correction_cache:
|
||||||
|
correction_cache[report_key] = report_correction_maps(report)
|
||||||
|
report_corrections, item_corrections, _device_corrections = correction_cache[report_key]
|
||||||
employee_name = report.employee.name if report.employee else ""
|
employee_name = report.employee.name if report.employee else ""
|
||||||
worker_type = "临时工" if (report.employee and report.employee.is_temporary) else "正式工"
|
worker_type = (
|
||||||
for item in report.items:
|
"临时工" if (report.employee and report.employee.is_temporary) else "正式工"
|
||||||
item_misc = is_misc_item(item)
|
)
|
||||||
process_name = (item.remark or item.process_name or "") if item_misc else (item.process_name or "")
|
item_misc = is_misc_item(item)
|
||||||
product = _product_for_item(db, product_cache, item)
|
process_name = (
|
||||||
is_cleaning = is_cleaning_item(item) or is_cleaning_product(product)
|
(item.remark or item.process_name or "") if item_misc else (item.process_name or "")
|
||||||
is_misc = item_misc or is_misc_product(product)
|
)
|
||||||
is_continuous_die = is_continuous_die_item(item) or is_continuous_die_product(product)
|
product = getattr(item, "_dashboard_product", None)
|
||||||
standard_beat = 0 if (is_cleaning or is_misc) else as_float(product.standard_beat if product else item.standard_beat)
|
is_cleaning = is_cleaning_item(item) or is_cleaning_product(product)
|
||||||
process_unit_price = as_float(item.process_unit_price_yuan)
|
is_misc = item_misc or is_misc_product(product)
|
||||||
stamping_method = item.stamping_method or (product.stamping_method if product else None)
|
is_continuous_die = is_continuous_die_item(item) or is_continuous_die_product(product)
|
||||||
key = (
|
item_standard_beat = getattr(item, "standard_beat", 0)
|
||||||
report.attendance_point_name,
|
standard_beat = (
|
||||||
report.report_date,
|
0
|
||||||
employee_name,
|
if (is_cleaning or is_misc)
|
||||||
report.employee_phone,
|
else as_float(product.standard_beat if product else item_standard_beat)
|
||||||
item.project_no,
|
)
|
||||||
item.product_name,
|
process_unit_price = as_float(getattr(item, "process_unit_price_yuan", 0))
|
||||||
process_name,
|
stamping_method = getattr(item, "stamping_method", None) or (
|
||||||
stamping_method or "",
|
product.stamping_method if product else None
|
||||||
)
|
)
|
||||||
group = groups.setdefault(
|
allocation_date = getattr(allocation, "allocation_date")
|
||||||
key,
|
attendance_point_name = getattr(
|
||||||
{
|
report, "attendance_point_name", getattr(allocation, "attendance_point_name", "")
|
||||||
"id": "__".join(str(part) for part in key),
|
)
|
||||||
"attendance_point_name": report.attendance_point_name,
|
employee_phone = getattr(
|
||||||
"report_date": report.report_date,
|
report, "employee_phone", getattr(allocation, "employee_phone", "")
|
||||||
"employee_phone": report.employee_phone,
|
)
|
||||||
"employee_name": employee_name,
|
key = (
|
||||||
"worker_type": worker_type,
|
attendance_point_name,
|
||||||
"project_no": item.project_no,
|
allocation_date,
|
||||||
"product_name": item.product_name,
|
employee_name,
|
||||||
"process_name": process_name,
|
employee_phone,
|
||||||
"report_ids": set(),
|
item.project_no,
|
||||||
"source_report_dates": set(),
|
item.product_name,
|
||||||
"effective_minutes": 0.0,
|
process_name,
|
||||||
"shift_day_minutes": 0.0,
|
stamping_method or "",
|
||||||
"shift_overtime_minutes": 0.0,
|
)
|
||||||
"shift_night_minutes": 0.0,
|
group = groups.setdefault(
|
||||||
"total_good_qty": 0.0,
|
key,
|
||||||
"total_defect_qty": 0.0,
|
{
|
||||||
"total_output_qty": 0.0,
|
"id": "__".join(str(part) for part in key),
|
||||||
"standard_beat": standard_beat,
|
"attendance_point_name": attendance_point_name,
|
||||||
"process_unit_price_yuan": process_unit_price,
|
"report_date": allocation_date,
|
||||||
"changeover_count": 0.0,
|
"employee_phone": employee_phone,
|
||||||
"reference_wage": 0.0,
|
"employee_name": employee_name,
|
||||||
"expected_workload": 0.0,
|
"worker_type": worker_type,
|
||||||
"profile_no": product.profile_no if product else None,
|
"project_no": item.project_no,
|
||||||
"material_code": product.material_code if product else item.material_code,
|
"product_name": item.product_name,
|
||||||
"material_name": product.material_name if product else item.material_name,
|
"process_name": process_name,
|
||||||
"raw_material_batch_numbers": set(),
|
"report_ids": set(),
|
||||||
"device_no": item.device_no,
|
"source_report_dates": set(),
|
||||||
"stamping_method": stamping_method,
|
"effective_minutes": 0.0,
|
||||||
"operator_count": as_float(product.operator_count) if product else as_float(item.operator_count),
|
"shift_day_minutes": 0.0,
|
||||||
"is_continuous_die": is_continuous_die,
|
"shift_overtime_minutes": 0.0,
|
||||||
"is_multi_person": is_multi_person_item(item),
|
"shift_night_minutes": 0.0,
|
||||||
"is_multi_person_assistant": False,
|
"total_good_qty": 0.0,
|
||||||
"is_misc": is_misc,
|
"total_defect_qty": 0.0,
|
||||||
"is_system_auto_submitted": False,
|
"total_output_qty": 0.0,
|
||||||
"is_voided": False,
|
"standard_beat": standard_beat,
|
||||||
"review_remarks": [],
|
"process_unit_price_yuan": process_unit_price,
|
||||||
"review_remark_keys": set(),
|
"changeover_count": 0.0,
|
||||||
"correction_pairs": [],
|
"reference_wage": 0.0,
|
||||||
"correction_pair_keys": set(),
|
"expected_workload": 0.0,
|
||||||
},
|
"profile_no": (
|
||||||
)
|
getattr(product, "profile_no", getattr(item, "profile_no", None))
|
||||||
_append_dashboard_corrections(group, report, item, report_corrections, item_corrections.get(item.id, {}))
|
if product
|
||||||
good_qty = as_float(item.good_qty)
|
else getattr(item, "profile_no", None)
|
||||||
defect_qty = as_float(item.defect_qty) + as_float(item.scrap_qty)
|
),
|
||||||
output_qty = good_qty + defect_qty
|
"material_code": (
|
||||||
allocated_minutes = 0 if is_cleaning else (as_float(item.allocated_minutes) or fallback_minutes)
|
getattr(product, "material_code", getattr(item, "material_code", None))
|
||||||
expected_workload = allocated_minutes * 60 / standard_beat if standard_beat > 0 else 0
|
if product
|
||||||
group["report_ids"].add(report.id)
|
else getattr(item, "material_code", None)
|
||||||
group["source_report_dates"].add(report.report_date)
|
),
|
||||||
group["effective_minutes"] += allocated_minutes
|
"material_name": (
|
||||||
group["expected_workload"] += expected_workload
|
getattr(product, "material_name", getattr(item, "material_name", None))
|
||||||
group["shift_day_minutes"] += as_float(getattr(item, "_shift_day_minutes", 0))
|
if product
|
||||||
group["shift_overtime_minutes"] += as_float(getattr(item, "_shift_overtime_minutes", 0))
|
else getattr(item, "material_name", None)
|
||||||
group["shift_night_minutes"] += as_float(getattr(item, "_shift_night_minutes", 0))
|
),
|
||||||
group["total_good_qty"] += good_qty
|
"raw_material_batch_numbers": set(),
|
||||||
group["reference_wage"] += good_qty * process_unit_price
|
"device_no": item.device_no,
|
||||||
group["changeover_count"] += as_float(item.changeover_count)
|
"stamping_method": stamping_method,
|
||||||
group["total_defect_qty"] += defect_qty
|
"operator_count": (
|
||||||
group["total_output_qty"] += output_qty
|
as_float(product.operator_count)
|
||||||
group["is_system_auto_submitted"] = (
|
if product
|
||||||
group["is_system_auto_submitted"] or bool(report.is_system_auto_submitted)
|
else as_float(getattr(item, "operator_count", 1))
|
||||||
)
|
),
|
||||||
group["is_voided"] = group["is_voided"] or bool(report.is_voided)
|
"is_continuous_die": is_continuous_die,
|
||||||
group["is_multi_person"] = group["is_multi_person"] or is_multi_person_item(item)
|
"is_multi_person": is_multi_person_item(item),
|
||||||
group["is_continuous_die"] = group["is_continuous_die"] or is_continuous_die
|
"is_multi_person_assistant": False,
|
||||||
group["is_multi_person_assistant"] = (
|
"is_misc": is_misc,
|
||||||
group["is_multi_person_assistant"] or bool(report.is_multi_person_assistant)
|
"is_system_auto_submitted": False,
|
||||||
)
|
"is_voided": False,
|
||||||
if report.review_remark:
|
"review_remarks": [],
|
||||||
review_remark = str(report.review_remark).strip()
|
"review_remark_keys": set(),
|
||||||
if review_remark and review_remark not in group["review_remark_keys"]:
|
"correction_pairs": [],
|
||||||
group["review_remark_keys"].add(review_remark)
|
"correction_pair_keys": set(),
|
||||||
group["review_remarks"].append(review_remark)
|
},
|
||||||
if item.raw_material_batch_no:
|
)
|
||||||
group["raw_material_batch_numbers"].add(item.raw_material_batch_no)
|
_append_dashboard_corrections(
|
||||||
|
group,
|
||||||
|
report,
|
||||||
|
item,
|
||||||
|
report_corrections,
|
||||||
|
item_corrections.get(getattr(item, "id", None), {}),
|
||||||
|
)
|
||||||
|
good_qty = as_float(getattr(allocation, "good_qty", 0))
|
||||||
|
defect_qty = as_float(getattr(allocation, "defect_qty", 0)) + as_float(
|
||||||
|
getattr(allocation, "scrap_qty", 0)
|
||||||
|
)
|
||||||
|
output_qty = good_qty + defect_qty
|
||||||
|
effective_minutes = as_float(getattr(allocation, "effective_minutes", 0))
|
||||||
|
expected_workload = effective_minutes * 60 / standard_beat if standard_beat > 0 else 0
|
||||||
|
group["report_ids"].add(report_key)
|
||||||
|
group["source_report_dates"].add(report.report_date)
|
||||||
|
group["effective_minutes"] += effective_minutes
|
||||||
|
group["expected_workload"] += expected_workload
|
||||||
|
group["shift_day_minutes"] += as_float(getattr(allocation, "day_minutes", 0))
|
||||||
|
group["shift_overtime_minutes"] += as_float(getattr(allocation, "overtime_minutes", 0))
|
||||||
|
group["shift_night_minutes"] += as_float(getattr(allocation, "night_minutes", 0))
|
||||||
|
group["total_good_qty"] += good_qty
|
||||||
|
group["reference_wage"] += as_float(getattr(allocation, "reference_wage", 0))
|
||||||
|
group["changeover_count"] += as_float(getattr(allocation, "changeover_count", 0))
|
||||||
|
group["total_defect_qty"] += defect_qty
|
||||||
|
group["total_output_qty"] += output_qty
|
||||||
|
group["is_system_auto_submitted"] = (
|
||||||
|
group["is_system_auto_submitted"] or bool(report.is_system_auto_submitted)
|
||||||
|
)
|
||||||
|
group["is_voided"] = group["is_voided"] or bool(report.is_voided)
|
||||||
|
group["is_multi_person"] = group["is_multi_person"] or is_multi_person_item(item)
|
||||||
|
group["is_continuous_die"] = group["is_continuous_die"] or is_continuous_die
|
||||||
|
group["is_multi_person_assistant"] = (
|
||||||
|
group["is_multi_person_assistant"] or bool(report.is_multi_person_assistant)
|
||||||
|
)
|
||||||
|
if report.review_remark:
|
||||||
|
review_remark = str(report.review_remark).strip()
|
||||||
|
if review_remark and review_remark not in group["review_remark_keys"]:
|
||||||
|
group["review_remark_keys"].add(review_remark)
|
||||||
|
group["review_remarks"].append(review_remark)
|
||||||
|
raw_material_batch_no = getattr(item, "raw_material_batch_no", None)
|
||||||
|
if raw_material_batch_no:
|
||||||
|
group["raw_material_batch_numbers"].add(raw_material_batch_no)
|
||||||
|
|
||||||
rows: list[DashboardRow] = []
|
rows: list[DashboardRow] = []
|
||||||
for group in groups.values():
|
for group in groups.values():
|
||||||
|
|||||||
@ -108,7 +108,8 @@ def export_dashboard_rows(rows: list) -> bytes:
|
|||||||
void_fill = PatternFill(fill_type="solid", fgColor="FFD9D9D9")
|
void_fill = PatternFill(fill_type="solid", fgColor="FFD9D9D9")
|
||||||
sheet.append([
|
sheet.append([
|
||||||
"考勤点",
|
"考勤点",
|
||||||
"报工日期",
|
"统计归属日期",
|
||||||
|
"原始报工日期",
|
||||||
"员工姓名",
|
"员工姓名",
|
||||||
"员工电话",
|
"员工电话",
|
||||||
"工种",
|
"工种",
|
||||||
@ -138,6 +139,13 @@ def export_dashboard_rows(rows: list) -> bytes:
|
|||||||
])
|
])
|
||||||
wage_summary: dict[tuple[str, str, str], dict[str, float | str]] = {}
|
wage_summary: dict[tuple[str, str, str], dict[str, float | str]] = {}
|
||||||
for row_index, row in enumerate(rows, start=2):
|
for row_index, row in enumerate(rows, start=2):
|
||||||
|
source_dates = getattr(row, "source_report_dates", []) or [
|
||||||
|
getattr(row, "source_report_date", None) or row.report_date
|
||||||
|
]
|
||||||
|
source_dates = sorted({item for item in source_dates if item})
|
||||||
|
source_date_text = "、".join(
|
||||||
|
item.isoformat() for item in source_dates
|
||||||
|
)
|
||||||
report_flags = []
|
report_flags = []
|
||||||
if getattr(row, "is_voided", False):
|
if getattr(row, "is_voided", False):
|
||||||
report_flags.append("作废")
|
report_flags.append("作废")
|
||||||
@ -152,6 +160,7 @@ def export_dashboard_rows(rows: list) -> bytes:
|
|||||||
sheet.append([
|
sheet.append([
|
||||||
row.attendance_point_name,
|
row.attendance_point_name,
|
||||||
row.report_date.isoformat(),
|
row.report_date.isoformat(),
|
||||||
|
source_date_text,
|
||||||
row.employee_name,
|
row.employee_name,
|
||||||
row.employee_phone,
|
row.employee_phone,
|
||||||
row.worker_type,
|
row.worker_type,
|
||||||
@ -199,9 +208,11 @@ def export_dashboard_rows(rows: list) -> bytes:
|
|||||||
total_good_qty = as_float(row.total_good_qty)
|
total_good_qty = as_float(row.total_good_qty)
|
||||||
expected_workload = as_float(row.expected_workload)
|
expected_workload = as_float(row.expected_workload)
|
||||||
if standard_beat > 0 and (actual_beat > standard_beat or actual_beat < standard_beat * 0.7):
|
if standard_beat > 0 and (actual_beat > standard_beat or actual_beat < standard_beat * 0.7):
|
||||||
sheet.cell(row=row_index, column=14).fill = warn_fill
|
sheet.cell(row=row_index, column=15).fill = warn_fill
|
||||||
if expected_workload > 0 and (total_good_qty < expected_workload or total_good_qty > expected_workload * 1.3):
|
if expected_workload > 0 and (
|
||||||
sheet.cell(row=row_index, column=11).fill = warn_fill
|
total_good_qty < expected_workload or total_good_qty > expected_workload * 1.3
|
||||||
|
):
|
||||||
|
sheet.cell(row=row_index, column=12).fill = warn_fill
|
||||||
if getattr(row, "is_voided", False):
|
if getattr(row, "is_voided", False):
|
||||||
for cell in sheet[row_index]:
|
for cell in sheet[row_index]:
|
||||||
cell.fill = void_fill
|
cell.fill = void_fill
|
||||||
|
|||||||
158
tests/test_dashboard_allocations.py
Normal file
158
tests/test_dashboard_allocations.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
from datetime import date
|
||||||
|
from io import BytesIO
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
|
||||||
|
from app.routers import dashboard
|
||||||
|
from app.services.excel_export import export_dashboard_rows
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_rows_from_allocations_groups_by_allocation_date_desc():
|
||||||
|
report_date = date(2026, 7, 25)
|
||||||
|
report = SimpleNamespace(
|
||||||
|
id=1,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
report_date=report_date,
|
||||||
|
employee_phone="13800000000",
|
||||||
|
employee=SimpleNamespace(name="张三", is_temporary=False),
|
||||||
|
items=[],
|
||||||
|
audit_logs=[],
|
||||||
|
session=None,
|
||||||
|
is_system_auto_submitted=False,
|
||||||
|
is_voided=False,
|
||||||
|
is_multi_person_assistant=False,
|
||||||
|
review_remark="复核通过",
|
||||||
|
)
|
||||||
|
item = SimpleNamespace(
|
||||||
|
id=10,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
device_no="A-01",
|
||||||
|
project_no="P1",
|
||||||
|
product_name="23#",
|
||||||
|
process_name="冲压",
|
||||||
|
remark=None,
|
||||||
|
raw_material_batch_no="BATCH-1",
|
||||||
|
material_code="M-1",
|
||||||
|
material_name="铝材",
|
||||||
|
profile_no="PF-1",
|
||||||
|
stamping_method="单冲",
|
||||||
|
operator_count=1,
|
||||||
|
process_unit_price_yuan=2,
|
||||||
|
standard_beat=1,
|
||||||
|
good_qty=650,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
changeover_count=2,
|
||||||
|
allocated_minutes=650,
|
||||||
|
)
|
||||||
|
report.items = [item]
|
||||||
|
allocations = [
|
||||||
|
SimpleNamespace(
|
||||||
|
report=report,
|
||||||
|
item=item,
|
||||||
|
allocation_date=date(2026, 7, 24),
|
||||||
|
night_minutes=350,
|
||||||
|
overtime_minutes=0,
|
||||||
|
day_minutes=0,
|
||||||
|
effective_minutes=350,
|
||||||
|
good_qty=350,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
changeover_count=1,
|
||||||
|
reference_wage=350,
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
report=report,
|
||||||
|
item=item,
|
||||||
|
allocation_date=date(2026, 7, 25),
|
||||||
|
night_minutes=0,
|
||||||
|
overtime_minutes=60,
|
||||||
|
day_minutes=240,
|
||||||
|
effective_minutes=300,
|
||||||
|
good_qty=300,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
changeover_count=1,
|
||||||
|
reference_wage=300,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
rows = dashboard._dashboard_rows_from_allocations(allocations)
|
||||||
|
|
||||||
|
assert [row.report_date for row in rows] == [date(2026, 7, 25), date(2026, 7, 24)]
|
||||||
|
assert [row.source_report_dates for row in rows] == [[report_date], [report_date]]
|
||||||
|
assert [row.source_report_date for row in rows] == [report_date, report_date]
|
||||||
|
assert [row.report_count for row in rows] == [1, 1]
|
||||||
|
|
||||||
|
current, previous = rows
|
||||||
|
assert current.effective_minutes == 300
|
||||||
|
assert current.shift_day_minutes == 240
|
||||||
|
assert current.shift_overtime_minutes == 60
|
||||||
|
assert current.shift_night_minutes == 0
|
||||||
|
assert current.total_good_qty == 300
|
||||||
|
assert current.total_defect_qty == 0
|
||||||
|
assert current.total_output_qty == 300
|
||||||
|
assert current.changeover_count == 1
|
||||||
|
assert current.reference_wage == 300
|
||||||
|
|
||||||
|
assert previous.effective_minutes == 350
|
||||||
|
assert previous.shift_day_minutes == 0
|
||||||
|
assert previous.shift_overtime_minutes == 0
|
||||||
|
assert previous.shift_night_minutes == 350
|
||||||
|
assert previous.total_good_qty == 350
|
||||||
|
assert previous.total_defect_qty == 0
|
||||||
|
assert previous.total_output_qty == 350
|
||||||
|
assert previous.changeover_count == 1
|
||||||
|
assert previous.reference_wage == 350
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_dashboard_rows_writes_allocation_and_source_dates_with_warning_columns():
|
||||||
|
row = SimpleNamespace(
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
report_date=date(2026, 7, 24),
|
||||||
|
source_report_dates=[date(2026, 7, 25), date(2026, 7, 23)],
|
||||||
|
source_report_date=None,
|
||||||
|
employee_name="张三",
|
||||||
|
employee_phone="13800000000",
|
||||||
|
worker_type="正式工",
|
||||||
|
project_no="P1",
|
||||||
|
product_name="23#",
|
||||||
|
process_name="冲压",
|
||||||
|
effective_minutes=60,
|
||||||
|
shift_distribution_text="白班1小时",
|
||||||
|
total_good_qty=50,
|
||||||
|
total_defect_qty=2,
|
||||||
|
expected_workload=100,
|
||||||
|
actual_beat=20,
|
||||||
|
standard_beat=10,
|
||||||
|
profile_no="PF-1",
|
||||||
|
material_code="M-1",
|
||||||
|
material_name="铝材",
|
||||||
|
raw_material_batch_no="BATCH-1",
|
||||||
|
device_no="A-01",
|
||||||
|
stamping_method="单冲",
|
||||||
|
changeover_count=1,
|
||||||
|
operator_count=1,
|
||||||
|
process_unit_price_yuan=2,
|
||||||
|
reference_wage=100,
|
||||||
|
is_voided=False,
|
||||||
|
is_multi_person=False,
|
||||||
|
is_continuous_die=False,
|
||||||
|
correction_pairs=[],
|
||||||
|
review_remark="复核通过",
|
||||||
|
)
|
||||||
|
|
||||||
|
workbook = load_workbook(BytesIO(export_dashboard_rows([row])))
|
||||||
|
sheet = workbook["报工看板"]
|
||||||
|
|
||||||
|
assert [sheet.cell(row=1, column=index).value for index in range(1, 5)] == [
|
||||||
|
"考勤点",
|
||||||
|
"统计归属日期",
|
||||||
|
"原始报工日期",
|
||||||
|
"员工姓名",
|
||||||
|
]
|
||||||
|
assert sheet.cell(row=2, column=2).value == "2026-07-24"
|
||||||
|
assert sheet.cell(row=2, column=3).value == "2026-07-23、2026-07-25"
|
||||||
|
assert sheet.cell(row=2, column=12).fill.fgColor.rgb == "FFFF6666"
|
||||||
|
assert sheet.cell(row=2, column=15).fill.fgColor.rgb == "FFFF6666"
|
||||||
@ -10,6 +10,7 @@ from app.models import (
|
|||||||
Personnel,
|
Personnel,
|
||||||
Product,
|
Product,
|
||||||
ProductionReport,
|
ProductionReport,
|
||||||
|
ProductionReportAllocation,
|
||||||
ProductionReportItem,
|
ProductionReportItem,
|
||||||
ReportStatus,
|
ReportStatus,
|
||||||
SessionStatus,
|
SessionStatus,
|
||||||
@ -92,7 +93,24 @@ def test_dashboard_rows_populate_source_report_dates_from_original_reports():
|
|||||||
scrap_qty=0,
|
scrap_qty=0,
|
||||||
allocated_minutes=60,
|
allocated_minutes=60,
|
||||||
)
|
)
|
||||||
db.add_all([session, report, item])
|
allocation = ProductionReportAllocation(
|
||||||
|
id=index,
|
||||||
|
report_id=report.id,
|
||||||
|
report_item_id=item.id,
|
||||||
|
attendance_point_name=point_name,
|
||||||
|
employee_phone=person.phone,
|
||||||
|
allocation_date=report_date,
|
||||||
|
day_minutes=60,
|
||||||
|
overtime_minutes=0,
|
||||||
|
night_minutes=0,
|
||||||
|
effective_minutes=60,
|
||||||
|
good_qty=10,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
changeover_count=0,
|
||||||
|
reference_wage=20,
|
||||||
|
)
|
||||||
|
db.add_all([session, report, item, allocation])
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
rows = _dashboard_rows(db, report_date, report_date)
|
rows = _dashboard_rows(db, report_date, report_date)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user