ForgeFlow-ERP/backend/tests/test_warehouse_operation_document_archive.py
2026-06-12 16:00:56 +08:00

306 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import unittest
from datetime import datetime
from decimal import Decimal
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import patch
from sqlalchemy import BigInteger, create_engine, select
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
@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.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 InventoryTxn, StockLot, WarehouseLocation # noqa: E402
from app.api.routes.inventory import create_warehouse_inbound, create_warehouse_outbound # noqa: E402
from app.schemas.operations import WarehouseInboundCreate, WarehouseOutboundCreate # noqa: E402
from app.services.document_archives import ( # noqa: E402
ARCHIVE_STATUS_READY,
DOCUMENT_TYPE_WAREHOUSE_OPERATION,
collect_archive_context,
collect_warehouse_operation_archive_context,
generate_document_archive,
)
class WarehouseOperationDocumentArchiveTest(unittest.TestCase):
def setUp(self) -> None:
engine = create_engine(
"sqlite+pysqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
future=True,
)
Base.metadata.create_all(engine)
self.SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
self.db: Session = self.SessionLocal()
self.now = datetime(2026, 6, 11, 9, 0, 0)
def tearDown(self) -> None:
self.db.close()
def _seed_customer_supplied_txn(self) -> InventoryTxn:
warehouse = Warehouse(
id=1,
warehouse_code="RAW",
warehouse_name="原材料库",
warehouse_type="RAW",
status="ACTIVE",
created_at=self.now,
updated_at=self.now,
)
location = WarehouseLocation(
id=2,
warehouse_id=warehouse.id,
location_code="RAW-A",
location_name="原材料暂存位",
status="ACTIVE",
created_at=self.now,
updated_at=self.now,
)
item = Item(
id=3,
item_code="RM00001",
item_name="冷轧钢板",
item_type="RAW_MATERIAL",
specification="1.2mm",
unit_weight_kg=Decimal("1"),
status="ACTIVE",
created_at=self.now,
updated_at=self.now,
)
source_lot = StockLot(
id=4,
lot_no="SRC-YL0001",
lot_role="RAW_MATERIAL",
item_id=item.id,
warehouse_id=warehouse.id,
source_doc_type="期初入库",
source_doc_id=1,
inbound_qty=Decimal("0"),
inbound_weight_kg=Decimal("100"),
remaining_qty=Decimal("0"),
remaining_weight_kg=Decimal("100"),
locked_qty=Decimal("0"),
locked_weight_kg=Decimal("0"),
unit_cost=Decimal("0"),
quality_status="PASS",
status="AVAILABLE",
created_at=self.now,
updated_at=self.now,
)
lot = StockLot(
id=5,
lot_no="YL0001",
lot_role="RAW_MATERIAL",
item_id=item.id,
warehouse_id=warehouse.id,
location_id=location.id,
source_doc_type="客料入库",
source_doc_id=1,
source_material_lot_id=source_lot.id,
inbound_qty=Decimal("0"),
inbound_weight_kg=Decimal("100"),
remaining_qty=Decimal("0"),
remaining_weight_kg=Decimal("100"),
locked_qty=Decimal("0"),
locked_weight_kg=Decimal("0"),
unit_cost=Decimal("0"),
quality_status="PASS",
status="AVAILABLE",
created_at=self.now,
updated_at=self.now,
)
txn = InventoryTxn(
id=6,
txn_no="TXN-CUSTOMER-001",
txn_type="CUSTOMER_SUPPLIED_IN",
item_id=item.id,
warehouse_id=warehouse.id,
location_id=location.id,
lot_id=lot.id,
qty_change=Decimal("0"),
weight_change_kg=Decimal("100"),
unit_cost=Decimal("0"),
amount=Decimal("0"),
source_doc_type="客料入库",
source_doc_id=1,
logistics_waybill_no="SF123",
logistics_freight_amount=Decimal("12.5"),
logistics_photo_url="/uploads/demo.jpg",
biz_time=self.now,
remark="客户来料测试",
created_at=self.now,
updated_at=self.now,
)
balance = StockBalance(
id=7,
item_id=item.id,
warehouse_id=warehouse.id,
location_id=location.id,
qty_on_hand=Decimal("0"),
weight_on_hand_kg=Decimal("100"),
qty_available=Decimal("0"),
weight_available_kg=Decimal("100"),
qty_allocated=Decimal("0"),
weight_allocated_kg=Decimal("0"),
avg_unit_cost=Decimal("0"),
updated_at=self.now,
)
self.db.add_all([warehouse, location, item, source_lot, lot, txn, balance])
self.db.commit()
return txn
def test_collect_warehouse_operation_archive_context_uses_inventory_txn(self) -> None:
txn = self._seed_customer_supplied_txn()
context = collect_warehouse_operation_archive_context(self.db, txn.id)
self.assertEqual(context.document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
self.assertEqual(context.business_id, txn.id)
self.assertEqual(context.title, "客料入库单归档")
self.assertEqual(context.document_no, "客料入库-TXN-CUSTOMER-001")
self.assertEqual(context.partner_label, "仓库")
self.assertEqual(context.partner_name, "原材料库")
self.assertEqual(context.status, "客料入库")
self.assertEqual(context.address, "原材料库 / 原材料暂存位")
self.assertEqual(context.total_amount, Decimal("0"))
self.assertEqual(len(context.lines), 1)
line = context.lines[0]
self.assertEqual(line.item_code, "RM00001")
self.assertEqual(line.item_name, "冷轧钢板")
self.assertEqual(line.quantity, Decimal("100.000000"))
self.assertEqual(line.delivered_or_received_quantity, Decimal("100.000000"))
self.assertIn("库存流水号TXN-CUSTOMER-001", line.remark or "")
self.assertIn("库存批次号YL0001", line.remark or "")
self.assertIn("来源库存批次号SRC-YL0001", line.remark or "")
self.assertIn("库位:原材料暂存位", line.remark or "")
self.assertIn("运单号SF123", line.remark or "")
self.assertIn("运费12.5", line.remark or "")
self.assertIn("辅助照片:已上传", line.remark or "")
self.assertIn("客户来料测试", line.remark or "")
def test_collect_archive_context_dispatches_warehouse_operation(self) -> None:
txn = self._seed_customer_supplied_txn()
context = collect_archive_context(self.db, DOCUMENT_TYPE_WAREHOUSE_OPERATION, txn.id)
self.assertEqual(context.document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
self.assertEqual(context.business_id, txn.id)
def test_unknown_english_txn_type_is_not_exposed_in_context(self) -> None:
txn = self._seed_customer_supplied_txn()
txn.txn_type = "unknown_code"
self.db.commit()
context = collect_warehouse_operation_archive_context(self.db, txn.id)
self.assertEqual(context.status, "未识别仓库操作")
self.assertEqual(context.title, "未识别仓库操作单归档")
self.assertEqual(context.document_no, "未识别仓库操作-TXN-CUSTOMER-001")
self.assertNotIn("unknown_code", context.remark)
self.assertNotIn("unknown_code", context.lines[0].remark or "")
def test_generate_document_archive_creates_pdf_for_warehouse_operation(self) -> None:
txn = self._seed_customer_supplied_txn()
with TemporaryDirectory() as tmp_dir, patch("app.services.document_archives.default_archive_root", return_value=tmp_dir):
result = generate_document_archive(self.db, DOCUMENT_TYPE_WAREHOUSE_OPERATION, txn.id)
archive = self.db.scalar(
select(DocumentArchive).where(
DocumentArchive.document_type == DOCUMENT_TYPE_WAREHOUSE_OPERATION,
DocumentArchive.business_id == txn.id,
)
)
self.assertIsNotNone(archive)
self.assertEqual(result.archive_status, ARCHIVE_STATUS_READY)
self.assertEqual(archive.status, ARCHIVE_STATUS_READY)
self.assertTrue(archive.file_name.endswith(".pdf"))
with open(archive.file_path, "rb") as pdf_file:
self.assertEqual(pdf_file.read(4), b"%PDF")
def test_customer_supplied_inbound_save_generates_warehouse_archive(self) -> None:
self._seed_customer_supplied_txn()
payload = WarehouseInboundCreate(
biz_type="CUSTOMER_SUPPLIED",
item_id=3,
warehouse_id=1,
location_id=2,
inbound_weight_kg=25,
inbound_qty=0,
unit_cost=0,
provider_name="测试客户",
waybill_no="SF999",
freight_amount=Decimal("8.5"),
remark="保存时生成仓库出入库单",
)
context = SimpleNamespace(user=SimpleNamespace(id=99))
with TemporaryDirectory() as tmp_dir, patch("app.services.document_archives.default_archive_root", return_value=tmp_dir):
result = create_warehouse_inbound(payload=payload, context=context, db=self.db)
self.assertEqual(result.archive_status, ARCHIVE_STATUS_READY)
self.assertEqual(result.archive_document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
self.assertIsNotNone(result.archive_business_id)
archive = self.db.scalar(
select(DocumentArchive).where(
DocumentArchive.document_type == DOCUMENT_TYPE_WAREHOUSE_OPERATION,
DocumentArchive.business_id == result.archive_business_id,
)
)
self.assertIsNotNone(archive)
self.assertTrue(archive.file_name.endswith(".pdf"))
def test_outsourcing_outbound_save_generates_warehouse_archive(self) -> None:
self._seed_customer_supplied_txn()
payload = WarehouseOutboundCreate(
biz_type="OUTSOURCING_OUT",
item_id=3,
warehouse_id=1,
location_id=2,
outbound_weight_kg=10,
outsourcing_party_name="测试委外厂",
waybill_no="SF-WW-001",
freight_amount=Decimal("9.9"),
remark="委外出库保存归档",
)
context = SimpleNamespace(user=SimpleNamespace(id=99))
with TemporaryDirectory() as tmp_dir, patch("app.services.document_archives.default_archive_root", return_value=tmp_dir):
result = create_warehouse_outbound(payload=payload, context=context, db=self.db)
self.assertEqual(result.archive_status, ARCHIVE_STATUS_READY)
self.assertEqual(result.archive_document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
self.assertIsNotNone(result.archive_business_id)
archive = self.db.scalar(
select(DocumentArchive).where(
DocumentArchive.document_type == DOCUMENT_TYPE_WAREHOUSE_OPERATION,
DocumentArchive.business_id == result.archive_business_id,
)
)
self.assertIsNotNone(archive)
self.assertTrue(archive.file_name.endswith(".pdf"))
if __name__ == "__main__":
unittest.main()