mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-20 05:10:26 -05:00
158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
"""Tests for API v1 inventory movements (including return/waste with devaluation)."""
|
|
|
|
import json
|
|
import pytest
|
|
|
|
pytestmark = [pytest.mark.api, pytest.mark.integration]
|
|
|
|
from decimal import Decimal
|
|
from flask import url_for
|
|
from app import db
|
|
from app.models import (
|
|
User,
|
|
ApiToken,
|
|
Warehouse,
|
|
StockItem,
|
|
WarehouseStock,
|
|
StockMovement,
|
|
StockLot,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def api_token(db_session, test_user):
|
|
"""Create an API token with write:projects (used for inventory movements)."""
|
|
token, plain_token = ApiToken.create_token(
|
|
user_id=test_user.id,
|
|
name="Inventory Test Token",
|
|
description="For inventory API tests",
|
|
scopes="read:projects,write:projects",
|
|
)
|
|
db.session.add(token)
|
|
db.session.commit()
|
|
return plain_token
|
|
|
|
|
|
@pytest.fixture
|
|
def test_warehouse(db_session, test_user):
|
|
"""Create a test warehouse."""
|
|
warehouse = Warehouse(name="API Test Warehouse", code="WH-API", created_by=test_user.id)
|
|
db.session.add(warehouse)
|
|
db.session.commit()
|
|
return warehouse
|
|
|
|
|
|
@pytest.fixture
|
|
def test_stock_item_trackable(db_session, test_user):
|
|
"""Create a trackable stock item with default_cost for devaluation tests."""
|
|
item = StockItem(
|
|
sku="API-TEST-001",
|
|
name="API Test Product",
|
|
created_by=test_user.id,
|
|
default_price=Decimal("10.00"),
|
|
default_cost=Decimal("5.00"),
|
|
is_trackable=True,
|
|
)
|
|
db.session.add(item)
|
|
db.session.commit()
|
|
return item
|
|
|
|
|
|
class TestInventoryMovementsAPI:
|
|
"""Test POST /api/v1/inventory/movements with return/waste devaluation."""
|
|
|
|
def test_create_return_with_devaluation(
|
|
self, client, test_user, api_token, test_stock_item_trackable, test_warehouse
|
|
):
|
|
"""POST return movement with devalue_enabled creates a devalued lot."""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(test_user.id)
|
|
|
|
headers = {"Authorization": f"Bearer {api_token}", "Content-Type": "application/json"}
|
|
payload = {
|
|
"movement_type": "return",
|
|
"stock_item_id": test_stock_item_trackable.id,
|
|
"warehouse_id": test_warehouse.id,
|
|
"quantity": "5.00",
|
|
"devalue_enabled": True,
|
|
"devalue_method": "fixed",
|
|
"devalue_unit_cost": "2.50",
|
|
"reason": "Returned with damage (API)",
|
|
}
|
|
|
|
response = client.post("/api/v1/inventory/movements", data=json.dumps(payload), headers=headers)
|
|
|
|
assert response.status_code == 201
|
|
data = response.get_json()
|
|
assert "message" in data
|
|
assert "movement" in data
|
|
assert data["movement"]["movement_type"] == "return"
|
|
assert float(data["movement"]["quantity"]) == 5.0
|
|
|
|
stock = WarehouseStock.query.filter_by(
|
|
warehouse_id=test_warehouse.id, stock_item_id=test_stock_item_trackable.id
|
|
).first()
|
|
assert stock is not None
|
|
assert stock.quantity_on_hand == Decimal("5.00")
|
|
|
|
lots = StockLot.query.filter_by(
|
|
stock_item_id=test_stock_item_trackable.id, warehouse_id=test_warehouse.id
|
|
).all()
|
|
assert len(lots) >= 1
|
|
devalued = [l for l in lots if l.lot_type == "devalued"]
|
|
assert len(devalued) >= 1
|
|
assert any(Decimal(str(l.quantity_on_hand)) == Decimal("5.00") for l in devalued)
|
|
assert any(Decimal(str(l.unit_cost)) == Decimal("2.50") for l in devalued)
|
|
|
|
def test_create_waste_with_devaluation(
|
|
self, client, test_user, api_token, test_stock_item_trackable, test_warehouse
|
|
):
|
|
"""POST waste movement with devalue_enabled consumes from a devalued lot."""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(test_user.id)
|
|
|
|
# Create stock first via model (no API for simple purchase in this test)
|
|
StockMovement.record_movement(
|
|
movement_type="purchase",
|
|
stock_item_id=test_stock_item_trackable.id,
|
|
warehouse_id=test_warehouse.id,
|
|
quantity=Decimal("10.00"),
|
|
moved_by=test_user.id,
|
|
unit_cost=Decimal("5.00"),
|
|
update_stock=True,
|
|
)
|
|
db.session.commit()
|
|
|
|
headers = {"Authorization": f"Bearer {api_token}", "Content-Type": "application/json"}
|
|
payload = {
|
|
"movement_type": "waste",
|
|
"stock_item_id": test_stock_item_trackable.id,
|
|
"warehouse_id": test_warehouse.id,
|
|
"quantity": "-4.00",
|
|
"devalue_enabled": True,
|
|
"devalue_method": "fixed",
|
|
"devalue_unit_cost": "1.00",
|
|
"reason": "Wasted impaired items (API)",
|
|
}
|
|
|
|
response = client.post("/api/v1/inventory/movements", data=json.dumps(payload), headers=headers)
|
|
|
|
assert response.status_code == 201
|
|
data = response.get_json()
|
|
assert "message" in data
|
|
assert "movement" in data
|
|
assert data["movement"]["movement_type"] == "waste"
|
|
assert float(data["movement"]["quantity"]) == -4.0
|
|
|
|
stock = WarehouseStock.query.filter_by(
|
|
warehouse_id=test_warehouse.id, stock_item_id=test_stock_item_trackable.id
|
|
).first()
|
|
assert stock is not None
|
|
assert stock.quantity_on_hand == Decimal("6.00")
|
|
|
|
lots = StockLot.query.filter_by(
|
|
stock_item_id=test_stock_item_trackable.id, warehouse_id=test_warehouse.id
|
|
).all()
|
|
devalued = [l for l in lots if l.lot_type == "devalued"]
|
|
assert any(Decimal(str(l.quantity_on_hand)) == Decimal("0.00") for l in devalued)
|