forked from erp-dev/erp
feat: allocation record for pre sales order
This commit is contained in:
140
business/allocation_services.py
Normal file
140
business/allocation_services.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Iterable, List
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from stock import models as stock_models
|
||||
from . import models
|
||||
|
||||
|
||||
def _normalize_stock_ids(stock_ids: Iterable[int | str]) -> List[int]:
|
||||
if not stock_ids:
|
||||
raise ValueError('stock_ids 不能为空')
|
||||
normalized: List[int] = []
|
||||
for raw in stock_ids:
|
||||
try:
|
||||
value = int(raw)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('stock_ids 必须为整数数组') from exc
|
||||
if value <= 0:
|
||||
raise ValueError('stock_ids 必须为正整数数组')
|
||||
normalized.append(value)
|
||||
return normalized
|
||||
|
||||
|
||||
def _serialize_stock_ids(stock_ids: Iterable[int]) -> str:
|
||||
return ','.join(str(value) for value in stock_ids)
|
||||
|
||||
|
||||
def validate_stock_details(*, merchant, stock_ids: Iterable[int | str]) -> List[stock_models.StockChangeDetail]:
|
||||
normalized_ids = _normalize_stock_ids(stock_ids)
|
||||
|
||||
details_by_id = stock_models.StockChangeDetail.objects.filter(
|
||||
id__in=normalized_ids,
|
||||
merchant=merchant,
|
||||
).in_bulk(field_name='id')
|
||||
|
||||
missing_ids = [value for value in normalized_ids if value not in details_by_id]
|
||||
if missing_ids:
|
||||
raise ValueError(f'库存明细不存在: {missing_ids}')
|
||||
|
||||
consumed_ids = [
|
||||
detail.id
|
||||
for detail in details_by_id.values()
|
||||
if getattr(detail, 'is_consumed', False)
|
||||
]
|
||||
if consumed_ids:
|
||||
raise ValueError(f'库存明细已被消费: {consumed_ids}')
|
||||
|
||||
frozen_ids = list(
|
||||
stock_models.StockFreeze.objects.filter(
|
||||
stock_detail_id__in=normalized_ids,
|
||||
status=stock_models.StockFreezeStatusEnum.FROZEN,
|
||||
).values_list('stock_detail_id', flat=True)
|
||||
)
|
||||
if frozen_ids:
|
||||
raise ValueError(f'库存明细已被冻结: {list(frozen_ids)}')
|
||||
|
||||
return [details_by_id[value] for value in normalized_ids]
|
||||
|
||||
|
||||
def create_allocation_record(
|
||||
*,
|
||||
item: models.PreSalesOrderItem,
|
||||
stock_ids: Iterable[int | str],
|
||||
quantity: Decimal | int | str,
|
||||
unit: str | None,
|
||||
scanned_by,
|
||||
remarks: str | None = None,
|
||||
) -> models.AllocationRecord:
|
||||
if quantity is None or str(quantity) == '':
|
||||
raise ValueError('quantity 不能为空')
|
||||
try:
|
||||
quantity_decimal = Decimal(str(quantity))
|
||||
except Exception as exc:
|
||||
raise ValueError('quantity 格式不正确') from exc
|
||||
if quantity_decimal <= 0:
|
||||
raise ValueError('quantity 必须大于 0')
|
||||
|
||||
if not unit:
|
||||
unit = getattr(item, 'unit', None)
|
||||
unit = (unit or '').strip()
|
||||
if not unit:
|
||||
raise ValueError('unit 不能为空')
|
||||
|
||||
order = getattr(item, 'pre_sales_order', None)
|
||||
if order is None:
|
||||
raise ValueError('明细缺少所属预销售单')
|
||||
|
||||
normalized_ids = _normalize_stock_ids(stock_ids)
|
||||
details = validate_stock_details(merchant=order.merchant, stock_ids=normalized_ids)
|
||||
|
||||
item_product_id = getattr(item, 'product_id', None)
|
||||
if item_product_id:
|
||||
mismatch_ids = list(
|
||||
stock_models.StockChangeDetail.objects.filter(
|
||||
id__in=normalized_ids,
|
||||
).exclude(product_id=item_product_id).values_list('id', flat=True)
|
||||
)
|
||||
if mismatch_ids:
|
||||
raise ValueError(f'库存明细产品不匹配: {mismatch_ids}')
|
||||
|
||||
with transaction.atomic():
|
||||
record = models.AllocationRecord.objects.create(
|
||||
pre_sales_order_item=item,
|
||||
stock_ids=_serialize_stock_ids(normalized_ids),
|
||||
quantity=quantity_decimal,
|
||||
unit=unit,
|
||||
status=models.AllocationRecordStatusEnum.ACTIVE,
|
||||
scanned_by=scanned_by,
|
||||
remarks=remarks,
|
||||
)
|
||||
|
||||
stock_models.StockFreeze.objects.bulk_create(
|
||||
[
|
||||
stock_models.StockFreeze(
|
||||
merchant=order.merchant,
|
||||
product=detail.product,
|
||||
warehouse=order.warehouse,
|
||||
stock_detail=detail,
|
||||
quantity=detail.quantity,
|
||||
unit=detail.unit,
|
||||
status=stock_models.StockFreezeStatusEnum.FROZEN,
|
||||
frozen_by=getattr(scanned_by, 'sys_user', None),
|
||||
frozen_with=record.id,
|
||||
)
|
||||
for detail in details
|
||||
]
|
||||
)
|
||||
|
||||
return 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 record
|
||||
Reference in New Issue
Block a user