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:
86
business/services.py
Normal file
86
business/services.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from basic_info import models as basic_info_models
|
||||
|
||||
from . import models
|
||||
from .tasks import create_purchase_order_stock_entries
|
||||
|
||||
|
||||
def _normalize_order_date(value) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return date.fromisoformat(value)
|
||||
except ValueError as exc:
|
||||
raise ValueError('order_date 格式不正确,应为 YYYY-MM-DD') from exc
|
||||
raise ValueError('order_date 格式不正确')
|
||||
|
||||
|
||||
def create_purchase_order(
|
||||
*,
|
||||
merchant: basic_info_models.Merchant,
|
||||
supplier: basic_info_models.Supplier,
|
||||
order_date,
|
||||
total_amount,
|
||||
warehouse_id: int,
|
||||
items: List[Dict[str, Any]],
|
||||
remarks: str | None = '',
|
||||
created_by=None,
|
||||
) -> models.PurchaseOrder:
|
||||
"""
|
||||
创建采购订单并触发异步创建入库单任务。
|
||||
|
||||
Args:
|
||||
merchant: 采购单所属商户
|
||||
supplier: 供应商
|
||||
order_date: 订单日期 (date)
|
||||
total_amount: 总金额
|
||||
warehouse_id: 入库仓库ID
|
||||
items: 产品明细,格式示例:
|
||||
[
|
||||
{'product_id': 1, 'quantities': ['10.00', '5.00']},
|
||||
{'product_id': 2, 'quantities': ['3.50']},
|
||||
]
|
||||
该结构会被传递给 StockFlowService,需满足其模式要求。
|
||||
remarks: 备注
|
||||
created_by: 创建者用户(可选,用于 stock 记录中的 created_by)
|
||||
"""
|
||||
if not items:
|
||||
raise ValueError('items 不能为空')
|
||||
if not warehouse_id:
|
||||
raise ValueError('warehouse_id 不能为空')
|
||||
|
||||
normalized_date = _normalize_order_date(order_date)
|
||||
total_amount = Decimal(str(total_amount))
|
||||
|
||||
with transaction.atomic():
|
||||
purchase_order = models.PurchaseOrder.objects.create(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
order_date=normalized_date,
|
||||
total_amount=total_amount,
|
||||
remarks=remarks,
|
||||
)
|
||||
|
||||
created_by_id = getattr(created_by, 'id', None)
|
||||
create_purchase_order_stock_entries.delay(
|
||||
purchase_order_id=purchase_order.id,
|
||||
warehouse_id=warehouse_id,
|
||||
items=items,
|
||||
created_by_id=created_by_id,
|
||||
)
|
||||
return purchase_order
|
||||
|
||||
56
business/tasks.py
Normal file
56
business/tasks.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from celery import shared_task
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from business import models as business_models
|
||||
from stock import models as stock_models
|
||||
from stock.services import StockFlowService
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@shared_task(bind=True)
|
||||
def create_purchase_order_stock_entries(
|
||||
self,
|
||||
*,
|
||||
purchase_order_id: int,
|
||||
warehouse_id: int,
|
||||
items: List[Dict[str, Any]],
|
||||
created_by_id: int | None = None,
|
||||
):
|
||||
"""
|
||||
为采购单创建入库记录。仅支持入库(StockFlowService.stock_in)。
|
||||
"""
|
||||
try:
|
||||
purchase_order = business_models.PurchaseOrder.objects.select_related('merchant').get(id=purchase_order_id)
|
||||
except business_models.PurchaseOrder.DoesNotExist:
|
||||
logger.error('PurchaseOrder %s 不存在,无法创建入库单', purchase_order_id)
|
||||
return {'error': 'purchase_order_not_found', 'purchase_order_id': purchase_order_id}
|
||||
|
||||
merchant = purchase_order.merchant
|
||||
|
||||
created_by = None
|
||||
if created_by_id:
|
||||
UserModel = get_user_model()
|
||||
created_by = UserModel.objects.filter(id=created_by_id).first()
|
||||
|
||||
service = StockFlowService(merchant=merchant, created_by=created_by)
|
||||
record, details, created_count = service.stock_in(
|
||||
warehouse_id=warehouse_id,
|
||||
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
source_id=purchase_order.id,
|
||||
items=items,
|
||||
)
|
||||
|
||||
payload = {
|
||||
'task_id': self.request.id,
|
||||
'purchase_order_id': purchase_order.id,
|
||||
'stock_change_record_id': getattr(record, 'id', None),
|
||||
'created_details_count': created_count,
|
||||
}
|
||||
logger.info('采购单 %s 入库任务完成: %s', purchase_order.id, payload)
|
||||
return payload
|
||||
|
||||
@@ -1,3 +1,126 @@
|
||||
from decimal import Decimal
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from stock import models as stock_models
|
||||
from business import models as business_models, services, tasks
|
||||
|
||||
|
||||
def create_basic_fixtures():
|
||||
merchant = basic_models.Merchant.objects.create(
|
||||
name='测试商户',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
supplier = basic_models.Supplier.objects.create(
|
||||
merchant=merchant,
|
||||
name='测试供应商',
|
||||
)
|
||||
warehouse = basic_models.WareHouse.objects.create(
|
||||
merchant=merchant,
|
||||
name='主仓',
|
||||
mode=basic_models.WareHouseModeEnum.RESTRICT_IN,
|
||||
)
|
||||
category = basic_models.ProductCategory.objects.create(
|
||||
merchant=merchant,
|
||||
name='面料',
|
||||
product_prefix='FAB',
|
||||
)
|
||||
product = basic_models.Product.objects.create(
|
||||
merchant=merchant,
|
||||
category=category,
|
||||
name='测试面料',
|
||||
human_id='FAB-001',
|
||||
unit=basic_models.ProductUnitEnum.METER,
|
||||
)
|
||||
return merchant, supplier, warehouse, product
|
||||
|
||||
|
||||
class PurchaseOrderServiceTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.merchant, self.supplier, self.warehouse, self.product = create_basic_fixtures()
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(username='creator', password='pass123')
|
||||
self.items: List[Dict[str, Any]] = [
|
||||
{'product_id': self.product.id, 'quantities': ['10.00', '5.00']},
|
||||
]
|
||||
|
||||
def test_create_purchase_order_triggers_task(self):
|
||||
with patch('business.services.create_purchase_order_stock_entries.delay') as mock_delay:
|
||||
purchase_order = services.create_purchase_order(
|
||||
merchant=self.merchant,
|
||||
supplier=self.supplier,
|
||||
order_date=timezone.now().date(),
|
||||
total_amount=Decimal('100.00'),
|
||||
warehouse_id=self.warehouse.id,
|
||||
items=self.items,
|
||||
remarks='自动化测试',
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
self.assertIsInstance(purchase_order, business_models.PurchaseOrder)
|
||||
mock_delay.assert_called_once_with(
|
||||
purchase_order_id=purchase_order.id,
|
||||
warehouse_id=self.warehouse.id,
|
||||
items=self.items,
|
||||
created_by_id=self.user.id,
|
||||
)
|
||||
|
||||
def test_create_purchase_order_without_items_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_purchase_order(
|
||||
merchant=self.merchant,
|
||||
supplier=self.supplier,
|
||||
order_date=timezone.now().date(),
|
||||
total_amount=Decimal('50.00'),
|
||||
warehouse_id=self.warehouse.id,
|
||||
items=[],
|
||||
)
|
||||
|
||||
|
||||
@override_settings(
|
||||
CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPAGATES=True,
|
||||
)
|
||||
class PurchaseOrderStockTaskTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.merchant, self.supplier, self.warehouse, self.product = create_basic_fixtures()
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(username='task-user', password='pass123')
|
||||
self.purchase_order = business_models.PurchaseOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
supplier=self.supplier,
|
||||
order_date=timezone.now().date(),
|
||||
total_amount=Decimal('100.00'),
|
||||
)
|
||||
self.items = [{'product_id': self.product.id, 'quantities': ['8.00']}]
|
||||
|
||||
def test_task_calls_stock_flow_service(self):
|
||||
with patch('business.tasks.StockFlowService') as mock_flow_cls:
|
||||
mock_instance = mock_flow_cls.return_value
|
||||
mock_instance.stock_in.return_value = (MagicMock(id=321), [], 2)
|
||||
|
||||
async_result = tasks.create_purchase_order_stock_entries.delay(
|
||||
purchase_order_id=self.purchase_order.id,
|
||||
warehouse_id=self.warehouse.id,
|
||||
items=self.items,
|
||||
created_by_id=self.user.id,
|
||||
)
|
||||
payload = async_result.get(timeout=5)
|
||||
|
||||
mock_flow_cls.assert_called_once_with(merchant=self.merchant, created_by=self.user)
|
||||
mock_instance.stock_in.assert_called_once_with(
|
||||
warehouse_id=self.warehouse.id,
|
||||
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
source_id=self.purchase_order.id,
|
||||
items=self.items,
|
||||
)
|
||||
self.assertEqual(payload['purchase_order_id'], self.purchase_order.id)
|
||||
self.assertEqual(payload['stock_change_record_id'], 321)
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Reference in New Issue
Block a user