forked from erp-dev/erp
feat: support pre_sales_order convert to sales_order api
This commit is contained in:
@@ -4,6 +4,7 @@ from decimal import Decimal
|
||||
from typing import Iterable, List
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from stock import models as stock_models
|
||||
from . import models
|
||||
@@ -133,8 +134,28 @@ def create_allocation_record(
|
||||
|
||||
|
||||
def cancel_allocation_record(*, record: models.AllocationRecord) -> models.AllocationRecord:
|
||||
if record.status == models.AllocationRecordStatusEnum.CANCELLED:
|
||||
return record
|
||||
record.status = models.AllocationRecordStatusEnum.CANCELLED
|
||||
record.save(update_fields=['status', 'updated_at'])
|
||||
return cancel_allocation_record_with_unfreeze(record=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.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
|
||||
|
||||
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='状态',
|
||||
)
|
||||
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):
|
||||
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 . import models
|
||||
from . import services as business_services
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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):
|
||||
return models.PrePurchaseOrder.objects.filter(merchant=merchant).select_related(
|
||||
'merchant', 'supplier', 'warehouse', 'created_by'
|
||||
|
||||
@@ -310,6 +310,7 @@ def create_sales_order(
|
||||
items: List[Dict[str, Any]],
|
||||
remarks: str | None = '',
|
||||
created_by=None,
|
||||
from_pre_sales_order_id: int | None = None,
|
||||
) -> models.SalesOrder:
|
||||
"""
|
||||
创建销售订单,后续审批通过后会触发出库任务。
|
||||
@@ -334,6 +335,7 @@ def create_sales_order(
|
||||
operator=operator,
|
||||
warehouse=warehouse,
|
||||
remarks=remarks,
|
||||
from_pre_sales_order_id=from_pre_sales_order_id,
|
||||
)
|
||||
bulk_objects = [
|
||||
models.SalesOrderItem(
|
||||
|
||||
@@ -181,5 +181,11 @@ class AllocationServicesTestCase(TestCase):
|
||||
unit='米',
|
||||
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)
|
||||
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 business import models as business_models
|
||||
from business import pre_order_services
|
||||
from business import allocation_services
|
||||
from stock import models as stock_models
|
||||
|
||||
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)
|
||||
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):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user