feat: 报工提交和审核刷新超额快照

This commit is contained in:
souplearn
2026-07-25 20:44:22 +08:00
parent 1ed48314be
commit b203b782de
4 changed files with 356 additions and 5 deletions
+297 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import date, datetime, timedelta
from decimal import Decimal
from types import SimpleNamespace
@@ -7,7 +7,16 @@ from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import sessionmaker
from app.database import Base
from app.models import Product, ProductionReport, ProductionReportItem, ReportStatus
from app.models import Equipment, Product, ProductionReport, 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,
ReportItemCorrection,
ReportSubmitBlock,
ReportSubmitItem,
ReportSubmitRequest,
)
from app.services.report_over_limit import (
OVER_LIMIT_STATUS_EXCEEDED,
OVER_LIMIT_STATUS_NOT_CHECKED,
@@ -15,7 +24,9 @@ from app.services.report_over_limit import (
current_report_qty,
evaluate_report_item_over_limit,
parse_process_order,
refresh_report_over_limit_snapshot,
)
from app.timezone import now
@compiles(BigInteger, "sqlite")
@@ -109,6 +120,44 @@ def _report(report_id: int, status: ReportStatus = ReportStatus.pending) -> Prod
)
def _numeric_product(*, gross_weight: Decimal = Decimal("2")) -> Product:
return Product(
attendance_point_name="总厂",
project_no="P1",
product_name="产品A",
device_no="",
process_name="1",
product_gross_weight_kg=gross_weight,
standard_beat=1,
standard_workload=0,
)
def _press_equipment() -> Equipment:
return Equipment(attendance_point_name="总厂", device_no="E1", device_type="冲压设备")
def _numeric_session(*, status: SessionStatus = SessionStatus.reporting) -> WorkSession:
end_at = now() - timedelta(minutes=1)
start_at = end_at - timedelta(hours=2)
return WorkSession(
attendance_point_name="总厂",
employee_phone="13800000000",
start_at=start_at,
end_at=end_at,
status=status,
devices=[
WorkSessionDevice(
attendance_point_name="总厂",
device_no="产品A",
process_name="1",
scanned_at=start_at,
sort_order=0,
)
],
)
def test_first_process_exceeds_material_limit_with_submit_snapshot(monkeypatch):
db = _sqlite_db()
try:
@@ -515,3 +564,249 @@ def test_history_process_names_are_compared_after_trim(monkeypatch):
assert result.status == OVER_LIMIT_STATUS_EXCEEDED
finally:
db.close()
def test_refresh_report_over_limit_snapshot_writes_item_and_report_summary(monkeypatch):
db = _sqlite_db()
try:
product = Product(
attendance_point_name="总厂",
project_no="P1",
product_name="产品A",
device_no="",
process_name="1",
product_gross_weight_kg=Decimal("2"),
standard_beat=1,
)
report = _report(1)
item = ProductionReportItem(
id=1,
report=report,
attendance_point_name="总厂",
device_no="A",
project_no="P1",
product_name="产品A",
raw_material_batch_no=" LOT-1 ",
process_name="1",
good_qty=60,
defect_qty=0,
scrap_qty=0,
standard_beat=1,
)
db.add_all([product, report, item])
db.flush()
monkeypatch.setattr(
"app.services.report_over_limit.read_batch_issued_weight_kg",
lambda db, batch_no, project_no, product_name: Decimal("100"),
)
refresh_report_over_limit_snapshot(
db,
report,
source="submit",
checked_at=datetime(2026, 7, 25, 10),
)
assert report.has_over_limit is True
assert report.over_limit_summary == "1项超额"
assert item.over_limit_status == OVER_LIMIT_STATUS_EXCEEDED
assert item.over_limit_check_source == "submit"
assert item.over_limit_checked_at == datetime(2026, 7, 25, 10)
assert item.over_limit_batch_no == "LOT-1"
assert item.over_limit_max_reportable_qty == Decimal("50.00")
finally:
db.close()
def test_refresh_report_over_limit_snapshot_skips_non_numeric_process(monkeypatch):
db = _sqlite_db()
try:
product = Product(
attendance_point_name="总厂",
project_no="P1",
product_name="产品A",
device_no="",
process_name="冲压",
product_gross_weight_kg=Decimal("2"),
standard_beat=1,
)
report = _report(1)
item = ProductionReportItem(
id=1,
report=report,
attendance_point_name="总厂",
device_no="A",
project_no="P1",
product_name="产品A",
raw_material_batch_no="LOT-1",
process_name="冲压",
good_qty=60,
defect_qty=0,
scrap_qty=0,
standard_beat=1,
)
db.add_all([product, report, item])
db.flush()
monkeypatch.setattr(
"app.services.report_over_limit.read_batch_issued_weight_kg",
lambda db, batch_no, project_no, product_name: Decimal("100"),
)
refresh_report_over_limit_snapshot(
db,
report,
source="submit",
checked_at=datetime(2026, 7, 25, 10),
)
assert report.has_over_limit is False
assert report.over_limit_summary is None
assert item.over_limit_status == OVER_LIMIT_STATUS_NOT_CHECKED
assert item.over_limit_type is None
assert item.over_limit_reason is None
finally:
db.close()
def test_refresh_report_over_limit_snapshot_accepts_source_positional_argument(monkeypatch):
db = _sqlite_db()
try:
report = _report(1)
item = ProductionReportItem(
id=1,
report=report,
attendance_point_name="总厂",
device_no="A",
project_no="P1",
product_name="产品A",
raw_material_batch_no="LOT-1",
process_name="冲压",
good_qty=60,
defect_qty=0,
scrap_qty=0,
standard_beat=1,
)
db.add_all([report, item])
db.flush()
refresh_report_over_limit_snapshot(db, report, "submit", checked_at=datetime(2026, 7, 25, 10))
assert item.over_limit_check_source == "submit"
finally:
db.close()
def test_submit_report_refreshes_over_limit_snapshot(monkeypatch):
db = _sqlite_db()
try:
user = SimpleNamespace(phone="13800000000")
session = _numeric_session()
db.add_all([_press_equipment(), _numeric_product(), session])
db.commit()
monkeypatch.setattr(reports_router, "report_out", lambda report, schedule=None: report)
monkeypatch.setattr(
"app.services.report_over_limit.read_batch_issued_weight_kg",
lambda db, batch_no, project_no, product_name: Decimal("100"),
)
report = reports_router.submit_report(
ReportSubmitRequest(
session_id=session.id,
device_blocks=[
ReportSubmitBlock(
device_no="产品A",
process_name="1",
items=[
ReportSubmitItem(
device_no="E1",
project_no="P1",
product_name="产品A",
process_name="1",
standard_beat=1,
raw_material_batch_no="LOT-1",
good_qty=60,
)
],
)
],
),
user=user,
db=db,
)
assert report.has_over_limit is True
assert report.over_limit_summary == "1项超额"
assert report.items[0].over_limit_status == OVER_LIMIT_STATUS_EXCEEDED
assert report.items[0].over_limit_check_source == "submit"
assert report.items[0].over_limit_current_report_qty == Decimal("60.00")
finally:
db.close()
def test_approve_report_refreshes_over_limit_snapshot_after_correction(monkeypatch):
db = _sqlite_db()
try:
user = SimpleNamespace(phone="13900000000", role=Role.admin)
session = _numeric_session(status=SessionStatus.submitted)
report = ProductionReport(
attendance_point_name="总厂",
session=session,
employee_phone="13800000000",
report_date=date(2026, 7, 25),
start_at=session.start_at,
end_at=session.end_at,
duration_minutes=120,
effective_minutes=120,
total_good_qty=40,
total_output_qty=40,
actual_beat=180,
standard_beat=1,
expected_workload=0,
pace_rate=0,
workload_rate=0,
status=ReportStatus.pending,
submitted_at=session.end_at,
items=[
ProductionReportItem(
attendance_point_name="总厂",
device_no="E1",
project_no="P1",
product_name="产品A",
raw_material_batch_no="LOT-1",
process_name="1",
standard_beat=1,
standard_workload=0,
good_qty=40,
defect_qty=0,
scrap_qty=0,
)
],
)
db.add_all([_press_equipment(), _numeric_product(), report])
db.commit()
monkeypatch.setattr(reviews_router, "accessible_point_names", lambda _db, _user: ["总厂"])
monkeypatch.setattr(reviews_router, "report_out", lambda report, schedule=None: report)
monkeypatch.setattr(
"app.services.report_over_limit.read_batch_issued_weight_kg",
lambda db, batch_no, project_no, product_name: Decimal("100"),
)
saved = reviews_router.approve_report(
report.id,
ApproveReportRequest(
item_corrections=[
ReportItemCorrection(id=report.items[0].id, good_qty=60)
]
),
user=user,
db=db,
)
assert saved.has_over_limit is True
assert saved.over_limit_summary == "1项超额"
assert saved.items[0].good_qty == 60
assert saved.items[0].over_limit_status == OVER_LIMIT_STATUS_EXCEEDED
assert saved.items[0].over_limit_check_source == "review"
assert saved.items[0].over_limit_current_report_qty == Decimal("60.00")
finally:
db.close()