202 lines
8.7 KiB
Python
202 lines
8.7 KiB
Python
"""
|
|
تستهای اطمینان انبار: ریاضی موجودی، تاریخچه گردش کالا و چرخهٔ کامل انتقال بین انبارها.
|
|
"""
|
|
from django.core.exceptions import PermissionDenied, ValidationError
|
|
from django.test import TestCase
|
|
|
|
from admin_panel.models import (ProductInStore, ProductStockHistory,
|
|
StoreTransferRequest)
|
|
from admin_panel.services import (approve_transfer_request,
|
|
cancel_transfer_request,
|
|
create_transfer_request,
|
|
get_product_current_stock,
|
|
reject_transfer_request)
|
|
|
|
from .factories import (current_stock, make_product, make_store, make_user,
|
|
stock_in)
|
|
|
|
|
|
class ProductInStoreMathTests(TestCase):
|
|
|
|
def setUp(self):
|
|
self.store = make_store('انبار مرکزی')
|
|
self.product = make_product('کالای تست', price=500)
|
|
|
|
def test_initial_in_records_history_and_stock(self):
|
|
row = stock_in(self.store, self.product, 10)
|
|
|
|
row.refresh_from_db()
|
|
self.assertEqual(row.current_stock_at_time, 10)
|
|
self.assertEqual(current_stock(self.store, self.product), 10)
|
|
|
|
history = ProductStockHistory.objects.filter(store_room=self.store, product=self.product)
|
|
self.assertEqual(history.count(), 1)
|
|
self.assertEqual(history.first().quantity_changed, 10)
|
|
self.assertEqual(history.first().current_stock, 10)
|
|
|
|
def test_multiple_movements_accumulate_correctly(self):
|
|
stock_in(self.store, self.product, 10)
|
|
ProductInStore.objects.create(
|
|
StoreRoom=self.store, Product=self.product, s_in=0, s_out=4,
|
|
desc='خروج تست', movement_type=ProductInStore.MovementType.DIRECT_OUT,
|
|
)
|
|
ProductInStore.objects.create(
|
|
StoreRoom=self.store, Product=self.product, s_in=7, s_out=0,
|
|
desc='ورود تست', movement_type=ProductInStore.MovementType.DIRECT_IN,
|
|
)
|
|
|
|
self.assertEqual(current_stock(self.store, self.product), 13)
|
|
self.assertEqual(get_product_current_stock(self.store, self.product), 13)
|
|
self.assertEqual(
|
|
ProductStockHistory.objects.filter(store_room=self.store, product=self.product).count(),
|
|
3,
|
|
)
|
|
|
|
def test_editing_a_row_updates_stock_and_history(self):
|
|
row = stock_in(self.store, self.product, 10)
|
|
|
|
row.s_in = 25
|
|
row.save()
|
|
|
|
row.refresh_from_db()
|
|
self.assertEqual(row.current_stock_at_time, 25)
|
|
self.assertEqual(current_stock(self.store, self.product), 25)
|
|
|
|
# آخرین رکورد تاریخچه باید تغییر (+15) را نشان دهد
|
|
last = ProductStockHistory.objects.filter(
|
|
store_room=self.store, product=self.product
|
|
).order_by('-id').first()
|
|
self.assertEqual(last.quantity_changed, 15)
|
|
self.assertEqual(last.current_stock, 25)
|
|
|
|
def test_deleting_a_row_creates_history_and_reduces_stock(self):
|
|
stock_in(self.store, self.product, 10)
|
|
out_row = ProductInStore.objects.create(
|
|
StoreRoom=self.store, Product=self.product, s_in=0, s_out=3,
|
|
desc='خروج', movement_type=ProductInStore.MovementType.DIRECT_OUT,
|
|
)
|
|
self.assertEqual(current_stock(self.store, self.product), 7)
|
|
|
|
out_row.delete()
|
|
|
|
self.assertEqual(current_stock(self.store, self.product), 10)
|
|
last = ProductStockHistory.objects.filter(
|
|
store_room=self.store, product=self.product
|
|
).order_by('-id').first()
|
|
self.assertEqual(last.quantity_changed, 3)
|
|
self.assertEqual(last.current_stock, 10)
|
|
|
|
def test_garbage_numeric_values_are_coerced_to_zero(self):
|
|
row = ProductInStore.objects.create(
|
|
StoreRoom=self.store, Product=self.product,
|
|
s_in='abc', s_out=None, desc='مقدار خراب',
|
|
)
|
|
row.refresh_from_db()
|
|
self.assertEqual(row.s_in, 0)
|
|
self.assertEqual(row.s_out, 0)
|
|
|
|
|
|
class TransferWorkflowTests(TestCase):
|
|
|
|
def setUp(self):
|
|
self.boss = make_user('boss', superuser=True)
|
|
self.store_a = make_store('مبدا', viewers=[self.boss])
|
|
self.store_b = make_store('مقصد', viewers=[self.boss])
|
|
self.product = make_product('کالای انتقالی', price=1000)
|
|
stock_in(self.store_a, self.product, 10)
|
|
|
|
def test_create_transfer_request_pending(self):
|
|
transfer = create_transfer_request(
|
|
source_store=self.store_a,
|
|
target_store=self.store_b,
|
|
product=self.product,
|
|
quantity=4,
|
|
requester=self.boss,
|
|
)
|
|
self.assertEqual(transfer.status, StoreTransferRequest.Status.PENDING)
|
|
# تا زمان تایید، موجودی هیچ انباری تغییر نمیکند
|
|
self.assertEqual(current_stock(self.store_a, self.product), 10)
|
|
self.assertEqual(current_stock(self.store_b, self.product), 0)
|
|
|
|
def test_create_rejects_insufficient_stock(self):
|
|
with self.assertRaises(ValidationError):
|
|
create_transfer_request(
|
|
source_store=self.store_a, target_store=self.store_b,
|
|
product=self.product, quantity=999, requester=self.boss,
|
|
)
|
|
|
|
def test_create_rejects_same_store(self):
|
|
with self.assertRaises(ValidationError):
|
|
create_transfer_request(
|
|
source_store=self.store_a, target_store=self.store_a,
|
|
product=self.product, quantity=1, requester=self.boss,
|
|
)
|
|
|
|
def test_create_rejects_non_positive_quantity(self):
|
|
with self.assertRaises(ValidationError):
|
|
create_transfer_request(
|
|
source_store=self.store_a, target_store=self.store_b,
|
|
product=self.product, quantity=0, requester=self.boss,
|
|
)
|
|
|
|
def test_approve_moves_stock_and_links_transactions(self):
|
|
transfer = create_transfer_request(
|
|
source_store=self.store_a, target_store=self.store_b,
|
|
product=self.product, quantity=4, requester=self.boss,
|
|
)
|
|
|
|
approved = approve_transfer_request(request_id=transfer.id, approver=self.boss)
|
|
|
|
self.assertEqual(approved.status, StoreTransferRequest.Status.APPROVED)
|
|
self.assertIsNotNone(approved.out_transaction_id)
|
|
self.assertIsNotNone(approved.in_transaction_id)
|
|
self.assertEqual(current_stock(self.store_a, self.product), 6)
|
|
self.assertEqual(current_stock(self.store_b, self.product), 4)
|
|
|
|
def test_double_approval_is_rejected(self):
|
|
transfer = create_transfer_request(
|
|
source_store=self.store_a, target_store=self.store_b,
|
|
product=self.product, quantity=4, requester=self.boss,
|
|
)
|
|
approve_transfer_request(request_id=transfer.id, approver=self.boss)
|
|
|
|
with self.assertRaises(ValidationError):
|
|
approve_transfer_request(request_id=transfer.id, approver=self.boss)
|
|
|
|
def test_reject_keeps_stock_untouched(self):
|
|
transfer = create_transfer_request(
|
|
source_store=self.store_a, target_store=self.store_b,
|
|
product=self.product, quantity=4, requester=self.boss,
|
|
)
|
|
rejected = reject_transfer_request(request_id=transfer.id, rejector=self.boss, reason='کالا خراب')
|
|
|
|
self.assertEqual(rejected.status, StoreTransferRequest.Status.REJECTED)
|
|
self.assertEqual(rejected.rejection_reason, 'کالا خراب')
|
|
self.assertEqual(current_stock(self.store_a, self.product), 10)
|
|
self.assertEqual(current_stock(self.store_b, self.product), 0)
|
|
|
|
def test_cancel_by_requester(self):
|
|
transfer = create_transfer_request(
|
|
source_store=self.store_a, target_store=self.store_b,
|
|
product=self.product, quantity=4, requester=self.boss,
|
|
)
|
|
canceled = cancel_transfer_request(request_id=transfer.id, user=self.boss)
|
|
self.assertEqual(canceled.status, StoreTransferRequest.Status.CANCELED)
|
|
|
|
def test_permission_denied_for_outsider(self):
|
|
outsider = make_user('outsider')
|
|
with self.assertRaises(PermissionDenied):
|
|
create_transfer_request(
|
|
source_store=self.store_a, target_store=self.store_b,
|
|
product=self.product, quantity=1, requester=outsider,
|
|
)
|
|
|
|
def test_approval_permission_denied_for_non_target_member(self):
|
|
transfer = create_transfer_request(
|
|
source_store=self.store_a, target_store=self.store_b,
|
|
product=self.product, quantity=4, requester=self.boss,
|
|
)
|
|
outsider = make_user('outsider2')
|
|
with self.assertRaises(PermissionDenied):
|
|
approve_transfer_request(request_id=transfer.id, approver=outsider)
|