forked from erp-dev/erp
fix: business cancel service merged
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user