forked from erp-dev/erp
162 lines
5.2 KiB
Python
162 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
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
|
|
|
|
|
|
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:
|
|
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
|