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()