feat: 经理看板展示报工超额信息
This commit is contained in:
parent
58d8272448
commit
7cdb528b1c
@ -264,6 +264,11 @@ def _dashboard_rows_from_allocations(allocations: list) -> list[DashboardRow]:
|
|||||||
"is_voided": False,
|
"is_voided": False,
|
||||||
"review_remarks": [],
|
"review_remarks": [],
|
||||||
"review_remark_keys": set(),
|
"review_remark_keys": set(),
|
||||||
|
"has_over_limit": False,
|
||||||
|
"over_limit_exceeded_keys": set(),
|
||||||
|
"over_limit_uncheckable_keys": set(),
|
||||||
|
"over_limit_reasons": [],
|
||||||
|
"over_limit_reason_keys": set(),
|
||||||
"correction_pairs": [],
|
"correction_pairs": [],
|
||||||
"correction_pair_keys": set(),
|
"correction_pair_keys": set(),
|
||||||
},
|
},
|
||||||
@ -303,6 +308,17 @@ def _dashboard_rows_from_allocations(allocations: list) -> list[DashboardRow]:
|
|||||||
group["is_multi_person_assistant"] = (
|
group["is_multi_person_assistant"] = (
|
||||||
group["is_multi_person_assistant"] or bool(report.is_multi_person_assistant)
|
group["is_multi_person_assistant"] or bool(report.is_multi_person_assistant)
|
||||||
)
|
)
|
||||||
|
over_limit_status = str(getattr(item, "over_limit_status", "") or "")
|
||||||
|
over_limit_reason = str(getattr(item, "over_limit_reason", "") or "").strip()
|
||||||
|
over_limit_item_key = getattr(item, "id", None) or id(item)
|
||||||
|
if over_limit_status == "exceeded":
|
||||||
|
group["has_over_limit"] = True
|
||||||
|
group["over_limit_exceeded_keys"].add(over_limit_item_key)
|
||||||
|
elif over_limit_status == "uncheckable":
|
||||||
|
group["over_limit_uncheckable_keys"].add(over_limit_item_key)
|
||||||
|
if over_limit_reason and over_limit_reason not in group["over_limit_reason_keys"]:
|
||||||
|
group["over_limit_reason_keys"].add(over_limit_reason)
|
||||||
|
group["over_limit_reasons"].append(over_limit_reason)
|
||||||
if report.review_remark:
|
if report.review_remark:
|
||||||
review_remark = str(report.review_remark).strip()
|
review_remark = str(report.review_remark).strip()
|
||||||
if review_remark and review_remark not in group["review_remark_keys"]:
|
if review_remark and review_remark not in group["review_remark_keys"]:
|
||||||
@ -333,6 +349,16 @@ def _dashboard_rows_from_allocations(allocations: list) -> list[DashboardRow]:
|
|||||||
"night": group["shift_night_minutes"],
|
"night": group["shift_night_minutes"],
|
||||||
}
|
}
|
||||||
source_report_dates = sorted(group["source_report_dates"])
|
source_report_dates = sorted(group["source_report_dates"])
|
||||||
|
exceeded_count = len(group["over_limit_exceeded_keys"])
|
||||||
|
uncheckable_count = len(group["over_limit_uncheckable_keys"])
|
||||||
|
if exceeded_count and uncheckable_count:
|
||||||
|
over_limit_summary = f"{exceeded_count}项超额,{uncheckable_count}项无法校验"
|
||||||
|
elif exceeded_count:
|
||||||
|
over_limit_summary = f"{exceeded_count}项超额"
|
||||||
|
elif uncheckable_count:
|
||||||
|
over_limit_summary = f"{uncheckable_count}项无法校验"
|
||||||
|
else:
|
||||||
|
over_limit_summary = None
|
||||||
rows.append(
|
rows.append(
|
||||||
DashboardRow(
|
DashboardRow(
|
||||||
id=group["id"],
|
id=group["id"],
|
||||||
@ -380,6 +406,9 @@ def _dashboard_rows_from_allocations(allocations: list) -> list[DashboardRow]:
|
|||||||
is_system_auto_submitted=group["is_system_auto_submitted"],
|
is_system_auto_submitted=group["is_system_auto_submitted"],
|
||||||
is_voided=group["is_voided"],
|
is_voided=group["is_voided"],
|
||||||
review_remark=";".join(group["review_remarks"]) or None,
|
review_remark=";".join(group["review_remarks"]) or None,
|
||||||
|
has_over_limit=group["has_over_limit"],
|
||||||
|
over_limit_summary=over_limit_summary,
|
||||||
|
over_limit_reasons=";".join(group["over_limit_reasons"]) or None,
|
||||||
correction_pairs=group["correction_pairs"],
|
correction_pairs=group["correction_pairs"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -658,6 +658,9 @@ class DashboardRow(BaseModel):
|
|||||||
is_system_auto_submitted: bool = False
|
is_system_auto_submitted: bool = False
|
||||||
is_voided: bool = False
|
is_voided: bool = False
|
||||||
review_remark: str | None = None
|
review_remark: str | None = None
|
||||||
|
has_over_limit: bool = False
|
||||||
|
over_limit_summary: str | None = None
|
||||||
|
over_limit_reasons: str | None = None
|
||||||
correction_pairs: list[dict[str, Any]] = Field(default_factory=list)
|
correction_pairs: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -133,6 +133,9 @@ def export_dashboard_rows(rows: list) -> bytes:
|
|||||||
"操作人数",
|
"操作人数",
|
||||||
"工序单价",
|
"工序单价",
|
||||||
"参考工资",
|
"参考工资",
|
||||||
|
"是否超额",
|
||||||
|
"超额摘要",
|
||||||
|
"超额原因",
|
||||||
"报工标记",
|
"报工标记",
|
||||||
"校正项",
|
"校正项",
|
||||||
"备注",
|
"备注",
|
||||||
@ -184,6 +187,9 @@ def export_dashboard_rows(rows: list) -> bytes:
|
|||||||
row.operator_count,
|
row.operator_count,
|
||||||
row.process_unit_price_yuan,
|
row.process_unit_price_yuan,
|
||||||
row.reference_wage,
|
row.reference_wage,
|
||||||
|
"是" if getattr(row, "has_over_limit", False) else "否",
|
||||||
|
getattr(row, "over_limit_summary", None),
|
||||||
|
getattr(row, "over_limit_reasons", None),
|
||||||
"、".join(report_flags),
|
"、".join(report_flags),
|
||||||
correction_text,
|
correction_text,
|
||||||
getattr(row, "review_remark", None),
|
getattr(row, "review_remark", None),
|
||||||
|
|||||||
@ -156,3 +156,237 @@ def test_export_dashboard_rows_writes_allocation_and_source_dates_with_warning_c
|
|||||||
assert sheet.cell(row=2, column=3).value == "2026-07-23、2026-07-25"
|
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=12).fill.fgColor.rgb == "FFFF6666"
|
||||||
assert sheet.cell(row=2, column=15).fill.fgColor.rgb == "FFFF6666"
|
assert sheet.cell(row=2, column=15).fill.fgColor.rgb == "FFFF6666"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_rows_include_over_limit_summary_from_reports():
|
||||||
|
report = SimpleNamespace(
|
||||||
|
id=1,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
employee_phone="13800000000",
|
||||||
|
report_date=date(2026, 7, 25),
|
||||||
|
has_over_limit=True,
|
||||||
|
over_limit_summary="1项超额",
|
||||||
|
review_remark=None,
|
||||||
|
employee=SimpleNamespace(name="张三", is_temporary=False),
|
||||||
|
items=[],
|
||||||
|
audit_logs=[],
|
||||||
|
session=None,
|
||||||
|
is_system_auto_submitted=False,
|
||||||
|
is_voided=False,
|
||||||
|
is_multi_person_assistant=False,
|
||||||
|
)
|
||||||
|
item = SimpleNamespace(
|
||||||
|
id=1,
|
||||||
|
report=report,
|
||||||
|
report_id=1,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
device_no="A",
|
||||||
|
project_no="P1",
|
||||||
|
product_name="产品A",
|
||||||
|
material_code="M1",
|
||||||
|
material_name="材料A",
|
||||||
|
raw_material_batch_no="LOT-1",
|
||||||
|
process_name="1",
|
||||||
|
remark=None,
|
||||||
|
stamping_method="冲压",
|
||||||
|
operator_count=1,
|
||||||
|
process_unit_price_yuan=1,
|
||||||
|
standard_beat=1,
|
||||||
|
good_qty=60,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
changeover_count=0,
|
||||||
|
allocated_minutes=60,
|
||||||
|
over_limit_status="exceeded",
|
||||||
|
over_limit_reason="1序累计报工数量超过本批次材料可支撑数量",
|
||||||
|
)
|
||||||
|
report.items = [item]
|
||||||
|
allocation = SimpleNamespace(
|
||||||
|
report=report,
|
||||||
|
item=item,
|
||||||
|
report_id=1,
|
||||||
|
allocation_date=date(2026, 7, 25),
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
employee_phone="13800000000",
|
||||||
|
good_qty=60,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
effective_minutes=60,
|
||||||
|
day_minutes=60,
|
||||||
|
overtime_minutes=0,
|
||||||
|
night_minutes=0,
|
||||||
|
reference_wage=60,
|
||||||
|
changeover_count=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = dashboard._dashboard_rows_from_allocations([allocation])
|
||||||
|
|
||||||
|
assert rows[0].has_over_limit is True
|
||||||
|
assert rows[0].over_limit_summary == "1项超额"
|
||||||
|
assert rows[0].over_limit_reasons == "1序累计报工数量超过本批次材料可支撑数量"
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_dashboard_rows_writes_over_limit_columns():
|
||||||
|
row = SimpleNamespace(
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
report_date=date(2026, 7, 24),
|
||||||
|
source_report_dates=[date(2026, 7, 24)],
|
||||||
|
source_report_date=None,
|
||||||
|
employee_name="张三",
|
||||||
|
employee_phone="13800000000",
|
||||||
|
worker_type="正式工",
|
||||||
|
project_no="P1",
|
||||||
|
product_name="23#",
|
||||||
|
process_name="1",
|
||||||
|
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,
|
||||||
|
has_over_limit=True,
|
||||||
|
over_limit_summary="1项超额",
|
||||||
|
over_limit_reasons="1序累计报工数量超过本批次材料可支撑数量",
|
||||||
|
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["报工看板"]
|
||||||
|
headers = [sheet.cell(row=1, column=index).value for index in range(1, sheet.max_column + 1)]
|
||||||
|
|
||||||
|
assert "是否超额" in headers
|
||||||
|
assert "超额摘要" in headers
|
||||||
|
assert "超额原因" in headers
|
||||||
|
assert sheet.cell(row=2, column=headers.index("是否超额") + 1).value == "是"
|
||||||
|
assert sheet.cell(row=2, column=headers.index("超额摘要") + 1).value == "1项超额"
|
||||||
|
assert sheet.cell(row=2, column=headers.index("超额原因") + 1).value == "1序累计报工数量超过本批次材料可支撑数量"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_over_limit_does_not_spread_to_other_items_in_same_report():
|
||||||
|
report = SimpleNamespace(
|
||||||
|
id=1,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
employee_phone="13800000000",
|
||||||
|
report_date=date(2026, 7, 25),
|
||||||
|
has_over_limit=True,
|
||||||
|
over_limit_summary="1项超额",
|
||||||
|
review_remark=None,
|
||||||
|
employee=SimpleNamespace(name="张三", is_temporary=False),
|
||||||
|
items=[],
|
||||||
|
audit_logs=[],
|
||||||
|
session=None,
|
||||||
|
is_system_auto_submitted=False,
|
||||||
|
is_voided=False,
|
||||||
|
is_multi_person_assistant=False,
|
||||||
|
)
|
||||||
|
exceeded_item = SimpleNamespace(
|
||||||
|
id=1,
|
||||||
|
report=report,
|
||||||
|
report_id=1,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
device_no="A",
|
||||||
|
project_no="P1",
|
||||||
|
product_name="产品A",
|
||||||
|
material_code="M1",
|
||||||
|
material_name="材料A",
|
||||||
|
raw_material_batch_no="LOT-1",
|
||||||
|
process_name="1",
|
||||||
|
remark=None,
|
||||||
|
stamping_method="冲压",
|
||||||
|
operator_count=1,
|
||||||
|
process_unit_price_yuan=1,
|
||||||
|
standard_beat=1,
|
||||||
|
good_qty=60,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
changeover_count=0,
|
||||||
|
allocated_minutes=60,
|
||||||
|
over_limit_status="exceeded",
|
||||||
|
over_limit_reason="1序累计报工数量超过本批次材料可支撑数量",
|
||||||
|
)
|
||||||
|
normal_item = SimpleNamespace(
|
||||||
|
id=2,
|
||||||
|
report=report,
|
||||||
|
report_id=1,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
device_no="B",
|
||||||
|
project_no="P1",
|
||||||
|
product_name="产品A",
|
||||||
|
material_code="M1",
|
||||||
|
material_name="材料A",
|
||||||
|
raw_material_batch_no="LOT-1",
|
||||||
|
process_name="2",
|
||||||
|
remark=None,
|
||||||
|
stamping_method="冲压",
|
||||||
|
operator_count=1,
|
||||||
|
process_unit_price_yuan=1,
|
||||||
|
standard_beat=1,
|
||||||
|
good_qty=40,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
changeover_count=0,
|
||||||
|
allocated_minutes=40,
|
||||||
|
over_limit_status="ok",
|
||||||
|
over_limit_reason=None,
|
||||||
|
)
|
||||||
|
report.items = [exceeded_item, normal_item]
|
||||||
|
allocations = [
|
||||||
|
SimpleNamespace(
|
||||||
|
report=report,
|
||||||
|
item=exceeded_item,
|
||||||
|
report_id=1,
|
||||||
|
allocation_date=date(2026, 7, 25),
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
employee_phone="13800000000",
|
||||||
|
good_qty=60,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
effective_minutes=60,
|
||||||
|
day_minutes=60,
|
||||||
|
overtime_minutes=0,
|
||||||
|
night_minutes=0,
|
||||||
|
reference_wage=60,
|
||||||
|
changeover_count=0,
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
report=report,
|
||||||
|
item=normal_item,
|
||||||
|
report_id=1,
|
||||||
|
allocation_date=date(2026, 7, 25),
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
employee_phone="13800000000",
|
||||||
|
good_qty=40,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
effective_minutes=40,
|
||||||
|
day_minutes=40,
|
||||||
|
overtime_minutes=0,
|
||||||
|
night_minutes=0,
|
||||||
|
reference_wage=40,
|
||||||
|
changeover_count=0,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
rows = dashboard._dashboard_rows_from_allocations(allocations)
|
||||||
|
rows_by_process = {row.process_name: row for row in rows}
|
||||||
|
|
||||||
|
assert rows_by_process["1"].has_over_limit is True
|
||||||
|
assert rows_by_process["1"].over_limit_reasons == "1序累计报工数量超过本批次材料可支撑数量"
|
||||||
|
assert rows_by_process["2"].has_over_limit is False
|
||||||
|
assert rows_by_process["2"].over_limit_summary is None
|
||||||
|
assert rows_by_process["2"].over_limit_reasons is None
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user