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.metrics import calculate_report_metrics, minutes_between, release_at_caps_work_time
|
||||||
from app.services.misc_work import is_misc_product
|
from app.services.misc_work import is_misc_product
|
||||||
from app.services.mold_locks import release_session_locks
|
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.report_lifecycle import purge_expired_voided_reports
|
||||||
from app.services.reporting_window import REPORTING_EXPIRED_DETAIL, is_reporting_expired
|
from app.services.reporting_window import REPORTING_EXPIRED_DETAIL, is_reporting_expired
|
||||||
from app.services.serializers import equipment_option, product_option, report_out
|
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)
|
release_session_locks(session, user.phone, "报工提交释放占用", submitted_at)
|
||||||
session.status = SessionStatus.submitted
|
session.status = SessionStatus.submitted
|
||||||
db.add(report)
|
db.add(report)
|
||||||
|
db.flush()
|
||||||
|
refresh_report_allocations(db, report, schedule=schedule, commit=False)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
saved = _get_report_or_404(db, report.id)
|
saved = _get_report_or_404(db, report.id)
|
||||||
@ -602,10 +605,13 @@ def submit_cleaning_report(
|
|||||||
items=report_items,
|
items=report_items,
|
||||||
)
|
)
|
||||||
db.add(report)
|
db.add(report)
|
||||||
|
schedule = get_work_schedule_config(db, point_name)
|
||||||
|
db.flush()
|
||||||
|
refresh_report_allocations(db, report, schedule=schedule, commit=False)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
saved = _get_report_or_404(db, report.id)
|
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)
|
@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.display_names import mold_process_display_name
|
||||||
from app.services.metrics import calculate_report_metrics
|
from app.services.metrics import calculate_report_metrics
|
||||||
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.report_allocations import refresh_report_allocations
|
||||||
from app.services.report_lifecycle import (
|
from app.services.report_lifecycle import (
|
||||||
can_edit_report,
|
can_edit_report,
|
||||||
mark_report_voided,
|
mark_report_voided,
|
||||||
@ -528,6 +529,7 @@ def approve_report(
|
|||||||
_validate_normal_report_devices(db, report)
|
_validate_normal_report_devices(db, report)
|
||||||
|
|
||||||
_apply_metrics(report, schedule)
|
_apply_metrics(report, schedule)
|
||||||
|
refresh_report_allocations(db, report, schedule=schedule, commit=False)
|
||||||
reviewed_at = now()
|
reviewed_at = now()
|
||||||
was_pending = report.status == ReportStatus.pending
|
was_pending = report.status == ReportStatus.pending
|
||||||
report.status = ReportStatus.approved
|
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.metrics import calculate_report_metrics, minutes_between, release_at_caps_work_time
|
||||||
from app.services.misc_work import is_misc_product
|
from app.services.misc_work import is_misc_product
|
||||||
from app.services.mold_locks import release_session_locks
|
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.services.work_schedule import DEFAULT_AUTO_SUBMIT_HOURS, get_work_schedule, get_work_schedule_config
|
||||||
from app.timezone import now
|
from app.timezone import now
|
||||||
|
|
||||||
@ -171,6 +172,8 @@ def auto_submit_session(db: Session, session: WorkSession, submitted_at: datetim
|
|||||||
items=items,
|
items=items,
|
||||||
)
|
)
|
||||||
db.add(report)
|
db.add(report)
|
||||||
|
db.flush()
|
||||||
|
refresh_report_allocations(db, report, schedule=schedule, commit=False)
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from app.models import (
|
|||||||
MoldLockFeedback,
|
MoldLockFeedback,
|
||||||
Personnel,
|
Personnel,
|
||||||
ProductionReport,
|
ProductionReport,
|
||||||
|
ProductionReportAllocation,
|
||||||
ProductionReportItem,
|
ProductionReportItem,
|
||||||
ReportAuditLog,
|
ReportAuditLog,
|
||||||
WorkSession,
|
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(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.execute(delete(ProductionReportItem).where(ProductionReportItem.report_id == report.id))
|
||||||
db.delete(report)
|
db.delete(report)
|
||||||
if session is not None:
|
if session is not None:
|
||||||
|
|||||||
@ -6,7 +6,32 @@ from sqlalchemy.ext.compiler import compiles
|
|||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from app.database import Base
|
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.metrics import calculate_report_metrics
|
||||||
from app.services.report_allocations import (
|
from app.services.report_allocations import (
|
||||||
allocation_summary_text,
|
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]
|
assert [row.good_qty for row in stored] == [1, 300]
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
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