fix: 完善报工归属输出安全性
This commit is contained in:
parent
b0cb217ce3
commit
bb9ce5601c
@ -192,6 +192,7 @@ def _dashboard_rows(
|
|||||||
"product_name": item.product_name,
|
"product_name": item.product_name,
|
||||||
"process_name": process_name,
|
"process_name": process_name,
|
||||||
"report_ids": set(),
|
"report_ids": set(),
|
||||||
|
"source_report_dates": set(),
|
||||||
"effective_minutes": 0.0,
|
"effective_minutes": 0.0,
|
||||||
"shift_day_minutes": 0.0,
|
"shift_day_minutes": 0.0,
|
||||||
"shift_overtime_minutes": 0.0,
|
"shift_overtime_minutes": 0.0,
|
||||||
@ -230,6 +231,7 @@ def _dashboard_rows(
|
|||||||
allocated_minutes = 0 if is_cleaning else (as_float(item.allocated_minutes) or fallback_minutes)
|
allocated_minutes = 0 if is_cleaning else (as_float(item.allocated_minutes) or fallback_minutes)
|
||||||
expected_workload = allocated_minutes * 60 / standard_beat if standard_beat > 0 else 0
|
expected_workload = allocated_minutes * 60 / standard_beat if standard_beat > 0 else 0
|
||||||
group["report_ids"].add(report.id)
|
group["report_ids"].add(report.id)
|
||||||
|
group["source_report_dates"].add(report.report_date)
|
||||||
group["effective_minutes"] += allocated_minutes
|
group["effective_minutes"] += allocated_minutes
|
||||||
group["expected_workload"] += expected_workload
|
group["expected_workload"] += expected_workload
|
||||||
group["shift_day_minutes"] += as_float(getattr(item, "_shift_day_minutes", 0))
|
group["shift_day_minutes"] += as_float(getattr(item, "_shift_day_minutes", 0))
|
||||||
@ -277,11 +279,14 @@ def _dashboard_rows(
|
|||||||
"overtime": group["shift_overtime_minutes"],
|
"overtime": group["shift_overtime_minutes"],
|
||||||
"night": group["shift_night_minutes"],
|
"night": group["shift_night_minutes"],
|
||||||
}
|
}
|
||||||
|
source_report_dates = sorted(group["source_report_dates"])
|
||||||
rows.append(
|
rows.append(
|
||||||
DashboardRow(
|
DashboardRow(
|
||||||
id=group["id"],
|
id=group["id"],
|
||||||
attendance_point_name=group["attendance_point_name"],
|
attendance_point_name=group["attendance_point_name"],
|
||||||
report_date=group["report_date"],
|
report_date=group["report_date"],
|
||||||
|
source_report_date=source_report_dates[-1] if source_report_dates else group["report_date"],
|
||||||
|
source_report_dates=source_report_dates,
|
||||||
employee_phone=group["employee_phone"],
|
employee_phone=group["employee_phone"],
|
||||||
employee_name=group["employee_name"],
|
employee_name=group["employee_name"],
|
||||||
worker_type=group["worker_type"],
|
worker_type=group["worker_type"],
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import inspect, select
|
||||||
|
from sqlalchemy.exc import NoInspectionAvailable
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy.orm.attributes import NO_VALUE
|
||||||
|
|
||||||
from app.models import (
|
from app.models import (
|
||||||
AttendancePoint,
|
AttendancePoint,
|
||||||
@ -358,7 +360,31 @@ def report_allocation_out(row) -> ReportAllocationOut:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _loaded_relationship_rows(obj, relationship_name: str) -> list:
|
||||||
|
try:
|
||||||
|
state = inspect(obj)
|
||||||
|
except NoInspectionAvailable:
|
||||||
|
return list(getattr(obj, relationship_name, []) or [])
|
||||||
|
if relationship_name not in state.attrs:
|
||||||
|
return list(getattr(obj, relationship_name, []) or [])
|
||||||
|
relationship_state = state.attrs[relationship_name]
|
||||||
|
if relationship_state.loaded_value is NO_VALUE:
|
||||||
|
return []
|
||||||
|
return list(relationship_state.value or [])
|
||||||
|
|
||||||
|
|
||||||
|
def _sorted_allocation_rows(rows) -> list:
|
||||||
|
return sorted(
|
||||||
|
list(rows or []),
|
||||||
|
key=lambda row: (
|
||||||
|
getattr(row, "allocation_date", None) or date.min,
|
||||||
|
getattr(row, "id", None) or 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def report_item_out(item: ProductionReportItem, corrections: dict | None = None) -> ReportItemOut:
|
def report_item_out(item: ProductionReportItem, corrections: dict | None = None) -> ReportItemOut:
|
||||||
|
allocation_rows = _sorted_allocation_rows(_loaded_relationship_rows(item, "allocations"))
|
||||||
return ReportItemOut(
|
return ReportItemOut(
|
||||||
id=item.id,
|
id=item.id,
|
||||||
attendance_point_name=item.attendance_point_name,
|
attendance_point_name=item.attendance_point_name,
|
||||||
@ -384,7 +410,7 @@ def report_item_out(item: ProductionReportItem, corrections: dict | None = None)
|
|||||||
is_continuous_die=is_continuous_die_item(item),
|
is_continuous_die=is_continuous_die_item(item),
|
||||||
is_multi_person=is_multi_person_item(item),
|
is_multi_person=is_multi_person_item(item),
|
||||||
remark=item.remark,
|
remark=item.remark,
|
||||||
allocations=[report_allocation_out(row) for row in getattr(item, "allocations", []) or []],
|
allocations=[report_allocation_out(row) for row in allocation_rows],
|
||||||
corrections=corrections or {},
|
corrections=corrections or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -423,6 +449,7 @@ def _cleaning_report_metrics(report: ProductionReport) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def report_out(report: ProductionReport, schedule: WorkScheduleConfig | None = None) -> ReportOut:
|
def report_out(report: ProductionReport, schedule: WorkScheduleConfig | None = None) -> ReportOut:
|
||||||
|
allocation_rows = _sorted_allocation_rows(_loaded_relationship_rows(report, "allocations"))
|
||||||
is_cleaning_report = _report_is_cleaning(report)
|
is_cleaning_report = _report_is_cleaning(report)
|
||||||
calculated_metrics = (
|
calculated_metrics = (
|
||||||
_cleaning_report_metrics(report)
|
_cleaning_report_metrics(report)
|
||||||
@ -504,6 +531,6 @@ def report_out(report: ProductionReport, schedule: WorkScheduleConfig | None = N
|
|||||||
if is_cleaning_report
|
if is_cleaning_report
|
||||||
else ("处理杂活已提交,等待管理员审核" if _report_is_misc_only(report) else build_result_text(metrics))
|
else ("处理杂活已提交,等待管理员审核" if _report_is_misc_only(report) else build_result_text(metrics))
|
||||||
),
|
),
|
||||||
allocation_summary_text=allocation_summary_text(list(getattr(report, "allocations", []) or [])),
|
allocation_summary_text=allocation_summary_text(allocation_rows),
|
||||||
corrections=report_corrections,
|
corrections=report_corrections,
|
||||||
)
|
)
|
||||||
|
|||||||
105
tests/test_dashboard_source_dates.py
Normal file
105
tests/test_dashboard_source_dates.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, create_engine
|
||||||
|
from sqlalchemy.ext.compiler import compiles
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from app.database import Base
|
||||||
|
from app.models import (
|
||||||
|
AttendancePoint,
|
||||||
|
Personnel,
|
||||||
|
Product,
|
||||||
|
ProductionReport,
|
||||||
|
ProductionReportItem,
|
||||||
|
ReportStatus,
|
||||||
|
SessionStatus,
|
||||||
|
WorkSession,
|
||||||
|
)
|
||||||
|
from app.routers.dashboard import _dashboard_rows
|
||||||
|
|
||||||
|
|
||||||
|
@compiles(BigInteger, "sqlite")
|
||||||
|
def _compile_big_integer_for_sqlite(type_, compiler, **kw):
|
||||||
|
return "INTEGER"
|
||||||
|
|
||||||
|
|
||||||
|
def _db_session():
|
||||||
|
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
SessionLocal = sessionmaker(bind=engine, future=True)
|
||||||
|
return SessionLocal()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_rows_populate_source_report_dates_from_original_reports():
|
||||||
|
db = _db_session()
|
||||||
|
try:
|
||||||
|
point_name = "总厂"
|
||||||
|
report_date = date(2026, 7, 25)
|
||||||
|
person = Personnel(phone="13800000000", name="张三")
|
||||||
|
product = Product(
|
||||||
|
attendance_point_name=point_name,
|
||||||
|
project_no="P1",
|
||||||
|
product_name="23#",
|
||||||
|
device_no="",
|
||||||
|
process_name="冲压",
|
||||||
|
operator_count=1,
|
||||||
|
process_unit_price_yuan=2,
|
||||||
|
standard_beat=1,
|
||||||
|
standard_workload=0,
|
||||||
|
)
|
||||||
|
db.add_all([
|
||||||
|
AttendancePoint(name=point_name),
|
||||||
|
person,
|
||||||
|
product,
|
||||||
|
])
|
||||||
|
for index, start_hour in enumerate([8, 10], start=1):
|
||||||
|
session = WorkSession(
|
||||||
|
id=index,
|
||||||
|
attendance_point_name=point_name,
|
||||||
|
employee_phone=person.phone,
|
||||||
|
start_at=datetime(2026, 7, 25, start_hour, 0),
|
||||||
|
end_at=datetime(2026, 7, 25, start_hour + 1, 0),
|
||||||
|
status=SessionStatus.submitted,
|
||||||
|
)
|
||||||
|
report = ProductionReport(
|
||||||
|
id=index,
|
||||||
|
session_id=session.id,
|
||||||
|
attendance_point_name=point_name,
|
||||||
|
employee_phone=person.phone,
|
||||||
|
report_date=report_date,
|
||||||
|
start_at=session.start_at,
|
||||||
|
end_at=session.end_at,
|
||||||
|
duration_minutes=60,
|
||||||
|
break_minutes=0,
|
||||||
|
effective_minutes=60,
|
||||||
|
status=ReportStatus.approved,
|
||||||
|
submitted_at=session.end_at,
|
||||||
|
)
|
||||||
|
item = ProductionReportItem(
|
||||||
|
id=index,
|
||||||
|
report_id=report.id,
|
||||||
|
attendance_point_name=point_name,
|
||||||
|
device_no="23#",
|
||||||
|
project_no=product.project_no,
|
||||||
|
product_name=product.product_name,
|
||||||
|
process_name=product.process_name,
|
||||||
|
operator_count=1,
|
||||||
|
process_unit_price_yuan=2,
|
||||||
|
standard_beat=1,
|
||||||
|
standard_workload=0,
|
||||||
|
good_qty=10,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
allocated_minutes=60,
|
||||||
|
)
|
||||||
|
db.add_all([session, report, item])
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
rows = _dashboard_rows(db, report_date, report_date)
|
||||||
|
|
||||||
|
assert len(rows) == 1
|
||||||
|
assert rows[0].report_count == 2
|
||||||
|
assert rows[0].source_report_date == report_date
|
||||||
|
assert rows[0].source_report_dates == [report_date]
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
@ -4,7 +4,7 @@ from types import SimpleNamespace
|
|||||||
|
|
||||||
from sqlalchemy import BigInteger, create_engine, select
|
from sqlalchemy import BigInteger, create_engine, select
|
||||||
from sqlalchemy.ext.compiler import compiles
|
from sqlalchemy.ext.compiler import compiles
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import raiseload, selectinload, sessionmaker
|
||||||
|
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
from app.models import (
|
from app.models import (
|
||||||
@ -511,16 +511,33 @@ def test_report_out_exposes_allocation_summary_and_item_allocations():
|
|||||||
changeover_count=Decimal("1.50"),
|
changeover_count=Decimal("1.50"),
|
||||||
reference_wage=Decimal("251.25"),
|
reference_wage=Decimal("251.25"),
|
||||||
)
|
)
|
||||||
item.allocations = [allocation]
|
previous_allocation = ProductionReportAllocation(
|
||||||
|
id=2,
|
||||||
|
report_id=1,
|
||||||
|
report_item_id=1,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
employee_phone="13800000000",
|
||||||
|
allocation_date=date(2026, 7, 24),
|
||||||
|
day_minutes=Decimal("0.00"),
|
||||||
|
overtime_minutes=Decimal("0.00"),
|
||||||
|
night_minutes=Decimal("60.00"),
|
||||||
|
effective_minutes=Decimal("60.00"),
|
||||||
|
good_qty=Decimal("40.00"),
|
||||||
|
defect_qty=Decimal("0.00"),
|
||||||
|
scrap_qty=Decimal("0.00"),
|
||||||
|
changeover_count=Decimal("0.00"),
|
||||||
|
reference_wage=Decimal("100.00"),
|
||||||
|
)
|
||||||
|
item.allocations = [allocation, previous_allocation]
|
||||||
report.items = [item]
|
report.items = [item]
|
||||||
report.allocations = [allocation]
|
report.allocations = [allocation, previous_allocation]
|
||||||
report.audit_logs = []
|
report.audit_logs = []
|
||||||
|
|
||||||
output = report_out(report)
|
output = report_out(report)
|
||||||
|
|
||||||
assert output.allocation_summary_text == "2026-07-25 白班2.01小时、加班0.5小时"
|
assert output.allocation_summary_text == "2026-07-24 夜班1小时;2026-07-25 白班2.01小时、加班0.5小时"
|
||||||
assert len(output.items[0].allocations) == 1
|
assert [row.allocation_date for row in output.items[0].allocations] == [date(2026, 7, 24), date(2026, 7, 25)]
|
||||||
item_allocation = output.items[0].allocations[0]
|
item_allocation = output.items[0].allocations[1]
|
||||||
assert item_allocation.allocation_date == date(2026, 7, 25)
|
assert item_allocation.allocation_date == date(2026, 7, 25)
|
||||||
assert item_allocation.day_minutes == 120.5
|
assert item_allocation.day_minutes == 120.5
|
||||||
assert item_allocation.overtime_minutes == 30.25
|
assert item_allocation.overtime_minutes == 30.25
|
||||||
@ -643,6 +660,81 @@ def _sqlite_db():
|
|||||||
return SessionLocal()
|
return SessionLocal()
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_out_skips_unloaded_allocations_without_lazy_load():
|
||||||
|
db = _sqlite_db()
|
||||||
|
try:
|
||||||
|
person = Personnel(phone="13800000000", name="张三")
|
||||||
|
session = WorkSession(
|
||||||
|
id=1,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
employee_phone=person.phone,
|
||||||
|
start_at=datetime(2026, 7, 25, 8, 0),
|
||||||
|
end_at=datetime(2026, 7, 25, 10, 0),
|
||||||
|
status=SessionStatus.submitted,
|
||||||
|
)
|
||||||
|
report = ProductionReport(
|
||||||
|
id=1,
|
||||||
|
session_id=session.id,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
employee_phone=person.phone,
|
||||||
|
report_date=date(2026, 7, 25),
|
||||||
|
start_at=datetime(2026, 7, 25, 8, 0),
|
||||||
|
end_at=datetime(2026, 7, 25, 10, 0),
|
||||||
|
break_minutes=0,
|
||||||
|
status=ReportStatus.pending,
|
||||||
|
submitted_at=datetime(2026, 7, 25, 10, 1),
|
||||||
|
)
|
||||||
|
item = ProductionReportItem(
|
||||||
|
id=1,
|
||||||
|
report_id=report.id,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
device_no="23#",
|
||||||
|
project_no="P1",
|
||||||
|
product_name="23#",
|
||||||
|
process_name="冲压",
|
||||||
|
process_unit_price_yuan=1,
|
||||||
|
good_qty=100,
|
||||||
|
defect_qty=0,
|
||||||
|
scrap_qty=0,
|
||||||
|
changeover_count=0,
|
||||||
|
standard_beat=1,
|
||||||
|
standard_workload=0,
|
||||||
|
)
|
||||||
|
allocation = ProductionReportAllocation(
|
||||||
|
id=1,
|
||||||
|
report_id=report.id,
|
||||||
|
report_item_id=item.id,
|
||||||
|
attendance_point_name="总厂",
|
||||||
|
employee_phone=person.phone,
|
||||||
|
allocation_date=date(2026, 7, 25),
|
||||||
|
day_minutes=120,
|
||||||
|
effective_minutes=120,
|
||||||
|
good_qty=100,
|
||||||
|
reference_wage=100,
|
||||||
|
)
|
||||||
|
db.add_all([person, session, report, item, allocation])
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
loaded_report = db.scalar(
|
||||||
|
select(ProductionReport)
|
||||||
|
.options(
|
||||||
|
selectinload(ProductionReport.employee),
|
||||||
|
selectinload(ProductionReport.items).raiseload(ProductionReportItem.allocations),
|
||||||
|
selectinload(ProductionReport.audit_logs),
|
||||||
|
selectinload(ProductionReport.session).selectinload(WorkSession.devices),
|
||||||
|
raiseload(ProductionReport.allocations),
|
||||||
|
)
|
||||||
|
.where(ProductionReport.id == report.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
output = report_out(loaded_report)
|
||||||
|
|
||||||
|
assert output.allocation_summary_text == ""
|
||||||
|
assert output.items[0].allocations == []
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
def _product(
|
def _product(
|
||||||
*,
|
*,
|
||||||
point_name: str = "总厂",
|
point_name: str = "总厂",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user