forked from erp-dev/erp
feat: settlement first api beta
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
"""日结模块信号处理函数"""
|
||||
import logging
|
||||
|
||||
from .models import NotificationChannelEnum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -22,7 +24,7 @@ def on_daily_settlement_completed(sender, **kwargs):
|
||||
return
|
||||
|
||||
# 根据通知渠道处理(目前只有企业微信)
|
||||
if config.notification_channel == config.NotificationChannelEnum.WECOM:
|
||||
if config.notification_channel == NotificationChannelEnum.WECOM:
|
||||
logger.info(
|
||||
f'[settlement.handlers] 商户 {merchant.id} 日结完成,'
|
||||
f'状态={status}, 模块={modules}, 错误={errors}, '
|
||||
|
||||
@@ -47,3 +47,4 @@ class DailySettlementConfig(ModelBase):
|
||||
class Meta:
|
||||
verbose_name = '日结配置'
|
||||
verbose_name_plural = '日结配置'
|
||||
db_table = 'daily_settlement_config'
|
||||
|
||||
@@ -1,10 +1,217 @@
|
||||
"""日结模块统计服务函数"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
|
||||
from django.db.models import Q, Sum, Case, When, Value, CharField, IntegerField
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from printing.models import PlateOrder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_plate_order_summary_by_customer(
|
||||
merchant_id: int,
|
||||
settlement_date: date,
|
||||
user=None
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取按客户分组的开版订单统计
|
||||
|
||||
Args:
|
||||
merchant_id: 商户ID
|
||||
settlement_date: 统计日期
|
||||
user: 当前用户(用于客户可见性过滤)
|
||||
|
||||
Returns:
|
||||
list[dict]: 客户统计列表
|
||||
"""
|
||||
logger.info(
|
||||
f'[settlement.services] 获取开版订单统计: '
|
||||
f'merchant_id={merchant_id}, date={settlement_date}'
|
||||
)
|
||||
|
||||
settlement_date = _validate_and_normalize_summary_args(
|
||||
merchant_id=merchant_id,
|
||||
settlement_date=settlement_date,
|
||||
)
|
||||
|
||||
queryset = _get_plate_order_queryset(merchant_id, user)
|
||||
aggregated_data = _aggregate_plate_orders_by_customer_and_type(
|
||||
queryset, settlement_date
|
||||
)
|
||||
return _format_plate_order_summary(aggregated_data)
|
||||
|
||||
|
||||
def _validate_and_normalize_summary_args(
|
||||
merchant_id: int,
|
||||
settlement_date: date,
|
||||
) -> date:
|
||||
"""校验并标准化统计参数。"""
|
||||
if not isinstance(merchant_id, int) or merchant_id <= 0:
|
||||
raise ValueError('merchant_id 必须为正整数')
|
||||
|
||||
if isinstance(settlement_date, datetime):
|
||||
settlement_date = settlement_date.date()
|
||||
|
||||
if not isinstance(settlement_date, date):
|
||||
raise ValueError('settlement_date 必须为 date 或 datetime 类型')
|
||||
|
||||
return settlement_date
|
||||
|
||||
|
||||
def _get_plate_order_queryset(merchant_id: int, user=None):
|
||||
"""
|
||||
获取开版订单的基础查询集
|
||||
|
||||
Args:
|
||||
merchant_id: 商户ID
|
||||
user: 当前用户(用于客户可见性过滤)
|
||||
|
||||
Returns:
|
||||
QuerySet[PlateOrder]: 过滤后的查询集
|
||||
"""
|
||||
queryset = PlateOrder.objects.filter(
|
||||
merchant_id=merchant_id,
|
||||
plate_type__isnull=False,
|
||||
production_method__isnull=False
|
||||
).select_related('customer')
|
||||
|
||||
if user and not user.is_superuser:
|
||||
emp = getattr(user, 'employee', None)
|
||||
if emp:
|
||||
visible_customer_filter = (
|
||||
Q(customer__created_by=emp) |
|
||||
Q(customer__visible_employees=emp)
|
||||
)
|
||||
no_customer_filter = Q(customer__isnull=True)
|
||||
queryset = queryset.filter(
|
||||
visible_customer_filter | no_customer_filter
|
||||
).distinct()
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
def _get_month_date_range(settlement_date: date) -> tuple[date, date]:
|
||||
"""
|
||||
获取从月初到指定日期的日期范围
|
||||
|
||||
Args:
|
||||
settlement_date: 统计日期
|
||||
|
||||
Returns:
|
||||
tuple[date, date]: (月初日期, 统计日期)
|
||||
"""
|
||||
month_start = settlement_date.replace(day=1)
|
||||
return month_start, settlement_date
|
||||
|
||||
|
||||
def _aggregate_plate_orders_by_customer_and_type(queryset, settlement_date: date):
|
||||
"""
|
||||
按客户和类型分组聚合订单数据
|
||||
|
||||
Args:
|
||||
queryset: 基础查询集
|
||||
settlement_date: 统计日期
|
||||
|
||||
Returns:
|
||||
QuerySet[PlateOrder]: 添加了聚合标注的查询集
|
||||
"""
|
||||
month_start, _ = _get_month_date_range(settlement_date)
|
||||
|
||||
queryset = queryset.annotate(
|
||||
type=Concat(
|
||||
'plate_type',
|
||||
Value('-'),
|
||||
'production_method',
|
||||
output_field=CharField()
|
||||
)
|
||||
).values(
|
||||
'customer__id',
|
||||
'customer__name',
|
||||
'type'
|
||||
).annotate(
|
||||
today=Sum(
|
||||
Case(
|
||||
When(
|
||||
plate_date__date=settlement_date,
|
||||
then=1
|
||||
),
|
||||
default=0,
|
||||
output_field=IntegerField()
|
||||
)
|
||||
),
|
||||
current_month=Sum(
|
||||
Case(
|
||||
When(
|
||||
plate_date__date__gte=month_start,
|
||||
plate_date__date__lte=settlement_date,
|
||||
then=1
|
||||
),
|
||||
default=0,
|
||||
output_field=IntegerField()
|
||||
)
|
||||
)
|
||||
).order_by('customer__name', 'customer__id', 'type')
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
def _format_plate_order_summary(aggregated_data):
|
||||
"""
|
||||
格式化聚合结果为 API 返回格式
|
||||
|
||||
Args:
|
||||
aggregated_data: 聚合后的查询集
|
||||
|
||||
Returns:
|
||||
list[dict]: 格式化后的数据
|
||||
"""
|
||||
result = {}
|
||||
|
||||
for item in aggregated_data:
|
||||
client_id = item['customer__id']
|
||||
client_name = item['customer__name']
|
||||
client_key = (client_id, client_name)
|
||||
if client_key not in result:
|
||||
result[client_key] = {
|
||||
'client_id': client_id,
|
||||
'client_name': client_name,
|
||||
'plate_order_count': []
|
||||
}
|
||||
|
||||
plate_order_count = {
|
||||
'type': item['type'],
|
||||
'today': item['today'],
|
||||
'current_month': item['current_month']
|
||||
}
|
||||
|
||||
result[client_key]['plate_order_count'].append(plate_order_count)
|
||||
|
||||
for client_key in result:
|
||||
result[client_key]['plate_order_count'] = _filter_zero_data(
|
||||
result[client_key]['plate_order_count']
|
||||
)
|
||||
|
||||
return [v for k, v in result.items() if v['plate_order_count']]
|
||||
|
||||
|
||||
def _filter_zero_data(plate_order_counts):
|
||||
"""
|
||||
过滤全0数据
|
||||
|
||||
Args:
|
||||
plate_order_counts: 订单统计列表
|
||||
|
||||
Returns:
|
||||
list[dict]: 过滤后的列表
|
||||
"""
|
||||
return [
|
||||
item for item in plate_order_counts
|
||||
if item['today'] > 0 or item['current_month'] > 0
|
||||
]
|
||||
|
||||
|
||||
def calculate_plate_order_daily_summary(
|
||||
merchant_id: int,
|
||||
settlement_date: datetime.date
|
||||
|
||||
@@ -26,8 +26,8 @@ def run_daily_settlement(self):
|
||||
from basic_info.models import Merchant
|
||||
|
||||
# 获取所有配置了统计模块的商户
|
||||
configs = DailySettlementConfig.objects.filter(
|
||||
settlement_modules__len__gt=0
|
||||
configs = DailySettlementConfig.objects.exclude(
|
||||
settlement_modules=[]
|
||||
).select_related('merchant')
|
||||
|
||||
logger.info(
|
||||
|
||||
60
settlement/test_integration_table_mapping.py
Normal file
60
settlement/test_integration_table_mapping.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from datetime import date
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from settlement.models import (
|
||||
DailySettlementConfig,
|
||||
NotificationChannelEnum,
|
||||
SettlementModuleEnum,
|
||||
)
|
||||
from settlement.tasks import run_daily_settlement
|
||||
|
||||
|
||||
class SettlementTableMappingIntegrationTestCase(TestCase):
|
||||
def test_daily_settlement_config_can_persist_in_real_db(self):
|
||||
merchant = basic_models.Merchant.objects.create(
|
||||
name='表映射测试商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
|
||||
config = DailySettlementConfig.objects.create(
|
||||
merchant=merchant,
|
||||
settlement_modules=[SettlementModuleEnum.PLATE_ORDER],
|
||||
notification_enabled=False,
|
||||
notification_channel=NotificationChannelEnum.NONE,
|
||||
)
|
||||
|
||||
fetched = DailySettlementConfig.objects.get(id=config.id)
|
||||
self.assertEqual(fetched.merchant_id, merchant.id)
|
||||
self.assertEqual(fetched.settlement_modules, [SettlementModuleEnum.PLATE_ORDER])
|
||||
|
||||
@patch('settlement.tasks.run_merchant_daily_settlement.delay')
|
||||
@patch('settlement.tasks.timezone.localdate')
|
||||
def test_run_daily_settlement_queries_real_config_table(
|
||||
self,
|
||||
mock_localdate,
|
||||
mock_delay,
|
||||
):
|
||||
mock_localdate.return_value = date(2026, 2, 9)
|
||||
|
||||
merchant = basic_models.Merchant.objects.create(
|
||||
name='任务查询测试商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
DailySettlementConfig.objects.create(
|
||||
merchant=merchant,
|
||||
settlement_modules=[SettlementModuleEnum.PLATE_ORDER],
|
||||
notification_enabled=False,
|
||||
notification_channel=NotificationChannelEnum.NONE,
|
||||
)
|
||||
|
||||
result = run_daily_settlement.run()
|
||||
|
||||
self.assertEqual(result['total_merchants'], 1)
|
||||
self.assertEqual(result['settlement_date'], '2026-02-08')
|
||||
mock_delay.assert_called_once_with(
|
||||
merchant_id=merchant.id,
|
||||
settlement_date='2026-02-08',
|
||||
)
|
||||
510
settlement/test_services.py
Normal file
510
settlement/test_services.py
Normal file
@@ -0,0 +1,510 @@
|
||||
"""Settlement Service 测试"""
|
||||
from datetime import date, datetime
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from printing import models as printing_models
|
||||
from settlement import services
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class SettlementServiceTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='测试商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY
|
||||
)
|
||||
|
||||
self.user = User.objects.create_user(
|
||||
username='test-user',
|
||||
password='pass123'
|
||||
)
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
sys_user=self.user,
|
||||
merchant=self.merchant,
|
||||
name='测试员工'
|
||||
)
|
||||
|
||||
self.customer1 = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='客户A',
|
||||
created_by=self.employee
|
||||
)
|
||||
self.customer2 = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='客户B',
|
||||
created_by=self.employee
|
||||
)
|
||||
|
||||
self.settlement_date = date(2026, 2, 8)
|
||||
|
||||
|
||||
class GetPlateOrderQuerySetTestCase(SettlementServiceTestCase):
|
||||
def test_filters_by_merchant(self):
|
||||
other_merchant = basic_models.Merchant.objects.create(
|
||||
name='其他商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY
|
||||
)
|
||||
other_customer = basic_models.Customer.objects.create(
|
||||
merchant=other_merchant,
|
||||
name='其他客户'
|
||||
)
|
||||
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位'
|
||||
)
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=other_merchant,
|
||||
customer=other_customer,
|
||||
plate_type='首版',
|
||||
production_method='定位'
|
||||
)
|
||||
|
||||
queryset = services._get_plate_order_queryset(
|
||||
self.merchant.id,
|
||||
self.user
|
||||
)
|
||||
|
||||
self.assertEqual(queryset.count(), 1)
|
||||
self.assertEqual(queryset.first().customer.name, '客户A')
|
||||
|
||||
def test_filters_by_plate_type_not_null(self):
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位'
|
||||
)
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type=None,
|
||||
production_method='定位'
|
||||
)
|
||||
|
||||
queryset = services._get_plate_order_queryset(
|
||||
self.merchant.id,
|
||||
self.user
|
||||
)
|
||||
|
||||
self.assertEqual(queryset.count(), 1)
|
||||
self.assertIsNotNone(queryset.first().plate_type)
|
||||
|
||||
def test_applies_customer_visibility_filter(self):
|
||||
other_employee = basic_models.Employee.objects.create(
|
||||
sys_user=User.objects.create_user('other', 'pass123'),
|
||||
merchant=self.merchant,
|
||||
name='其他员工'
|
||||
)
|
||||
|
||||
customer_visible = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='可见客户',
|
||||
created_by=other_employee
|
||||
)
|
||||
customer_visible.visible_employees.set([self.employee])
|
||||
customer_invisible = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='不可见客户',
|
||||
created_by=other_employee
|
||||
)
|
||||
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=customer_visible,
|
||||
plate_type='首版',
|
||||
production_method='定位'
|
||||
)
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=customer_invisible,
|
||||
plate_type='首版',
|
||||
production_method='定位'
|
||||
)
|
||||
|
||||
queryset = services._get_plate_order_queryset(
|
||||
self.merchant.id,
|
||||
self.user
|
||||
)
|
||||
|
||||
self.assertEqual(queryset.count(), 1)
|
||||
self.assertEqual(queryset.first().customer.name, '可见客户')
|
||||
|
||||
def test_superuser_sees_all_customers(self):
|
||||
self.user.is_superuser = True
|
||||
self.user.save()
|
||||
|
||||
other_employee = basic_models.Employee.objects.create(
|
||||
sys_user=User.objects.create_user('other', 'pass123'),
|
||||
merchant=self.merchant,
|
||||
name='其他员工'
|
||||
)
|
||||
customer_invisible = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='不可见客户',
|
||||
created_by=other_employee
|
||||
)
|
||||
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=customer_invisible,
|
||||
plate_type='首版',
|
||||
production_method='定位'
|
||||
)
|
||||
|
||||
queryset = services._get_plate_order_queryset(
|
||||
self.merchant.id,
|
||||
self.user
|
||||
)
|
||||
|
||||
self.assertEqual(queryset.count(), 1)
|
||||
|
||||
|
||||
class GetMonthDateRangeTestCase(TestCase):
|
||||
def test_returns_month_start_and_settlement_date(self):
|
||||
settlement_date = date(2026, 2, 8)
|
||||
month_start, end_date = services._get_month_date_range(settlement_date)
|
||||
|
||||
self.assertEqual(month_start, date(2026, 2, 1))
|
||||
self.assertEqual(end_date, date(2026, 2, 8))
|
||||
|
||||
def test_handles_month_start(self):
|
||||
settlement_date = date(2026, 2, 1)
|
||||
month_start, end_date = services._get_month_date_range(settlement_date)
|
||||
|
||||
self.assertEqual(month_start, date(2026, 2, 1))
|
||||
self.assertEqual(end_date, date(2026, 2, 1))
|
||||
|
||||
def test_handles_month_end(self):
|
||||
settlement_date = date(2026, 2, 28)
|
||||
month_start, end_date = services._get_month_date_range(settlement_date)
|
||||
|
||||
self.assertEqual(month_start, date(2026, 2, 1))
|
||||
self.assertEqual(end_date, date(2026, 2, 28))
|
||||
|
||||
def test_handles_different_months(self):
|
||||
settlement_date = date(2026, 3, 15)
|
||||
month_start, end_date = services._get_month_date_range(settlement_date)
|
||||
|
||||
self.assertEqual(month_start, date(2026, 3, 1))
|
||||
self.assertEqual(end_date, date(2026, 3, 15))
|
||||
|
||||
|
||||
class AggregatePlateOrdersByCustomerAndTypeTestCase(SettlementServiceTestCase):
|
||||
def test_aggregates_by_customer_and_type(self):
|
||||
today = self.settlement_date
|
||||
yesterday = date(2026, 2, 7)
|
||||
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(today, datetime.min.time()))
|
||||
)
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='修改',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(yesterday, datetime.min.time()))
|
||||
)
|
||||
|
||||
queryset = services._get_plate_order_queryset(
|
||||
self.merchant.id,
|
||||
self.user
|
||||
)
|
||||
aggregated = services._aggregate_plate_orders_by_customer_and_type(
|
||||
queryset, self.settlement_date
|
||||
)
|
||||
|
||||
self.assertEqual(len(aggregated), 2)
|
||||
|
||||
def test_calculates_today_count(self):
|
||||
today = self.settlement_date
|
||||
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(today, datetime.min.time()))
|
||||
)
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(date(2026, 2, 7), datetime.min.time()))
|
||||
)
|
||||
|
||||
queryset = services._get_plate_order_queryset(
|
||||
self.merchant.id,
|
||||
self.user
|
||||
)
|
||||
aggregated = services._aggregate_plate_orders_by_customer_and_type(
|
||||
queryset, self.settlement_date
|
||||
)
|
||||
|
||||
today_item = [item for item in aggregated if item['type'] == '首版-定位'][0]
|
||||
self.assertEqual(today_item['today'], 1)
|
||||
|
||||
def test_calculates_current_month_count(self):
|
||||
today = self.settlement_date
|
||||
month_start = date(2026, 2, 1)
|
||||
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(today, datetime.min.time()))
|
||||
)
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(month_start, datetime.min.time()))
|
||||
)
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(date(2026, 1, 31), datetime.min.time()))
|
||||
)
|
||||
|
||||
queryset = services._get_plate_order_queryset(
|
||||
self.merchant.id,
|
||||
self.user
|
||||
)
|
||||
aggregated = services._aggregate_plate_orders_by_customer_and_type(
|
||||
queryset, self.settlement_date
|
||||
)
|
||||
|
||||
today_item = [item for item in aggregated if item['type'] == '首版-定位'][0]
|
||||
self.assertEqual(today_item['current_month'], 2)
|
||||
|
||||
def test_filters_null_production_method(self):
|
||||
today = self.settlement_date
|
||||
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(today, datetime.min.time()))
|
||||
)
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method=None,
|
||||
plate_date=timezone.make_aware(datetime.combine(today, datetime.min.time()))
|
||||
)
|
||||
|
||||
queryset = services._get_plate_order_queryset(
|
||||
self.merchant.id,
|
||||
self.user
|
||||
)
|
||||
|
||||
self.assertEqual(queryset.count(), 1)
|
||||
self.assertEqual(queryset.first().production_method, '定位')
|
||||
|
||||
|
||||
class FilterZeroDataTestCase(SettlementServiceTestCase):
|
||||
def test_filters_all_zero_data(self):
|
||||
data = [
|
||||
{'type': '首版-定位', 'today': 0, 'current_month': 0},
|
||||
{'type': '修改-定位', 'today': 0, 'current_month': 0}
|
||||
]
|
||||
|
||||
result = services._filter_zero_data(data)
|
||||
|
||||
self.assertEqual(len(result), 0)
|
||||
|
||||
def test_keeps_non_zero_today(self):
|
||||
data = [
|
||||
{'type': '首版-定位', 'today': 1, 'current_month': 0},
|
||||
{'type': '修改-定位', 'today': 0, 'current_month': 0}
|
||||
]
|
||||
|
||||
result = services._filter_zero_data(data)
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]['type'], '首版-定位')
|
||||
|
||||
def test_keeps_non_zero_current_month(self):
|
||||
data = [
|
||||
{'type': '首版-定位', 'today': 0, 'current_month': 5},
|
||||
{'type': '修改-定位', 'today': 0, 'current_month': 0}
|
||||
]
|
||||
|
||||
result = services._filter_zero_data(data)
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]['type'], '首版-定位')
|
||||
|
||||
|
||||
class FormatPlateOrderSummaryTestCase(SettlementServiceTestCase):
|
||||
def test_formats_aggregated_data(self):
|
||||
aggregated = [
|
||||
{
|
||||
'customer__id': self.customer1.id,
|
||||
'customer__name': '客户A',
|
||||
'type': '首版-定位',
|
||||
'today': 5,
|
||||
'current_month': 10
|
||||
},
|
||||
{
|
||||
'customer__id': self.customer1.id,
|
||||
'customer__name': '客户A',
|
||||
'type': '修改-定位',
|
||||
'today': 2,
|
||||
'current_month': 3
|
||||
}
|
||||
]
|
||||
|
||||
result = services._format_plate_order_summary(aggregated)
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]['client_id'], self.customer1.id)
|
||||
self.assertEqual(result[0]['client_name'], '客户A')
|
||||
self.assertEqual(len(result[0]['plate_order_count']), 2)
|
||||
|
||||
def test_filters_customers_with_zero_data(self):
|
||||
aggregated = [
|
||||
{
|
||||
'customer__id': self.customer1.id,
|
||||
'customer__name': '客户A',
|
||||
'type': '首版-定位',
|
||||
'today': 0,
|
||||
'current_month': 0
|
||||
}
|
||||
]
|
||||
|
||||
result = services._format_plate_order_summary(aggregated)
|
||||
|
||||
self.assertEqual(len(result), 0)
|
||||
|
||||
def test_groups_by_customer(self):
|
||||
aggregated = [
|
||||
{
|
||||
'customer__id': self.customer1.id,
|
||||
'customer__name': '客户A',
|
||||
'type': '首版-定位',
|
||||
'today': 5,
|
||||
'current_month': 10
|
||||
},
|
||||
{
|
||||
'customer__id': self.customer2.id,
|
||||
'customer__name': '客户B',
|
||||
'type': '首版-定位',
|
||||
'today': 3,
|
||||
'current_month': 6
|
||||
}
|
||||
]
|
||||
|
||||
result = services._format_plate_order_summary(aggregated)
|
||||
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertEqual(result[0]['client_id'], self.customer1.id)
|
||||
self.assertEqual(result[0]['client_name'], '客户A')
|
||||
self.assertEqual(result[1]['client_id'], self.customer2.id)
|
||||
self.assertEqual(result[1]['client_name'], '客户B')
|
||||
|
||||
def test_groups_same_name_customers_by_customer_id(self):
|
||||
aggregated = [
|
||||
{
|
||||
'customer__id': self.customer1.id,
|
||||
'customer__name': '同名客户',
|
||||
'type': '首版-定位',
|
||||
'today': 1,
|
||||
'current_month': 2
|
||||
},
|
||||
{
|
||||
'customer__id': self.customer2.id,
|
||||
'customer__name': '同名客户',
|
||||
'type': '修改-定位',
|
||||
'today': 3,
|
||||
'current_month': 4
|
||||
}
|
||||
]
|
||||
|
||||
result = services._format_plate_order_summary(aggregated)
|
||||
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertNotEqual(result[0]['client_id'], result[1]['client_id'])
|
||||
|
||||
|
||||
class GetPlateOrderSummaryByCustomerTestCase(SettlementServiceTestCase):
|
||||
def test_returns_summary_for_multiple_customers(self):
|
||||
today = self.settlement_date
|
||||
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(today, datetime.min.time()))
|
||||
)
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer2,
|
||||
plate_type='首版',
|
||||
production_method='匹布',
|
||||
plate_date=timezone.make_aware(datetime.combine(today, datetime.min.time()))
|
||||
)
|
||||
|
||||
result = services.get_plate_order_summary_by_customer(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user
|
||||
)
|
||||
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertIn('client_id', result[0])
|
||||
|
||||
def test_filters_zero_data(self):
|
||||
today = self.settlement_date
|
||||
|
||||
printing_models.PlateOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer1,
|
||||
plate_type='首版',
|
||||
production_method='定位',
|
||||
plate_date=timezone.make_aware(datetime.combine(date(2026, 1, 1), datetime.min.time()))
|
||||
)
|
||||
|
||||
result = services.get_plate_order_summary_by_customer(
|
||||
self.merchant.id,
|
||||
self.settlement_date,
|
||||
self.user
|
||||
)
|
||||
|
||||
self.assertEqual(len(result), 0)
|
||||
|
||||
def test_raises_for_invalid_merchant_id(self):
|
||||
with self.assertRaises(ValueError):
|
||||
services.get_plate_order_summary_by_customer(
|
||||
merchant_id=0,
|
||||
settlement_date=self.settlement_date,
|
||||
user=self.user
|
||||
)
|
||||
|
||||
def test_raises_for_invalid_settlement_date(self):
|
||||
with self.assertRaises(ValueError):
|
||||
services.get_plate_order_summary_by_customer(
|
||||
merchant_id=self.merchant.id,
|
||||
settlement_date='2026-02-08',
|
||||
user=self.user
|
||||
)
|
||||
229
settlement/test_tasks_handlers_admin.py
Normal file
229
settlement/test_tasks_handlers_admin.py
Normal file
@@ -0,0 +1,229 @@
|
||||
from datetime import date
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.test import TestCase
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from settlement import handlers
|
||||
from settlement.admin import DailySettlementConfigAdmin
|
||||
from settlement.models import (
|
||||
DailySettlementConfig,
|
||||
NotificationChannelEnum,
|
||||
SettlementModuleEnum,
|
||||
)
|
||||
from settlement.tasks import run_daily_settlement, run_merchant_daily_settlement
|
||||
|
||||
|
||||
class SettlementTasksTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='任务测试商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
self.config = DailySettlementConfig.objects.create(
|
||||
merchant=self.merchant,
|
||||
settlement_modules=[SettlementModuleEnum.PLATE_ORDER],
|
||||
notification_enabled=False,
|
||||
notification_channel=NotificationChannelEnum.NONE,
|
||||
)
|
||||
|
||||
@patch('settlement.tasks.run_merchant_daily_settlement.delay')
|
||||
@patch('settlement.tasks.timezone.localdate')
|
||||
def test_run_daily_settlement_dispatches_only_configured_merchants(
|
||||
self,
|
||||
mock_localdate,
|
||||
mock_delay,
|
||||
):
|
||||
mock_localdate.return_value = date(2026, 2, 9)
|
||||
|
||||
merchant_without_modules = basic_models.Merchant.objects.create(
|
||||
name='无模块商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
DailySettlementConfig.objects.create(
|
||||
merchant=merchant_without_modules,
|
||||
settlement_modules=[],
|
||||
notification_enabled=False,
|
||||
notification_channel=NotificationChannelEnum.NONE,
|
||||
)
|
||||
|
||||
result = run_daily_settlement.run()
|
||||
|
||||
self.assertEqual(result['total_merchants'], 1)
|
||||
self.assertEqual(result['settlement_date'], '2026-02-08')
|
||||
mock_delay.assert_called_once_with(
|
||||
merchant_id=self.merchant.id,
|
||||
settlement_date='2026-02-08',
|
||||
)
|
||||
|
||||
@patch('settlement.tasks.daily_settlement_completed.send')
|
||||
@patch('settlement.tasks.calculate_printing_order_daily_summary')
|
||||
@patch('settlement.tasks.calculate_plate_order_daily_summary')
|
||||
def test_run_merchant_daily_settlement_success(
|
||||
self,
|
||||
mock_plate_summary,
|
||||
mock_printing_summary,
|
||||
mock_signal_send,
|
||||
):
|
||||
self.config.settlement_modules = [
|
||||
SettlementModuleEnum.PLATE_ORDER,
|
||||
SettlementModuleEnum.PRINTING_ORDER,
|
||||
]
|
||||
self.config.save(update_fields=['settlement_modules'])
|
||||
|
||||
mock_plate_summary.return_value = {'total_orders': 1}
|
||||
mock_printing_summary.return_value = {'total_orders': 2}
|
||||
|
||||
result = run_merchant_daily_settlement.run(
|
||||
merchant_id=self.merchant.id,
|
||||
settlement_date='2026-02-08',
|
||||
)
|
||||
|
||||
self.assertEqual(result['merchant_id'], self.merchant.id)
|
||||
self.assertEqual(result['settlement_date'], '2026-02-08')
|
||||
self.assertEqual(result['status'], 'success')
|
||||
self.assertEqual(result['errors'], {})
|
||||
mock_plate_summary.assert_called_once()
|
||||
mock_printing_summary.assert_called_once()
|
||||
mock_signal_send.assert_called_once()
|
||||
|
||||
@patch('settlement.tasks.daily_settlement_completed.send')
|
||||
@patch('settlement.tasks.calculate_printing_order_daily_summary')
|
||||
@patch('settlement.tasks.calculate_plate_order_daily_summary')
|
||||
def test_run_merchant_daily_settlement_partial_when_one_module_fails(
|
||||
self,
|
||||
mock_plate_summary,
|
||||
mock_printing_summary,
|
||||
mock_signal_send,
|
||||
):
|
||||
self.config.settlement_modules = [
|
||||
SettlementModuleEnum.PLATE_ORDER,
|
||||
SettlementModuleEnum.PRINTING_ORDER,
|
||||
]
|
||||
self.config.save(update_fields=['settlement_modules'])
|
||||
|
||||
mock_plate_summary.side_effect = RuntimeError('plate failed')
|
||||
mock_printing_summary.return_value = {'total_orders': 2}
|
||||
|
||||
result = run_merchant_daily_settlement.run(
|
||||
merchant_id=self.merchant.id,
|
||||
settlement_date='2026-02-08',
|
||||
)
|
||||
|
||||
self.assertEqual(result['status'], 'partial')
|
||||
self.assertIn(SettlementModuleEnum.PLATE_ORDER, result['errors'])
|
||||
self.assertEqual(result['errors'][SettlementModuleEnum.PLATE_ORDER], 'plate failed')
|
||||
mock_signal_send.assert_called_once()
|
||||
|
||||
@patch('settlement.tasks.daily_settlement_completed.send')
|
||||
@patch('settlement.tasks.calculate_plate_order_daily_summary')
|
||||
def test_run_merchant_daily_settlement_failed_when_all_modules_fail(
|
||||
self,
|
||||
mock_plate_summary,
|
||||
mock_signal_send,
|
||||
):
|
||||
self.config.settlement_modules = [SettlementModuleEnum.PLATE_ORDER]
|
||||
self.config.save(update_fields=['settlement_modules'])
|
||||
|
||||
mock_plate_summary.side_effect = RuntimeError('plate failed')
|
||||
|
||||
result = run_merchant_daily_settlement.run(
|
||||
merchant_id=self.merchant.id,
|
||||
settlement_date='2026-02-08',
|
||||
)
|
||||
|
||||
self.assertEqual(result['status'], 'failed')
|
||||
self.assertIn(SettlementModuleEnum.PLATE_ORDER, result['errors'])
|
||||
mock_signal_send.assert_called_once()
|
||||
|
||||
@patch('settlement.tasks.daily_settlement_completed.send')
|
||||
@patch('settlement.tasks.calculate_plate_order_daily_summary')
|
||||
@patch('settlement.tasks.timezone.localdate')
|
||||
def test_run_merchant_daily_settlement_uses_yesterday_when_no_date(
|
||||
self,
|
||||
mock_localdate,
|
||||
mock_plate_summary,
|
||||
mock_signal_send,
|
||||
):
|
||||
mock_localdate.return_value = date(2026, 2, 9)
|
||||
self.config.settlement_modules = [SettlementModuleEnum.PLATE_ORDER]
|
||||
self.config.save(update_fields=['settlement_modules'])
|
||||
mock_plate_summary.return_value = {'total_orders': 1}
|
||||
|
||||
result = run_merchant_daily_settlement.run(merchant_id=self.merchant.id)
|
||||
|
||||
self.assertEqual(result['settlement_date'], '2026-02-08')
|
||||
mock_signal_send.assert_called_once()
|
||||
|
||||
|
||||
class SettlementHandlersTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='处理器测试商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
|
||||
@patch('settlement.handlers.logger.info')
|
||||
def test_handler_logs_when_notification_disabled(self, mock_logger_info):
|
||||
DailySettlementConfig.objects.create(
|
||||
merchant=self.merchant,
|
||||
settlement_modules=[SettlementModuleEnum.PLATE_ORDER],
|
||||
notification_enabled=False,
|
||||
notification_channel=NotificationChannelEnum.NONE,
|
||||
)
|
||||
|
||||
handlers.on_daily_settlement_completed(
|
||||
sender=self.__class__,
|
||||
merchant=self.merchant,
|
||||
settlement_date=date(2026, 2, 8),
|
||||
status='success',
|
||||
modules=[SettlementModuleEnum.PLATE_ORDER],
|
||||
errors={},
|
||||
task_id='task-1',
|
||||
)
|
||||
|
||||
self.assertTrue(mock_logger_info.called)
|
||||
|
||||
@patch('settlement.handlers.logger.info')
|
||||
def test_handler_logs_wecom_when_notification_enabled(self, mock_logger_info):
|
||||
DailySettlementConfig.objects.create(
|
||||
merchant=self.merchant,
|
||||
settlement_modules=[SettlementModuleEnum.PLATE_ORDER],
|
||||
notification_enabled=True,
|
||||
notification_channel=NotificationChannelEnum.WECOM,
|
||||
)
|
||||
|
||||
handlers.on_daily_settlement_completed(
|
||||
sender=self.__class__,
|
||||
merchant=self.merchant,
|
||||
settlement_date=date(2026, 2, 8),
|
||||
status='success',
|
||||
modules=[SettlementModuleEnum.PLATE_ORDER],
|
||||
errors={},
|
||||
task_id='task-2',
|
||||
)
|
||||
|
||||
self.assertTrue(mock_logger_info.called)
|
||||
|
||||
|
||||
class SettlementAdminTestCase(TestCase):
|
||||
def test_get_readonly_fields(self):
|
||||
admin_obj = DailySettlementConfigAdmin(
|
||||
DailySettlementConfig,
|
||||
admin_site=AdminSite(),
|
||||
)
|
||||
|
||||
self.assertEqual(admin_obj.get_readonly_fields(request=None, obj=None), [])
|
||||
|
||||
merchant = basic_models.Merchant.objects.create(
|
||||
name='后台测试商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
config = DailySettlementConfig.objects.create(
|
||||
merchant=merchant,
|
||||
settlement_modules=[SettlementModuleEnum.PLATE_ORDER],
|
||||
notification_enabled=False,
|
||||
notification_channel=NotificationChannelEnum.NONE,
|
||||
)
|
||||
self.assertEqual(admin_obj.get_readonly_fields(request=None, obj=config), ['merchant'])
|
||||
Reference in New Issue
Block a user