1
0
forked from erp-dev/erp
Files
erpnew/business/pre_order_services.py

528 lines
19 KiB
Python

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 _normalize_pre_purchase_items(
*,
merchant: basic_info_models.Merchant,
items: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
# 与预销售单明细字段保持一致的弱关联产品输入结构
return _normalize_pre_sales_items(merchant=merchant, items=items)
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,
operator: basic_info_models.Employee,
created_by=None,
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 operator.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,
operator=operator,
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
]
)
# domain event: pre sales order created
# must be fired only after commit to avoid triggering on rollbacks
def _send_created_signal():
try:
from .signals import pre_sales_order_created
pre_sales_order_created.send(
sender=models.PreSalesOrder,
instance=pre_sales_order,
created_by=created_by,
operator=operator,
items_count=len(normalized_items),
)
except Exception:
import logging
logging.getLogger(__name__).exception(
'[business.pre_order_services] 触发 pre_sales_order_created signal 失败(已忽略)'
)
transaction.on_commit(_send_created_signal)
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()
def list_pre_purchase_orders(*, merchant: basic_info_models.Merchant):
return models.PrePurchaseOrder.objects.filter(merchant=merchant).select_related(
'merchant', 'supplier', 'warehouse', 'created_by'
).prefetch_related('items')
def get_pre_purchase_order(*, merchant: basic_info_models.Merchant, pre_purchase_order_id: int) -> models.PrePurchaseOrder:
try:
return models.PrePurchaseOrder.objects.select_related(
'merchant', 'supplier', 'warehouse', 'created_by'
).prefetch_related('items').get(id=pre_purchase_order_id, merchant=merchant)
except models.PrePurchaseOrder.DoesNotExist as exc:
raise ValueError('预采购单不存在') from exc
def create_pre_purchase_order(
*,
merchant: basic_info_models.Merchant,
supplier_id: int | str | None,
warehouse_id: int | str | None,
operator: basic_info_models.Employee,
created_by=None,
kind: int | str | None = None,
items: List[Dict[str, Any]],
remarks: str | None = '',
) -> models.PrePurchaseOrder:
if not supplier_id:
raise ValueError('缺少供应商 ID')
if not warehouse_id:
raise ValueError('缺少仓库 ID')
try:
supplier_id_int = int(supplier_id)
except (TypeError, ValueError) as exc:
raise ValueError('supplier 必须为整数') 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:
supplier = basic_info_models.Supplier.objects.get(id=supplier_id_int, merchant=merchant)
except basic_info_models.Supplier.DoesNotExist as exc:
raise ValueError(f'供应商 {supplier_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 operator.merchant_id != merchant.id:
raise ValueError('经办人所属商户与预采购单所属商户不一致')
normalized_items = _normalize_pre_purchase_items(merchant=merchant, items=items)
with transaction.atomic():
pre_purchase_order = models.PrePurchaseOrder.objects.create(
merchant=merchant,
supplier=supplier,
warehouse=warehouse,
created_by=created_by,
operator=operator,
kind=kind_int or models.PurchaseOrderKindEnum.WHOLESALE,
remarks=remarks,
)
try:
pre_purchase_order.full_clean()
except ValidationError as exc:
raise ValueError(str(exc)) from exc
models.PrePurchaseOrderItem.objects.bulk_create(
[
models.PrePurchaseOrderItem(
pre_purchase_order=pre_purchase_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_purchase_order.refresh_from_db()
return pre_purchase_order
def update_pre_purchase_order(
*,
pre_purchase_order: models.PrePurchaseOrder,
supplier_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.PrePurchaseOrder:
merchant = pre_purchase_order.merchant
new_supplier = pre_purchase_order.supplier
if supplier_id is not None:
if supplier_id == '':
raise ValueError('supplier 必须为整数')
try:
supplier_id_int = int(supplier_id)
except (TypeError, ValueError) as exc:
raise ValueError('supplier 必须为整数') from exc
try:
new_supplier = basic_info_models.Supplier.objects.get(id=supplier_id_int, merchant=merchant)
except basic_info_models.Supplier.DoesNotExist as exc:
raise ValueError(f'供应商 {supplier_id_int} 不存在') from exc
new_warehouse = pre_purchase_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_purchase_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_purchase_order.remarks
if items is None:
raise ValueError('items 需要为非空数组')
normalized_items = _normalize_pre_purchase_items(merchant=merchant, items=items)
with transaction.atomic():
pre_purchase_order.supplier = new_supplier
pre_purchase_order.warehouse = new_warehouse
pre_purchase_order.kind = new_kind
pre_purchase_order.remarks = new_remarks
try:
pre_purchase_order.full_clean()
except ValidationError as exc:
raise ValueError(str(exc)) from exc
pre_purchase_order.save(update_fields=['supplier', 'warehouse', 'kind', 'remarks', 'updated_at'])
pre_purchase_order.items.all().delete()
models.PrePurchaseOrderItem.objects.bulk_create(
[
models.PrePurchaseOrderItem(
pre_purchase_order=pre_purchase_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_purchase_order.refresh_from_db()
return pre_purchase_order
def delete_pre_purchase_order(*, pre_purchase_order: models.PrePurchaseOrder) -> None:
with transaction.atomic():
pre_purchase_order.delete()