1
0
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:
2025-11-28 15:01:11 +08:00
parent 53f2a24b20
commit 0c62398120
36 changed files with 1634 additions and 478 deletions

View File

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