620 lines
22 KiB
Python
620 lines
22 KiB
Python
from __future__ import annotations
|
||
|
||
import unittest
|
||
from datetime import UTC, datetime
|
||
from decimal import Decimal
|
||
from types import SimpleNamespace
|
||
|
||
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.models.base import Base # noqa: E402
|
||
from app.models.document_archive import DocumentArchive # noqa: E402
|
||
from app.models.master_data import Item, Product, StockBalance, Unit, Warehouse # noqa: E402
|
||
from app.models.operations import InventoryTxn, StockLot, WarehouseLocation # noqa: E402
|
||
from app.models.org import Department, Employee, User # noqa: E402
|
||
from app.schemas.operations import ( # noqa: E402
|
||
SpecialWarehouseAdjustmentCreate,
|
||
SpecialWarehouseAdjustmentLineCreate,
|
||
)
|
||
from app.api.routes.inventory import create_special_warehouse_adjustment # noqa: E402
|
||
from app.services.document_archives import ARCHIVE_STATUS_READY, DOCUMENT_TYPE_WAREHOUSE_OPERATION # noqa: E402
|
||
from app.services.operations import get_inventory_txn_ledger_query # noqa: E402
|
||
from app.services.special_inventory_adjustment import create_special_inventory_adjustment # noqa: E402
|
||
|
||
|
||
class SpecialWarehouseAdjustmentTest(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:
|
||
now = datetime(2026, 6, 6, 9, 0, tzinfo=UTC)
|
||
self.department = Department(
|
||
id=1,
|
||
dept_code="D-SPECIAL",
|
||
dept_name="仓库部",
|
||
dept_type="WAREHOUSE",
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.employee = Employee(
|
||
id=1,
|
||
employee_code="EMP-SPECIAL-001",
|
||
employee_name="特殊调整员",
|
||
dept_id=1,
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.user = User(
|
||
id=1,
|
||
username="special_tester",
|
||
password_hash="x",
|
||
employee_id=1,
|
||
dept_id=1,
|
||
nickname="特殊调整员",
|
||
is_super_admin=1,
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.unit = Unit(id=1, unit_code="PCS", unit_name="个", precision_digits=0, created_at=now, updated_at=now)
|
||
self.raw_item = Item(
|
||
id=1,
|
||
item_code="原材料00001",
|
||
item_name="304不锈钢卷",
|
||
item_type="RAW_MATERIAL",
|
||
unit_weight_kg=Decimal("0"),
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.aux_item = Item(
|
||
id=2,
|
||
item_code="辅料00001",
|
||
item_name="包装薄膜",
|
||
item_type="RAW_MATERIAL",
|
||
unit_weight_kg=Decimal("0"),
|
||
stock_unit_id=1,
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.finished_item = Item(
|
||
id=3,
|
||
item_code="产品需规00001",
|
||
item_name="冲压支架",
|
||
item_type="FINISHED_GOOD",
|
||
unit_weight_kg=Decimal("2.5"),
|
||
stock_unit_id=1,
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.finished_product = Product(
|
||
id=1,
|
||
item_id=3,
|
||
drawing_no="FG-DWG-001",
|
||
product_family="支架",
|
||
version_no="V1",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.raw_warehouse = Warehouse(
|
||
id=1,
|
||
warehouse_code="WH-RAW",
|
||
warehouse_name="原材料库",
|
||
warehouse_type="RAW",
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.aux_warehouse = Warehouse(
|
||
id=2,
|
||
warehouse_code="WH-AUX",
|
||
warehouse_name="辅料库",
|
||
warehouse_type="AUX",
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.finished_warehouse = Warehouse(
|
||
id=3,
|
||
warehouse_code="WH-FG",
|
||
warehouse_name="成品库",
|
||
warehouse_type="FINISHED",
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.raw_location = WarehouseLocation(
|
||
id=1,
|
||
warehouse_id=1,
|
||
location_code="RAW-A",
|
||
location_name="原材料默认库位",
|
||
is_default=1,
|
||
is_locked=0,
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.aux_location = WarehouseLocation(
|
||
id=2,
|
||
warehouse_id=2,
|
||
location_code="AUX-A",
|
||
location_name="辅料默认库位",
|
||
is_default=1,
|
||
is_locked=0,
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.finished_location = WarehouseLocation(
|
||
id=3,
|
||
warehouse_id=3,
|
||
location_code="FG-A",
|
||
location_name="成品默认库位",
|
||
is_default=1,
|
||
is_locked=0,
|
||
status="ACTIVE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.raw_lot = StockLot(
|
||
id=1,
|
||
lot_no="RAW-LOT-001",
|
||
lot_role="INBOUND_RAW",
|
||
item_id=1,
|
||
warehouse_id=1,
|
||
location_id=1,
|
||
source_doc_type="OPENING_STOCK",
|
||
source_doc_id=1,
|
||
inbound_qty=Decimal("0"),
|
||
inbound_weight_kg=Decimal("100"),
|
||
remaining_qty=Decimal("0"),
|
||
remaining_weight_kg=Decimal("80"),
|
||
locked_qty=Decimal("0"),
|
||
locked_weight_kg=Decimal("0"),
|
||
unit_cost=Decimal("10"),
|
||
quality_status="PASS",
|
||
status="AVAILABLE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.aux_lot = StockLot(
|
||
id=2,
|
||
lot_no="AUX-LOT-001",
|
||
lot_role="INVENTORY",
|
||
item_id=2,
|
||
warehouse_id=2,
|
||
location_id=2,
|
||
source_doc_type="OPENING_STOCK",
|
||
source_doc_id=2,
|
||
inbound_qty=Decimal("10"),
|
||
inbound_weight_kg=Decimal("0"),
|
||
remaining_qty=Decimal("6"),
|
||
remaining_weight_kg=Decimal("0"),
|
||
locked_qty=Decimal("0"),
|
||
locked_weight_kg=Decimal("0"),
|
||
unit_cost=Decimal("2.5"),
|
||
quality_status="PASS",
|
||
status="AVAILABLE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.finished_lot = StockLot(
|
||
id=3,
|
||
lot_no="FG-LOT-001",
|
||
lot_role="INBOUND_FINISHED",
|
||
item_id=3,
|
||
warehouse_id=3,
|
||
location_id=3,
|
||
source_doc_type="FG_RECEIPT",
|
||
source_doc_id=3,
|
||
inbound_qty=Decimal("20"),
|
||
inbound_weight_kg=Decimal("50"),
|
||
remaining_qty=Decimal("12"),
|
||
remaining_weight_kg=Decimal("30"),
|
||
locked_qty=Decimal("0"),
|
||
locked_weight_kg=Decimal("0"),
|
||
unit_cost=Decimal("18"),
|
||
quality_status="PASS",
|
||
status="AVAILABLE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.raw_balance = StockBalance(
|
||
id=1,
|
||
item_id=1,
|
||
warehouse_id=1,
|
||
location_id=1,
|
||
qty_on_hand=Decimal("0"),
|
||
weight_on_hand_kg=Decimal("80"),
|
||
qty_available=Decimal("0"),
|
||
weight_available_kg=Decimal("80"),
|
||
avg_unit_cost=Decimal("10"),
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.aux_balance = StockBalance(
|
||
id=2,
|
||
item_id=2,
|
||
warehouse_id=2,
|
||
location_id=2,
|
||
qty_on_hand=Decimal("6"),
|
||
weight_on_hand_kg=Decimal("0"),
|
||
qty_available=Decimal("6"),
|
||
weight_available_kg=Decimal("0"),
|
||
avg_unit_cost=Decimal("2.5"),
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.finished_balance = StockBalance(
|
||
id=3,
|
||
item_id=3,
|
||
warehouse_id=3,
|
||
location_id=3,
|
||
qty_on_hand=Decimal("12"),
|
||
weight_on_hand_kg=Decimal("30"),
|
||
qty_available=Decimal("12"),
|
||
weight_available_kg=Decimal("30"),
|
||
avg_unit_cost=Decimal("18"),
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add_all(
|
||
[
|
||
self.department,
|
||
self.employee,
|
||
self.user,
|
||
self.unit,
|
||
self.raw_item,
|
||
self.aux_item,
|
||
self.finished_item,
|
||
self.finished_product,
|
||
self.raw_warehouse,
|
||
self.aux_warehouse,
|
||
self.finished_warehouse,
|
||
self.raw_location,
|
||
self.aux_location,
|
||
self.finished_location,
|
||
self.raw_lot,
|
||
self.aux_lot,
|
||
self.finished_lot,
|
||
self.raw_balance,
|
||
self.aux_balance,
|
||
self.finished_balance,
|
||
]
|
||
)
|
||
self.db.commit()
|
||
|
||
def _adjustment_payload(
|
||
self,
|
||
*,
|
||
direction: str,
|
||
warehouse_id: int,
|
||
item_id: int,
|
||
lot_id: int | None = None,
|
||
location_id: int | None = None,
|
||
adjust_qty: Decimal = Decimal("0"),
|
||
adjust_weight_kg: Decimal = Decimal("0"),
|
||
reason: str | None = None,
|
||
unit_cost: Decimal | None = None,
|
||
line_reason: str | None = None,
|
||
) -> SpecialWarehouseAdjustmentCreate:
|
||
return SpecialWarehouseAdjustmentCreate(
|
||
warehouse_id=warehouse_id,
|
||
direction=direction,
|
||
reason=reason,
|
||
lines=[
|
||
SpecialWarehouseAdjustmentLineCreate(
|
||
item_id=item_id,
|
||
lot_id=lot_id,
|
||
location_id=location_id,
|
||
adjust_qty=adjust_qty,
|
||
adjust_weight_kg=adjust_weight_kg,
|
||
unit_cost=unit_cost,
|
||
line_reason=line_reason,
|
||
)
|
||
],
|
||
)
|
||
|
||
def _only_txn(self, txn_type: str) -> InventoryTxn:
|
||
return self.db.scalar(select(InventoryTxn).where(InventoryTxn.txn_type == txn_type))
|
||
|
||
def _auth_context(self):
|
||
return SimpleNamespace(user=SimpleNamespace(id=self.user.id))
|
||
|
||
def _seed_second_raw_lot(self) -> StockLot:
|
||
now = datetime(2026, 6, 6, 9, 0, tzinfo=UTC)
|
||
second_lot = StockLot(
|
||
id=99,
|
||
lot_no="RAW-LOT-002",
|
||
lot_role="INBOUND_RAW",
|
||
item_id=self.raw_item.id,
|
||
warehouse_id=self.raw_warehouse.id,
|
||
location_id=self.raw_location.id,
|
||
source_doc_type="OPENING_STOCK",
|
||
source_doc_id=99,
|
||
inbound_qty=Decimal("0"),
|
||
inbound_weight_kg=Decimal("20"),
|
||
remaining_qty=Decimal("0"),
|
||
remaining_weight_kg=Decimal("20"),
|
||
locked_qty=Decimal("0"),
|
||
locked_weight_kg=Decimal("0"),
|
||
unit_cost=Decimal("10"),
|
||
quality_status="PASS",
|
||
status="AVAILABLE",
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.raw_balance.weight_on_hand_kg += Decimal("20")
|
||
self.raw_balance.weight_available_kg += Decimal("20")
|
||
self.db.add_all([second_lot, self.raw_balance])
|
||
self.db.commit()
|
||
return second_lot
|
||
|
||
def test_special_out_requires_remark_and_keeps_inventory_unchanged(self) -> None:
|
||
before_lot_weight = self.raw_lot.remaining_weight_kg
|
||
before_balance_weight = self.raw_balance.weight_on_hand_kg
|
||
|
||
with self.assertRaises(ValueError) as exc:
|
||
create_special_inventory_adjustment(
|
||
self.db,
|
||
self._adjustment_payload(
|
||
direction="OUT",
|
||
warehouse_id=self.raw_warehouse.id,
|
||
item_id=self.raw_item.id,
|
||
lot_id=self.raw_lot.id,
|
||
adjust_weight_kg=Decimal("5"),
|
||
reason="",
|
||
),
|
||
user_id=9,
|
||
)
|
||
|
||
self.assertIn("特殊出库必须填写说明", str(exc.exception))
|
||
self.db.refresh(self.raw_lot)
|
||
self.db.refresh(self.raw_balance)
|
||
self.assertEqual(self.raw_lot.remaining_weight_kg, before_lot_weight)
|
||
self.assertEqual(self.raw_balance.weight_on_hand_kg, before_balance_weight)
|
||
self.assertIsNone(self._only_txn("SPECIAL_OUT"))
|
||
|
||
def test_raw_special_out_deducts_lot_balance_by_weight_and_writes_ledger(self) -> None:
|
||
create_special_inventory_adjustment(
|
||
self.db,
|
||
self._adjustment_payload(
|
||
direction="OUT",
|
||
warehouse_id=self.raw_warehouse.id,
|
||
item_id=self.raw_item.id,
|
||
lot_id=self.raw_lot.id,
|
||
adjust_weight_kg=Decimal("12.5"),
|
||
reason="样品寄出",
|
||
),
|
||
user_id=9,
|
||
)
|
||
|
||
self.db.refresh(self.raw_lot)
|
||
self.db.refresh(self.raw_balance)
|
||
txn = self._only_txn("SPECIAL_OUT")
|
||
self.assertEqual(self.raw_lot.remaining_qty, Decimal("0.000000"))
|
||
self.assertEqual(self.raw_lot.remaining_weight_kg, Decimal("67.500000"))
|
||
self.assertEqual(self.raw_balance.qty_on_hand, Decimal("0.000000"))
|
||
self.assertEqual(self.raw_balance.weight_on_hand_kg, Decimal("67.500000"))
|
||
self.assertEqual(self.raw_balance.weight_available_kg, Decimal("67.500000"))
|
||
self.assertIsNotNone(txn)
|
||
self.assertEqual(txn.lot_id, self.raw_lot.id)
|
||
self.assertEqual(txn.qty_change, Decimal("0.000000"))
|
||
self.assertEqual(txn.weight_change_kg, Decimal("-12.500000"))
|
||
self.assertIn("特殊出库说明:样品寄出", txn.remark)
|
||
self.assertIn("调整前重量:80", txn.remark)
|
||
self.assertIn("调整后重量:67.5", txn.remark)
|
||
|
||
def test_raw_special_out_is_visible_in_inventory_transaction_ledger(self) -> None:
|
||
create_special_inventory_adjustment(
|
||
self.db,
|
||
self._adjustment_payload(
|
||
direction="OUT",
|
||
warehouse_id=self.raw_warehouse.id,
|
||
item_id=self.raw_item.id,
|
||
lot_id=self.raw_lot.id,
|
||
adjust_weight_kg=Decimal("6.5"),
|
||
reason="研发打样领出",
|
||
),
|
||
user_id=9,
|
||
)
|
||
|
||
rows = self.db.execute(
|
||
get_inventory_txn_ledger_query(warehouse_type="RAW", txn_type="SPECIAL_OUT")
|
||
).mappings().all()
|
||
|
||
self.assertEqual(len(rows), 1)
|
||
row = rows[0]
|
||
self.assertEqual(row["direction"], "OUT")
|
||
self.assertEqual(row["txn_type"], "SPECIAL_OUT")
|
||
self.assertIn("特殊出库说明:研发打样领出", row["remark"])
|
||
|
||
def test_special_adjustment_generates_archive_for_each_adjustment_transaction(self) -> None:
|
||
second_lot = self._seed_second_raw_lot()
|
||
|
||
result = create_special_warehouse_adjustment(
|
||
SpecialWarehouseAdjustmentCreate(
|
||
warehouse_id=self.raw_warehouse.id,
|
||
direction="OUT",
|
||
reason="研发打样多批次领出",
|
||
lines=[
|
||
SpecialWarehouseAdjustmentLineCreate(
|
||
item_id=self.raw_item.id,
|
||
lot_id=self.raw_lot.id,
|
||
adjust_weight_kg=Decimal("5"),
|
||
),
|
||
SpecialWarehouseAdjustmentLineCreate(
|
||
item_id=self.raw_item.id,
|
||
lot_id=second_lot.id,
|
||
adjust_weight_kg=Decimal("7"),
|
||
),
|
||
],
|
||
),
|
||
context=self._auth_context(),
|
||
db=self.db,
|
||
)
|
||
|
||
txns = self.db.scalars(
|
||
select(InventoryTxn).where(InventoryTxn.txn_type == "SPECIAL_OUT").order_by(InventoryTxn.id)
|
||
).all()
|
||
self.assertEqual(len(txns), 2)
|
||
self.assertEqual(result.archive_status, ARCHIVE_STATUS_READY)
|
||
self.assertEqual(result.archive_document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
|
||
self.assertEqual(result.archive_business_id, 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 txns]),
|
||
)
|
||
).all()
|
||
)
|
||
self.assertEqual(archive_business_ids, {txn.id for txn in txns})
|
||
|
||
def test_special_out_rejects_quantity_greater_than_auxiliary_available_quantity(self) -> None:
|
||
with self.assertRaises(ValueError) as exc:
|
||
create_special_inventory_adjustment(
|
||
self.db,
|
||
self._adjustment_payload(
|
||
direction="OUT",
|
||
warehouse_id=self.aux_warehouse.id,
|
||
item_id=self.aux_item.id,
|
||
lot_id=self.aux_lot.id,
|
||
adjust_qty=Decimal("7"),
|
||
reason="盘亏调整",
|
||
),
|
||
user_id=9,
|
||
)
|
||
|
||
self.assertIn("特殊出库数量不能超过当前可用数量", str(exc.exception))
|
||
self.db.refresh(self.aux_lot)
|
||
self.db.refresh(self.aux_balance)
|
||
self.assertEqual(self.aux_lot.remaining_qty, Decimal("6.000000"))
|
||
self.assertEqual(self.aux_balance.qty_available, Decimal("6.000000"))
|
||
self.assertIsNone(self._only_txn("SPECIAL_OUT"))
|
||
|
||
def test_auxiliary_special_in_increases_quantity_only_and_writes_ledger(self) -> None:
|
||
create_special_inventory_adjustment(
|
||
self.db,
|
||
self._adjustment_payload(
|
||
direction="IN",
|
||
warehouse_id=self.aux_warehouse.id,
|
||
item_id=self.aux_item.id,
|
||
location_id=self.aux_location.id,
|
||
adjust_qty=Decimal("4"),
|
||
adjust_weight_kg=Decimal("99"),
|
||
reason="供应商补送",
|
||
unit_cost=Decimal("2.5"),
|
||
),
|
||
user_id=9,
|
||
)
|
||
|
||
lot = self.db.scalar(
|
||
select(StockLot).where(
|
||
StockLot.item_id == self.aux_item.id,
|
||
StockLot.warehouse_id == self.aux_warehouse.id,
|
||
StockLot.source_doc_type == "SPECIAL_ADJUSTMENT",
|
||
)
|
||
)
|
||
self.db.refresh(self.aux_balance)
|
||
txn = self._only_txn("SPECIAL_IN")
|
||
self.assertIsNotNone(lot)
|
||
self.assertEqual(lot.inbound_qty, Decimal("4.000000"))
|
||
self.assertEqual(lot.remaining_qty, Decimal("4.000000"))
|
||
self.assertEqual(lot.inbound_weight_kg, Decimal("0.000000"))
|
||
self.assertEqual(lot.remaining_weight_kg, Decimal("0.000000"))
|
||
self.assertEqual(self.aux_balance.qty_on_hand, Decimal("10.000000"))
|
||
self.assertEqual(self.aux_balance.qty_available, Decimal("10.000000"))
|
||
self.assertEqual(self.aux_balance.weight_on_hand_kg, Decimal("0.000000"))
|
||
self.assertEqual(self.aux_balance.weight_available_kg, Decimal("0.000000"))
|
||
self.assertIsNotNone(txn)
|
||
self.assertEqual(txn.qty_change, Decimal("4.000000"))
|
||
self.assertEqual(txn.weight_change_kg, Decimal("0.000000"))
|
||
self.assertIn("特殊入库说明:供应商补送", txn.remark)
|
||
|
||
def test_raw_special_in_generates_global_raw_lot_number(self) -> None:
|
||
create_special_inventory_adjustment(
|
||
self.db,
|
||
self._adjustment_payload(
|
||
direction="IN",
|
||
warehouse_id=self.raw_warehouse.id,
|
||
item_id=self.raw_item.id,
|
||
location_id=self.raw_location.id,
|
||
adjust_weight_kg=Decimal("15"),
|
||
reason="供应商补送原料",
|
||
unit_cost=Decimal("10"),
|
||
),
|
||
user_id=9,
|
||
)
|
||
|
||
lot = self.db.scalar(
|
||
select(StockLot).where(
|
||
StockLot.item_id == self.raw_item.id,
|
||
StockLot.warehouse_id == self.raw_warehouse.id,
|
||
StockLot.source_doc_type == "SPECIAL_ADJUSTMENT",
|
||
)
|
||
)
|
||
|
||
self.assertIsNotNone(lot)
|
||
self.assertEqual(lot.lot_no, "YL0001")
|
||
|
||
def test_finished_special_out_deducts_quantity_and_weight_together(self) -> None:
|
||
create_special_inventory_adjustment(
|
||
self.db,
|
||
self._adjustment_payload(
|
||
direction="OUT",
|
||
warehouse_id=self.finished_warehouse.id,
|
||
item_id=self.finished_item.id,
|
||
lot_id=self.finished_lot.id,
|
||
adjust_qty=Decimal("3"),
|
||
adjust_weight_kg=Decimal("7.5"),
|
||
reason="展会样品",
|
||
),
|
||
user_id=9,
|
||
)
|
||
|
||
self.db.refresh(self.finished_lot)
|
||
self.db.refresh(self.finished_balance)
|
||
txn = self._only_txn("SPECIAL_OUT")
|
||
self.assertEqual(self.finished_lot.remaining_qty, Decimal("9.000000"))
|
||
self.assertEqual(self.finished_lot.remaining_weight_kg, Decimal("22.500000"))
|
||
self.assertEqual(self.finished_balance.qty_on_hand, Decimal("9.000000"))
|
||
self.assertEqual(self.finished_balance.qty_available, Decimal("9.000000"))
|
||
self.assertEqual(self.finished_balance.weight_on_hand_kg, Decimal("22.500000"))
|
||
self.assertEqual(self.finished_balance.weight_available_kg, Decimal("22.500000"))
|
||
self.assertIsNotNone(txn)
|
||
self.assertEqual(txn.qty_change, Decimal("-3.000000"))
|
||
self.assertEqual(txn.weight_change_kg, Decimal("-7.500000"))
|
||
self.assertIn("特殊出库说明:展会样品", txn.remark)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|