forked from erp-dev/erp
feat: running on docker fully, and added rabbitmq/worker container
This commit is contained in:
@@ -13,13 +13,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _raise_unimplemented_mode():
|
||||
raise ValueError('仓库出入库模式为【严进严出】,该模式暂未支持创建出入库记录')
|
||||
raise ValueError('仓库出入库模式为【严进严出】,该模式暂未支持当前操作')
|
||||
|
||||
|
||||
def _ensure_strict_mode(warehouse: basic_models.WareHouse):
|
||||
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('仓库出入库模式为【宽进宽出】,请使用宽松模式接口创建出入库记录')
|
||||
|
||||
@@ -32,6 +34,105 @@ def _ensure_relaxed_mode(warehouse: basic_models.WareHouse):
|
||||
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,
|
||||
@@ -69,7 +170,17 @@ def create_stock_change_record_with_details(
|
||||
|
||||
if warehouse.merchant_id != merchant.id:
|
||||
raise ValueError('仓库不属于当前商户')
|
||||
_ensure_strict_mode(warehouse)
|
||||
|
||||
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
|
||||
@@ -85,31 +196,19 @@ def create_stock_change_record_with_details(
|
||||
)
|
||||
logger.info('创建新库存变动记录 ID: %s', stock_change_record.id)
|
||||
|
||||
for product_data in products:
|
||||
product_id = product_data.get('product')
|
||||
quantities = product_data.get('quantity', [])
|
||||
|
||||
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
|
||||
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)
|
||||
@@ -228,6 +327,160 @@ def create_stock_change_record_relaxed(
|
||||
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 _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查找库存记录"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user