1
0
forked from erp-dev/erp

feat: running on docker fully, and added rabbitmq/worker container

This commit is contained in:
2025-11-25 15:56:13 +08:00
parent cd6a2370fc
commit d90917e764
34 changed files with 2474 additions and 74 deletions

View File

@@ -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查找库存记录"""