project initialization
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
This commit is contained in:
350
backend/tests/unit/models/test_retail_models.py
Normal file
350
backend/tests/unit/models/test_retail_models.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
Unit tests for Retail Models
|
||||
|
||||
Tests for retail module models:
|
||||
- Product
|
||||
- Sale
|
||||
|
||||
Author: Claude
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from decimal import Decimal
|
||||
from datetime import date
|
||||
|
||||
from backend.src.core.models.tenant import Tenant
|
||||
from backend.src.core.models.user import User
|
||||
from backend.src.modules.retail.models.product import Product
|
||||
from backend.src.modules.retail.models.sale import Sale, SaleItem
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ProductModelTest(TestCase):
|
||||
"""Test cases for Product model"""
|
||||
|
||||
def setUp(self):
|
||||
self.tenant = Tenant.objects.create(
|
||||
name='Test Business Sdn Bhd',
|
||||
schema_name='test_business',
|
||||
domain='testbusiness.com',
|
||||
business_type='retail'
|
||||
)
|
||||
|
||||
self.user = User.objects.create_user(
|
||||
username='testuser',
|
||||
email='user@test.com',
|
||||
password='test123',
|
||||
tenant=self.tenant
|
||||
)
|
||||
|
||||
self.product_data = {
|
||||
'tenant': self.tenant,
|
||||
'sku': 'PRD-001',
|
||||
'name': 'Test Product',
|
||||
'description': 'A test product for unit testing',
|
||||
'category': 'electronics',
|
||||
'brand': 'Test Brand',
|
||||
'barcode': '1234567890123',
|
||||
'unit': 'piece',
|
||||
'current_stock': 100,
|
||||
'minimum_stock': 10,
|
||||
'maximum_stock': 500,
|
||||
'reorder_point': 15,
|
||||
'purchase_price': Decimal('50.00'),
|
||||
'selling_price': Decimal('100.00'),
|
||||
'wholesale_price': Decimal('80.00'),
|
||||
'tax_rate': 10.0,
|
||||
'is_taxable': True,
|
||||
'is_active': True,
|
||||
'requires_prescription': False,
|
||||
'is_halal': True,
|
||||
'msme_certified': True,
|
||||
'created_by': self.user
|
||||
}
|
||||
|
||||
def test_create_product(self):
|
||||
"""Test creating a new product"""
|
||||
product = Product.objects.create(**self.product_data)
|
||||
self.assertEqual(product.tenant, self.tenant)
|
||||
self.assertEqual(product.sku, self.product_data['sku'])
|
||||
self.assertEqual(product.name, self.product_data['name'])
|
||||
self.assertEqual(product.current_stock, self.product_data['current_stock'])
|
||||
self.assertEqual(product.purchase_price, self.product_data['purchase_price'])
|
||||
self.assertEqual(product.selling_price, self.product_data['selling_price'])
|
||||
self.assertTrue(product.is_active)
|
||||
self.assertTrue(product.is_halal)
|
||||
|
||||
def test_product_string_representation(self):
|
||||
"""Test product string representation"""
|
||||
product = Product.objects.create(**self.product_data)
|
||||
self.assertEqual(str(product), f"{product.name} ({product.sku})")
|
||||
|
||||
def test_product_is_low_stock(self):
|
||||
"""Test product low stock detection"""
|
||||
product = Product.objects.create(**self.product_data)
|
||||
|
||||
# Normal stock level
|
||||
self.assertFalse(product.is_low_stock)
|
||||
|
||||
# Low stock level
|
||||
product.current_stock = 5
|
||||
product.save()
|
||||
self.assertTrue(product.is_low_stock)
|
||||
|
||||
def test_product_profit_margin(self):
|
||||
"""Test product profit margin calculation"""
|
||||
product = Product.objects.create(**self.product_data)
|
||||
|
||||
expected_margin = ((product.selling_price - product.purchase_price) / product.selling_price) * 100
|
||||
self.assertAlmostEqual(product.profit_margin, expected_margin)
|
||||
|
||||
def test_product_category_choices(self):
|
||||
"""Test product category validation"""
|
||||
invalid_data = self.product_data.copy()
|
||||
invalid_data['category'] = 'invalid_category'
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
Product.objects.create(**invalid_data)
|
||||
|
||||
def test_product_unit_choices(self):
|
||||
"""Test product unit validation"""
|
||||
invalid_data = self.product_data.copy()
|
||||
invalid_data['unit'] = 'invalid_unit'
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
Product.objects.create(**invalid_data)
|
||||
|
||||
def test_product_barcode_validation(self):
|
||||
"""Test product barcode validation"""
|
||||
# Valid barcode
|
||||
product = Product.objects.create(**self.product_data)
|
||||
self.assertEqual(product.barcode, self.product_data['barcode'])
|
||||
|
||||
# Invalid barcode (too long)
|
||||
invalid_data = self.product_data.copy()
|
||||
invalid_data['barcode'] = '1' * 14
|
||||
with self.assertRaises(Exception):
|
||||
Product.objects.create(**invalid_data)
|
||||
|
||||
def test_product_stock_validation(self):
|
||||
"""Test product stock validation"""
|
||||
invalid_data = self.product_data.copy()
|
||||
invalid_data['current_stock'] = -1
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
Product.objects.create(**invalid_data)
|
||||
|
||||
invalid_data['current_stock'] = 0
|
||||
invalid_data['minimum_stock'] = -5
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
Product.objects.create(**invalid_data)
|
||||
|
||||
|
||||
class SaleModelTest(TestCase):
|
||||
"""Test cases for Sale and SaleItem models"""
|
||||
|
||||
def setUp(self):
|
||||
self.tenant = Tenant.objects.create(
|
||||
name='Test Business Sdn Bhd',
|
||||
schema_name='test_business',
|
||||
domain='testbusiness.com',
|
||||
business_type='retail'
|
||||
)
|
||||
|
||||
self.user = User.objects.create_user(
|
||||
username='testuser',
|
||||
email='user@test.com',
|
||||
password='test123',
|
||||
tenant=self.tenant
|
||||
)
|
||||
|
||||
self.product1 = Product.objects.create(
|
||||
tenant=self.tenant,
|
||||
sku='PRD-001',
|
||||
name='Product 1',
|
||||
category='electronics',
|
||||
unit='piece',
|
||||
current_stock=100,
|
||||
minimum_stock=10,
|
||||
purchase_price=Decimal('50.00'),
|
||||
selling_price=Decimal('100.00'),
|
||||
tax_rate=10.0,
|
||||
created_by=self.user
|
||||
)
|
||||
|
||||
self.product2 = Product.objects.create(
|
||||
tenant=self.tenant,
|
||||
sku='PRD-002',
|
||||
name='Product 2',
|
||||
category='electronics',
|
||||
unit='piece',
|
||||
current_stock=50,
|
||||
minimum_stock=5,
|
||||
purchase_price=Decimal('30.00'),
|
||||
selling_price=Decimal('60.00'),
|
||||
tax_rate=10.0,
|
||||
created_by=self.user
|
||||
)
|
||||
|
||||
self.sale_data = {
|
||||
'tenant': self.tenant,
|
||||
'invoice_number': 'INV-2024010001',
|
||||
'customer_name': 'Test Customer',
|
||||
'customer_email': 'customer@test.com',
|
||||
'customer_phone': '+60123456789',
|
||||
'customer_ic': '000101-01-0001',
|
||||
'sale_date': timezone.now(),
|
||||
'status': 'completed',
|
||||
'payment_method': 'cash',
|
||||
'payment_status': 'paid',
|
||||
'sales_person': self.user,
|
||||
'notes': 'Test sale for unit testing'
|
||||
}
|
||||
|
||||
def test_create_sale(self):
|
||||
"""Test creating a new sale"""
|
||||
sale = Sale.objects.create(**self.sale_data)
|
||||
self.assertEqual(sale.tenant, self.tenant)
|
||||
self.assertEqual(sale.invoice_number, self.sale_data['invoice_number'])
|
||||
self.assertEqual(sale.customer_name, self.sale_data['customer_name'])
|
||||
self.assertEqual(sale.status, self.sale_data['status'])
|
||||
self.assertEqual(sale.payment_status, self.sale_data['payment_status'])
|
||||
self.assertEqual(sale.sales_person, self.user)
|
||||
|
||||
def test_sale_string_representation(self):
|
||||
"""Test sale string representation"""
|
||||
sale = Sale.objects.create(**self.sale_data)
|
||||
self.assertEqual(str(sale), f"Invoice #{sale.invoice_number} - {sale.customer_name}")
|
||||
|
||||
def test_create_sale_item(self):
|
||||
"""Test creating a sale item"""
|
||||
sale = Sale.objects.create(**self.sale_data)
|
||||
|
||||
sale_item_data = {
|
||||
'sale': sale,
|
||||
'product': self.product1,
|
||||
'quantity': 2,
|
||||
'unit_price': Decimal('100.00'),
|
||||
'discount_percentage': 0.0,
|
||||
'tax_rate': 10.0,
|
||||
'notes': 'Test sale item'
|
||||
}
|
||||
|
||||
sale_item = SaleItem.objects.create(**sale_item_data)
|
||||
self.assertEqual(sale_item.sale, sale)
|
||||
self.assertEqual(sale_item.product, self.product1)
|
||||
self.assertEqual(sale_item.quantity, 2)
|
||||
self.assertEqual(sale_item.unit_price, Decimal('100.00'))
|
||||
|
||||
def test_sale_item_subtotal(self):
|
||||
"""Test sale item subtotal calculation"""
|
||||
sale = Sale.objects.create(**self.sale_data)
|
||||
|
||||
sale_item = SaleItem.objects.create(
|
||||
sale=sale,
|
||||
product=self.product1,
|
||||
quantity=2,
|
||||
unit_price=Decimal('100.00'),
|
||||
tax_rate=10.0
|
||||
)
|
||||
|
||||
expected_subtotal = Decimal('200.00') # 2 * 100.00
|
||||
self.assertEqual(sale_item.subtotal, expected_subtotal)
|
||||
|
||||
def test_sale_item_tax_amount(self):
|
||||
"""Test sale item tax amount calculation"""
|
||||
sale = Sale.objects.create(**self.sale_data)
|
||||
|
||||
sale_item = SaleItem.objects.create(
|
||||
sale=sale,
|
||||
product=self.product1,
|
||||
quantity=2,
|
||||
unit_price=Decimal('100.00'),
|
||||
tax_rate=10.0
|
||||
)
|
||||
|
||||
expected_tax = Decimal('20.00') # 200.00 * 0.10
|
||||
self.assertEqual(sale_item.tax_amount, expected_tax)
|
||||
|
||||
def test_sale_item_total_amount(self):
|
||||
"""Test sale item total amount calculation"""
|
||||
sale = Sale.objects.create(**self.sale_data)
|
||||
|
||||
sale_item = SaleItem.objects.create(
|
||||
sale=sale,
|
||||
product=self.product1,
|
||||
quantity=2,
|
||||
unit_price=Decimal('100.00'),
|
||||
tax_rate=10.0
|
||||
)
|
||||
|
||||
expected_total = Decimal('220.00') # 200.00 + 20.00
|
||||
self.assertEqual(sale_item.total_amount, expected_total)
|
||||
|
||||
def test_sale_calculate_totals(self):
|
||||
"""Test sale total calculations"""
|
||||
sale = Sale.objects.create(**self.sale_data)
|
||||
|
||||
# Create multiple sale items
|
||||
SaleItem.objects.create(
|
||||
sale=sale,
|
||||
product=self.product1,
|
||||
quantity=2,
|
||||
unit_price=Decimal('100.00'),
|
||||
tax_rate=10.0
|
||||
)
|
||||
|
||||
SaleItem.objects.create(
|
||||
sale=sale,
|
||||
product=self.product2,
|
||||
quantity=1,
|
||||
unit_price=Decimal('60.00'),
|
||||
tax_rate=10.0
|
||||
)
|
||||
|
||||
# Test the calculate_totals method
|
||||
sale.calculate_totals()
|
||||
|
||||
expected_subtotal = Decimal('260.00') # 200.00 + 60.00
|
||||
expected_tax = Decimal('26.00') # 20.00 + 6.00
|
||||
expected_total = Decimal('286.00') # 260.00 + 26.00
|
||||
|
||||
self.assertEqual(sale.subtotal_amount, expected_subtotal)
|
||||
self.assertEqual(sale.tax_amount, expected_tax)
|
||||
self.assertEqual(sale.total_amount, expected_total)
|
||||
|
||||
def test_sale_status_choices(self):
|
||||
"""Test sale status validation"""
|
||||
invalid_data = self.sale_data.copy()
|
||||
invalid_data['status'] = 'invalid_status'
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
Sale.objects.create(**invalid_data)
|
||||
|
||||
def test_sale_payment_method_choices(self):
|
||||
"""Test sale payment method validation"""
|
||||
invalid_data = self.sale_data.copy()
|
||||
invalid_data['payment_method'] = 'invalid_method'
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
Sale.objects.create(**invalid_data)
|
||||
|
||||
def test_malaysian_customer_validation(self):
|
||||
"""Test Malaysian customer validation"""
|
||||
# Valid Malaysian IC
|
||||
sale = Sale.objects.create(**self.sale_data)
|
||||
self.assertEqual(sale.customer_ic, self.sale_data['customer_ic'])
|
||||
|
||||
# Valid Malaysian phone
|
||||
self.assertEqual(sale.customer_phone, self.sale_data['customer_phone'])
|
||||
|
||||
# Invalid phone number
|
||||
invalid_data = self.sale_data.copy()
|
||||
invalid_data['customer_phone'] = '12345'
|
||||
with self.assertRaises(Exception):
|
||||
Sale.objects.create(**invalid_data)
|
||||
Reference in New Issue
Block a user