forked from erp-dev/erp
631 lines
23 KiB
Python
631 lines
23 KiB
Python
from . import models
|
||
from django.db import transaction
|
||
from django.db.models import Sum
|
||
from django.utils import timezone
|
||
from sse.services import push_simple_message_with_object_id
|
||
from basic_info import models as basic_models
|
||
from decimal import Decimal, InvalidOperation
|
||
import logging
|
||
from typing import List, Dict, Any, Tuple
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _raise_unimplemented_mode():
|
||
raise ValueError('仓库出入库模式为【严进严出】,该模式暂未支持当前操作')
|
||
|
||
|
||
def _ensure_strict_mode(warehouse: basic_models.WareHouse, *, allow_restrict_in_out: bool = False):
|
||
if warehouse.mode == basic_models.WareHouseModeEnum.RESTRICT_IN:
|
||
return
|
||
if warehouse.mode == basic_models.WareHouseModeEnum.RESTRICT_IN_OUT:
|
||
if allow_restrict_in_out:
|
||
return
|
||
_raise_unimplemented_mode()
|
||
raise ValueError('仓库出入库模式为【宽进宽出】,请使用宽松模式接口创建出入库记录')
|
||
|
||
|
||
def _ensure_relaxed_mode(warehouse: basic_models.WareHouse):
|
||
if warehouse.mode == basic_models.WareHouseModeEnum.UNRESTRICTED:
|
||
return
|
||
if warehouse.mode == basic_models.WareHouseModeEnum.RESTRICT_IN_OUT:
|
||
_raise_unimplemented_mode()
|
||
raise ValueError('仓库出入库模式为【严进宽出】,请使用严谨模式接口创建出入库记录')
|
||
|
||
|
||
def _create_details_from_quantities(
|
||
*,
|
||
stock_change_record: models.StockChangeRecord,
|
||
merchant: basic_models.Merchant,
|
||
products: List[Dict[str, Any]],
|
||
) -> Tuple[List[models.StockChangeDetail], int]:
|
||
created_details: List[models.StockChangeDetail] = []
|
||
created_count = 0
|
||
|
||
for product_data in products:
|
||
product_id = product_data.get('product')
|
||
quantities = product_data.get('quantity') or []
|
||
|
||
if not product_id or not quantities:
|
||
raise ValueError('产品数据不完整,缺少 product 或 quantity')
|
||
|
||
try:
|
||
product = basic_models.Product.objects.get(id=product_id)
|
||
except basic_models.Product.DoesNotExist:
|
||
raise ValueError(f'产品ID {product_id} 不存在')
|
||
|
||
if product.merchant_id != merchant.id:
|
||
raise ValueError(f'产品ID {product_id} 不属于当前商户')
|
||
|
||
for quantity in quantities:
|
||
detail = models.StockChangeDetail.objects.create(
|
||
stock_change_record=stock_change_record,
|
||
product=product,
|
||
quantity=quantity,
|
||
merchant=merchant,
|
||
unit=product.unit,
|
||
)
|
||
created_details.append(detail)
|
||
created_count += 1
|
||
|
||
return created_details, created_count
|
||
|
||
|
||
def _create_consumption_details(
|
||
*,
|
||
stock_change_record: models.StockChangeRecord,
|
||
merchant: basic_models.Merchant,
|
||
products: List[Dict[str, Any]],
|
||
) -> List[models.StockChangeDetail]:
|
||
created_details: List[models.StockChangeDetail] = []
|
||
|
||
for product_data in products:
|
||
product_id = product_data.get('product')
|
||
consume_ids = product_data.get('consume_with') or []
|
||
|
||
if not product_id or not consume_ids:
|
||
raise ValueError('严进严出出库必须提供 consume_with 列表')
|
||
|
||
try:
|
||
product = basic_models.Product.objects.get(id=product_id)
|
||
except basic_models.Product.DoesNotExist:
|
||
raise ValueError(f'产品ID {product_id} 不存在')
|
||
|
||
if product.merchant_id != merchant.id:
|
||
raise ValueError(f'产品ID {product_id} 不属于当前商户')
|
||
|
||
inbound_details = models.StockChangeDetail.objects.select_related('stock_change_record').filter(
|
||
id__in=consume_ids
|
||
)
|
||
if inbound_details.count() != len(consume_ids):
|
||
raise ValueError('部分入库明细不存在')
|
||
|
||
inbound_map = {detail.id: detail for detail in inbound_details}
|
||
|
||
for detail_id in consume_ids:
|
||
inbound_detail = inbound_map.get(detail_id)
|
||
if inbound_detail is None:
|
||
raise ValueError('部分入库明细不存在')
|
||
if inbound_detail.stock_change_record.warehouse_id != stock_change_record.warehouse_id:
|
||
raise ValueError('入库明细与当前仓库不一致')
|
||
if inbound_detail.stock_change_record.merchant_id != merchant.id:
|
||
raise ValueError('入库明细与当前商户不一致')
|
||
if not inbound_detail.stock_change_record.is_incoming:
|
||
raise ValueError('只能引用入库明细进行出库')
|
||
if inbound_detail.is_consumed:
|
||
raise ValueError(f'入库明细 {inbound_detail.id} 已被消耗')
|
||
if inbound_detail.product_id != product_id:
|
||
raise ValueError('入库明细与当前产品不匹配')
|
||
|
||
detail = models.StockChangeDetail.objects.create(
|
||
stock_change_record=stock_change_record,
|
||
product=product,
|
||
quantity=inbound_detail.quantity,
|
||
merchant=merchant,
|
||
unit=inbound_detail.unit,
|
||
consume_with=inbound_detail,
|
||
)
|
||
inbound_detail.is_consumed = True
|
||
inbound_detail.save(update_fields=['is_consumed'])
|
||
created_details.append(detail)
|
||
|
||
return created_details
|
||
|
||
|
||
def create_stock_change_record_with_details(
|
||
*,
|
||
merchant: basic_models.Merchant,
|
||
created_by,
|
||
type: int,
|
||
warehouse_id: int,
|
||
source_type: int,
|
||
source_id: int | None = None,
|
||
products: List[Dict[str, Any]] | None = None,
|
||
) -> Tuple[models.StockChangeRecord, List[models.StockChangeDetail], int]:
|
||
"""
|
||
创建库存变动记录及其明细
|
||
|
||
参数:
|
||
merchant: 当前商户
|
||
created_by: 操作人(可为空)
|
||
type: 出入库类型
|
||
warehouse_id: 仓库ID
|
||
source_type: 来源类型
|
||
source_id: 来源单据ID,可选
|
||
products: 产品及数量列表
|
||
"""
|
||
|
||
if not merchant:
|
||
raise ValueError('必须提供商户信息')
|
||
if not warehouse_id:
|
||
raise ValueError('必须提供仓库ID')
|
||
if not products:
|
||
raise ValueError('产品列表不能为空')
|
||
|
||
try:
|
||
warehouse = basic_models.WareHouse.objects.get(id=warehouse_id)
|
||
except basic_models.WareHouse.DoesNotExist:
|
||
raise ValueError(f'仓库ID {warehouse_id} 不存在')
|
||
|
||
if warehouse.merchant_id != merchant.id:
|
||
raise ValueError('仓库不属于当前商户')
|
||
|
||
allow_restrict_in_out = (
|
||
warehouse.mode == basic_models.WareHouseModeEnum.RESTRICT_IN_OUT
|
||
and type == models.StockChangeTypeEnum.ADD
|
||
)
|
||
is_strict_outgoing = (
|
||
warehouse.mode == basic_models.WareHouseModeEnum.RESTRICT_IN_OUT
|
||
and type == models.StockChangeTypeEnum.REMOVE
|
||
)
|
||
if not is_strict_outgoing:
|
||
_ensure_strict_mode(warehouse, allow_restrict_in_out=allow_restrict_in_out)
|
||
|
||
created_details: List[models.StockChangeDetail] = []
|
||
created_count = 0
|
||
|
||
with transaction.atomic():
|
||
stock_change_record = models.StockChangeRecord.objects.create(
|
||
type=type,
|
||
warehouse=warehouse,
|
||
source_type=source_type,
|
||
source_id=source_id,
|
||
merchant=merchant,
|
||
created_by=created_by,
|
||
)
|
||
logger.info('创建新库存变动记录 ID: %s', stock_change_record.id)
|
||
|
||
if is_strict_outgoing:
|
||
created_details = _create_consumption_details(
|
||
stock_change_record=stock_change_record,
|
||
merchant=merchant,
|
||
products=products,
|
||
)
|
||
created_count = len(created_details)
|
||
else:
|
||
created_details, created_count = _create_details_from_quantities(
|
||
stock_change_record=stock_change_record,
|
||
merchant=merchant,
|
||
products=products,
|
||
)
|
||
|
||
if warehouse.merchant.auto_complete_stock_change:
|
||
make_stock_change_completed(stock_change_record)
|
||
logger.info('自动确认库存变动记录 ID: %s', stock_change_record.id)
|
||
|
||
return stock_change_record, created_details, created_count
|
||
|
||
|
||
def _to_decimal(value, field_name: str) -> Decimal:
|
||
try:
|
||
return Decimal(str(value))
|
||
except (InvalidOperation, TypeError):
|
||
raise ValueError(f'{field_name} 必须是合法的数值')
|
||
|
||
|
||
def _split_quantities(total: Decimal, unit_size: Decimal) -> List[Decimal]:
|
||
if total <= 0:
|
||
raise ValueError('quantity.value 必须大于 0')
|
||
if unit_size <= 0:
|
||
raise ValueError('quantity.unit_count 必须大于 0')
|
||
|
||
quantities: List[Decimal] = []
|
||
num_full = int(total // unit_size)
|
||
remainder = total % unit_size
|
||
|
||
if num_full == 0:
|
||
quantities.append(total)
|
||
else:
|
||
quantities.extend([unit_size] * num_full)
|
||
if remainder > 0:
|
||
quantities.append(remainder)
|
||
|
||
return quantities
|
||
|
||
|
||
def create_stock_change_record_relaxed(
|
||
*,
|
||
merchant: basic_models.Merchant,
|
||
created_by,
|
||
type: int,
|
||
warehouse_id: int,
|
||
source_type: int,
|
||
source_id: int | None = None,
|
||
products: List[Dict[str, Any]] | None = None,
|
||
) -> Tuple[models.StockChangeRecord, List[models.StockChangeDetail], int]:
|
||
"""
|
||
宽松模式:根据总数量和单条数量自动拆分明细
|
||
"""
|
||
|
||
if not merchant:
|
||
raise ValueError('必须提供商户信息')
|
||
if not warehouse_id:
|
||
raise ValueError('必须提供仓库ID')
|
||
if not products:
|
||
raise ValueError('产品列表不能为空')
|
||
|
||
try:
|
||
warehouse = basic_models.WareHouse.objects.get(id=warehouse_id)
|
||
except basic_models.WareHouse.DoesNotExist:
|
||
raise ValueError(f'仓库ID {warehouse_id} 不存在')
|
||
|
||
if warehouse.merchant_id != merchant.id:
|
||
raise ValueError('仓库不属于当前商户')
|
||
_ensure_relaxed_mode(warehouse)
|
||
|
||
created_details: List[models.StockChangeDetail] = []
|
||
|
||
with transaction.atomic():
|
||
stock_change_record = models.StockChangeRecord.objects.create(
|
||
type=type,
|
||
warehouse=warehouse,
|
||
source_type=source_type,
|
||
source_id=source_id,
|
||
merchant=merchant,
|
||
created_by=created_by,
|
||
)
|
||
logger.info('创建宽松模式库存变动记录 ID: %s', stock_change_record.id)
|
||
|
||
for product_data in products:
|
||
product_id = product_data.get('product')
|
||
quantity_data = product_data.get('quantity') or {}
|
||
|
||
if not product_id:
|
||
raise ValueError('产品ID不能为空')
|
||
|
||
total_value = quantity_data.get('value')
|
||
unit_size = quantity_data.get('unit_count', 1)
|
||
|
||
total_value = _to_decimal(total_value, 'quantity.value')
|
||
unit_size = _to_decimal(unit_size, 'quantity.unit_count')
|
||
|
||
try:
|
||
product = basic_models.Product.objects.get(id=product_id)
|
||
except basic_models.Product.DoesNotExist:
|
||
raise ValueError(f'产品ID {product_id} 不存在')
|
||
|
||
if product.merchant_id != merchant.id:
|
||
raise ValueError(f'产品ID {product_id} 不属于当前商户')
|
||
|
||
quantities = _split_quantities(total_value, unit_size)
|
||
|
||
for quantity in quantities:
|
||
detail = models.StockChangeDetail.objects.create(
|
||
stock_change_record=stock_change_record,
|
||
product=product,
|
||
quantity=quantity,
|
||
merchant=merchant,
|
||
unit=product.unit,
|
||
)
|
||
created_details.append(detail)
|
||
|
||
if warehouse.merchant.auto_complete_stock_change:
|
||
make_stock_change_completed(stock_change_record)
|
||
logger.info('自动确认宽松模式库存变动记录 ID: %s', stock_change_record.id)
|
||
|
||
return stock_change_record, created_details, len(created_details)
|
||
|
||
|
||
class StockFlowService:
|
||
"""
|
||
统一出入库服务,屏蔽仓库模式差异。
|
||
|
||
items 结构支持以下字段:
|
||
- product_id: 产品ID,必填
|
||
- quantities: List[Decimal | str],用于严谨/严进严出入库
|
||
- value: Decimal | str,总数量,用于宽进宽出
|
||
- num_of_rolls: Decimal | int,每条数量,用于宽进宽出
|
||
- consume_detail_ids: List[int],严进严出出库消耗的入库明细ID
|
||
"""
|
||
|
||
def __init__(self, *, merchant: basic_models.Merchant, created_by):
|
||
if merchant is None:
|
||
raise ValueError('StockFlowService 初始化必须提供商户')
|
||
self.merchant = merchant
|
||
self.created_by = created_by
|
||
|
||
def stock_in(
|
||
self,
|
||
*,
|
||
warehouse_id: int,
|
||
source_type: int,
|
||
source_id: int | None = None,
|
||
items: List[Dict[str, Any]],
|
||
):
|
||
warehouse = self._get_and_validate_warehouse(warehouse_id)
|
||
products = self._build_products_payload(warehouse, is_incoming=True, items=items)
|
||
return self._create_record(
|
||
warehouse=warehouse,
|
||
record_type=models.StockChangeTypeEnum.ADD,
|
||
source_type=source_type,
|
||
source_id=source_id,
|
||
products=products,
|
||
)
|
||
|
||
def stock_out(
|
||
self,
|
||
*,
|
||
warehouse_id: int,
|
||
source_type: int,
|
||
source_id: int | None = None,
|
||
items: List[Dict[str, Any]],
|
||
):
|
||
warehouse = self._get_and_validate_warehouse(warehouse_id)
|
||
products = self._build_products_payload(warehouse, is_incoming=False, items=items)
|
||
return self._create_record(
|
||
warehouse=warehouse,
|
||
record_type=models.StockChangeTypeEnum.REMOVE,
|
||
source_type=source_type,
|
||
source_id=source_id,
|
||
products=products,
|
||
)
|
||
|
||
def offset_stock_change(
|
||
self,
|
||
*,
|
||
source_record_id: int,
|
||
reason: str,
|
||
items: List[Dict[str, Any]] | None = None,
|
||
request_id: str | None = None,
|
||
extra_meta: Dict[str, Any] | None = None,
|
||
) -> Tuple[models.StockChangeRecord, List[models.StockChangeDetail], int]:
|
||
"""
|
||
创建针对既有库存变动记录的红冲(对冲)占位接口。
|
||
|
||
Args:
|
||
source_record_id: 需要被红冲的库存变动记录 ID。
|
||
reason: 红冲原因,必须明确记录以便审计。
|
||
items: 可选的部分红冲明细(支持宽进/严进等结构);为空时表示全量对冲。
|
||
request_id: 幂等键;同一 request_id 的请求应视为一次操作。
|
||
extra_meta: 额外的上下文信息(触发来源、操作者备注等)。
|
||
|
||
Returns:
|
||
新生成的反向 `StockChangeRecord`、其明细列表以及明细数量。
|
||
"""
|
||
raise NotImplementedError('库存红冲功能尚未实现')
|
||
|
||
def _get_and_validate_warehouse(self, warehouse_id: int) -> basic_models.WareHouse:
|
||
if not warehouse_id:
|
||
raise ValueError('必须提供仓库ID')
|
||
try:
|
||
warehouse = basic_models.WareHouse.objects.get(id=warehouse_id)
|
||
except basic_models.WareHouse.DoesNotExist:
|
||
raise ValueError(f'仓库ID {warehouse_id} 不存在')
|
||
if warehouse.merchant_id != self.merchant.id:
|
||
raise ValueError('仓库不属于当前商户')
|
||
return warehouse
|
||
|
||
def _create_record(
|
||
self,
|
||
*,
|
||
warehouse: basic_models.WareHouse,
|
||
record_type: int,
|
||
source_type: int,
|
||
source_id: int | None,
|
||
products: List[Dict[str, Any]],
|
||
):
|
||
common_kwargs = dict(
|
||
merchant=self.merchant,
|
||
created_by=self.created_by,
|
||
type=record_type,
|
||
warehouse_id=warehouse.id,
|
||
source_type=source_type,
|
||
source_id=source_id,
|
||
products=products,
|
||
)
|
||
|
||
if warehouse.mode == basic_models.WareHouseModeEnum.UNRESTRICTED:
|
||
return create_stock_change_record_relaxed(**common_kwargs)
|
||
|
||
# 严谨或严进严出模式统一由严谨服务处理
|
||
return create_stock_change_record_with_details(**common_kwargs)
|
||
|
||
def _build_products_payload(
|
||
self,
|
||
warehouse: basic_models.WareHouse,
|
||
*,
|
||
is_incoming: bool,
|
||
items: List[Dict[str, Any]],
|
||
) -> List[Dict[str, Any]]:
|
||
if not items:
|
||
raise ValueError('产品明细不能为空')
|
||
|
||
mode = warehouse.mode
|
||
if mode == basic_models.WareHouseModeEnum.UNRESTRICTED:
|
||
return self._build_relaxed_items(items)
|
||
|
||
if mode == basic_models.WareHouseModeEnum.RESTRICT_IN_OUT and not is_incoming:
|
||
return self._build_restrict_out_items(items)
|
||
|
||
# 严谨模式 + 严进严出入库复用数量列表
|
||
return self._build_strict_items(items)
|
||
|
||
@staticmethod
|
||
def _build_strict_items(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
payload: List[Dict[str, Any]] = []
|
||
for item in items:
|
||
product_id = item.get('product_id')
|
||
quantities = item.get('quantities') or []
|
||
if not product_id or not quantities:
|
||
raise ValueError('严谨模式需要提供 product_id 与 quantities 列表')
|
||
payload.append({'product': product_id, 'quantity': quantities})
|
||
return payload
|
||
|
||
@staticmethod
|
||
def _build_relaxed_items(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
payload: List[Dict[str, Any]] = []
|
||
for item in items:
|
||
product_id = item.get('product_id')
|
||
total_value = item.get('value')
|
||
unit_size = item.get('num_of_rolls', 1)
|
||
if not product_id or total_value is None:
|
||
raise ValueError('宽进宽出需要提供 product_id 与 value')
|
||
payload.append({
|
||
'product': product_id,
|
||
'quantity': {
|
||
'value': total_value,
|
||
'unit_count': unit_size,
|
||
}
|
||
})
|
||
return payload
|
||
|
||
@staticmethod
|
||
def _build_restrict_out_items(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
payload: List[Dict[str, Any]] = []
|
||
for item in items:
|
||
product_id = item.get('product_id')
|
||
consume_ids = item.get('consume_detail_ids') or []
|
||
if not product_id or not consume_ids:
|
||
raise ValueError('严进严出出库需要提供 product_id 与 consume_detail_ids')
|
||
payload.append({
|
||
'product': product_id,
|
||
'consume_with': consume_ids,
|
||
})
|
||
return payload
|
||
|
||
|
||
def find_inventory(product_id: int, warehouse_id: int) -> models.Inventory | None:
|
||
"""根据产品ID和仓库ID查找库存记录"""
|
||
|
||
try:
|
||
inventory = models.Inventory.objects.get(
|
||
product_id=product_id,
|
||
warehouse_id=warehouse_id,
|
||
)
|
||
return inventory
|
||
except models.Inventory.DoesNotExist:
|
||
return None
|
||
|
||
|
||
def create_stock_snapshot(stock_change_detail: models.StockChangeDetail, inventory: models.Inventory) -> models.StockSnapshot:
|
||
"""创建库存快照记录"""
|
||
|
||
if not stock_change_detail or stock_change_detail.id is None:
|
||
raise ValueError("无效的库存变动明细记录")
|
||
|
||
delta = stock_change_detail.quantity if stock_change_detail.stock_change_record.is_incoming else -stock_change_detail.quantity
|
||
rolls_delta = 1 if stock_change_detail.stock_change_record.is_incoming else -1
|
||
|
||
# 必须在库存变动发生后创建快照
|
||
snapshot = models.StockSnapshot(
|
||
merchant_id=stock_change_detail.merchant_id,
|
||
product_id=inventory.product_id,
|
||
warehouse_id=inventory.warehouse_id,
|
||
delta=delta,
|
||
quantity_before=inventory.quantity - delta,
|
||
quantity_after=inventory.quantity,
|
||
stock_change_record_id=stock_change_detail.stock_change_record.id,
|
||
unit=stock_change_detail.unit,
|
||
num_of_rolls=inventory.num_of_rolls + rolls_delta,
|
||
)
|
||
snapshot.save()
|
||
logger.info(
|
||
f'创建库存快照,库存ID: {inventory.id}, 数量: {inventory.quantity}, '
|
||
f'匹数: {inventory.num_of_rolls}'
|
||
)
|
||
return snapshot
|
||
|
||
|
||
def make_stock_change_completed(stock_change_record: models.StockChangeRecord) -> bool:
|
||
"""处理库存变动完成后的逻辑, 实际扣减库存也在这里发生"""
|
||
# TODO: 这里可以添加事务处理以确保数据一致性
|
||
|
||
if not stock_change_record or stock_change_record.id is None:
|
||
# 无效的库存变动记录
|
||
return False
|
||
|
||
if stock_change_record.is_finished:
|
||
return True
|
||
|
||
|
||
# TODO: 逻辑更改 = 出入库单现在不记录库存总量了
|
||
# 需要根据产品分别统计变动数量(要考虑不同单位)
|
||
|
||
for detail in stock_change_record.details.all():
|
||
detail: models.StockChangeDetail
|
||
|
||
logger.info(
|
||
f'处理库存变动明细,产品ID: {detail.product_id}, '
|
||
f'数量: {detail.quantity}, 单位: {detail.get_unit_display()}'
|
||
)
|
||
|
||
inventory_record = find_inventory(
|
||
product_id=detail.product_id,
|
||
warehouse_id=stock_change_record.warehouse_id,
|
||
)
|
||
|
||
# 确定方向
|
||
positive = 1 if stock_change_record.is_incoming else -1
|
||
|
||
if inventory_record:
|
||
# 更新现有库存记录
|
||
inventory_record.quantity += detail.quantity * positive
|
||
inventory_record.num_of_rolls += positive
|
||
logger.info(
|
||
f'更新库存记录 {inventory_record.id},新数量: {inventory_record.quantity}, '
|
||
f'新匹数: {inventory_record.num_of_rolls}'
|
||
)
|
||
else:
|
||
# 创建新库存记录
|
||
inventory_record = models.Inventory(
|
||
merchant_id=stock_change_record.merchant_id,
|
||
product_id=detail.product_id,
|
||
warehouse_id=stock_change_record.warehouse_id,
|
||
quantity=detail.quantity * positive,
|
||
num_of_rolls=1 if stock_change_record.is_incoming else -1,
|
||
)
|
||
logger.info(
|
||
f'创建新库存记录 {inventory_record.id},数量: {inventory_record.quantity}, '
|
||
f'匹数: {inventory_record.num_of_rolls}'
|
||
)
|
||
inventory_record.save()
|
||
# 发送库存变动通知
|
||
push_simple_message_with_object_id(
|
||
event_type='stock_change',
|
||
message=f'产品ID {detail.product_id} 位于 {stock_change_record.warehouse_id} 的库存已更新',
|
||
object_id=inventory_record.id
|
||
)
|
||
|
||
# 无论库存记录原本是否存在都创建库存快照
|
||
create_stock_snapshot(detail, inventory_record)
|
||
|
||
# 所有明细处理完毕,标记库存变动记录为已完成
|
||
stock_change_record.is_finished = True
|
||
stock_change_record.finished_at = timezone.now()
|
||
stock_change_record.save()
|
||
logger.info(
|
||
f'库存变动记录 {stock_change_record.id} 标记为已完成。'
|
||
)
|
||
return True
|
||
|
||
|
||
def get_frozen_stock_count(product_id: int, warehouse_id: int) -> int:
|
||
"""获取指定产品在指定仓库的冻结库存数量"""
|
||
total_frozen = models.StockFreeze.objects.filter(
|
||
product_id=product_id,
|
||
warehouse_id=warehouse_id,
|
||
status=models.StockFreezeStatusEnum.FROZEN
|
||
).aggregate(total=Sum('quantity'))['total'] or 0
|
||
return total_frozen
|