332 lines
12 KiB
Python
332 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from datetime import date, datetime
|
|
from unittest.mock import patch
|
|
|
|
from sqlalchemy import BigInteger, create_engine, select
|
|
from sqlalchemy.ext.compiler import compiles
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
|
|
@compiles(BigInteger, "sqlite")
|
|
def _compile_big_integer_for_sqlite(type_, compiler, **kw) -> str:
|
|
_ = type_, compiler, kw
|
|
return "INTEGER"
|
|
|
|
|
|
import app.models.document_archive # noqa: E402,F401
|
|
import app.models.master_data # noqa: E402,F401
|
|
import app.models.miniapp # noqa: E402,F401
|
|
import app.models.operations # noqa: E402,F401
|
|
import app.models.org # noqa: E402,F401
|
|
import app.models.planning # noqa: E402,F401
|
|
import app.models.sales # noqa: E402,F401
|
|
from app.api.routes.quality import inspect_receipt_item # noqa: E402
|
|
from app.models.base import Base # noqa: E402
|
|
from app.models.document_archive import DocumentArchive # noqa: E402
|
|
from app.models.master_data import Item, StockBalance, Warehouse # noqa: E402
|
|
from app.models.operations import PurchaseOrder, PurchaseOrderItem, PurchaseReceipt, PurchaseReceiptItem, StockLot, Supplier # noqa: E402
|
|
from app.models.org import Department, Employee, User # noqa: E402
|
|
from app.schemas.document_archives import DocumentArchiveGenerateResult # noqa: E402
|
|
from app.schemas.operations import QualityInspectionAction # noqa: E402
|
|
from app.services.auth import AuthContext # noqa: E402
|
|
|
|
|
|
class QualityInspectionDocumentArchiveTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
|
Base.metadata.create_all(engine)
|
|
self.SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
|
|
self.db: Session = self.SessionLocal()
|
|
self._seed_inspection_data()
|
|
|
|
def tearDown(self) -> None:
|
|
self.db.close()
|
|
|
|
def _seed_inspection_data(self) -> None:
|
|
now = datetime(2026, 6, 11, 9, 0, 0)
|
|
self.department = Department(
|
|
id=1,
|
|
dept_code="D-QC-001",
|
|
dept_name="质检部",
|
|
dept_type="QUALITY",
|
|
status="ACTIVE",
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.employee = Employee(
|
|
id=1,
|
|
employee_code="EMP-QC-001",
|
|
employee_name="质检员王工",
|
|
dept_id=1,
|
|
mobile="13800000001",
|
|
status="ACTIVE",
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.user = User(
|
|
id=1,
|
|
username="quality_tester",
|
|
password_hash="x",
|
|
employee_id=1,
|
|
dept_id=1,
|
|
nickname="质检测试员",
|
|
is_super_admin=1,
|
|
status="ACTIVE",
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.supplier = Supplier(
|
|
id=10,
|
|
supplier_code="SUP-QC-001",
|
|
supplier_name="质检供应商",
|
|
short_name="质检供应商",
|
|
lead_time_days=0,
|
|
default_tax_rate=0,
|
|
status="ACTIVE",
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.material_item = Item(
|
|
id=20,
|
|
item_code="RM-QC-001",
|
|
item_name="质检原材料",
|
|
item_type="RAW_MATERIAL",
|
|
specification="D60",
|
|
material_grade="45#",
|
|
unit_weight_kg=1,
|
|
status="ACTIVE",
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.warehouse = Warehouse(
|
|
id=30,
|
|
warehouse_code="WH-RAW",
|
|
warehouse_name="原材料库",
|
|
warehouse_type="RAW",
|
|
status="ACTIVE",
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.purchase_order = PurchaseOrder(
|
|
id=40,
|
|
po_no="采购2026-00001",
|
|
supplier_id=self.supplier.id,
|
|
order_date=date(2026, 6, 11),
|
|
expected_date=date(2026, 6, 20),
|
|
warning_lead_days=0,
|
|
purchaser_employee_id=None,
|
|
target_warehouse_type="RAW",
|
|
tax_rate=0.13,
|
|
total_amount=660,
|
|
status="PENDING_QC",
|
|
remark=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.purchase_order_item = PurchaseOrderItem(
|
|
id=50,
|
|
purchase_order_id=self.purchase_order.id,
|
|
line_no=1,
|
|
material_item_id=self.material_item.id,
|
|
source_demand_id=None,
|
|
order_qty=0,
|
|
order_weight_kg=100,
|
|
received_qty=0,
|
|
received_weight_kg=0,
|
|
unit_price=6.6,
|
|
line_amount=660,
|
|
status="OPEN",
|
|
remark=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.receipt = PurchaseReceipt(
|
|
id=60,
|
|
receipt_no="入库2026-00001",
|
|
purchase_order_id=self.purchase_order.id,
|
|
warehouse_id=self.warehouse.id,
|
|
receipt_date=datetime(2026, 6, 12, 10, 0, 0),
|
|
receiver_employee_id=None,
|
|
supplier_delivery_no=None,
|
|
logistics_waybill_no="YD-QC-001",
|
|
logistics_freight_amount=50,
|
|
logistics_photo_url=None,
|
|
status="PENDING_QC",
|
|
remark=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.receipt_item = PurchaseReceiptItem(
|
|
id=70,
|
|
receipt_id=self.receipt.id,
|
|
line_no=1,
|
|
purchase_order_item_id=self.purchase_order_item.id,
|
|
material_item_id=self.material_item.id,
|
|
location_id=None,
|
|
lot_no="YL0001",
|
|
merge_to_lot_id=None,
|
|
received_qty=0,
|
|
received_weight_kg=100,
|
|
accepted_qty=0,
|
|
accepted_weight_kg=0,
|
|
unit_cost=6.6,
|
|
status="PENDING_QC",
|
|
remark=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.stock_lot = StockLot(
|
|
id=80,
|
|
lot_no=self.receipt_item.lot_no,
|
|
parent_lot_id=None,
|
|
lot_role="INBOUND_RAW",
|
|
material_sub_batch_no=None,
|
|
item_id=self.material_item.id,
|
|
warehouse_id=self.warehouse.id,
|
|
location_id=None,
|
|
source_doc_type="PURCHASE_RECEIPT",
|
|
source_doc_id=self.receipt.id,
|
|
source_line_id=self.receipt_item.id,
|
|
source_material_lot_id=None,
|
|
source_material_sub_batch_no=None,
|
|
source_material_summary=None,
|
|
inbound_qty=0,
|
|
inbound_weight_kg=100,
|
|
remaining_qty=0,
|
|
remaining_weight_kg=100,
|
|
locked_qty=0,
|
|
locked_weight_kg=100,
|
|
unit_cost=6.6,
|
|
production_date=None,
|
|
expire_date=None,
|
|
quality_status="PENDING_QC",
|
|
logistics_waybill_no="YD-QC-001",
|
|
logistics_freight_amount=50,
|
|
logistics_photo_url=None,
|
|
status="LOCKED",
|
|
remark="到货待检",
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.balance = StockBalance(
|
|
id=90,
|
|
item_id=self.material_item.id,
|
|
warehouse_id=self.warehouse.id,
|
|
location_id=None,
|
|
qty_on_hand=0,
|
|
weight_on_hand_kg=100,
|
|
qty_available=0,
|
|
weight_available_kg=0,
|
|
qty_allocated=0,
|
|
weight_allocated_kg=0,
|
|
avg_unit_cost=6.6,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
self.db.add_all([
|
|
self.department,
|
|
self.employee,
|
|
self.user,
|
|
self.supplier,
|
|
self.material_item,
|
|
self.warehouse,
|
|
self.purchase_order,
|
|
self.purchase_order_item,
|
|
self.receipt,
|
|
self.receipt_item,
|
|
self.stock_lot,
|
|
self.balance,
|
|
])
|
|
self.db.commit()
|
|
|
|
def _auth_context(self) -> AuthContext:
|
|
return AuthContext(
|
|
user=self.user,
|
|
employee=self.employee,
|
|
department=self.department,
|
|
role_codes=["ADMIN"],
|
|
role_names=["管理员"],
|
|
permission_codes=[],
|
|
)
|
|
|
|
def _archive_result(self, *, business_id: int) -> DocumentArchiveGenerateResult:
|
|
return DocumentArchiveGenerateResult(
|
|
business_id=business_id,
|
|
document_type="质量校验单",
|
|
document_no="质检-入库2026-00001-1",
|
|
archive_status="已归档",
|
|
archive_version=1,
|
|
archive_error_message=None,
|
|
)
|
|
|
|
def test_inspect_receipt_item_generates_quality_archive_and_returns_metadata(self) -> None:
|
|
def fake_generate_archive(
|
|
db: Session,
|
|
document_type: str,
|
|
business_id: int,
|
|
created_by: int | None = None,
|
|
) -> DocumentArchiveGenerateResult:
|
|
db.add(
|
|
DocumentArchive(
|
|
document_type=document_type,
|
|
business_id=business_id,
|
|
document_no="质检-入库2026-00001-1",
|
|
archive_version=1,
|
|
template_version="单据纸面V1",
|
|
file_format="PDF",
|
|
file_name="质检-入库2026-00001-1.pdf",
|
|
file_path="/tmp/质检-入库2026-00001-1.pdf",
|
|
file_hash="quality123",
|
|
status="已归档",
|
|
error_message=None,
|
|
created_by=created_by,
|
|
created_at=datetime(2026, 6, 12, 10, 30, 0),
|
|
updated_at=datetime(2026, 6, 12, 10, 30, 0),
|
|
)
|
|
)
|
|
db.commit()
|
|
return self._archive_result(business_id=business_id)
|
|
|
|
with patch("app.api.routes.quality.generate_document_archive", side_effect=fake_generate_archive) as archive_mock:
|
|
result = inspect_receipt_item(
|
|
self.receipt_item.id,
|
|
QualityInspectionAction(result="PASS", remark="表面无划伤"),
|
|
context=self._auth_context(),
|
|
db=self.db,
|
|
)
|
|
|
|
archive_mock.assert_called_once_with(self.db, "质量校验单", int(self.receipt_item.id), created_by=1)
|
|
self.assertEqual(result.status, "PASSED")
|
|
self.assertEqual(result.quality_status, "PASS")
|
|
self.assertEqual(result.archive_status, "已归档")
|
|
self.assertEqual(result.archive_version, 1)
|
|
self.assertIsNone(result.archive_error_message)
|
|
|
|
saved_archive = self.db.scalar(
|
|
select(DocumentArchive).where(
|
|
DocumentArchive.document_type == "质量校验单",
|
|
DocumentArchive.business_id == self.receipt_item.id,
|
|
)
|
|
)
|
|
self.assertIsNotNone(saved_archive)
|
|
|
|
def test_inspect_receipt_item_returns_failed_archive_metadata_when_archive_generation_raises(self) -> None:
|
|
with patch("app.api.routes.quality.generate_document_archive", side_effect=RuntimeError("renderer exploded")):
|
|
result = inspect_receipt_item(
|
|
self.receipt_item.id,
|
|
QualityInspectionAction(result="REJECT", remark="硬度不合格"),
|
|
context=self._auth_context(),
|
|
db=self.db,
|
|
)
|
|
|
|
self.assertEqual(result.status, "REJECTED")
|
|
self.assertEqual(result.archive_status, "归档失败")
|
|
self.assertIsNone(result.archive_version)
|
|
self.assertTrue(result.archive_error_message and result.archive_error_message.startswith("归档失败:"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|