1
0
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:
2025-11-26 21:49:42 +08:00
parent 6bf0465d05
commit a9c75a13fa
26 changed files with 7158 additions and 216 deletions

View File

@@ -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),

View File

@@ -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]