867 lines
28 KiB
Python
867 lines
28 KiB
Python
from datetime import date, datetime, timedelta
|
|
from decimal import Decimal
|
|
from types import SimpleNamespace
|
|
|
|
from sqlalchemy import BigInteger, create_engine, inspect
|
|
from sqlalchemy.ext.compiler import compiles
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from app.database import Base
|
|
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,
|
|
OVER_LIMIT_TYPE_UNCHECKABLE,
|
|
current_report_qty,
|
|
evaluate_report_item_over_limit,
|
|
parse_process_order,
|
|
refresh_report_over_limit_snapshot,
|
|
)
|
|
from app.services.serializers import report_out
|
|
from app.timezone import now
|
|
|
|
|
|
@compiles(BigInteger, "sqlite")
|
|
def _compile_big_integer_for_sqlite(type_, compiler, **kw):
|
|
return "INTEGER"
|
|
|
|
|
|
def test_report_over_limit_columns_exist_on_sqlite_metadata():
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
inspector = inspect(engine)
|
|
|
|
report_column_rows = inspector.get_columns("production_reports")
|
|
item_column_rows = inspector.get_columns("production_report_items")
|
|
report_columns = {column["name"] for column in report_column_rows}
|
|
item_columns = {column["name"] for column in item_column_rows}
|
|
report_column_by_name = {column["name"]: column for column in report_column_rows}
|
|
item_column_by_name = {column["name"]: column for column in item_column_rows}
|
|
report_indexes = {index["name"]: tuple(index["column_names"]) for index in inspector.get_indexes("production_reports")}
|
|
item_indexes = {
|
|
index["name"]: tuple(index["column_names"])
|
|
for index in inspector.get_indexes("production_report_items")
|
|
}
|
|
|
|
assert {"has_over_limit", "over_limit_summary"}.issubset(report_columns)
|
|
assert {
|
|
"over_limit_status",
|
|
"over_limit_type",
|
|
"over_limit_reason",
|
|
"over_limit_checked_at",
|
|
"over_limit_check_source",
|
|
"over_limit_batch_no",
|
|
"over_limit_product_gross_weight_kg",
|
|
"over_limit_issued_weight_kg",
|
|
"over_limit_max_reportable_qty",
|
|
"over_limit_previous_good_qty",
|
|
"over_limit_current_cumulative_qty",
|
|
"over_limit_current_report_qty",
|
|
}.issubset(item_columns)
|
|
|
|
assert report_indexes["idx_reports_over_limit"] == ("has_over_limit", "submitted_at")
|
|
assert item_indexes["idx_report_items_over_limit_status"] == ("over_limit_status",)
|
|
assert item_indexes["idx_report_items_process_batch"] == (
|
|
"attendance_point_name",
|
|
"project_no",
|
|
"product_name",
|
|
"raw_material_batch_no",
|
|
"process_name",
|
|
)
|
|
assert report_column_by_name["has_over_limit"]["nullable"] is False
|
|
assert str(report_column_by_name["has_over_limit"]["default"]).strip("'\"") in {"0", "false", "FALSE"}
|
|
assert item_column_by_name["over_limit_status"]["nullable"] is False
|
|
assert str(item_column_by_name["over_limit_status"]["default"]).strip("'\"") == "not_checked"
|
|
|
|
|
|
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 test_parse_process_order_accepts_numeric_process_names():
|
|
assert parse_process_order("1") == 1
|
|
assert parse_process_order("1序") == 1
|
|
assert parse_process_order(" 02 序 ") == 2
|
|
|
|
|
|
def test_parse_process_order_rejects_special_and_text_process_names():
|
|
assert parse_process_order("杂活") is None
|
|
assert parse_process_order("清洗") is None
|
|
assert parse_process_order("第一序") is None
|
|
|
|
|
|
def test_current_report_qty_uses_good_defect_and_scrap():
|
|
item = SimpleNamespace(good_qty=Decimal("90"), defect_qty=Decimal("5"), scrap_qty=Decimal("5"))
|
|
|
|
assert current_report_qty(item) == Decimal("100.00")
|
|
|
|
|
|
def _report(report_id: int, status: ReportStatus = ReportStatus.pending) -> ProductionReport:
|
|
return ProductionReport(
|
|
id=report_id,
|
|
session_id=report_id,
|
|
attendance_point_name="总厂",
|
|
employee_phone="13800000000",
|
|
report_date=datetime(2026, 7, 25).date(),
|
|
start_at=datetime(2026, 7, 25, 8),
|
|
end_at=datetime(2026, 7, 25, 10),
|
|
status=status,
|
|
)
|
|
|
|
|
|
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:
|
|
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"),
|
|
)
|
|
|
|
result = evaluate_report_item_over_limit(db, report, item, exclude_report_id=report.id)
|
|
|
|
assert result.status == OVER_LIMIT_STATUS_EXCEEDED
|
|
assert result.max_reportable_qty == Decimal("50.00")
|
|
assert result.current_report_qty == Decimal("60.00")
|
|
assert result.current_cumulative_qty == Decimal("60.00")
|
|
assert "1序累计报工数量超过本批次材料可支撑数量" in result.reason
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_evaluate_defaults_to_excluding_current_report(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=50,
|
|
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"),
|
|
)
|
|
|
|
result = evaluate_report_item_over_limit(db, report, item)
|
|
|
|
assert result.current_cumulative_qty == Decimal("50.00")
|
|
assert result.status != OVER_LIMIT_STATUS_EXCEEDED
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_current_report_sibling_items_contribute_to_current_cumulative(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)
|
|
first_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=30,
|
|
defect_qty=0,
|
|
scrap_qty=0,
|
|
standard_beat=1,
|
|
)
|
|
second_item = ProductionReportItem(
|
|
id=2,
|
|
report=report,
|
|
attendance_point_name="总厂",
|
|
device_no="B",
|
|
project_no="P1",
|
|
product_name="产品A",
|
|
raw_material_batch_no=" LOT-1 ",
|
|
process_name="1",
|
|
good_qty=30,
|
|
defect_qty=0,
|
|
scrap_qty=0,
|
|
standard_beat=1,
|
|
)
|
|
db.add_all([product, report, first_item, second_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"),
|
|
)
|
|
|
|
result = evaluate_report_item_over_limit(db, report, first_item)
|
|
|
|
assert result.current_report_qty == Decimal("30.00")
|
|
assert result.current_cumulative_qty == Decimal("60.00")
|
|
assert result.status == OVER_LIMIT_STATUS_EXCEEDED
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_history_batch_numbers_are_compared_after_trim(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,
|
|
)
|
|
previous_report = _report(1, ReportStatus.approved)
|
|
previous_item = ProductionReportItem(
|
|
id=1,
|
|
report=previous_report,
|
|
attendance_point_name="总厂",
|
|
device_no="A",
|
|
project_no="P1",
|
|
product_name="产品A",
|
|
raw_material_batch_no=" LOT-1 ",
|
|
process_name="1",
|
|
good_qty=30,
|
|
defect_qty=0,
|
|
scrap_qty=0,
|
|
standard_beat=1,
|
|
)
|
|
report = _report(2)
|
|
item = ProductionReportItem(
|
|
id=2,
|
|
report=report,
|
|
attendance_point_name="总厂",
|
|
device_no="B",
|
|
project_no="P1",
|
|
product_name="产品A",
|
|
raw_material_batch_no="LOT-1",
|
|
process_name="1",
|
|
good_qty=30,
|
|
defect_qty=0,
|
|
scrap_qty=0,
|
|
standard_beat=1,
|
|
)
|
|
db.add_all([product, previous_report, previous_item, 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"),
|
|
)
|
|
|
|
result = evaluate_report_item_over_limit(db, report, item)
|
|
|
|
assert result.current_cumulative_qty == Decimal("60.00")
|
|
assert result.status == OVER_LIMIT_STATUS_EXCEEDED
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_erp_weight_lookup_sql_error_has_distinct_uncheckable_reason():
|
|
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=30,
|
|
defect_qty=0,
|
|
scrap_qty=0,
|
|
standard_beat=1,
|
|
)
|
|
db.add_all([product, report, item])
|
|
db.flush()
|
|
|
|
result = evaluate_report_item_over_limit(db, report, item)
|
|
|
|
assert result.over_limit_type == OVER_LIMIT_TYPE_UNCHECKABLE
|
|
assert result.reason == "读取批次投入重量失败,无法校验超额"
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_evaluate_report_item_over_limit_does_not_write_snapshot_fields(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"),
|
|
)
|
|
|
|
result = evaluate_report_item_over_limit(db, report, item)
|
|
|
|
assert result.status == OVER_LIMIT_STATUS_EXCEEDED
|
|
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_later_process_exceeds_previous_good_quantity():
|
|
db = _sqlite_db()
|
|
try:
|
|
db.add_all(
|
|
[
|
|
Product(
|
|
attendance_point_name="总厂",
|
|
project_no="P1",
|
|
product_name="产品A",
|
|
device_no="",
|
|
process_name="1",
|
|
standard_beat=1,
|
|
),
|
|
Product(
|
|
attendance_point_name="总厂",
|
|
project_no="P1",
|
|
product_name="产品A",
|
|
device_no="",
|
|
process_name="3",
|
|
standard_beat=1,
|
|
),
|
|
]
|
|
)
|
|
previous_report = _report(1, ReportStatus.approved)
|
|
previous_item = ProductionReportItem(
|
|
id=1,
|
|
report=previous_report,
|
|
attendance_point_name="总厂",
|
|
device_no="A",
|
|
project_no="P1",
|
|
product_name="产品A",
|
|
raw_material_batch_no="LOT-1",
|
|
process_name="1",
|
|
good_qty=100,
|
|
defect_qty=10,
|
|
scrap_qty=5,
|
|
standard_beat=1,
|
|
)
|
|
report = _report(2)
|
|
report.employee_phone = "13800000001"
|
|
item = ProductionReportItem(
|
|
id=2,
|
|
report=report,
|
|
attendance_point_name="总厂",
|
|
device_no="B",
|
|
project_no="P1",
|
|
product_name="产品A",
|
|
raw_material_batch_no="LOT-1",
|
|
process_name="3",
|
|
good_qty=98,
|
|
defect_qty=3,
|
|
scrap_qty=2,
|
|
standard_beat=1,
|
|
)
|
|
db.add_all([previous_report, previous_item, report, item])
|
|
db.flush()
|
|
|
|
result = evaluate_report_item_over_limit(db, report, item, exclude_report_id=report.id)
|
|
|
|
assert result.status == OVER_LIMIT_STATUS_EXCEEDED
|
|
assert result.previous_good_qty == Decimal("100.00")
|
|
assert result.current_report_qty == Decimal("103.00")
|
|
assert result.current_cumulative_qty == Decimal("103.00")
|
|
assert "3序累计报工数量超过1序累计成品数量" in result.reason
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_history_process_names_are_compared_after_trim(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,
|
|
)
|
|
previous_report = _report(1, ReportStatus.approved)
|
|
previous_item = ProductionReportItem(
|
|
id=1,
|
|
report=previous_report,
|
|
attendance_point_name="总厂",
|
|
device_no="A",
|
|
project_no="P1",
|
|
product_name="产品A",
|
|
raw_material_batch_no="LOT-1",
|
|
process_name=" 01序 ",
|
|
good_qty=30,
|
|
defect_qty=0,
|
|
scrap_qty=0,
|
|
standard_beat=1,
|
|
)
|
|
report = _report(2)
|
|
item = ProductionReportItem(
|
|
id=2,
|
|
report=report,
|
|
attendance_point_name="总厂",
|
|
device_no="B",
|
|
project_no="P1",
|
|
product_name="产品A",
|
|
raw_material_batch_no="LOT-1",
|
|
process_name="1",
|
|
good_qty=30,
|
|
defect_qty=0,
|
|
scrap_qty=0,
|
|
standard_beat=1,
|
|
)
|
|
db.add_all([product, previous_report, previous_item, 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"),
|
|
)
|
|
|
|
result = evaluate_report_item_over_limit(db, report, item)
|
|
|
|
assert result.current_cumulative_qty == Decimal("60.00")
|
|
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()
|
|
|
|
|
|
def test_report_out_exposes_over_limit_snapshot_fields():
|
|
report = ProductionReport(
|
|
id=1,
|
|
session_id=1,
|
|
attendance_point_name="总厂",
|
|
employee_phone="13800000000",
|
|
report_date=datetime(2026, 7, 25).date(),
|
|
start_at=datetime(2026, 7, 25, 8),
|
|
end_at=datetime(2026, 7, 25, 10),
|
|
status=ReportStatus.pending,
|
|
submitted_at=datetime(2026, 7, 25, 10),
|
|
has_over_limit=True,
|
|
over_limit_summary="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,
|
|
over_limit_status=OVER_LIMIT_STATUS_EXCEEDED,
|
|
over_limit_type="first_process_material",
|
|
over_limit_reason="1序累计报工数量超过本批次材料可支撑数量",
|
|
over_limit_checked_at=datetime(2026, 7, 25, 10),
|
|
over_limit_check_source="submit",
|
|
over_limit_batch_no="LOT-1",
|
|
over_limit_product_gross_weight_kg=Decimal("2.0000"),
|
|
over_limit_issued_weight_kg=Decimal("100.000000"),
|
|
over_limit_max_reportable_qty=Decimal("50.00"),
|
|
over_limit_previous_good_qty=None,
|
|
over_limit_current_cumulative_qty=Decimal("60.00"),
|
|
over_limit_current_report_qty=Decimal("60.00"),
|
|
)
|
|
report.items = [item]
|
|
report.allocations = []
|
|
report.audit_logs = []
|
|
|
|
output = report_out(report)
|
|
|
|
assert output.has_over_limit is True
|
|
assert output.over_limit_summary == "1项超额"
|
|
assert output.items[0].over_limit_status == OVER_LIMIT_STATUS_EXCEEDED
|
|
assert output.items[0].over_limit_reason == "1序累计报工数量超过本批次材料可支撑数量"
|
|
assert output.items[0].over_limit_max_reportable_qty == 50
|