forked from erp-dev/erp
feat: added print_count api, added merchant setting model, added manual mode setting for purchase order
This commit is contained in:
@@ -1,21 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, List
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
|
||||
from basic_info import models as basic_info_models
|
||||
from stock import models as stock_models
|
||||
from stock.services import StockFlowService
|
||||
from basic_info.services import MerchantSettingService
|
||||
|
||||
from . import models
|
||||
from .tasks import create_purchase_order_stock_entries
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _normalize_order_date(value) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
@@ -34,8 +38,8 @@ def create_purchase_order(
|
||||
merchant: basic_info_models.Merchant,
|
||||
supplier: basic_info_models.Supplier,
|
||||
order_date,
|
||||
total_amount,
|
||||
warehouse_id: int,
|
||||
warehouse: basic_info_models.WareHouse,
|
||||
operator: basic_info_models.Employee,
|
||||
items: List[Dict[str, Any]],
|
||||
remarks: str | None = '',
|
||||
created_by=None,
|
||||
@@ -47,40 +51,190 @@ def create_purchase_order(
|
||||
merchant: 采购单所属商户
|
||||
supplier: 供应商
|
||||
order_date: 订单日期 (date)
|
||||
total_amount: 总金额
|
||||
warehouse_id: 入库仓库ID
|
||||
items: 产品明细,格式示例:
|
||||
[
|
||||
{'product_id': 1, 'quantities': ['10.00', '5.00']},
|
||||
{'product_id': 2, 'quantities': ['3.50']},
|
||||
]
|
||||
该结构会被传递给 StockFlowService,需满足其模式要求。
|
||||
warehouse: 入库仓库实例
|
||||
operator: 经办人
|
||||
items: 产品明细,字段会根据仓库模式校验
|
||||
remarks: 备注
|
||||
created_by: 创建者用户(可选,用于 stock 记录中的 created_by)
|
||||
"""
|
||||
if not items:
|
||||
raise ValueError('items 不能为空')
|
||||
if not warehouse_id:
|
||||
raise ValueError('warehouse_id 不能为空')
|
||||
|
||||
normalized_date = _normalize_order_date(order_date)
|
||||
total_amount = Decimal(str(total_amount))
|
||||
purchase_items, stock_flow_items = _normalize_purchase_items(
|
||||
merchant=merchant,
|
||||
warehouse=warehouse,
|
||||
items=items,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
purchase_order = models.PurchaseOrder.objects.create(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
order_date=normalized_date,
|
||||
total_amount=total_amount,
|
||||
purchase_date=normalized_date,
|
||||
operator=operator,
|
||||
warehouse=warehouse,
|
||||
remarks=remarks,
|
||||
)
|
||||
bulk_objects = [
|
||||
models.PurchaseOrderItem(
|
||||
purchase_order=purchase_order,
|
||||
product=item_data['product'],
|
||||
price=item_data['price'],
|
||||
color=item_data.get('color'),
|
||||
quantity=item_data['quantity'],
|
||||
unit=item_data['unit'],
|
||||
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'],
|
||||
batch_number=item_data.get('batch_number'),
|
||||
remarks=item_data.get('remarks'),
|
||||
)
|
||||
for item_data in purchase_items
|
||||
]
|
||||
models.PurchaseOrderItem.objects.bulk_create(bulk_objects)
|
||||
|
||||
created_by_id = getattr(created_by, 'id', None)
|
||||
create_purchase_order_stock_entries.delay(
|
||||
purchase_order_id=purchase_order.id,
|
||||
warehouse_id=warehouse_id,
|
||||
items=items,
|
||||
created_by_id=created_by_id,
|
||||
)
|
||||
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,
|
||||
)
|
||||
return purchase_order
|
||||
|
||||
|
||||
def _normalize_purchase_items(
|
||||
*,
|
||||
merchant: basic_info_models.Merchant,
|
||||
warehouse: basic_info_models.WareHouse,
|
||||
items: List[Dict[str, Any]],
|
||||
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
||||
"""
|
||||
根据仓库模式校验采购明细,并返回
|
||||
- purchase_items: 用于创建 PurchaseOrderItem
|
||||
- stock_flow_items: 传递给 StockFlowService 的 items 结构
|
||||
"""
|
||||
purchase_items: List[Dict[str, Any]] = []
|
||||
stock_flow_items: List[Dict[str, Any]] = []
|
||||
|
||||
warehouse_mode = warehouse.mode
|
||||
|
||||
for index, raw_item in enumerate(items):
|
||||
product_id = raw_item.get('product_id')
|
||||
if not product_id:
|
||||
raise ValueError(f'items[{index}].product_id 不能为空')
|
||||
try:
|
||||
product = basic_info_models.Product.objects.get(id=product_id, merchant=merchant)
|
||||
except basic_info_models.Product.DoesNotExist as exc:
|
||||
raise ValueError(f'产品 {product_id} 不存在或不属于当前商户') from exc
|
||||
|
||||
price = _to_decimal(raw_item.get('price', '0'), f'items[{index}].price')
|
||||
empty_diff_percent = _to_decimal(raw_item.get('empty_diff_percent', '0'), f'items[{index}].empty_diff_percent')
|
||||
color = raw_item.get('color')
|
||||
batch_number = raw_item.get('batch_number')
|
||||
remarks = raw_item.get('remarks')
|
||||
unit = raw_item.get('unit') or product.get_unit_display() or '米'
|
||||
|
||||
if warehouse_mode == basic_info_models.WareHouseModeEnum.UNRESTRICTED:
|
||||
if 'numbers' in raw_item and raw_item['numbers']:
|
||||
raise ValueError(f'仓库为宽进模式,items[{index}] 不应提供 numbers')
|
||||
quantity = _to_positive_int(raw_item.get('quantity'), f'items[{index}].quantity')
|
||||
num_of_rolls = _to_positive_int(raw_item.get('num_of_rolls'), f'items[{index}].num_of_rolls')
|
||||
quantity_of_rolls = None
|
||||
stock_flow_items.append({
|
||||
'product_id': product.id,
|
||||
'value': str(quantity),
|
||||
'num_of_rolls': num_of_rolls,
|
||||
})
|
||||
else:
|
||||
numbers = raw_item.get('numbers')
|
||||
if not numbers or not isinstance(numbers, list):
|
||||
raise ValueError(f'仓库为严进模式,items[{index}] 需要提供 numbers 数组')
|
||||
normalized_numbers = [
|
||||
str(_to_positive_int(value, f'items[{index}].numbers[{pos}]'))
|
||||
for pos, value in enumerate(numbers)
|
||||
]
|
||||
num_of_rolls = len(normalized_numbers)
|
||||
quantity = sum(int(val) for val in normalized_numbers)
|
||||
quantity_of_rolls = ','.join(normalized_numbers)
|
||||
stock_flow_items.append({
|
||||
'product_id': product.id,
|
||||
'quantities': normalized_numbers,
|
||||
})
|
||||
|
||||
purchase_items.append({
|
||||
'product': product,
|
||||
'price': price,
|
||||
'color': color,
|
||||
'quantity': quantity,
|
||||
'unit': unit,
|
||||
'empty_diff_percent': empty_diff_percent,
|
||||
'quantity_of_rolls': quantity_of_rolls,
|
||||
'num_of_rolls': num_of_rolls,
|
||||
'batch_number': batch_number,
|
||||
'remarks': remarks,
|
||||
})
|
||||
|
||||
return purchase_items, stock_flow_items
|
||||
|
||||
|
||||
def create_purchase_order_stock_entries_sync(
|
||||
*,
|
||||
purchase_order_id: int,
|
||||
warehouse_id: int,
|
||||
items: List[Dict[str, Any]],
|
||||
created_by_id: int | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
根据采购单生成入库记录。
|
||||
"""
|
||||
try:
|
||||
purchase_order = models.PurchaseOrder.objects.select_related('merchant').get(id=purchase_order_id)
|
||||
except models.PurchaseOrder.DoesNotExist:
|
||||
logger.error('PurchaseOrder %s 不存在,无法创建入库单', purchase_order_id)
|
||||
return {'error': 'purchase_order_not_found', 'purchase_order_id': purchase_order_id}
|
||||
|
||||
merchant = purchase_order.merchant
|
||||
|
||||
created_by = None
|
||||
if created_by_id:
|
||||
UserModel = get_user_model()
|
||||
created_by = UserModel.objects.filter(id=created_by_id).first()
|
||||
|
||||
service = StockFlowService(merchant=merchant, created_by=created_by)
|
||||
record, details, created_count = service.stock_in(
|
||||
warehouse_id=warehouse_id,
|
||||
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
source_id=purchase_order.id,
|
||||
items=items,
|
||||
)
|
||||
|
||||
payload = {
|
||||
'purchase_order_id': purchase_order.id,
|
||||
'stock_change_record_id': getattr(record, 'id', None),
|
||||
'created_details_count': created_count,
|
||||
}
|
||||
logger.info('采购单 %s 入库任务完成: %s', purchase_order.id, payload)
|
||||
return payload
|
||||
|
||||
|
||||
def _to_decimal(value, field_name: str) -> Decimal:
|
||||
try:
|
||||
return Decimal(str(value))
|
||||
except (InvalidOperation, TypeError) as exc:
|
||||
raise ValueError(f'{field_name} 必须是合法数值') from exc
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user