1
0
forked from erp-dev/erp

fix: business cancel service merged

This commit is contained in:
2026-06-11 15:33:31 +08:00
parent 65564f772a
commit 80db7867f4
18 changed files with 574 additions and 125 deletions

View File

@@ -140,6 +140,54 @@ class BalanceService:
return Decimal('0')
return balance.balance
@staticmethod
def get_customer_statement_balance(
*,
merchant: basic_info_models.Merchant,
customer: basic_info_models.Customer,
) -> Decimal:
"""
返回客户对账单口径的当前余额。
与 get_customer_balance 不同,这里会叠加 ExternalCustomerStatementOrder
中尚未写入本地 CustomerBalance 的外部销售/退货净额,使 customer balance API
与 statements API 的 current_balance 保持一致。
"""
return BalanceService.get_customer_balance(
merchant=merchant,
customer=customer,
) + BalanceService.get_customer_external_statement_balance_adjustment(
merchant=merchant,
customer=customer,
)
@staticmethod
def get_customer_external_statement_balance_adjustment(
*,
merchant: basic_info_models.Merchant,
customer: basic_info_models.Customer,
) -> Decimal:
"""
计算外部客户对账单业务单据对余额的净影响。
公式需与 _CustomerStatementBuilder._build_external_statement_records 保持一致:
- 外部销售:毛额 - 折扣 - 现场收款
- 外部销售退货:-(退货金额 + 退货折扣)
"""
adjustment = Decimal('0')
qs = models.ExternalCustomerStatementOrder.objects.filter(
merchant=merchant,
customer=customer,
).only('category', 'total_amount', 'zk_amount', 'sf_amount')
for order in qs:
sf = order.sf_amount or Decimal('0')
zk = order.zk_amount or Decimal('0')
if order.category == models.ExternalCustomerStatementCategoryEnum.SALE:
adjustment += (order.total_amount or Decimal('0')) - zk - sf
else:
adjustment -= (order.total_amount or Decimal('0')) + zk
return adjustment
@staticmethod
def get_supplier_balance(
*,
@@ -158,7 +206,29 @@ class BalanceService:
logger = logging.getLogger(__name__)
def _normalize_sales_order_kind(value) -> int:
"""
校验并归一化销售单类型kind
None / 空字符串视为未提供,回退到默认类型(大货 WHOLESALE
其余值必须是 SalesOrderKindEnum 中合法的整数枚举。
"""
if value in (None, ''):
return models.SalesOrderKindEnum.WHOLESALE
try:
kind_value = int(value)
except (TypeError, ValueError) as exc:
raise ValueError('kind 必须为合法的销售单类型枚举值') from exc
if kind_value not in models.SalesOrderKindEnum.values:
raise ValueError('kind 不是合法的销售单类型枚举值')
return kind_value
def _normalize_order_date(value) -> date:
if isinstance(value, date):
return value
if isinstance(value, datetime):
@@ -324,6 +394,7 @@ def create_sales_order(
remarks: str | None = '',
created_by=None,
from_pre_sales_order_id: int | None = None,
kind: int | None = None,
) -> models.SalesOrder:
"""
创建销售订单,后续审批通过后会触发出库任务。
@@ -331,7 +402,10 @@ def create_sales_order(
if not items:
raise ValueError('items 不能为空')
normalized_kind = _normalize_sales_order_kind(kind)
normalized_date = _normalize_order_date(order_date)
sales_items, stock_flow_items = _normalize_order_items(
merchant=merchant,
warehouse=warehouse,
@@ -349,7 +423,9 @@ def create_sales_order(
warehouse=warehouse,
remarks=remarks,
from_pre_sales_order_id=from_pre_sales_order_id,
kind=normalized_kind,
)
bulk_objects = [
models.SalesOrderItem(
sales_order=sales_order,
@@ -385,6 +461,7 @@ def update_sales_order(
operator: basic_info_models.Employee | None = None,
items: List[Dict[str, Any]] | None = None,
remarks: str | None = '',
kind: int | None = None,
) -> models.SalesOrder:
"""
更新销售订单(仅限审批中状态)。
@@ -397,6 +474,9 @@ def update_sales_order(
new_operator = operator or sales_order.operator
new_order_date = _normalize_order_date(order_date or sales_order.sales_date)
remarks = remarks if remarks is not None else sales_order.remarks
# kind 未提供时保持原值;提供则校验后更新
new_kind = sales_order.kind if kind in (None, '') else _normalize_sales_order_kind(kind)
if not items:
raise ValueError('items 需要为非空数组')
@@ -415,10 +495,12 @@ def update_sales_order(
sales_order.warehouse = new_warehouse
sales_order.operator = new_operator
sales_order.remarks = remarks
sales_order.kind = new_kind
sales_order.save(
update_fields=['customer', 'sales_date', 'warehouse', 'operator', 'remarks', 'updated_at']
update_fields=['customer', 'sales_date', 'warehouse', 'operator', 'remarks', 'kind', 'updated_at']
)
sales_order.items.all().delete()
bulk_objects = [
models.SalesOrderItem(
@@ -977,16 +1059,13 @@ def review_payment_order(
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
if order.status == models.PaymentOrderStatusEnum.APPROVED:
raise ValueError('已审批的付款单无法作废')
with transaction.atomic():
locked = models.PaymentOrder.objects.select_for_update().get(id=order.id)
locked.status = models.PaymentOrderStatusEnum.CANCELLED
locked.save(update_fields=['status', 'updated_at'])
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
return _cancel_order_impl(
order=order,
order_model_cls=models.PaymentOrder,
approved_status=models.PaymentOrderStatusEnum.APPROVED,
cancelled_status=models.PaymentOrderStatusEnum.CANCELLED,
error_label='付款单',
)
def review_receipt_order(
@@ -1030,16 +1109,13 @@ def review_receipt_order(
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
if order.status == models.ReceiptOrderStatusEnum.APPROVED:
raise ValueError('已审批的收款单无法作废')
with transaction.atomic():
locked = models.ReceiptOrder.objects.select_for_update().get(id=order.id)
locked.status = models.ReceiptOrderStatusEnum.CANCELLED
locked.save(update_fields=['status', 'updated_at'])
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
return _cancel_order_impl(
order=order,
order_model_cls=models.ReceiptOrder,
approved_status=models.ReceiptOrderStatusEnum.APPROVED,
cancelled_status=models.ReceiptOrderStatusEnum.CANCELLED,
error_label='收款单',
)
def _normalize_order_items(
@@ -1243,23 +1319,52 @@ def _approve_purchase_order(
return locked_order
def _cancel_purchase_order(purchase_order: models.PurchaseOrder) -> models.PurchaseOrder:
def _cancel_order_impl(
*,
order,
order_model_cls,
approved_status,
cancelled_status,
error_label: str,
stock_source_type=None,
):
"""统一作废实现:禁止作废已审批单据,可选检查出入库记录。
:param order: 待作废的单据实例(需提供 .id 用于加锁)
:param order_model_cls: 单据模型类(用于 select_for_update
:param approved_status: 已审批状态枚举值
:param cancelled_status: 作废状态枚举值
:param error_label: 中文错误提示中的单据名称
:param stock_source_type: StockChangeSourceEnum 值;为 None 则不检查出入库记录
:returns: 刷新后的模型实例
"""
with transaction.atomic():
locked = models.PurchaseOrder.objects.select_related('merchant').select_for_update().get(id=purchase_order.id)
if locked.status == models.PurchaseOrderStatusEnum.APPROVED:
raise ValueError('已审批的采购单无法作废')
if _order_has_stock_records(
locked = order_model_cls.objects.select_for_update().get(id=order.id)
if locked.status == approved_status:
raise ValueError(f'已审批的{error_label}无法作废')
if stock_source_type is not None and _order_has_stock_records(
merchant_id=locked.merchant_id,
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
source_type=stock_source_type,
source_id=locked.id,
):
raise ValueError('采购单已生成出入库记录,无法作废')
locked.status = models.PurchaseOrderStatusEnum.CANCELLED
raise ValueError(f'{error_label}已生成出入库记录,无法作废')
locked.status = cancelled_status
locked.save(update_fields=['status', 'updated_at'])
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
def _cancel_purchase_order(purchase_order: models.PurchaseOrder) -> models.PurchaseOrder:
return _cancel_order_impl(
order=purchase_order,
order_model_cls=models.PurchaseOrder,
approved_status=models.PurchaseOrderStatusEnum.APPROVED,
cancelled_status=models.PurchaseOrderStatusEnum.CANCELLED,
error_label='采购单',
stock_source_type=stock_models.StockChangeSourceEnum.PURCHASE,
)
def _approve_sales_order(
sales_order: models.SalesOrder,
reviewed_by,
@@ -1323,22 +1428,14 @@ def _approve_sales_order(
def _cancel_sales_order(sales_order: models.SalesOrder) -> models.SalesOrder:
with transaction.atomic():
locked = models.SalesOrder.objects.select_related('merchant').select_for_update().get(id=sales_order.id)
if locked.status == models.SalesOrderStatusEnum.APPROVED:
raise ValueError('已审批的销售单无法作废')
if _order_has_stock_records(
merchant_id=locked.merchant_id,
source_type=stock_models.StockChangeSourceEnum.SALES,
source_id=locked.id,
):
raise ValueError('销售单已生成出入库记录,无法作废')
locked.status = models.SalesOrderStatusEnum.CANCELLED
locked.save(update_fields=['status', 'updated_at'])
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
return _cancel_order_impl(
order=sales_order,
order_model_cls=models.SalesOrder,
approved_status=models.SalesOrderStatusEnum.APPROVED,
cancelled_status=models.SalesOrderStatusEnum.CANCELLED,
error_label='销售单',
stock_source_type=stock_models.StockChangeSourceEnum.SALES,
)
def _approve_purchase_return_order(
@@ -1384,22 +1481,14 @@ def _approve_purchase_return_order(
def _cancel_purchase_return_order(
purchase_return_order: models.PurchaseReturnOrder,
) -> models.PurchaseReturnOrder:
with transaction.atomic():
locked = models.PurchaseReturnOrder.objects.select_related('merchant').select_for_update().get(
id=purchase_return_order.id
)
if locked.status == models.PurchaseReturnStatusEnum.APPROVED:
raise ValueError('已审批的采购退货单无法作废')
if _order_has_stock_records(
merchant_id=locked.merchant_id,
source_type=stock_models.StockChangeSourceEnum.PURCHASE_RETURN,
source_id=locked.id,
):
raise ValueError('采购退货单已生成出入库记录,无法作废')
locked.status = models.PurchaseReturnStatusEnum.CANCELLED
locked.save(update_fields=['status', 'updated_at'])
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
return _cancel_order_impl(
order=purchase_return_order,
order_model_cls=models.PurchaseReturnOrder,
approved_status=models.PurchaseReturnStatusEnum.APPROVED,
cancelled_status=models.PurchaseReturnStatusEnum.CANCELLED,
error_label='采购退货单',
stock_source_type=stock_models.StockChangeSourceEnum.PURCHASE_RETURN,
)
def _approve_sales_return_order(
@@ -1445,22 +1534,14 @@ def _approve_sales_return_order(
def _cancel_sales_return_order(
sales_return_order: models.SalesReturnOrder,
) -> models.SalesReturnOrder:
with transaction.atomic():
locked = models.SalesReturnOrder.objects.select_related('merchant').select_for_update().get(
id=sales_return_order.id
)
if locked.status == models.SalesReturnStatusEnum.APPROVED:
raise ValueError('已审批的销售退货单无法作废')
if _order_has_stock_records(
merchant_id=locked.merchant_id,
source_type=stock_models.StockChangeSourceEnum.SALES_RETURN,
source_id=locked.id,
):
raise ValueError('销售退货单已生成出入库记录,无法作废')
locked.status = models.SalesReturnStatusEnum.CANCELLED
locked.save(update_fields=['status', 'updated_at'])
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
return _cancel_order_impl(
order=sales_return_order,
order_model_cls=models.SalesReturnOrder,
approved_status=models.SalesReturnStatusEnum.APPROVED,
cancelled_status=models.SalesReturnStatusEnum.CANCELLED,
error_label='销售退货单',
stock_source_type=stock_models.StockChangeSourceEnum.SALES_RETURN,
)
def create_purchase_order_stock_entries_sync(
@@ -1919,10 +2000,9 @@ def build_customer_statement(
"""
根据客户历史单据生成对账记录,供多个 API 复用。
"""
local_balance = BalanceService.get_customer_balance(merchant=merchant, customer=customer)
builder = _CustomerStatementBuilder(merchant=merchant, current_balance=local_balance)
statement_balance = BalanceService.get_customer_statement_balance(merchant=merchant, customer=customer)
builder = _CustomerStatementBuilder(merchant=merchant, current_balance=statement_balance)
records = builder.collect_records(customer)
builder.adjust_current_balance(builder.external_balance_adjustment)
return builder.build_payload(
counterparty_id=customer.id,
counterparty_name=customer.name,