forked from erp-dev/erp
feat: support pre_sales_order convert to sales_order api
This commit is contained in:
@@ -136,6 +136,8 @@ class PreSalesAllocationAPITestCase(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(delete_resp.status_code, status.HTTP_200_OK)
|
self.assertEqual(delete_resp.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(delete_resp.data['status'], business_models.AllocationRecordStatusEnum.CANCELLED)
|
self.assertEqual(delete_resp.data['status'], business_models.AllocationRecordStatusEnum.CANCELLED)
|
||||||
|
freeze = stock_models.StockFreeze.objects.get(stock_detail=self.stock_detail)
|
||||||
|
self.assertEqual(freeze.status, stock_models.StockFreezeStatusEnum.CANCELLED)
|
||||||
|
|
||||||
def test_create_allocation_with_invalid_stock(self):
|
def test_create_allocation_with_invalid_stock(self):
|
||||||
order = self._create_order()
|
order = self._create_order()
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from basic_info.models import (
|
|||||||
EmployeeStatusEnum,
|
EmployeeStatusEnum,
|
||||||
)
|
)
|
||||||
from business import models as business_models
|
from business import models as business_models
|
||||||
|
from business import allocation_services
|
||||||
|
from stock import models as stock_models
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
@@ -37,6 +39,11 @@ class PreSalesOrderAPITestCase(TestCase):
|
|||||||
name='预销售仓库',
|
name='预销售仓库',
|
||||||
mode=WareHouseModeEnum.UNRESTRICTED,
|
mode=WareHouseModeEnum.UNRESTRICTED,
|
||||||
)
|
)
|
||||||
|
self.warehouse_strict_out = WareHouse.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
name='预销售严出仓',
|
||||||
|
mode=WareHouseModeEnum.RESTRICT_IN_OUT,
|
||||||
|
)
|
||||||
category = ProductCategory.objects.create(
|
category = ProductCategory.objects.create(
|
||||||
merchant=self.merchant,
|
merchant=self.merchant,
|
||||||
name='预销售品类',
|
name='预销售品类',
|
||||||
@@ -78,6 +85,20 @@ class PreSalesOrderAPITestCase(TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.strict_payload = {
|
||||||
|
'customer': self.customer.id,
|
||||||
|
'warehouse': self.warehouse_strict_out.id,
|
||||||
|
'kind': business_models.SalesOrderKindEnum.WHOLESALE,
|
||||||
|
'remarks': '预销售单备注',
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'product_id': self.product.id,
|
||||||
|
'quantity': '10',
|
||||||
|
'unit': '米',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
def _create(self, payload=None) -> int:
|
def _create(self, payload=None) -> int:
|
||||||
resp = self.client.post('/api/v1/pre-sales-orders/', payload or self.payload, format='json')
|
resp = self.client.post('/api/v1/pre-sales-orders/', payload or self.payload, format='json')
|
||||||
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
||||||
@@ -121,3 +142,50 @@ class PreSalesOrderAPITestCase(TestCase):
|
|||||||
resp = self.client.delete(f'/api/v1/pre-sales-orders/{order_id}/')
|
resp = self.client.delete(f'/api/v1/pre-sales-orders/{order_id}/')
|
||||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertFalse(business_models.PreSalesOrder.objects.filter(id=order_id).exists())
|
self.assertFalse(business_models.PreSalesOrder.objects.filter(id=order_id).exists())
|
||||||
|
|
||||||
|
def test_convert_to_sales_order_rejects_non_strict_out(self):
|
||||||
|
order_id = self._create()
|
||||||
|
resp = self.client.post(f'/api/v1/pre-sales-orders/{order_id}/convert-to-sales/', {}, format='json')
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertIn('仅支持严进严出仓库模式', resp.data.get('error', ''))
|
||||||
|
|
||||||
|
def test_convert_to_sales_order_success(self):
|
||||||
|
resp = self.client.post('/api/v1/pre-sales-orders/', self.strict_payload, format='json')
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
||||||
|
order_id = resp.data['id']
|
||||||
|
|
||||||
|
order = business_models.PreSalesOrder.objects.get(id=order_id)
|
||||||
|
item = order.items.first()
|
||||||
|
|
||||||
|
stock_record = stock_models.StockChangeRecord.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
type=stock_models.StockChangeTypeEnum.ADD,
|
||||||
|
warehouse=self.warehouse_strict_out,
|
||||||
|
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||||
|
created_by=self.user,
|
||||||
|
)
|
||||||
|
stock_detail = stock_models.StockChangeDetail.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
product=self.product,
|
||||||
|
unit=ProductUnitEnum.METER,
|
||||||
|
stock_change_record=stock_record,
|
||||||
|
quantity='10',
|
||||||
|
)
|
||||||
|
allocation_services.create_allocation_record(
|
||||||
|
item=item,
|
||||||
|
stock_ids=[stock_detail.id],
|
||||||
|
quantity='10',
|
||||||
|
unit='米',
|
||||||
|
scanned_by=self.employee,
|
||||||
|
)
|
||||||
|
|
||||||
|
convert_resp = self.client.post(
|
||||||
|
f'/api/v1/pre-sales-orders/{order_id}/convert-to-sales/',
|
||||||
|
{},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
self.assertEqual(convert_resp.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertIn('id', convert_resp.data)
|
||||||
|
self.assertEqual(convert_resp.data['status'], business_models.SalesOrderStatusEnum.PENDING)
|
||||||
|
sales_order = business_models.SalesOrder.objects.get(id=convert_resp.data['id'])
|
||||||
|
self.assertEqual(sales_order.from_pre_sales_order_id, order_id)
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ urlpatterns = [
|
|||||||
path('sales-return-orders/<int:pk>/review/', sales_return_views.SalesReturnOrderReviewView.as_view(), name='sales_return_order_review'),
|
path('sales-return-orders/<int:pk>/review/', sales_return_views.SalesReturnOrderReviewView.as_view(), name='sales_return_order_review'),
|
||||||
path('pre-sales-orders/', pre_sales_views.PreSalesOrderView.as_view(), name='pre_sales_orders'),
|
path('pre-sales-orders/', pre_sales_views.PreSalesOrderView.as_view(), name='pre_sales_orders'),
|
||||||
path('pre-sales-orders/<int:pk>/', pre_sales_views.PreSalesOrderDetailView.as_view(), name='pre_sales_order_detail'),
|
path('pre-sales-orders/<int:pk>/', pre_sales_views.PreSalesOrderDetailView.as_view(), name='pre_sales_order_detail'),
|
||||||
|
path(
|
||||||
|
'pre-sales-orders/<int:pk>/convert-to-sales/',
|
||||||
|
pre_sales_views.PreSalesOrderConvertToSalesOrderView.as_view(),
|
||||||
|
name='pre_sales_order_convert_to_sales',
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
'pre-sales-order-items/<int:item_id>/allocations/',
|
'pre-sales-order-items/<int:item_id>/allocations/',
|
||||||
pre_sales_views.PreSalesOrderItemAllocationView.as_view(),
|
pre_sales_views.PreSalesOrderItemAllocationView.as_view(),
|
||||||
|
|||||||
@@ -279,6 +279,41 @@ class PreSalesOrderDetailView(StockChangeViewMixin, views.APIView):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class PreSalesOrderConvertToSalesOrderView(StockChangeViewMixin, views.APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request, pk: int):
|
||||||
|
if not self.check_employee_permission(request):
|
||||||
|
return self.permission_error_response('无权限访问')
|
||||||
|
|
||||||
|
merchant = request.user.employee.merchant
|
||||||
|
employee = request.user.employee
|
||||||
|
|
||||||
|
try:
|
||||||
|
sales_order = pre_order_services.convert_pre_sales_order_to_sales_order(
|
||||||
|
merchant=merchant,
|
||||||
|
pre_sales_order_id=pk,
|
||||||
|
operator=employee,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
except PermissionError as exc:
|
||||||
|
return Response({'error': str(exc)}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
except business_models.PreSalesOrder.DoesNotExist:
|
||||||
|
return self.not_found_response('预销售单不存在')
|
||||||
|
except ValueError as exc:
|
||||||
|
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'id': sales_order.id,
|
||||||
|
'human_id': sales_order.human_id,
|
||||||
|
'status': sales_order.status,
|
||||||
|
'message': '预销售单已转换为销售单,等待审批',
|
||||||
|
},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PreSalesOrderItemAllocationView(StockChangeViewMixin, views.APIView):
|
class PreSalesOrderItemAllocationView(StockChangeViewMixin, views.APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@@ -336,5 +371,8 @@ class PreSalesOrderItemAllocationDetailView(StockChangeViewMixin, views.APIView)
|
|||||||
except business_models.AllocationRecord.DoesNotExist:
|
except business_models.AllocationRecord.DoesNotExist:
|
||||||
return self.not_found_response('配货记录不存在')
|
return self.not_found_response('配货记录不存在')
|
||||||
|
|
||||||
updated = allocation_services.cancel_allocation_record(record=record)
|
updated = allocation_services.cancel_allocation_record_with_unfreeze(
|
||||||
|
record=record,
|
||||||
|
cancelled_by=request.user,
|
||||||
|
)
|
||||||
return Response(AllocationRecordSerializer(updated).data, status=status.HTTP_200_OK)
|
return Response(AllocationRecordSerializer(updated).data, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from decimal import Decimal
|
|||||||
from typing import Iterable, List
|
from typing import Iterable, List
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from stock import models as stock_models
|
from stock import models as stock_models
|
||||||
from . import models
|
from . import models
|
||||||
@@ -133,8 +134,28 @@ def create_allocation_record(
|
|||||||
|
|
||||||
|
|
||||||
def cancel_allocation_record(*, record: models.AllocationRecord) -> models.AllocationRecord:
|
def cancel_allocation_record(*, record: models.AllocationRecord) -> models.AllocationRecord:
|
||||||
if record.status == models.AllocationRecordStatusEnum.CANCELLED:
|
return cancel_allocation_record_with_unfreeze(record=record)
|
||||||
return record
|
|
||||||
|
|
||||||
|
def cancel_allocation_record_with_unfreeze(
|
||||||
|
*,
|
||||||
|
record: models.AllocationRecord,
|
||||||
|
cancelled_by=None,
|
||||||
|
reason: str | None = None,
|
||||||
|
) -> models.AllocationRecord:
|
||||||
|
with transaction.atomic():
|
||||||
|
if record.status != models.AllocationRecordStatusEnum.CANCELLED:
|
||||||
record.status = models.AllocationRecordStatusEnum.CANCELLED
|
record.status = models.AllocationRecordStatusEnum.CANCELLED
|
||||||
record.save(update_fields=['status', 'updated_at'])
|
record.save(update_fields=['status', 'updated_at'])
|
||||||
|
|
||||||
|
stock_models.StockFreeze.objects.filter(
|
||||||
|
frozen_with=record.id,
|
||||||
|
status=stock_models.StockFreezeStatusEnum.FROZEN,
|
||||||
|
).update(
|
||||||
|
status=stock_models.StockFreezeStatusEnum.CANCELLED,
|
||||||
|
cancelled_by=cancelled_by,
|
||||||
|
cancelled_at=timezone.now(),
|
||||||
|
reason=reason,
|
||||||
|
)
|
||||||
|
|
||||||
return record
|
return record
|
||||||
|
|||||||
16
business/migrations/0026_salesorder_from_pre_sales_order.py
Normal file
16
business/migrations/0026_salesorder_from_pre_sales_order.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('business', '0025_allocationrecord'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salesorder',
|
||||||
|
name='from_pre_sales_order_id',
|
||||||
|
field=models.BigIntegerField(blank=True, null=True, verbose_name='来源预销售单ID'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -273,6 +273,11 @@ class SalesOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCounterpa
|
|||||||
verbose_name='状态',
|
verbose_name='状态',
|
||||||
)
|
)
|
||||||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||||||
|
from_pre_sales_order_id = models.BigIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='来源预销售单ID',
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
order_id = self.human_id or str(self.id)
|
order_id = self.human_id or str(self.id)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from django.utils import timezone
|
|||||||
from basic_info import models as basic_info_models
|
from basic_info import models as basic_info_models
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
from . import services as business_services
|
||||||
|
|
||||||
|
|
||||||
def render_pre_sales_order_created_markdown(
|
def render_pre_sales_order_created_markdown(
|
||||||
@@ -398,6 +399,73 @@ def delete_pre_sales_order(*, pre_sales_order: models.PreSalesOrder) -> None:
|
|||||||
pre_sales_order.delete()
|
pre_sales_order.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def convert_pre_sales_order_to_sales_order(
|
||||||
|
*,
|
||||||
|
merchant: basic_info_models.Merchant,
|
||||||
|
pre_sales_order_id: int,
|
||||||
|
operator: basic_info_models.Employee,
|
||||||
|
created_by=None,
|
||||||
|
order_date=None,
|
||||||
|
) -> models.SalesOrder:
|
||||||
|
order = models.PreSalesOrder.objects.select_related(
|
||||||
|
'customer',
|
||||||
|
'warehouse',
|
||||||
|
'operator',
|
||||||
|
'created_by',
|
||||||
|
).prefetch_related('items', 'items__allocation_records').get(
|
||||||
|
id=pre_sales_order_id,
|
||||||
|
merchant=merchant,
|
||||||
|
)
|
||||||
|
|
||||||
|
warehouse = order.warehouse
|
||||||
|
if warehouse.mode != basic_info_models.WareHouseModeEnum.RESTRICT_IN_OUT:
|
||||||
|
raise PermissionError('仅支持严进严出仓库模式')
|
||||||
|
|
||||||
|
items_payload: List[Dict[str, Any]] = []
|
||||||
|
for item in order.items.all():
|
||||||
|
if not item.product_id:
|
||||||
|
raise ValueError(f'明细 {item.id} 缺少 product_id')
|
||||||
|
if item.quantity is None or str(item.quantity) == '':
|
||||||
|
raise ValueError(f'明细 {item.id} 缺少 quantity')
|
||||||
|
|
||||||
|
consume_ids: List[int] = []
|
||||||
|
for record in item.allocation_records.all():
|
||||||
|
if record.status != models.AllocationRecordStatusEnum.ACTIVE:
|
||||||
|
continue
|
||||||
|
consume_ids.extend(record.stock_id_list)
|
||||||
|
|
||||||
|
if not consume_ids:
|
||||||
|
raise ValueError(f'明细 {item.id} 缺少配货库存明细')
|
||||||
|
|
||||||
|
items_payload.append(
|
||||||
|
{
|
||||||
|
'product_id': item.product_id,
|
||||||
|
'price': '0',
|
||||||
|
'quantity': str(item.quantity),
|
||||||
|
'unit': item.unit or None,
|
||||||
|
'color': item.color,
|
||||||
|
'spec': item.spec,
|
||||||
|
'remarks': item.remarks,
|
||||||
|
'order_quantity': item.order_quantity,
|
||||||
|
'consume_detail_ids': consume_ids,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved_order_date = order_date or (order.created_at.date() if order.created_at else timezone.localdate())
|
||||||
|
|
||||||
|
return business_services.create_sales_order(
|
||||||
|
merchant=merchant,
|
||||||
|
customer=order.customer,
|
||||||
|
order_date=resolved_order_date,
|
||||||
|
warehouse=warehouse,
|
||||||
|
operator=operator,
|
||||||
|
items=items_payload,
|
||||||
|
remarks=order.remarks or '',
|
||||||
|
created_by=created_by,
|
||||||
|
from_pre_sales_order_id=order.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_pre_purchase_orders(*, merchant: basic_info_models.Merchant):
|
def list_pre_purchase_orders(*, merchant: basic_info_models.Merchant):
|
||||||
return models.PrePurchaseOrder.objects.filter(merchant=merchant).select_related(
|
return models.PrePurchaseOrder.objects.filter(merchant=merchant).select_related(
|
||||||
'merchant', 'supplier', 'warehouse', 'created_by'
|
'merchant', 'supplier', 'warehouse', 'created_by'
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ def create_sales_order(
|
|||||||
items: List[Dict[str, Any]],
|
items: List[Dict[str, Any]],
|
||||||
remarks: str | None = '',
|
remarks: str | None = '',
|
||||||
created_by=None,
|
created_by=None,
|
||||||
|
from_pre_sales_order_id: int | None = None,
|
||||||
) -> models.SalesOrder:
|
) -> models.SalesOrder:
|
||||||
"""
|
"""
|
||||||
创建销售订单,后续审批通过后会触发出库任务。
|
创建销售订单,后续审批通过后会触发出库任务。
|
||||||
@@ -334,6 +335,7 @@ def create_sales_order(
|
|||||||
operator=operator,
|
operator=operator,
|
||||||
warehouse=warehouse,
|
warehouse=warehouse,
|
||||||
remarks=remarks,
|
remarks=remarks,
|
||||||
|
from_pre_sales_order_id=from_pre_sales_order_id,
|
||||||
)
|
)
|
||||||
bulk_objects = [
|
bulk_objects = [
|
||||||
models.SalesOrderItem(
|
models.SalesOrderItem(
|
||||||
|
|||||||
@@ -181,5 +181,11 @@ class AllocationServicesTestCase(TestCase):
|
|||||||
unit='米',
|
unit='米',
|
||||||
scanned_by=self.employee,
|
scanned_by=self.employee,
|
||||||
)
|
)
|
||||||
updated = allocation_services.cancel_allocation_record(record=record)
|
updated = allocation_services.cancel_allocation_record_with_unfreeze(
|
||||||
|
record=record,
|
||||||
|
cancelled_by=self.user,
|
||||||
|
reason='测试撤销',
|
||||||
|
)
|
||||||
self.assertEqual(updated.status, business_models.AllocationRecordStatusEnum.CANCELLED)
|
self.assertEqual(updated.status, business_models.AllocationRecordStatusEnum.CANCELLED)
|
||||||
|
freeze = stock_models.StockFreeze.objects.get(stock_detail=self.stock_detail)
|
||||||
|
self.assertEqual(freeze.status, stock_models.StockFreezeStatusEnum.CANCELLED)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from django.db import transaction
|
|||||||
from basic_info import models as basic_models
|
from basic_info import models as basic_models
|
||||||
from business import models as business_models
|
from business import models as business_models
|
||||||
from business import pre_order_services
|
from business import pre_order_services
|
||||||
|
from business import allocation_services
|
||||||
|
from stock import models as stock_models
|
||||||
|
|
||||||
from .fixtures import create_sales_fixtures
|
from .fixtures import create_sales_fixtures
|
||||||
|
|
||||||
@@ -123,6 +125,74 @@ class PreSalesOrderServiceTestCase(TestCase):
|
|||||||
pre_order_services.delete_pre_sales_order(pre_sales_order=pre_sales_order)
|
pre_order_services.delete_pre_sales_order(pre_sales_order=pre_sales_order)
|
||||||
self.assertFalse(business_models.PreSalesOrder.objects.filter(id=order_id).exists())
|
self.assertFalse(business_models.PreSalesOrder.objects.filter(id=order_id).exists())
|
||||||
|
|
||||||
|
def test_convert_pre_sales_order_to_sales_order_rejects_non_strict_out(self):
|
||||||
|
pre_sales_order = pre_order_services.create_pre_sales_order(
|
||||||
|
merchant=self.merchant,
|
||||||
|
customer_id=self.customer.id,
|
||||||
|
warehouse_id=self.warehouse_relaxed.id,
|
||||||
|
created_by=self.user,
|
||||||
|
operator=self.operator,
|
||||||
|
items=[{'product_id': self.product.id, 'quantity': '10', 'unit': '米'}],
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(PermissionError):
|
||||||
|
pre_order_services.convert_pre_sales_order_to_sales_order(
|
||||||
|
merchant=self.merchant,
|
||||||
|
pre_sales_order_id=pre_sales_order.id,
|
||||||
|
operator=self.operator,
|
||||||
|
created_by=self.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_convert_pre_sales_order_to_sales_order_success(self):
|
||||||
|
pre_sales_order = pre_order_services.create_pre_sales_order(
|
||||||
|
merchant=self.merchant,
|
||||||
|
customer_id=self.customer.id,
|
||||||
|
warehouse_id=self.warehouse_strict_out.id,
|
||||||
|
created_by=self.user,
|
||||||
|
operator=self.operator,
|
||||||
|
items=[{'product_id': self.product.id, 'quantity': '10', 'unit': '米'}],
|
||||||
|
)
|
||||||
|
item = pre_sales_order.items.first()
|
||||||
|
|
||||||
|
stock_record = stock_models.StockChangeRecord.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
type=stock_models.StockChangeTypeEnum.ADD,
|
||||||
|
warehouse=self.warehouse_strict_out,
|
||||||
|
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||||
|
created_by=self.user,
|
||||||
|
)
|
||||||
|
stock_detail = stock_models.StockChangeDetail.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
product=self.product,
|
||||||
|
unit=basic_models.ProductUnitEnum.METER,
|
||||||
|
stock_change_record=stock_record,
|
||||||
|
quantity='10',
|
||||||
|
)
|
||||||
|
|
||||||
|
allocation_services.create_allocation_record(
|
||||||
|
item=item,
|
||||||
|
stock_ids=[stock_detail.id],
|
||||||
|
quantity='10',
|
||||||
|
unit='米',
|
||||||
|
scanned_by=self.operator,
|
||||||
|
)
|
||||||
|
|
||||||
|
sales_order = pre_order_services.convert_pre_sales_order_to_sales_order(
|
||||||
|
merchant=self.merchant,
|
||||||
|
pre_sales_order_id=pre_sales_order.id,
|
||||||
|
operator=self.operator,
|
||||||
|
created_by=self.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(sales_order.customer_id, self.customer.id)
|
||||||
|
self.assertEqual(sales_order.warehouse_id, self.warehouse_strict_out.id)
|
||||||
|
self.assertEqual(sales_order.from_pre_sales_order_id, pre_sales_order.id)
|
||||||
|
self.assertEqual(sales_order.status, business_models.SalesOrderStatusEnum.PENDING)
|
||||||
|
self.assertEqual(sales_order.items.count(), 1)
|
||||||
|
sales_item = sales_order.items.first()
|
||||||
|
self.assertEqual(sales_item.product_id, self.product.id)
|
||||||
|
self.assertEqual(sales_item.consume_detail_ids, str(stock_detail.id))
|
||||||
|
|
||||||
|
|
||||||
class PreSalesOrderSignalTestCase(TransactionTestCase):
|
class PreSalesOrderSignalTestCase(TransactionTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -135,6 +135,73 @@
|
|||||||
- `DELETE /api/v1/pre-sales-orders/{id}/`
|
- `DELETE /api/v1/pre-sales-orders/{id}/`
|
||||||
- 响应:`204 NO CONTENT`
|
- 响应:`204 NO CONTENT`
|
||||||
|
|
||||||
|
## 6) 转换为销售单(仅严进严出仓库)
|
||||||
|
|
||||||
|
- `POST /api/v1/pre-sales-orders/{id}/convert-to-sales/`
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- 仅支持仓库模式为 **严进严出(RESTRICT_IN_OUT)** 的预销售单
|
||||||
|
- 若不是严进严出,返回 `403`
|
||||||
|
- 需要已存在有效配货记录(用于生成销售单明细的 `consume_detail_ids`)
|
||||||
|
|
||||||
|
响应:`201 CREATED`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1001,
|
||||||
|
"human_id": "XS20260202000001",
|
||||||
|
"status": 1,
|
||||||
|
"message": "预销售单已转换为销售单,等待审批"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
错误示例(403):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "仅支持严进严出仓库模式"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
错误示例(400):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "明细 123 缺少配货库存明细"
|
||||||
|
}
|
||||||
|
|
||||||
|
## 7) 配货记录(Allocations)
|
||||||
|
|
||||||
|
### 7.1 创建配货记录
|
||||||
|
|
||||||
|
- `POST /api/v1/pre-sales-order-items/{item_id}/allocations/`
|
||||||
|
|
||||||
|
请求体参数(JSON):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"stock_ids": [1, 2, 3],
|
||||||
|
"quantity": "10.00",
|
||||||
|
"unit": "KG",
|
||||||
|
"remarks": "备注信息"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:`201 CREATED`,返回配货记录详情。
|
||||||
|
|
||||||
|
### 7.2 撤销配货记录(含解冻库存)
|
||||||
|
|
||||||
|
- `DELETE /api/v1/pre-sales-order-items/{item_id}/allocations/{pk}/`
|
||||||
|
|
||||||
|
行为说明:
|
||||||
|
|
||||||
|
- 将配货记录状态置为撤销(status=2)
|
||||||
|
- **同时解冻**该配货记录创建时冻结的库存(StockFreeze.status=取消)
|
||||||
|
|
||||||
|
响应:`200 OK`,返回配货记录详情。
|
||||||
|
```
|
||||||
|
|
||||||
## 预销售单字段说明(响应)
|
## 预销售单字段说明(响应)
|
||||||
|
|
||||||
预销售单对象字段(`results[]` 与详情一致):
|
预销售单对象字段(`results[]` 与详情一致):
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ This module is intended for reusable business logic that may be called by:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from api_v1.utils.wecom_webhook import send_wecom_webhook_message
|
from api_v1.utils.wecom_webhook import send_wecom_webhook_message
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
from printing.models import PrintingJob
|
from printing.models import PrintingJob
|
||||||
|
|
||||||
|
|
||||||
@@ -188,6 +191,7 @@ def send_printing_job_latest_state_wecom(
|
|||||||
"""
|
"""
|
||||||
Reusable entrypoint for command / future API.
|
Reusable entrypoint for command / future API.
|
||||||
"""
|
"""
|
||||||
|
from printing.models import PrintingJob
|
||||||
job = (
|
job = (
|
||||||
PrintingJob.objects.select_related("printing_order", "business_object")
|
PrintingJob.objects.select_related("printing_order", "business_object")
|
||||||
.filter(id=int(printing_job_id))
|
.filter(id=int(printing_job_id))
|
||||||
|
|||||||
Reference in New Issue
Block a user