forked from erp-dev/erp
feat: purchase_order approve and calcel
This commit is contained in:
@@ -84,6 +84,7 @@ def create_purchase_order(
|
||||
color=item_data.get('color'),
|
||||
quantity=item_data['quantity'],
|
||||
unit=item_data['unit'],
|
||||
spec=item_data.get('spec'),
|
||||
empty_diff_percent=item_data['empty_diff_percent'],
|
||||
quantity_of_rolls=item_data.get('quantity_of_rolls'),
|
||||
num_of_rolls=item_data['num_of_rolls'],
|
||||
@@ -94,18 +95,42 @@ def create_purchase_order(
|
||||
]
|
||||
models.PurchaseOrderItem.objects.bulk_create(bulk_objects)
|
||||
|
||||
created_by_id = getattr(created_by, 'id', None)
|
||||
if MerchantSettingService.get_setting(merchant, basic_info_models.MerchantSettingKeyEnum.AUTO_CREATE_STOCK_CHANGE_TASKS).value is True:
|
||||
logger.info('自动创建出入库记录任务已开启,创建入库记录任务')
|
||||
create_purchase_order_stock_entries.delay(
|
||||
purchase_order_id=purchase_order.id,
|
||||
warehouse_id=warehouse.id,
|
||||
items=stock_flow_items,
|
||||
created_by_id=created_by_id,
|
||||
)
|
||||
purchase_order.refresh_from_db()
|
||||
return purchase_order
|
||||
|
||||
|
||||
def review_purchase_order(
|
||||
*,
|
||||
purchase_order: models.PurchaseOrder | None = None,
|
||||
purchase_order_id: int | None = None,
|
||||
target_status: models.PurchaseOrderStatusEnum,
|
||||
reviewed_by=None,
|
||||
) -> models.PurchaseOrder:
|
||||
"""
|
||||
审批或作废采购单。
|
||||
|
||||
当目标状态为 APPROVED 且开启自动入库任务时,将触发入库 Celery 任务;
|
||||
当目标状态为 CANCELLED 时,会在更新状态前确认未生成任何出入库单。
|
||||
"""
|
||||
order = _resolve_purchase_order_instance(purchase_order, purchase_order_id)
|
||||
|
||||
if target_status not in {
|
||||
models.PurchaseOrderStatusEnum.APPROVED,
|
||||
models.PurchaseOrderStatusEnum.CANCELLED,
|
||||
}:
|
||||
raise ValueError('target_status 只能是 APPROVED 或 CANCELLED')
|
||||
|
||||
if order.status == target_status:
|
||||
return order
|
||||
|
||||
if target_status == models.PurchaseOrderStatusEnum.APPROVED:
|
||||
if order.status == models.PurchaseOrderStatusEnum.CANCELLED:
|
||||
raise ValueError('作废状态的采购单无法再次审批')
|
||||
return _approve_purchase_order(order, reviewed_by)
|
||||
|
||||
return _cancel_purchase_order(order)
|
||||
|
||||
|
||||
def _normalize_purchase_items(
|
||||
*,
|
||||
merchant: basic_info_models.Merchant,
|
||||
@@ -136,6 +161,7 @@ def _normalize_purchase_items(
|
||||
color = raw_item.get('color')
|
||||
batch_number = raw_item.get('batch_number')
|
||||
remarks = raw_item.get('remarks')
|
||||
spec = raw_item.get('spec')
|
||||
unit = raw_item.get('unit') or product.get_unit_display() or '米'
|
||||
|
||||
if warehouse_mode == basic_info_models.WareHouseModeEnum.UNRESTRICTED:
|
||||
@@ -176,11 +202,48 @@ def _normalize_purchase_items(
|
||||
'num_of_rolls': num_of_rolls,
|
||||
'batch_number': batch_number,
|
||||
'remarks': remarks,
|
||||
'spec': spec,
|
||||
})
|
||||
|
||||
return purchase_items, stock_flow_items
|
||||
|
||||
|
||||
def _approve_purchase_order(
|
||||
purchase_order: models.PurchaseOrder,
|
||||
reviewed_by,
|
||||
) -> models.PurchaseOrder:
|
||||
stock_flow_items = _build_stock_flow_items_from_order(purchase_order)
|
||||
|
||||
with transaction.atomic():
|
||||
purchase_order.status = models.PurchaseOrderStatusEnum.APPROVED
|
||||
purchase_order.save(update_fields=['status', 'updated_at'])
|
||||
|
||||
created_by_id = getattr(reviewed_by, 'id', None)
|
||||
if _auto_stock_task_enabled(purchase_order.merchant):
|
||||
logger.info('审批通过采购单 %s,触发入库任务', purchase_order.id)
|
||||
create_purchase_order_stock_entries.delay(
|
||||
purchase_order_id=purchase_order.id,
|
||||
warehouse_id=purchase_order.warehouse_id,
|
||||
items=stock_flow_items,
|
||||
created_by_id=created_by_id,
|
||||
)
|
||||
|
||||
purchase_order.refresh_from_db(fields=['status', 'updated_at'])
|
||||
return purchase_order
|
||||
|
||||
|
||||
def _cancel_purchase_order(purchase_order: models.PurchaseOrder) -> models.PurchaseOrder:
|
||||
if _purchase_order_has_stock_records(purchase_order):
|
||||
raise ValueError('采购单已生成出入库记录,无法作废')
|
||||
|
||||
with transaction.atomic():
|
||||
purchase_order.status = models.PurchaseOrderStatusEnum.CANCELLED
|
||||
purchase_order.save(update_fields=['status', 'updated_at'])
|
||||
|
||||
purchase_order.refresh_from_db(fields=['status', 'updated_at'])
|
||||
return purchase_order
|
||||
|
||||
|
||||
def create_purchase_order_stock_entries_sync(
|
||||
*,
|
||||
purchase_order_id: int,
|
||||
@@ -221,6 +284,74 @@ def create_purchase_order_stock_entries_sync(
|
||||
return payload
|
||||
|
||||
|
||||
def _build_stock_flow_items_from_order(purchase_order: models.PurchaseOrder) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
根据采购单明细还原 StockFlowService 所需的 items 结构。
|
||||
"""
|
||||
warehouse_mode = purchase_order.warehouse.mode
|
||||
items_payload: List[Dict[str, Any]] = []
|
||||
|
||||
order_items = purchase_order.items.all()
|
||||
if not order_items:
|
||||
raise ValueError('采购单没有任何明细,无法生成入库记录')
|
||||
|
||||
if warehouse_mode == basic_info_models.WareHouseModeEnum.UNRESTRICTED:
|
||||
for item in order_items:
|
||||
items_payload.append({
|
||||
'product_id': item.product_id,
|
||||
'value': str(item.quantity),
|
||||
'num_of_rolls': item.num_of_rolls,
|
||||
})
|
||||
return items_payload
|
||||
|
||||
# 严进 / 严进严出模式
|
||||
for item in order_items:
|
||||
raw_numbers = (item.quantity_of_rolls or '').split(',')
|
||||
normalized = [value.strip() for value in raw_numbers if value.strip()]
|
||||
if not normalized:
|
||||
raise ValueError('严进仓采购单缺少 numbers 数据,无法生成入库记录')
|
||||
items_payload.append({
|
||||
'product_id': item.product_id,
|
||||
'quantities': normalized,
|
||||
})
|
||||
return items_payload
|
||||
|
||||
|
||||
def _auto_stock_task_enabled(merchant: basic_info_models.Merchant) -> bool:
|
||||
try:
|
||||
setting = MerchantSettingService.get_setting(
|
||||
merchant,
|
||||
basic_info_models.MerchantSettingKeyEnum.AUTO_CREATE_STOCK_CHANGE_TASKS,
|
||||
)
|
||||
except basic_info_models.MerchantSetting.DoesNotExist:
|
||||
logger.warning('商户 %s 未配置 auto_create_stock_change_tasks,默认关闭', merchant.id)
|
||||
return False
|
||||
return setting.value is True
|
||||
|
||||
|
||||
def _purchase_order_has_stock_records(purchase_order: models.PurchaseOrder) -> bool:
|
||||
return stock_models.StockChangeRecord.objects.filter(
|
||||
merchant_id=purchase_order.merchant_id,
|
||||
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
source_id=purchase_order.id,
|
||||
).exists()
|
||||
|
||||
|
||||
def _resolve_purchase_order_instance(
|
||||
purchase_order: models.PurchaseOrder | None,
|
||||
purchase_order_id: int | None,
|
||||
) -> models.PurchaseOrder:
|
||||
if purchase_order is None and purchase_order_id is None:
|
||||
raise ValueError('必须提供 purchase_order 或 purchase_order_id')
|
||||
|
||||
if purchase_order is not None:
|
||||
purchase_order_id = purchase_order.id
|
||||
|
||||
return models.PurchaseOrder.objects.select_related('merchant', 'warehouse').prefetch_related('items').get(
|
||||
id=purchase_order_id
|
||||
)
|
||||
|
||||
|
||||
def _to_decimal(value, field_name: str) -> Decimal:
|
||||
try:
|
||||
return Decimal(str(value))
|
||||
|
||||
Reference in New Issue
Block a user