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, ROUND_DOWN 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 _to_positive_int(value, field_name: str) -> int: if value is None: raise ValueError(f'{field_name} 不能为空') decimal_value = _to_decimal(value, field_name) if decimal_value <= 0: raise ValueError(f'{field_name} 必须大于 0') if decimal_value != decimal_value.to_integral_value(): raise ValueError(f'{field_name} 必须为整数') return int(decimal_value) 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 _split_quantities_evenly(total: Decimal, rolls: int) -> List[Decimal]: if total <= 0: raise ValueError('quantity.value 必须大于 0') if rolls <= 0: raise ValueError('quantity.num_of_rolls 必须大于 0') quantum = Decimal('0.01') base = (total / Decimal(rolls)).quantize(quantum, rounding=ROUND_DOWN) if base == 0 and total < quantum: return [total] quantities: List[Decimal] = [base for _ in range(rolls)] distributed = base * rolls remainder = (total - distributed).quantize(quantum) idx = 0 while remainder > 0 and quantities: increment = min(quantum, remainder) quantities[idx] += increment remainder -= increment idx = (idx + 1) % rolls return [qty for qty in quantities if qty > 0] 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 = _to_decimal(quantity_data.get('value'), 'quantity.value') roll_count = quantity_data.get('num_of_rolls') unit_size = quantity_data.get('unit_count', 1) 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} 不属于当前商户') if roll_count is not None: roll_count = _to_positive_int(roll_count, 'quantity.num_of_rolls') quantities = _split_quantities_evenly(total_value, roll_count) else: unit_size = _to_decimal(unit_size, 'quantity.unit_count') 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`、其明细列表以及明细数量。 """ if not reason or not reason.strip(): raise ValueError('reason 不能为空') if items: raise ValueError('当前版本暂不支持部分红冲,请省略 items 参数') offset_record, created_details = self._perform_stock_offset( source_record_id=source_record_id, reason=reason.strip(), request_id=request_id, extra_meta=extra_meta or {}, ) return offset_record, created_details, len(created_details) def _perform_stock_offset( self, *, source_record_id: int, reason: str, request_id: str | None, extra_meta: Dict[str, Any], ) -> Tuple[models.StockChangeRecord, List[models.StockChangeDetail]]: with transaction.atomic(): source_record = ( models.StockChangeRecord.objects.select_related('warehouse', 'merchant') .prefetch_related('details__product', 'details__consume_with') .select_for_update() .get(id=source_record_id) ) if source_record.merchant_id != self.merchant.id: raise ValueError('库存变动记录不属于当前商户') if not source_record.is_finished: raise ValueError('仅允许对已完成的库存变动执行红冲') if models.StockSnapshot.objects.filter( stock_change_record=source_record, offset_id__isnull=False, ).exists(): raise ValueError('该库存变动记录已执行红冲') source_details = list(source_record.details.all().order_by('id')) if not source_details: raise ValueError('库存变动记录缺少明细,无法红冲') reverse_type = ( models.StockChangeTypeEnum.ADD if source_record.is_outgoing else models.StockChangeTypeEnum.REMOVE ) remarks = f'红冲原记录 {source_record.id}: {reason}' if request_id: remarks = f'[{request_id}] {remarks}' offset_record = models.StockChangeRecord.objects.create( merchant=self.merchant, type=reverse_type, warehouse=source_record.warehouse, source_type=models.StockChangeSourceEnum.OFFSET, source_id=source_record.id, created_by=self.created_by, remarks=remarks[:500], ) created_details: List[models.StockChangeDetail] = [] for detail in source_details: new_detail = models.StockChangeDetail.objects.create( merchant=self.merchant, stock_change_record=offset_record, product=detail.product, quantity=detail.quantity, unit=detail.unit, ) created_details.append(new_detail) if source_record.is_outgoing: consume_ids = [detail.consume_with_id for detail in source_details if detail.consume_with_id] if consume_ids: models.StockChangeDetail.objects.filter(id__in=consume_ids).update(is_consumed=False) if not make_stock_change_completed(offset_record): raise ValueError('红冲库存记录创建失败') self._link_offset_snapshots( source_record=source_record, offset_record=offset_record, ) logger.info('库存变动记录 %s 已红冲,生成记录 %s', source_record_id, offset_record.id) return offset_record, created_details @staticmethod def _link_offset_snapshots( *, source_record: models.StockChangeRecord, offset_record: models.StockChangeRecord, ): original_snapshots = list( models.StockSnapshot.objects.filter(stock_change_record=source_record).order_by('id') ) if not original_snapshots: return new_snapshots = list( models.StockSnapshot.objects.filter(stock_change_record=offset_record).order_by('id') ) if not new_snapshots: raise ValueError('红冲记录未生成任何库存快照') now = timezone.now() pair_count = min(len(original_snapshots), len(new_snapshots)) for index in range(pair_count): original_snapshot = original_snapshots[index] offset_snapshot = new_snapshots[index] original_snapshot.cancelled = True original_snapshot.cancelled_at = now original_snapshot.offset_id = offset_snapshot.id original_snapshot.save(update_fields=['cancelled', 'cancelled_at', 'offset_id']) offset_snapshot.offset_to = original_snapshot.id offset_snapshot.offset_at = now offset_snapshot.save(update_fields=['offset_to', 'offset_at']) if len(original_snapshots) != len(new_snapshots): logger.warning( '红冲快照数量不一致:原始 %s 条,新快照 %s 条', len(original_snapshots), len(new_snapshots), ) 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') roll_count = item.get('num_of_rolls') unit_size = item.get('unit_size') or item.get('unit_count') if not product_id or total_value is None: raise ValueError('宽进宽出需要提供 product_id 与 value') quantity_payload: Dict[str, Any] = {'value': total_value} if roll_count is not None: quantity_payload['num_of_rolls'] = roll_count elif unit_size is not None: quantity_payload['unit_count'] = unit_size else: quantity_payload['unit_count'] = 1 payload.append({'product': product_id, 'quantity': quantity_payload}) 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 _aggregate_transfer_details(details: List[models.StockChangeDetail]) -> Dict[int, Dict[str, Any]]: aggregated: Dict[int, Dict[str, Any]] = {} for detail in details: product_id = detail.product_id if product_id not in aggregated: aggregated[product_id] = { 'product': detail.product, 'unit': detail.unit, 'total': Decimal('0'), 'num_of_rolls': 0, } aggregated[product_id]['total'] += Decimal(detail.quantity) aggregated[product_id]['num_of_rolls'] += 1 return aggregated def create_transfer_order( *, merchant: basic_models.Merchant, created_by, operator: basic_models.Employee, from_warehouse_id: int, to_warehouse_id: int, items: List[Dict[str, Any]], transfer_date=None, remarks: str | None = '', request_id: str | None = None, ) -> models.TransferOrder: if not merchant: raise ValueError('必须提供商户信息') if operator is None: raise ValueError('必须提供调拨经办人') if operator.merchant_id != merchant.id: raise ValueError('经办人不属于当前商户') if not items: raise ValueError('调拨产品明细不能为空') if not from_warehouse_id or not to_warehouse_id: raise ValueError('必须提供调出仓和调入仓') if from_warehouse_id == to_warehouse_id: raise ValueError('调入仓与调出仓不能相同') transfer_date = transfer_date or timezone.now().date() if request_id: existing = models.TransferOrder.objects.filter(merchant=merchant, request_id=request_id).first() if existing: return existing try: from_warehouse = basic_models.WareHouse.objects.get(id=from_warehouse_id, merchant=merchant) except basic_models.WareHouse.DoesNotExist: raise ValueError('调出仓不存在或不属于当前商户') try: to_warehouse = basic_models.WareHouse.objects.get(id=to_warehouse_id, merchant=merchant) except basic_models.WareHouse.DoesNotExist: raise ValueError('调入仓不存在或不属于当前商户') if from_warehouse.mode != to_warehouse.mode: raise ValueError('调拨要求调出仓与调入仓的出入库模式一致') if from_warehouse.mode == basic_models.WareHouseModeEnum.RESTRICT_IN_OUT: raise ValueError('调拨暂不支持严进严出模式,请先转为其他模式') flow_service = StockFlowService(merchant=merchant, created_by=created_by) with transaction.atomic(): transfer_order = models.TransferOrder.objects.create( merchant=merchant, from_warehouse=from_warehouse, to_warehouse=to_warehouse, operator=operator, created_by=created_by, transfer_date=transfer_date, status=models.TransferOrderStatusEnum.DRAFT, remarks=remarks or '', mode=from_warehouse.mode, request_id=request_id, ) out_record, out_details, _ = flow_service.stock_out( warehouse_id=from_warehouse.id, source_type=models.StockChangeSourceEnum.TRANSPORT_OUT, source_id=transfer_order.id, items=items, ) in_record, _, _ = flow_service.stock_in( warehouse_id=to_warehouse.id, source_type=models.StockChangeSourceEnum.TRANSPORT_IN, source_id=transfer_order.id, items=items, ) make_stock_change_completed(out_record) make_stock_change_completed(in_record) aggregated = _aggregate_transfer_details(out_details) transfer_items = [ models.TransferOrderItem( transfer_order=transfer_order, product=data['product'], unit=data['unit'], total_quantity=data['total'], num_of_rolls=data['num_of_rolls'], ) for data in aggregated.values() ] if transfer_items: models.TransferOrderItem.objects.bulk_create(transfer_items) transfer_order.outgoing_record = out_record transfer_order.incoming_record = in_record transfer_order.status = models.TransferOrderStatusEnum.COMPLETED transfer_order.save(update_fields=['outgoing_record', 'incoming_record', 'status']) return transfer_order 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 # 必须在库存变动发生后创建快照 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, ) 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