feat: 报工保存后刷新归属明细
This commit is contained in:
parent
5918b5ddde
commit
ebb490590f
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user