1
0
forked from erp-dev/erp

feat: purchase_order approve and calcel

This commit is contained in:
2025-11-28 18:25:06 +08:00
parent 0c62398120
commit 4b3cd8eb75
14 changed files with 1019 additions and 43 deletions

View File

@@ -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))