1
0
forked from erp-dev/erp

feat: allocation record for pre sales order

This commit is contained in:
2026-02-01 22:57:16 +08:00
parent a633a617d5
commit ac065e115e
14 changed files with 1955 additions and 44 deletions

View 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