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.sales import create_sales_order, list_sales_orders # 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 # noqa: E402 from app.models.sales import Customer # noqa: E402 from app.schemas.database import SalesOrderCreate, SalesOrderItemCreate # noqa: E402 from app.schemas.document_archives import DocumentArchiveGenerateResult # noqa: E402 class SalesOrderDocumentArchiveTest(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() now = datetime.now() self.customer = Customer( id=1, customer_code="CUS-001", customer_name="测试客户", short_name="客户", address="上海市测试路 1 号", credit_days=30, status="ACTIVE", created_at=now, updated_at=now, ) self.product = Item( id=10, item_code="FG-001", item_name="测试成品", item_type="成品", specification="A-100", unit_weight_kg=1.25, status="ACTIVE", created_at=now, updated_at=now, ) self.db.add_all([self.customer, self.product]) self.db.commit() def tearDown(self) -> None: self.db.close() def _payload(self) -> SalesOrderCreate: return SalesOrderCreate( customer_id=self.customer.id, sales_employee_id=None, promised_date=date(2026, 6, 20), delivery_address="上海市测试路 2 号", tax_rate=0.13, remark="归档测试", items=[ SalesOrderItemCreate( product_item_id=self.product.id, order_qty=2, unit_price=100, promised_date=date(2026, 6, 20), customer_part_no="CPN-001", ) ], ) def test_create_sales_order_generates_archive_and_returns_archive_metadata(self) -> None: def fake_generate_archive( db: Session, document_type: str, business_id: int, ) -> DocumentArchiveGenerateResult: db.add( DocumentArchive( document_type=document_type, business_id=business_id, document_no="销售2026-00001", archive_version=1, template_version="单据纸面V1", file_format="PDF", file_name="销售2026-00001.pdf", file_path="/tmp/销售2026-00001.pdf", file_hash="abc123", status="已归档", error_message=None, created_by=None, ) ) db.commit() return DocumentArchiveGenerateResult( business_id=business_id, document_type=document_type, document_no="销售2026-00001", archive_status="已归档", archive_version=1, archive_error_message=None, ) with ( patch("app.api.routes.sales.ensure_employee_has_permission"), patch("app.api.routes.sales.generate_document_archive", side_effect=fake_generate_archive) as archive_mock, ): result = create_sales_order(self._payload(), self.db) archive_mock.assert_called_once_with(self.db, "销售订单", int(result.order_id)) 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.business_id == result.order_id)) self.assertIsNotNone(saved_archive) def test_list_sales_orders_exposes_latest_archive_fields(self) -> None: with ( patch("app.api.routes.sales.ensure_employee_has_permission"), patch( "app.api.routes.sales.generate_document_archive", return_value=DocumentArchiveGenerateResult( business_id=1, document_type="销售订单", document_no="销售2026-00001", archive_status="已归档", archive_version=1, archive_error_message=None, ), ), ): result = create_sales_order(self._payload(), self.db) created_at = datetime(2026, 6, 11, 10, 30, 0) self.db.add_all( [ DocumentArchive( document_type="销售订单", business_id=result.order_id, document_no=result.order_no, archive_version=1, template_version="单据纸面V1", file_format="PDF", file_name="v1.pdf", file_path="/tmp/v1.pdf", file_hash="v1", status="已归档", error_message=None, created_by=None, created_at=created_at, updated_at=created_at, ), DocumentArchive( document_type="销售订单", business_id=result.order_id, document_no=result.order_no, archive_version=2, template_version="单据纸面V1", file_format="PDF", file_name="v2.pdf", file_path="/tmp/v2.pdf", file_hash="v2", status="归档失败", error_message="render failed", created_by=None, created_at=datetime(2026, 6, 11, 10, 35, 0), updated_at=datetime(2026, 6, 11, 10, 35, 0), ), ] ) self.db.commit() rows = list_sales_orders(limit=200, db=self.db) self.assertEqual(len(rows), 1) self.assertEqual(rows[0].archive_status, "归档失败") self.assertEqual(rows[0].archive_version, 2) self.assertEqual(rows[0].archive_created_at, datetime(2026, 6, 11, 10, 35, 0)) self.assertEqual(rows[0].archive_error_message, "render failed") def test_list_sales_orders_uses_pdf_archive_when_same_version_has_other_format(self) -> None: with ( patch("app.api.routes.sales.ensure_employee_has_permission"), patch( "app.api.routes.sales.generate_document_archive", return_value=DocumentArchiveGenerateResult( business_id=1, document_type="销售订单", document_no="销售2026-00001", archive_status="已归档", archive_version=1, archive_error_message=None, ), ), ): result = create_sales_order(self._payload(), self.db) self.db.add_all( [ DocumentArchive( document_type="销售订单", business_id=result.order_id, document_no=result.order_no, archive_version=1, template_version="单据纸面V1", file_format="PDF", file_name="order.pdf", file_path="/tmp/order.pdf", file_hash="pdf", status="已归档", error_message=None, created_by=None, created_at=datetime(2026, 6, 11, 10, 30, 0), updated_at=datetime(2026, 6, 11, 10, 30, 0), ), DocumentArchive( document_type="销售订单", business_id=result.order_id, document_no=result.order_no, archive_version=1, template_version="单据纸面V1", file_format="DOCX", file_name="order.docx", file_path="/tmp/order.docx", file_hash="docx", status="DOCX不应显示", error_message="wrong format", created_by=None, created_at=datetime(2026, 6, 11, 10, 35, 0), updated_at=datetime(2026, 6, 11, 10, 35, 0), ), ] ) self.db.commit() rows = list_sales_orders(limit=200, db=self.db) self.assertEqual(len(rows), 1) self.assertEqual(rows[0].archive_status, "已归档") self.assertEqual(rows[0].archive_version, 1) self.assertIsNone(rows[0].archive_error_message) def test_create_sales_order_returns_failed_archive_metadata_when_archive_generation_raises(self) -> None: with ( patch("app.api.routes.sales.ensure_employee_has_permission"), patch("app.api.routes.sales.generate_document_archive", side_effect=RuntimeError("renderer exploded")), ): result = create_sales_order(self._payload(), self.db) self.assertIsNotNone(result.order_id) self.assertTrue(result.order_no) 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()