feat: 报工保存后刷新归属明细

This commit is contained in:
souplearn 2026-07-25 04:16:08 +08:00
parent 5918b5ddde
commit ebb490590f
5 changed files with 327 additions and 2 deletions

View File

@ -39,6 +39,7 @@ from app.services.display_names import mold_process_display_name
from app.services.metrics import calculate_report_metrics, minutes_between, release_at_caps_work_time
from app.services.misc_work import is_misc_product
from app.services.mold_locks import release_session_locks
from app.services.report_allocations import refresh_report_allocations
from app.services.report_lifecycle import purge_expired_voided_reports
from app.services.reporting_window import REPORTING_EXPIRED_DETAIL, is_reporting_expired
from app.services.serializers import equipment_option, product_option, report_out
@ -456,6 +457,8 @@ def submit_report(
release_session_locks(session, user.phone, "报工提交释放占用", submitted_at)
session.status = SessionStatus.submitted
db.add(report)
db.flush()
refresh_report_allocations(db, report, schedule=schedule, commit=False)
db.commit()
saved = _get_report_or_404(db, report.id)
@ -602,10 +605,13 @@ def submit_cleaning_report(
items=report_items,
)
db.add(report)
schedule = get_work_schedule_config(db, point_name)
db.flush()
refresh_report_allocations(db, report, schedule=schedule, commit=False)
db.commit()
saved = _get_report_or_404(db, report.id)
return report_out(saved, get_work_schedule_config(db, saved.attendance_point_name))
return report_out(saved, schedule)
@router.get("/mine", response_model=PageResponse)

View File

@ -29,6 +29,7 @@ from app.services.continuous_die import is_continuous_die_item, is_continuous_di
from app.services.display_names import mold_process_display_name
from app.services.metrics import calculate_report_metrics
from app.services.misc_work import is_misc_item, is_misc_product
from app.services.report_allocations import refresh_report_allocations
from app.services.report_lifecycle import (
can_edit_report,
mark_report_voided,
@ -528,6 +529,7 @@ def approve_report(
_validate_normal_report_devices(db, report)
_apply_metrics(report, schedule)
refresh_report_allocations(db, report, schedule=schedule, commit=False)
reviewed_at = now()
was_pending = report.status == ReportStatus.pending
report.status = ReportStatus.approved

View File

@ -18,6 +18,7 @@ from app.models import (
from app.services.metrics import calculate_report_metrics, minutes_between, release_at_caps_work_time
from app.services.misc_work import is_misc_product
from app.services.mold_locks import release_session_locks
from app.services.report_allocations import refresh_report_allocations
from app.services.work_schedule import DEFAULT_AUTO_SUBMIT_HOURS, get_work_schedule, get_work_schedule_config
from app.timezone import now
@ -171,6 +172,8 @@ def auto_submit_session(db: Session, session: WorkSession, submitted_at: datetim
items=items,
)
db.add(report)
db.flush()
refresh_report_allocations(db, report, schedule=schedule, commit=False)
return report

View File

@ -8,6 +8,7 @@ from app.models import (
MoldLockFeedback,
Personnel,
ProductionReport,
ProductionReportAllocation,
ProductionReportItem,
ReportAuditLog,
WorkSession,
@ -86,6 +87,7 @@ def purge_expired_voided_reports(db: Session) -> int:
)
)
db.execute(delete(ReportAuditLog).where(ReportAuditLog.report_id == report.id))
db.execute(delete(ProductionReportAllocation).where(ProductionReportAllocation.report_id == report.id))
db.execute(delete(ProductionReportItem).where(ProductionReportItem.report_id == report.id))
db.delete(report)
if session is not None:

View File

@ -6,7 +6,32 @@ from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import sessionmaker
from app.database import Base
from app.models import ProductionReport, ProductionReportAllocation, ProductionReportItem
from app.models import (
AttendancePoint,
Equipment,
Product,
ProductionReport,
ProductionReportAllocation,
ProductionReportItem,
ReportStatus,
Role,
SessionStatus,
WorkSession,
WorkSessionDevice,
)
from app.routers import reports as reports_router
from app.routers import reviews as reviews_router
from app.schemas import (
ApproveReportRequest,
CleaningReportSubmitItem,
CleaningReportSubmitRequest,
ReportItemCorrection,
ReportSubmitBlock,
ReportSubmitItem,
ReportSubmitRequest,
)
from app.services import auto_submit as auto_submit_service
from app.services.report_lifecycle import purge_expired_voided_reports
from app.services.metrics import calculate_report_metrics
from app.services.report_allocations import (
allocation_summary_text,
@ -481,3 +506,290 @@ def test_refresh_report_allocations_replaces_existing_rows_and_flushes():
assert [row.good_qty for row in stored] == [1, 300]
finally:
db.close()
def _sqlite_db():
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine, future=True)
return SessionLocal()
def _product(
*,
point_name: str = "总厂",
project_no: str = "P1",
product_name: str = "23#",
process_name: str = "冲压",
stamping_method: str | None = None,
):
return Product(
attendance_point_name=point_name,
project_no=project_no,
product_name=product_name,
device_no="",
process_name=process_name,
stamping_method=stamping_method,
operator_count=1,
process_unit_price_yuan=2,
standard_beat=1,
standard_workload=0,
)
def _press_equipment(point_name: str = "总厂", device_no: str = "23#"):
return Equipment(
attendance_point_name=point_name,
device_no=device_no,
device_type="冲压设备",
)
def _session_with_device(
*,
point_name: str = "总厂",
employee_phone: str = "13800000000",
start_at: datetime = datetime(2026, 7, 25, 8, 0),
end_at: datetime = datetime(2026, 7, 25, 11, 0),
):
return WorkSession(
attendance_point_name=point_name,
employee_phone=employee_phone,
start_at=start_at,
end_at=end_at,
status=SessionStatus.reporting,
devices=[
WorkSessionDevice(
attendance_point_name=point_name,
device_no="23#",
process_name="冲压",
scanned_at=start_at,
sort_order=0,
)
],
)
def _stored_allocations(db, report_id: int):
return db.scalars(
select(ProductionReportAllocation).where(ProductionReportAllocation.report_id == report_id)
).all()
def test_submit_report_creates_allocations(monkeypatch):
db = _sqlite_db()
try:
user = SimpleNamespace(phone="13800000000")
session = _session_with_device(employee_phone=user.phone)
db.add_all([session, _press_equipment(), _product()])
db.commit()
monkeypatch.setattr(reports_router, "report_out", lambda report, schedule=None: report)
report = reports_router.submit_report(
ReportSubmitRequest(
session_id=session.id,
device_blocks=[
ReportSubmitBlock(
device_no="23#",
process_name="冲压",
items=[
ReportSubmitItem(
device_no="23#",
project_no="P1",
product_name="23#",
process_name="冲压",
standard_beat=1,
good_qty=180,
)
],
)
],
),
user=user,
db=db,
)
allocations = _stored_allocations(db, report.id)
assert len(allocations) == 1
assert allocations[0].report_item_id == report.items[0].id
assert allocations[0].effective_minutes == 180
assert allocations[0].good_qty == 180
finally:
db.close()
def test_submit_cleaning_report_creates_zero_minute_allocation(monkeypatch):
db = _sqlite_db()
try:
point_name = "总厂"
user = SimpleNamespace(phone="13800000000", role=Role.manager)
db.add_all(
[
AttendancePoint(name=point_name, latitude=0, longitude=0, radius_meters=500, is_active=True),
Equipment(attendance_point_name=point_name, device_no="C1", device_type="清洗设备"),
_product(
point_name=point_name,
project_no="CLEAN1",
product_name="清洗产品",
process_name="清洗",
stamping_method="清洗",
),
]
)
db.commit()
monkeypatch.setattr(reports_router, "report_out", lambda report, schedule=None: report)
report = reports_router.submit_cleaning_report(
CleaningReportSubmitRequest(
latitude=0,
longitude=0,
items=[
CleaningReportSubmitItem(
attendance_point_name=point_name,
device_no="C1",
product_name="清洗产品",
process_name="清洗",
quantity=25,
)
],
),
user=user,
db=db,
)
allocations = _stored_allocations(db, report.id)
assert len(allocations) == 1
assert allocations[0].effective_minutes == 0
assert allocations[0].good_qty == 25
finally:
db.close()
def test_auto_submit_session_creates_allocations():
db = _sqlite_db()
try:
session = _session_with_device(
start_at=datetime(2026, 7, 25, 8, 0),
end_at=None,
)
session.status = SessionStatus.active
db.add_all([session, _product()])
db.commit()
report = auto_submit_service.auto_submit_session(
db,
session,
submitted_at=datetime(2026, 7, 25, 11, 0),
)
db.flush()
allocations = _stored_allocations(db, report.id)
assert len(allocations) == 1
assert allocations[0].report_item_id == report.items[0].id
assert allocations[0].effective_minutes == 180
finally:
db.close()
def test_approve_report_refreshes_allocations_after_edit_save(monkeypatch):
db = _sqlite_db()
try:
user = SimpleNamespace(phone="13900000000", role=Role.admin)
session = _session_with_device()
session.status = SessionStatus.submitted
report = ProductionReport(
attendance_point_name="总厂",
session=session,
employee_phone="13800000000",
report_date=date(2026, 7, 25),
start_at=datetime(2026, 7, 25, 8, 0),
end_at=datetime(2026, 7, 25, 11, 0),
duration_minutes=180,
effective_minutes=180,
total_good_qty=100,
total_output_qty=100,
actual_beat=1.8,
standard_beat=1,
expected_workload=100,
pace_rate=100,
workload_rate=100,
status=ReportStatus.pending,
submitted_at=datetime(2026, 7, 25, 11, 1),
items=[
ProductionReportItem(
attendance_point_name="总厂",
device_no="23#",
project_no="P1",
product_name="23#",
process_name="冲压",
process_unit_price_yuan=2,
standard_beat=1,
standard_workload=0,
good_qty=100,
defect_qty=0,
scrap_qty=0,
)
],
)
db.add_all([_press_equipment(), _product(), report])
db.flush()
db.add(
ProductionReportAllocation(
report_id=report.id,
report_item_id=report.items[0].id,
attendance_point_name="总厂",
employee_phone="13800000000",
allocation_date=date(2026, 7, 20),
effective_minutes=1,
good_qty=1,
)
)
db.commit()
monkeypatch.setattr(reviews_router, "accessible_point_names", lambda _db, _user: ["总厂"])
monkeypatch.setattr(reviews_router, "report_out", lambda report, schedule=None: report)
saved = reviews_router.approve_report(
report.id,
ApproveReportRequest(
item_corrections=[
ReportItemCorrection(id=report.items[0].id, good_qty=200)
]
),
user=user,
db=db,
)
allocations = _stored_allocations(db, saved.id)
assert len(allocations) == 1
assert allocations[0].allocation_date == date(2026, 7, 25)
assert allocations[0].good_qty == 200
assert allocations[0].reference_wage == 400
finally:
db.close()
def test_purge_expired_voided_reports_deletes_allocations_before_report_items():
report = SimpleNamespace(id=1, session=None)
operations: list[str] = []
class _Rows:
def all(self):
return [report]
class _FakeDb:
def scalars(self, _statement):
return _Rows()
def execute(self, statement):
operations.append(statement.table.name)
def delete(self, _obj):
operations.append("delete-report")
def commit(self):
operations.append("commit")
purged_count = purge_expired_voided_reports(_FakeDb())
assert purged_count == 1
assert operations.index("production_report_allocations") < operations.index("production_report_items")