forked from erp-dev/erp
feat: big version, added tasks for backup_database and stock change, added health check api, approve sse (support channel via merchant)
This commit is contained in:
@@ -67,6 +67,7 @@ class StockChangeViewMixin:
|
||||
"""构建明细数据"""
|
||||
return {
|
||||
'id': detail.id,
|
||||
'stock_change_record': detail.stock_change_record_id,
|
||||
'product': detail.product_id,
|
||||
'product_name': detail.product.name,
|
||||
'quantity': float(detail.quantity),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
@@ -12,6 +13,279 @@ from stock import models as stock_models, services as stock_services
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CreateStockChangeAPITestCase(TestCase):
|
||||
"""测试标准库存变动创建 API"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='测试商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
self.category = basic_models.ProductCategory.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='布料',
|
||||
product_prefix='FAB',
|
||||
)
|
||||
self.product = basic_models.Product.objects.create(
|
||||
merchant=self.merchant,
|
||||
category=self.category,
|
||||
name='测试布料',
|
||||
human_id='FAB-001',
|
||||
unit=basic_models.ProductUnitEnum.METER,
|
||||
)
|
||||
self.warehouse = basic_models.WareHouse.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='严谨仓库',
|
||||
mode=basic_models.WareHouseModeEnum.RESTRICT_IN,
|
||||
)
|
||||
self.other_merchant = basic_models.Merchant.objects.create(
|
||||
name='其他商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
other_category = basic_models.ProductCategory.objects.create(
|
||||
merchant=self.other_merchant,
|
||||
name='其他布料',
|
||||
product_prefix='FABO',
|
||||
)
|
||||
self.other_product = basic_models.Product.objects.create(
|
||||
merchant=self.other_merchant,
|
||||
category=other_category,
|
||||
name='外部布料',
|
||||
human_id='FAB-999',
|
||||
unit=basic_models.ProductUnitEnum.METER,
|
||||
)
|
||||
|
||||
self.user = User.objects.create_user(username='strict_user', password='pass123')
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
sys_user=self.user,
|
||||
name='仓管员',
|
||||
mobile='13800138002',
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE,
|
||||
)
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_create_stock_change_success(self):
|
||||
payload = {
|
||||
'type': stock_models.StockChangeTypeEnum.ADD,
|
||||
'warehouse': self.warehouse.id,
|
||||
'source_type': stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
'source_id': 10,
|
||||
'products': [
|
||||
{'product': self.product.id, 'quantity': ['10.50', '5.25']},
|
||||
]
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/stock-change/', payload, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data['created_details_count'], 2)
|
||||
self.assertEqual(len(response.data['details']), 2)
|
||||
self.assertEqual(response.data['stock_change_record']['warehouse'], self.warehouse.id)
|
||||
|
||||
record_id = response.data['stock_change_record']['id']
|
||||
record = stock_models.StockChangeRecord.objects.get(id=record_id)
|
||||
self.assertEqual(record.details.count(), 2)
|
||||
|
||||
def test_create_stock_change_products_required(self):
|
||||
payload = {
|
||||
'type': stock_models.StockChangeTypeEnum.ADD,
|
||||
'warehouse': self.warehouse.id,
|
||||
'source_type': stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
'source_id': 11,
|
||||
'products': [],
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/stock-change/', payload, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data['error'], '产品列表不能为空')
|
||||
|
||||
def test_create_stock_change_product_not_visible(self):
|
||||
payload = {
|
||||
'type': stock_models.StockChangeTypeEnum.ADD,
|
||||
'warehouse': self.warehouse.id,
|
||||
'source_type': stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
'products': [
|
||||
{'product': self.other_product.id, 'quantity': ['5.00']},
|
||||
]
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/stock-change/', payload, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(
|
||||
response.data['error'],
|
||||
f'产品ID {self.other_product.id} 对当前用户不可见',
|
||||
)
|
||||
|
||||
def test_create_stock_change_no_employee_permission_denied(self):
|
||||
class DummyUser:
|
||||
def __init__(self, username):
|
||||
self.username = username
|
||||
self.is_authenticated = True
|
||||
|
||||
dummy_user = DummyUser('no_emp_user')
|
||||
self.client.force_authenticate(user=dummy_user)
|
||||
payload = {
|
||||
'type': stock_models.StockChangeTypeEnum.ADD,
|
||||
'warehouse': self.warehouse.id,
|
||||
'source_type': stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
'products': [
|
||||
{'product': self.product.id, 'quantity': ['5.00']},
|
||||
]
|
||||
}
|
||||
|
||||
logging.disable(logging.NOTSET)
|
||||
self.addCleanup(logging.disable, logging.CRITICAL)
|
||||
with self.assertLogs('api_v1.views.stock_change_views.mixins', level='ERROR') as cm:
|
||||
response = self.client.post('/api/v1/stock-change/', payload, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.data['error'], '无权限访问')
|
||||
self.assertTrue(any('无员工信息' in msg for msg in cm.output))
|
||||
|
||||
|
||||
class CreateStockChangeRelaxedAPITestCase(TestCase):
|
||||
"""测试宽松模式库存变动创建 API"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='宽松商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
self.category = basic_models.ProductCategory.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='面料',
|
||||
product_prefix='FAB',
|
||||
)
|
||||
self.product = basic_models.Product.objects.create(
|
||||
merchant=self.merchant,
|
||||
category=self.category,
|
||||
name='宽松布料',
|
||||
human_id='FAB-100',
|
||||
unit=basic_models.ProductUnitEnum.METER,
|
||||
)
|
||||
self.relaxed_warehouse = basic_models.WareHouse.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='宽进宽出仓',
|
||||
mode=basic_models.WareHouseModeEnum.UNRESTRICTED,
|
||||
)
|
||||
self.other_merchant = basic_models.Merchant.objects.create(
|
||||
name='RelaxedOther',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
other_category = basic_models.ProductCategory.objects.create(
|
||||
merchant=self.other_merchant,
|
||||
name='外部面料',
|
||||
product_prefix='REL',
|
||||
)
|
||||
self.other_product = basic_models.Product.objects.create(
|
||||
merchant=self.other_merchant,
|
||||
category=other_category,
|
||||
name='外部布料',
|
||||
human_id='REL-002',
|
||||
unit=basic_models.ProductUnitEnum.METER,
|
||||
)
|
||||
|
||||
self.user = User.objects.create_user(username='relaxed_user', password='pass123')
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
sys_user=self.user,
|
||||
name='宽松仓管员',
|
||||
mobile='13800138003',
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE,
|
||||
)
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_create_stock_change_relaxed_success(self):
|
||||
payload = {
|
||||
'type': stock_models.StockChangeTypeEnum.ADD,
|
||||
'warehouse': self.relaxed_warehouse.id,
|
||||
'source_type': stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
'source_id': 20,
|
||||
'products': [
|
||||
{
|
||||
'product': self.product.id,
|
||||
'quantity': {'value': '12', 'unit_count': '5'}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/stock-change/relaxed/', payload, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data['created_details_count'], 3)
|
||||
record_id = response.data['stock_change_record']['id']
|
||||
details = stock_models.StockChangeDetail.objects.filter(stock_change_record_id=record_id)
|
||||
self.assertEqual(details.count(), 3)
|
||||
|
||||
def test_create_stock_change_relaxed_missing_required(self):
|
||||
payload = {
|
||||
'warehouse': self.relaxed_warehouse.id,
|
||||
'source_type': stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
'products': [
|
||||
{
|
||||
'product': self.product.id,
|
||||
'quantity': {'value': '10', 'unit_count': '4'}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/stock-change/relaxed/', payload, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data['error'], '缺少必要参数')
|
||||
|
||||
def test_create_stock_change_relaxed_product_not_visible(self):
|
||||
payload = {
|
||||
'type': stock_models.StockChangeTypeEnum.ADD,
|
||||
'warehouse': self.relaxed_warehouse.id,
|
||||
'source_type': stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
'products': [
|
||||
{
|
||||
'product': self.other_product.id,
|
||||
'quantity': {'value': '8', 'unit_count': '4'}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/stock-change/relaxed/', payload, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(
|
||||
response.data['error'],
|
||||
f'产品ID {self.other_product.id} 对当前用户不可见',
|
||||
)
|
||||
|
||||
def test_create_stock_change_relaxed_no_employee_permission_denied(self):
|
||||
class DummyUser:
|
||||
def __init__(self, username):
|
||||
self.username = username
|
||||
self.is_authenticated = True
|
||||
|
||||
dummy_user = DummyUser('relaxed_no_emp')
|
||||
self.client.force_authenticate(user=dummy_user)
|
||||
payload = {
|
||||
'type': stock_models.StockChangeTypeEnum.ADD,
|
||||
'warehouse': self.relaxed_warehouse.id,
|
||||
'source_type': stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
'products': [
|
||||
{
|
||||
'product': self.product.id,
|
||||
'quantity': {'value': '10', 'unit_count': '5'}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
logging.disable(logging.NOTSET)
|
||||
self.addCleanup(logging.disable, logging.CRITICAL)
|
||||
with self.assertLogs('api_v1.views.stock_change_views.mixins', level='ERROR') as cm:
|
||||
response = self.client.post('/api/v1/stock-change/relaxed/', payload, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.data['error'], '无权限访问')
|
||||
self.assertTrue(any('无员工信息' in msg for msg in cm.output))
|
||||
|
||||
|
||||
class StockChangeRestrictAPITestCase(TestCase):
|
||||
"""测试严进严出模式的 API"""
|
||||
|
||||
@@ -132,7 +406,7 @@ class ListStockChangeDetailsAPITestCase(TestCase):
|
||||
self.warehouse = basic_models.WareHouse.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='测试仓库',
|
||||
mode=basic_models.WareHouseModeEnum.UNRESTRICTED,
|
||||
mode=basic_models.WareHouseModeEnum.RESTRICT_IN,
|
||||
)
|
||||
|
||||
# 创建另一个产品和仓库用于测试过滤
|
||||
@@ -146,7 +420,7 @@ class ListStockChangeDetailsAPITestCase(TestCase):
|
||||
self.warehouse2 = basic_models.WareHouse.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='测试仓库2',
|
||||
mode=basic_models.WareHouseModeEnum.UNRESTRICTED,
|
||||
mode=basic_models.WareHouseModeEnum.RESTRICT_IN,
|
||||
)
|
||||
|
||||
self.user = User.objects.create_user(username='detail_user', password='pass123')
|
||||
@@ -209,9 +483,9 @@ class ListStockChangeDetailsAPITestCase(TestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# 应该返回仓库1中产品1的所有明细(record1和record2的明细)
|
||||
self.assertEqual(response.data['count'], 3) # record1有2条明细,record2有1条明细
|
||||
self.assertEqual(len(response.data['results']), 3)
|
||||
# 默认 direction=inbound,因此仅包含仓库1中产品1的入库明细(record1 的 2 条)
|
||||
self.assertEqual(response.data['count'], 2)
|
||||
self.assertEqual(len(response.data['results']), 2)
|
||||
|
||||
# 检查返回数据结构
|
||||
result = response.data['results'][0]
|
||||
|
||||
Reference in New Issue
Block a user