ForgeFlow-ERP/backend/tests/test_sales_order_delivery_trace.py
2026-06-14 21:05:49 +08:00

758 lines
30 KiB
Python

from __future__ import annotations
import unittest
from datetime import date, datetime
from decimal import Decimal
from fastapi import HTTPException
from sqlalchemy import BigInteger, create_engine, select
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import Session, sessionmaker
@compiles(BigInteger, "sqlite")
def _compile_bigint_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_delivery, create_sales_order # 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 Delivery, DeliveryItem, InventoryTxn, StockLot # noqa: E402
from app.models.org import Department, Employee, User # noqa: E402
from app.models.sales import Customer, SalesOrder, SalesOrderItem # noqa: E402
from app.schemas.database import SalesOrderCreate, SalesOrderItemCreate # noqa: E402
from app.schemas.operations import DeliveryCreate, DeliveryItemCreate # noqa: E402
from app.services.auth import AuthContext # noqa: E402
from app.services.document_archives import ARCHIVE_STATUS_READY, DOCUMENT_TYPE_WAREHOUSE_OPERATION # noqa: E402
from app.services.operations import get_delivery_items_query # noqa: E402
from app.services.sales_planning import get_sales_order_query # noqa: E402
class SalesOrderDeliveryTraceTest(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_master_data()
def tearDown(self) -> None:
self.db.close()
def _seed_master_data(self) -> None:
self.department = Department(id=1, dept_code="D-SALES", dept_name="销售部", dept_type="SALES", status="ACTIVE")
self.employee = Employee(id=1, employee_code="EMP-001", employee_name="张三", dept_id=1, status="ACTIVE")
self.user = User(
id=1,
username="tester",
password_hash="x",
employee_id=1,
dept_id=1,
nickname="测试员",
is_super_admin=1,
status="ACTIVE",
)
self.customer = Customer(
id=1,
customer_code="CUS-001",
customer_name="五金客户A",
short_name="客户A",
contact_name="李工",
contact_phone="13800000000",
address="宁波测试路1号",
credit_days=30,
status="ACTIVE",
)
self.product = Item(
id=1,
item_code="FG-001",
item_name="测试支架",
item_type="FINISHED_GOOD",
unit_weight_kg=Decimal("2.000000"),
safety_stock_weight_kg=Decimal("0"),
status="ACTIVE",
)
self.raw_material = Item(
id=2,
item_code="MAT-001",
item_name="冷轧钢板",
item_type="RAW_MATERIAL",
specification="1.0*1000",
material_grade="SPCC",
unit_weight_kg=Decimal("1.000000"),
safety_stock_weight_kg=Decimal("0"),
status="ACTIVE",
)
self.warehouse = Warehouse(id=1, warehouse_code="WH-FG", warehouse_name="成品库", warehouse_type="FINISHED", status="ACTIVE")
self.db.add_all([self.department, self.employee, self.user, self.customer, self.product, self.raw_material, self.warehouse])
self.db.flush()
self.raw_lot = StockLot(
id=1,
lot_no="RML-001",
lot_role="INBOUND_RAW",
item_id=self.raw_material.id,
warehouse_id=self.warehouse.id,
source_doc_type="TEST",
source_doc_id=1,
inbound_qty=Decimal("100"),
inbound_weight_kg=Decimal("100"),
remaining_qty=Decimal("60"),
remaining_weight_kg=Decimal("60"),
locked_qty=Decimal("0"),
locked_weight_kg=Decimal("0"),
unit_cost=Decimal("5"),
quality_status="PASS",
status="AVAILABLE",
)
self.finished_lot = StockLot(
id=2,
lot_no="FGL-001",
parent_lot_id=self.raw_lot.id,
lot_role="FINISHED_FROM_RAW_BATCH",
item_id=self.product.id,
warehouse_id=self.warehouse.id,
source_doc_type="COMPLETION",
source_doc_id=1,
source_line_id=1,
source_material_lot_id=self.raw_lot.id,
source_material_sub_batch_no="RML-001",
source_material_summary="RML-001 / 冷轧钢板 / 40kg / ¥200",
inbound_qty=Decimal("20"),
inbound_weight_kg=Decimal("40"),
remaining_qty=Decimal("20"),
remaining_weight_kg=Decimal("40"),
locked_qty=Decimal("0"),
locked_weight_kg=Decimal("0"),
unit_cost=Decimal("10"),
production_date=date.today(),
quality_status="PASS",
status="AVAILABLE",
)
self.finished_balance = StockBalance(
id=1,
item_id=self.product.id,
warehouse_id=self.warehouse.id,
location_id=None,
qty_on_hand=Decimal("20"),
weight_on_hand_kg=Decimal("40"),
qty_available=Decimal("20"),
weight_available_kg=Decimal("40"),
qty_allocated=Decimal("0"),
weight_allocated_kg=Decimal("0"),
avg_unit_cost=Decimal("10"),
)
self.db.add_all([self.raw_lot, self.finished_lot, self.finished_balance])
self.db.commit()
def _seed_second_finished_lot(self) -> StockLot:
second_lot = StockLot(
id=3,
lot_no="FGL-002",
parent_lot_id=self.raw_lot.id,
lot_role="FINISHED_FROM_RAW_BATCH",
item_id=self.product.id,
warehouse_id=self.warehouse.id,
source_doc_type="COMPLETION",
source_doc_id=2,
source_line_id=2,
source_material_lot_id=self.raw_lot.id,
source_material_sub_batch_no="RML-001",
source_material_summary="RML-001 / 冷轧钢板 / 10kg / ¥50",
inbound_qty=Decimal("5"),
inbound_weight_kg=Decimal("10"),
remaining_qty=Decimal("5"),
remaining_weight_kg=Decimal("10"),
locked_qty=Decimal("0"),
locked_weight_kg=Decimal("0"),
unit_cost=Decimal("10"),
production_date=date.today(),
quality_status="PASS",
status="AVAILABLE",
)
self.finished_balance.qty_on_hand += Decimal("5")
self.finished_balance.weight_on_hand_kg += Decimal("10")
self.finished_balance.qty_available += Decimal("5")
self.finished_balance.weight_available_kg += Decimal("10")
self.db.add_all([second_lot, self.finished_balance])
self.db.commit()
return second_lot
def _auth_context(self) -> AuthContext:
return AuthContext(
user=self.user,
employee=self.employee,
department=self.department,
role_codes=["ADMIN"],
role_names=["管理员"],
permission_codes=[],
)
def _seed_employee_permission(
self,
permission_code: str,
permission_name: str,
module_code: str,
role_code: str,
role_name: str,
) -> Employee:
from app.models.org import Permission, Role, RolePermission, UserRole
now = datetime(2026, 6, 8, 9, 0, 0)
permission = self.db.scalar(select(Permission).where(Permission.permission_code == permission_code))
if not permission:
permission = Permission(
permission_code=permission_code,
permission_name=permission_name,
module_code=module_code,
action_code="VIEW",
status="ACTIVE",
created_at=now,
updated_at=now,
)
self.db.add(permission)
self.db.flush()
role = self.db.scalar(select(Role).where(Role.role_code == role_code))
if not role:
role = Role(
role_code=role_code,
role_name=role_name,
role_scope="SYSTEM",
status="ACTIVE",
remark=None,
created_at=now,
updated_at=now,
)
self.db.add(role)
self.db.flush()
role_permission = self.db.scalar(
select(RolePermission).where(
RolePermission.role_id == role.id,
RolePermission.permission_id == permission.id,
)
)
if not role_permission:
self.db.add(RolePermission(role_id=role.id, permission_id=permission.id, created_at=now))
user_role = self.db.scalar(select(UserRole).where(UserRole.user_id == self.user.id, UserRole.role_id == role.id))
if not user_role:
self.db.add(UserRole(user_id=self.user.id, role_id=role.id, created_at=now))
self.db.commit()
return self.employee
def _seed_sales_employee_permission(self) -> Employee:
return self._seed_employee_permission(
"MENU_SALES_PLANNING",
"客户名录/销售订单菜单",
"SALES_PLANNING",
"SALES",
"销售人员",
)
def _seed_inventory_ledger_employee_permission(self) -> Employee:
return self._seed_employee_permission(
"MENU_INVENTORY_LEDGER",
"百华仓库",
"INVENTORY_LEDGER",
"WAREHOUSE",
"仓库人员",
)
def test_create_sales_order_persists_sales_employee(self) -> None:
sales_employee = self._seed_sales_employee_permission()
order_result = create_sales_order(
SalesOrderCreate(
customer_id=self.customer.id,
sales_employee_id=sales_employee.id,
promised_date=date.today(),
delivery_address=self.customer.address,
tax_rate=0,
items=[
SalesOrderItemCreate(
product_item_id=self.product.id,
order_qty=3,
unit_price=12.5,
customer_part_no="C-PART-001",
)
],
),
db=self.db,
)
order = self.db.get(SalesOrder, order_result.order_id)
order_row = self.db.execute(get_sales_order_query(limit=10)).mappings().first()
self.assertEqual(order.sales_employee_id, sales_employee.id)
self.assertEqual(order_row["sales_employee_id"], sales_employee.id)
self.assertEqual(order_row["sales_employee_name"], sales_employee.employee_name)
def test_sales_order_delivery_updates_fulfillment_and_trace_fields(self) -> None:
sales_employee = self._seed_sales_employee_permission()
order_result = create_sales_order(
SalesOrderCreate(
customer_id=self.customer.id,
sales_employee_id=sales_employee.id,
promised_date=date.today(),
delivery_address=self.customer.address,
tax_rate=0,
items=[
SalesOrderItemCreate(
product_item_id=self.product.id,
order_qty=10,
unit_price=12.5,
customer_part_no="C-PART-001",
)
],
),
db=self.db,
)
order_item = self.db.scalar(select(SalesOrderItem).where(SalesOrderItem.sales_order_id == order_result.order_id))
create_delivery(
DeliveryCreate(
sales_order_id=order_result.order_id,
customer_id=self.customer.id,
warehouse_id=self.warehouse.id,
consignee_name=self.customer.contact_name,
consignee_phone=self.customer.contact_phone,
delivery_address=self.customer.address,
waybill_no="YD-XS-001",
freight_amount=36.8,
items=[
DeliveryItemCreate(
sales_order_item_id=order_item.id,
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=4,
delivery_weight_kg=8,
unit_price=12.5,
)
],
),
context=self._auth_context(),
db=self.db,
)
self.db.refresh(order_item)
sales_order = self.db.get(SalesOrder, order_result.order_id)
delivery_item = self.db.scalar(select(DeliveryItem))
delivery = self.db.scalar(select(Delivery))
trace_row = self.db.execute(get_delivery_items_query(limit=10)).mappings().first()
stock_balance = self.db.scalar(select(StockBalance).where(StockBalance.item_id == self.product.id))
self.assertEqual(delivery.logistics_waybill_no, "YD-XS-001")
self.assertEqual(float(delivery.logistics_freight_amount), 36.8)
self.assertEqual(float(order_item.delivered_qty), 4)
self.assertEqual(order_item.status, "PARTIAL")
self.assertEqual(sales_order.status, "PARTIAL")
self.assertEqual(delivery_item.sales_order_item_id, order_item.id)
self.assertEqual(trace_row["source_material_lot_no"], "RML-001")
self.assertEqual(trace_row["source_material_code"], "MAT-001")
self.assertEqual(trace_row["source_material_name"], "冷轧钢板")
self.assertEqual(trace_row["source_material_sub_batch_no"], "RML-001")
self.assertEqual(trace_row["lot_no"], "FGL-001")
self.assertEqual(float(stock_balance.qty_on_hand), 16)
self.assertEqual(float(stock_balance.qty_available), 16)
def test_delivery_trace_aliases_legacy_sub_batch_to_source_lot_no(self) -> None:
self.finished_lot.source_material_lot_id = None
self.finished_lot.source_material_sub_batch_no = "JH_MAT001_0001"
self.db.commit()
sales_employee = self._seed_sales_employee_permission()
order_result = create_sales_order(
SalesOrderCreate(
customer_id=self.customer.id,
sales_employee_id=sales_employee.id,
promised_date=date.today(),
delivery_address=self.customer.address,
tax_rate=0,
items=[
SalesOrderItemCreate(
product_item_id=self.product.id,
order_qty=1,
unit_price=12.5,
)
],
),
db=self.db,
)
order_item = self.db.scalar(select(SalesOrderItem).where(SalesOrderItem.sales_order_id == order_result.order_id))
create_delivery(
DeliveryCreate(
sales_order_id=order_result.order_id,
customer_id=self.customer.id,
warehouse_id=self.warehouse.id,
consignee_name=self.customer.contact_name,
consignee_phone=self.customer.contact_phone,
delivery_address=self.customer.address,
waybill_no="YD-XS-LEGACY",
freight_amount=10,
items=[
DeliveryItemCreate(
sales_order_item_id=order_item.id,
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=1,
delivery_weight_kg=2,
unit_price=12.5,
)
],
),
context=self._auth_context(),
db=self.db,
)
trace_row = self.db.execute(get_delivery_items_query(limit=10)).mappings().first()
self.assertEqual(trace_row["source_material_lot_no"], "JH_MAT001_0001")
self.assertEqual(trace_row["source_material_sub_batch_no"], "JH_MAT001_0001")
def test_delivery_line_link_infers_sales_order_header(self) -> None:
sales_employee = self._seed_sales_employee_permission()
order_result = create_sales_order(
SalesOrderCreate(
customer_id=self.customer.id,
sales_employee_id=sales_employee.id,
promised_date=date.today(),
delivery_address=self.customer.address,
tax_rate=0,
items=[
SalesOrderItemCreate(
product_item_id=self.product.id,
order_qty=6,
unit_price=10,
)
],
),
db=self.db,
)
order_item = self.db.scalar(select(SalesOrderItem).where(SalesOrderItem.sales_order_id == order_result.order_id))
delivery_result = create_delivery(
DeliveryCreate(
customer_id=self.customer.id,
warehouse_id=self.warehouse.id,
waybill_no="YD-XS-002",
freight_amount=20,
items=[
DeliveryItemCreate(
sales_order_item_id=order_item.id,
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=2,
delivery_weight_kg=4,
unit_price=10,
)
],
),
context=self._auth_context(),
db=self.db,
)
delivery = self.db.get(Delivery, delivery_result.delivery_id)
sales_order = self.db.get(SalesOrder, order_result.order_id)
sales_out_txn = self.db.scalar(select(InventoryTxn).where(InventoryTxn.txn_type == "SALES_OUT"))
self.assertEqual(delivery_result.sales_order_id, order_result.order_id)
self.assertIsNotNone(sales_out_txn)
self.assertEqual(delivery_result.archive_status, ARCHIVE_STATUS_READY)
self.assertEqual(delivery_result.archive_document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
self.assertEqual(delivery_result.archive_business_id, sales_out_txn.id)
archive = self.db.scalar(
select(DocumentArchive).where(
DocumentArchive.document_type == DOCUMENT_TYPE_WAREHOUSE_OPERATION,
DocumentArchive.business_id == sales_out_txn.id,
)
)
self.assertIsNotNone(archive)
self.assertEqual(archive.status, ARCHIVE_STATUS_READY)
self.assertEqual(delivery.sales_order_id, order_result.order_id)
self.assertEqual(sales_order.status, "PARTIAL")
def test_delivery_generates_archive_for_each_sales_out_transaction(self) -> None:
second_lot = self._seed_second_finished_lot()
sales_employee = self._seed_sales_employee_permission()
order_result = create_sales_order(
SalesOrderCreate(
customer_id=self.customer.id,
sales_employee_id=sales_employee.id,
promised_date=date.today(),
delivery_address=self.customer.address,
tax_rate=0,
items=[
SalesOrderItemCreate(
product_item_id=self.product.id,
order_qty=6,
unit_price=10,
)
],
),
db=self.db,
)
order_item = self.db.scalar(select(SalesOrderItem).where(SalesOrderItem.sales_order_id == order_result.order_id))
delivery_result = create_delivery(
DeliveryCreate(
customer_id=self.customer.id,
warehouse_id=self.warehouse.id,
waybill_no="YD-MULTI-LOT",
freight_amount=20,
items=[
DeliveryItemCreate(
sales_order_item_id=order_item.id,
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=2,
delivery_weight_kg=4,
unit_price=10,
),
DeliveryItemCreate(
sales_order_item_id=order_item.id,
product_item_id=self.product.id,
lot_id=second_lot.id,
delivery_qty=1,
delivery_weight_kg=2,
unit_price=10,
),
],
),
context=self._auth_context(),
db=self.db,
)
sales_out_txns = self.db.scalars(
select(InventoryTxn).where(InventoryTxn.txn_type == "SALES_OUT").order_by(InventoryTxn.id)
).all()
self.assertEqual(len(sales_out_txns), 2)
self.assertEqual(delivery_result.archive_business_id, sales_out_txns[0].id)
archive_business_ids = set(
self.db.scalars(
select(DocumentArchive.business_id).where(
DocumentArchive.document_type == DOCUMENT_TYPE_WAREHOUSE_OPERATION,
DocumentArchive.business_id.in_([txn.id for txn in sales_out_txns]),
)
).all()
)
self.assertEqual(archive_business_ids, {txn.id for txn in sales_out_txns})
def test_create_delivery_accepts_inventory_ledger_shipper(self) -> None:
shipper = self._seed_inventory_ledger_employee_permission()
delivery_result = create_delivery(
DeliveryCreate(
customer_id=self.customer.id,
warehouse_id=self.warehouse.id,
shipper_employee_id=shipper.id,
waybill_no="YD-INVENTORY-SHIPPER",
freight_amount=12,
items=[
DeliveryItemCreate(
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=1,
delivery_weight_kg=2,
unit_price=0,
)
],
),
context=self._auth_context(),
db=self.db,
)
delivery = self.db.get(Delivery, delivery_result.delivery_id)
self.assertEqual(delivery.shipper_employee_id, shipper.id)
def test_sales_delivery_requires_logistics_info(self) -> None:
sales_employee = self._seed_sales_employee_permission()
order_result = create_sales_order(
SalesOrderCreate(
customer_id=self.customer.id,
sales_employee_id=sales_employee.id,
promised_date=date.today(),
delivery_address=self.customer.address,
tax_rate=0,
items=[
SalesOrderItemCreate(
product_item_id=self.product.id,
order_qty=6,
unit_price=10,
)
],
),
db=self.db,
)
order_item = self.db.scalar(select(SalesOrderItem).where(SalesOrderItem.sales_order_id == order_result.order_id))
with self.assertRaises(HTTPException) as exc:
create_delivery(
DeliveryCreate(
customer_id=self.customer.id,
warehouse_id=self.warehouse.id,
items=[
DeliveryItemCreate(
sales_order_item_id=order_item.id,
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=2,
delivery_weight_kg=4,
unit_price=10,
)
],
),
context=self._auth_context(),
db=self.db,
)
self.assertEqual(exc.exception.status_code, 400)
self.assertIn("运单号", exc.exception.detail)
self.assertIn("运费", exc.exception.detail)
def test_sales_delivery_allows_order_photo_instead_of_waybill(self) -> None:
sales_employee = self._seed_sales_employee_permission()
order_result = create_sales_order(
SalesOrderCreate(
customer_id=self.customer.id,
sales_employee_id=sales_employee.id,
promised_date=date.today(),
delivery_address=self.customer.address,
tax_rate=0,
items=[
SalesOrderItemCreate(
product_item_id=self.product.id,
order_qty=6,
unit_price=10,
)
],
),
db=self.db,
)
order_item = self.db.scalar(select(SalesOrderItem).where(SalesOrderItem.sales_order_id == order_result.order_id))
delivery_result = create_delivery(
DeliveryCreate(
customer_id=self.customer.id,
warehouse_id=self.warehouse.id,
order_photo_url="/inventory/logistics-photos/sales-photo.jpg",
freight_amount=20,
items=[
DeliveryItemCreate(
sales_order_item_id=order_item.id,
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=2,
delivery_weight_kg=4,
unit_price=10,
)
],
),
context=self._auth_context(),
db=self.db,
)
delivery = self.db.get(Delivery, delivery_result.delivery_id)
txn = self.db.scalar(select(InventoryTxn).where(InventoryTxn.txn_type == "SALES_OUT"))
self.assertIsNone(delivery.logistics_waybill_no)
self.assertEqual(delivery.logistics_photo_url, "/inventory/logistics-photos/sales-photo.jpg")
self.assertEqual(txn.logistics_photo_url, "/inventory/logistics-photos/sales-photo.jpg")
def test_direct_customer_delivery_without_sales_order_preserves_trace_and_inventory(self) -> None:
delivery_result = create_delivery(
DeliveryCreate(
customer_id=self.customer.id,
warehouse_id=self.warehouse.id,
consignee_name=self.customer.contact_name,
consignee_phone=self.customer.contact_phone,
delivery_address=self.customer.address,
waybill_no="YD-DIRECT-001",
freight_amount=18,
items=[
DeliveryItemCreate(
sales_order_item_id=None,
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=3,
delivery_weight_kg=6,
unit_price=0,
)
],
),
context=self._auth_context(),
db=self.db,
)
delivery = self.db.get(Delivery, delivery_result.delivery_id)
delivery_item = self.db.scalar(select(DeliveryItem).where(DeliveryItem.delivery_id == delivery.id))
trace_row = self.db.execute(get_delivery_items_query(limit=10, delivery_id=delivery.id)).mappings().first()
stock_balance = self.db.scalar(select(StockBalance).where(StockBalance.item_id == self.product.id))
sales_out_txn = self.db.scalar(select(InventoryTxn).where(InventoryTxn.txn_type == "SALES_OUT"))
self.assertIsNone(delivery.sales_order_id)
self.assertEqual(delivery.customer_id, self.customer.id)
self.assertIsNone(delivery_item.sales_order_item_id)
self.assertEqual(trace_row["source_material_lot_no"], "RML-001")
self.assertEqual(trace_row["source_material_code"], "MAT-001")
self.assertEqual(trace_row["source_material_name"], "冷轧钢板")
self.assertEqual(trace_row["source_material_sub_batch_no"], "RML-001")
self.assertEqual(trace_row["source_material_summary"], "RML-001 / 冷轧钢板 / 40kg / ¥200")
self.assertEqual(float(stock_balance.qty_on_hand), 17)
self.assertEqual(float(stock_balance.qty_available), 17)
self.assertEqual(sales_out_txn.source_doc_type, "DELIVERY")
self.assertEqual(sales_out_txn.source_doc_id, delivery.id)
def test_sales_delivery_rejects_non_finished_warehouse(self) -> None:
raw_warehouse = Warehouse(
id=2,
warehouse_code="WH-RAW",
warehouse_name="原材料库",
warehouse_type="RAW",
status="ACTIVE",
)
self.db.add(raw_warehouse)
self.db.commit()
with self.assertRaises(HTTPException) as exc:
create_delivery(
DeliveryCreate(
customer_id=self.customer.id,
warehouse_id=raw_warehouse.id,
waybill_no="YD-WRONG-WH",
freight_amount=12,
items=[
DeliveryItemCreate(
product_item_id=self.product.id,
lot_id=self.finished_lot.id,
delivery_qty=1,
delivery_weight_kg=2,
unit_price=0,
)
],
),
context=self._auth_context(),
db=self.db,
)
self.assertEqual(exc.exception.status_code, 400)
self.assertIn("销售出库必须选择成品库", exc.exception.detail)
if __name__ == "__main__":
unittest.main()