forked from erp-dev/erp
feat: added pre sales order in business module, and explode tests of business module
This commit is contained in:
297
business/pre_order_services.py
Normal file
297
business/pre_order_services.py
Normal file
@@ -0,0 +1,297 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from django.db import transaction
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from basic_info import models as basic_info_models
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
def _to_decimal(value: Any, *, field_name: str) -> Decimal:
|
||||
if value is None or value == '':
|
||||
raise ValueError(f'{field_name} 不能为空')
|
||||
try:
|
||||
return Decimal(str(value))
|
||||
except (InvalidOperation, ValueError) as exc:
|
||||
raise ValueError(f'{field_name} 格式不正确') from exc
|
||||
|
||||
|
||||
def _to_optional_int(value: Any) -> Optional[int]:
|
||||
if value is None or value == '':
|
||||
return None
|
||||
try:
|
||||
parsed = int(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('order_quantity 必须为整数') from exc
|
||||
if parsed < 0:
|
||||
raise ValueError('order_quantity 不能为负数')
|
||||
return parsed
|
||||
|
||||
|
||||
def _to_optional_positive_int(value: Any, *, field_name: str) -> Optional[int]:
|
||||
if value is None or value == '':
|
||||
return None
|
||||
try:
|
||||
parsed = int(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(f'{field_name} 必须为整数') from exc
|
||||
if parsed <= 0:
|
||||
raise ValueError(f'{field_name} 必须大于 0')
|
||||
return parsed
|
||||
|
||||
|
||||
def _normalize_pre_sales_items(
|
||||
*,
|
||||
merchant: basic_info_models.Merchant,
|
||||
items: List[Dict[str, Any]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
if not isinstance(items, list) or not items:
|
||||
raise ValueError('items 需要为非空数组')
|
||||
|
||||
product_ids: List[int] = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError('items 每一项需要为对象')
|
||||
product_id = item.get('product_id') or item.get('product')
|
||||
if not product_id:
|
||||
raise ValueError('明细缺少 product_id')
|
||||
try:
|
||||
product_ids.append(int(product_id))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('product_id 必须为整数') from exc
|
||||
|
||||
products_by_id = basic_info_models.Product.objects.filter(
|
||||
merchant=merchant,
|
||||
id__in=product_ids,
|
||||
).in_bulk(field_name='id')
|
||||
|
||||
normalized: List[Dict[str, Any]] = []
|
||||
for item in items:
|
||||
product_id = int(item.get('product_id') or item.get('product'))
|
||||
product = products_by_id.get(product_id)
|
||||
if not product:
|
||||
raise ValueError(f'产品 {product_id} 不存在')
|
||||
|
||||
quantity = _to_decimal(item.get('quantity'), field_name='quantity')
|
||||
unit = (item.get('unit') or '').strip()
|
||||
if not unit:
|
||||
unit = str(product.unit)
|
||||
if not unit:
|
||||
raise ValueError('unit 不能为空')
|
||||
|
||||
normalized.append(
|
||||
{
|
||||
'product_id': product_id,
|
||||
'product_name': (item.get('product_name') or product.name or '').strip(),
|
||||
'color': item.get('color'),
|
||||
'quantity': quantity,
|
||||
'unit': unit,
|
||||
'spec': item.get('spec'),
|
||||
'quantity_of_rolls': item.get('quantity_of_rolls'),
|
||||
'num_of_rolls': _to_optional_positive_int(item.get('num_of_rolls'), field_name='num_of_rolls'),
|
||||
'order_quantity': _to_optional_int(item.get('order_quantity')),
|
||||
'remarks': item.get('remarks'),
|
||||
}
|
||||
)
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def list_pre_sales_orders(*, merchant: basic_info_models.Merchant):
|
||||
return models.PreSalesOrder.objects.filter(merchant=merchant).select_related(
|
||||
'merchant', 'customer', 'warehouse', 'created_by'
|
||||
).prefetch_related('items')
|
||||
|
||||
|
||||
def get_pre_sales_order(*, merchant: basic_info_models.Merchant, pre_sales_order_id: int) -> models.PreSalesOrder:
|
||||
try:
|
||||
return models.PreSalesOrder.objects.select_related(
|
||||
'merchant', 'customer', 'warehouse', 'created_by'
|
||||
).prefetch_related('items').get(id=pre_sales_order_id, merchant=merchant)
|
||||
except models.PreSalesOrder.DoesNotExist as exc:
|
||||
raise ValueError('预销售单不存在') from exc
|
||||
|
||||
|
||||
def create_pre_sales_order(
|
||||
*,
|
||||
merchant: basic_info_models.Merchant,
|
||||
customer_id: int | str | None,
|
||||
warehouse_id: int | str | None,
|
||||
created_by: basic_info_models.Employee,
|
||||
kind: int | str | None = None,
|
||||
items: List[Dict[str, Any]],
|
||||
remarks: str | None = '',
|
||||
) -> models.PreSalesOrder:
|
||||
if not customer_id:
|
||||
raise ValueError('缺少客户 ID')
|
||||
if not warehouse_id:
|
||||
raise ValueError('缺少仓库 ID')
|
||||
|
||||
try:
|
||||
customer_id_int = int(customer_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('customer 必须为整数') from exc
|
||||
|
||||
try:
|
||||
warehouse_id_int = int(warehouse_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('warehouse 必须为整数') from exc
|
||||
|
||||
kind_int: int | None
|
||||
if kind is None or kind == '':
|
||||
kind_int = None
|
||||
else:
|
||||
try:
|
||||
kind_int = int(kind)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('kind 必须为整数') from exc
|
||||
|
||||
try:
|
||||
customer = basic_info_models.Customer.objects.get(id=customer_id_int, merchant=merchant)
|
||||
except basic_info_models.Customer.DoesNotExist as exc:
|
||||
raise ValueError(f'客户 {customer_id_int} 不存在') from exc
|
||||
|
||||
try:
|
||||
warehouse = basic_info_models.WareHouse.objects.get(id=warehouse_id_int, merchant=merchant)
|
||||
except basic_info_models.WareHouse.DoesNotExist as exc:
|
||||
raise ValueError(f'仓库 {warehouse_id_int} 不存在') from exc
|
||||
|
||||
if created_by.merchant_id != merchant.id:
|
||||
raise ValueError('创建者所属商户与预销售单所属商户不一致')
|
||||
|
||||
normalized_items = _normalize_pre_sales_items(merchant=merchant, items=items)
|
||||
|
||||
with transaction.atomic():
|
||||
pre_sales_order = models.PreSalesOrder.objects.create(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
warehouse=warehouse,
|
||||
created_by=created_by,
|
||||
kind=kind_int or models.SalesOrderKindEnum.WHOLESALE,
|
||||
remarks=remarks,
|
||||
)
|
||||
|
||||
# enforce cross-merchant consistency
|
||||
try:
|
||||
pre_sales_order.full_clean()
|
||||
except ValidationError as exc:
|
||||
raise ValueError(str(exc)) from exc
|
||||
|
||||
models.PreSalesOrderItem.objects.bulk_create(
|
||||
[
|
||||
models.PreSalesOrderItem(
|
||||
pre_sales_order=pre_sales_order,
|
||||
product_id=item['product_id'],
|
||||
product_name=item.get('product_name') or '',
|
||||
color=item.get('color'),
|
||||
quantity=item.get('quantity'),
|
||||
unit=item.get('unit') or '',
|
||||
spec=item.get('spec'),
|
||||
quantity_of_rolls=item.get('quantity_of_rolls'),
|
||||
num_of_rolls=item.get('num_of_rolls') or 1,
|
||||
order_quantity=item.get('order_quantity'),
|
||||
remarks=item.get('remarks'),
|
||||
)
|
||||
for item in normalized_items
|
||||
]
|
||||
)
|
||||
|
||||
pre_sales_order.refresh_from_db()
|
||||
return pre_sales_order
|
||||
|
||||
|
||||
def update_pre_sales_order(
|
||||
*,
|
||||
pre_sales_order: models.PreSalesOrder,
|
||||
customer_id: int | str | None = None,
|
||||
warehouse_id: int | str | None = None,
|
||||
kind: int | str | None = None,
|
||||
items: List[Dict[str, Any]] | None = None,
|
||||
remarks: str | None = None,
|
||||
) -> models.PreSalesOrder:
|
||||
merchant = pre_sales_order.merchant
|
||||
|
||||
new_customer = pre_sales_order.customer
|
||||
if customer_id is not None:
|
||||
if customer_id == '':
|
||||
raise ValueError('customer 必须为整数')
|
||||
try:
|
||||
customer_id_int = int(customer_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('customer 必须为整数') from exc
|
||||
try:
|
||||
new_customer = basic_info_models.Customer.objects.get(id=customer_id_int, merchant=merchant)
|
||||
except basic_info_models.Customer.DoesNotExist as exc:
|
||||
raise ValueError(f'客户 {customer_id_int} 不存在') from exc
|
||||
|
||||
new_warehouse = pre_sales_order.warehouse
|
||||
if warehouse_id is not None:
|
||||
if warehouse_id == '':
|
||||
raise ValueError('warehouse 必须为整数')
|
||||
try:
|
||||
warehouse_id_int = int(warehouse_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('warehouse 必须为整数') from exc
|
||||
try:
|
||||
new_warehouse = basic_info_models.WareHouse.objects.get(id=warehouse_id_int, merchant=merchant)
|
||||
except basic_info_models.WareHouse.DoesNotExist as exc:
|
||||
raise ValueError(f'仓库 {warehouse_id_int} 不存在') from exc
|
||||
|
||||
if kind is None or kind == '':
|
||||
new_kind = pre_sales_order.kind
|
||||
else:
|
||||
try:
|
||||
new_kind = int(kind)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('kind 必须为整数') from exc
|
||||
new_remarks = remarks if remarks is not None else pre_sales_order.remarks
|
||||
|
||||
if items is None:
|
||||
raise ValueError('items 需要为非空数组')
|
||||
|
||||
normalized_items = _normalize_pre_sales_items(merchant=merchant, items=items)
|
||||
|
||||
with transaction.atomic():
|
||||
pre_sales_order.customer = new_customer
|
||||
pre_sales_order.warehouse = new_warehouse
|
||||
pre_sales_order.kind = new_kind
|
||||
pre_sales_order.remarks = new_remarks
|
||||
|
||||
try:
|
||||
pre_sales_order.full_clean()
|
||||
except ValidationError as exc:
|
||||
raise ValueError(str(exc)) from exc
|
||||
|
||||
pre_sales_order.save(update_fields=['customer', 'warehouse', 'kind', 'remarks', 'updated_at'])
|
||||
|
||||
pre_sales_order.items.all().delete()
|
||||
models.PreSalesOrderItem.objects.bulk_create(
|
||||
[
|
||||
models.PreSalesOrderItem(
|
||||
pre_sales_order=pre_sales_order,
|
||||
product_id=item['product_id'],
|
||||
product_name=item.get('product_name') or '',
|
||||
color=item.get('color'),
|
||||
quantity=item.get('quantity'),
|
||||
unit=item.get('unit') or '',
|
||||
spec=item.get('spec'),
|
||||
quantity_of_rolls=item.get('quantity_of_rolls'),
|
||||
num_of_rolls=item.get('num_of_rolls') or 1,
|
||||
order_quantity=item.get('order_quantity'),
|
||||
remarks=item.get('remarks'),
|
||||
)
|
||||
for item in normalized_items
|
||||
]
|
||||
)
|
||||
|
||||
pre_sales_order.refresh_from_db()
|
||||
return pre_sales_order
|
||||
|
||||
|
||||
def delete_pre_sales_order(*, pre_sales_order: models.PreSalesOrder) -> None:
|
||||
with transaction.atomic():
|
||||
pre_sales_order.delete()
|
||||
Reference in New Issue
Block a user