""" تست‌های اطمینان انبار: ریاضی موجودی، تاریخچه گردش کالا و چرخهٔ کامل انتقال بین انبارها. """ 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)