1
0
forked from erp-dev/erp

feat: red flush

This commit is contained in:
2026-06-12 13:57:21 +08:00
parent 80db7867f4
commit 0bcaa2db44
33 changed files with 2997 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import logging
import uuid
from collections import OrderedDict
from datetime import date, datetime
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
@@ -34,8 +35,9 @@ class BalanceService:
request_id: str | None = None,
remarks: str | None = '',
extra_meta: Dict[str, Any] | None = None,
):
BalanceService._adjust_balance(
red_flush_id=None,
) -> models.BalanceChangeRecord:
return BalanceService._adjust_balance(
merchant=merchant,
counterparty=supplier,
balance_model=models.SupplierBalance,
@@ -47,6 +49,7 @@ class BalanceService:
request_id=request_id,
remarks=remarks,
extra_meta=extra_meta,
red_flush_id=red_flush_id,
)
@staticmethod
@@ -60,8 +63,9 @@ class BalanceService:
request_id: str | None = None,
remarks: str | None = '',
extra_meta: Dict[str, Any] | None = None,
):
BalanceService._adjust_balance(
red_flush_id=None,
) -> models.BalanceChangeRecord:
return BalanceService._adjust_balance(
merchant=merchant,
counterparty=customer,
balance_model=models.CustomerBalance,
@@ -73,6 +77,7 @@ class BalanceService:
request_id=request_id,
remarks=remarks,
extra_meta=extra_meta,
red_flush_id=red_flush_id,
)
@staticmethod
@@ -89,7 +94,8 @@ class BalanceService:
request_id: str | None,
remarks: str | None,
extra_meta: Dict[str, Any] | None,
):
red_flush_id,
) -> models.BalanceChangeRecord:
meta_payload = extra_meta or {}
remarks_value = remarks or ''
with transaction.atomic():
@@ -118,13 +124,14 @@ class BalanceService:
'request_id': request_id,
'remarks': remarks_value,
'extra_meta': meta_payload,
'red_flush_id': red_flush_id,
}
if target_type == models.BalanceChangeTargetEnum.SUPPLIER:
record_kwargs['supplier'] = counterparty
else:
record_kwargs['customer'] = counterparty
models.BalanceChangeRecord.objects.create(**record_kwargs)
return models.BalanceChangeRecord.objects.create(**record_kwargs)
@staticmethod
def get_customer_balance(
@@ -1118,6 +1125,317 @@ def review_receipt_order(
)
def red_flush_purchase_order(
*,
merchant: basic_info_models.Merchant,
purchase_order: models.PurchaseOrder | None = None,
purchase_order_id: int | None = None,
red_flushed_by=None,
reason: str | None = '',
) -> models.PurchaseOrder:
order = _resolve_purchase_order_instance(purchase_order, purchase_order_id)
_ensure_order_merchant(order, merchant, '采购单')
return _red_flush_order_impl(
order=order,
order_model_cls=models.PurchaseOrder,
approved_status=models.PurchaseOrderStatusEnum.APPROVED,
balance_source_type=models.BalanceChangeSourceEnum.PURCHASE_ORDER,
stock_source_type=stock_models.StockChangeSourceEnum.PURCHASE,
counterparty_field='supplier',
error_label='采购单',
red_flushed_by=red_flushed_by,
reason=reason,
)
def red_flush_sales_order(
*,
merchant: basic_info_models.Merchant,
sales_order: models.SalesOrder | None = None,
sales_order_id: int | None = None,
red_flushed_by=None,
reason: str | None = '',
) -> models.SalesOrder:
order = _resolve_sales_order_instance(sales_order, sales_order_id)
_ensure_order_merchant(order, merchant, '销售单')
return _red_flush_order_impl(
order=order,
order_model_cls=models.SalesOrder,
approved_status=models.SalesOrderStatusEnum.APPROVED,
balance_source_type=models.BalanceChangeSourceEnum.SALES_ORDER,
stock_source_type=stock_models.StockChangeSourceEnum.SALES,
counterparty_field='customer',
error_label='销售单',
red_flushed_by=red_flushed_by,
reason=reason,
)
def red_flush_purchase_return_order(
*,
merchant: basic_info_models.Merchant,
purchase_return_order: models.PurchaseReturnOrder | None = None,
purchase_return_order_id: int | None = None,
red_flushed_by=None,
reason: str | None = '',
) -> models.PurchaseReturnOrder:
order = _resolve_purchase_return_order_instance(purchase_return_order, purchase_return_order_id)
_ensure_order_merchant(order, merchant, '采购退货单')
return _red_flush_order_impl(
order=order,
order_model_cls=models.PurchaseReturnOrder,
approved_status=models.PurchaseReturnStatusEnum.APPROVED,
balance_source_type=models.BalanceChangeSourceEnum.PURCHASE_RETURN_ORDER,
stock_source_type=stock_models.StockChangeSourceEnum.PURCHASE_RETURN,
counterparty_field='supplier',
error_label='采购退货单',
red_flushed_by=red_flushed_by,
reason=reason,
)
def red_flush_sales_return_order(
*,
merchant: basic_info_models.Merchant,
sales_return_order: models.SalesReturnOrder | None = None,
sales_return_order_id: int | None = None,
red_flushed_by=None,
reason: str | None = '',
) -> models.SalesReturnOrder:
order = _resolve_sales_return_order_instance(sales_return_order, sales_return_order_id)
_ensure_order_merchant(order, merchant, '销售退货单')
return _red_flush_order_impl(
order=order,
order_model_cls=models.SalesReturnOrder,
approved_status=models.SalesReturnStatusEnum.APPROVED,
balance_source_type=models.BalanceChangeSourceEnum.SALES_RETURN_ORDER,
stock_source_type=stock_models.StockChangeSourceEnum.SALES_RETURN,
counterparty_field='customer',
error_label='销售退货单',
red_flushed_by=red_flushed_by,
reason=reason,
)
def red_flush_payment_order(
*,
merchant: basic_info_models.Merchant,
payment_order: models.PaymentOrder | None = None,
payment_order_id: int | None = None,
red_flushed_by=None,
reason: str | None = '',
) -> models.PaymentOrder:
order = _resolve_payment_order_instance(payment_order, payment_order_id)
_ensure_order_merchant(order, merchant, '付款单')
return _red_flush_order_impl(
order=order,
order_model_cls=models.PaymentOrder,
approved_status=models.PaymentOrderStatusEnum.APPROVED,
balance_source_type=models.BalanceChangeSourceEnum.PAYMENT_ORDER,
stock_source_type=None,
counterparty_field='supplier',
error_label='付款单',
red_flushed_by=red_flushed_by,
reason=reason,
)
def red_flush_receipt_order(
*,
merchant: basic_info_models.Merchant,
receipt_order: models.ReceiptOrder | None = None,
receipt_order_id: int | None = None,
red_flushed_by=None,
reason: str | None = '',
) -> models.ReceiptOrder:
order = _resolve_receipt_order_instance(receipt_order, receipt_order_id)
_ensure_order_merchant(order, merchant, '收款单')
return _red_flush_order_impl(
order=order,
order_model_cls=models.ReceiptOrder,
approved_status=models.ReceiptOrderStatusEnum.APPROVED,
balance_source_type=models.BalanceChangeSourceEnum.RECEIPT_ORDER,
stock_source_type=None,
counterparty_field='customer',
error_label='收款单',
red_flushed_by=red_flushed_by,
reason=reason,
)
def _ensure_order_merchant(order, merchant: basic_info_models.Merchant, error_label: str) -> None:
if merchant is None:
raise ValueError('必须提供 merchant')
if order.merchant_id != merchant.id:
raise ValueError(f'{error_label}不属于当前商户')
def _red_flush_order_impl(
*,
order,
order_model_cls,
approved_status,
balance_source_type: models.BalanceChangeSourceEnum,
stock_source_type: stock_models.StockChangeSourceEnum | None,
counterparty_field: str,
error_label: str,
red_flushed_by=None,
reason: str | None = '',
):
reason_text = (reason or '').strip() or f'红冲{error_label}'
current_red_flush_id = uuid.uuid4()
with transaction.atomic():
locked_order = order_model_cls.objects.select_for_update().get(id=order.id)
if locked_order.status != approved_status:
raise ValueError(f'仅允许红冲已审批的{error_label}')
if locked_order.is_red_flushed:
raise ValueError(f'{error_label}已红冲,不能重复红冲')
_ensure_order_not_external_source(locked_order, error_label)
original_balance_record = _get_original_balance_change_record(
merchant_id=locked_order.merchant_id,
source_type=balance_source_type,
source_id=locked_order.id,
error_label=error_label,
)
reverse_record = _create_reverse_balance_change_record(
original_record=original_balance_record,
order=locked_order,
counterparty_field=counterparty_field,
red_flush_id=current_red_flush_id,
reason=reason_text,
)
now = timezone.now()
original_balance_record.cancelled = True
original_balance_record.cancelled_at = now
original_balance_record.offset_id = reverse_record.id
original_balance_record.red_flush_id = current_red_flush_id
original_balance_record.save(update_fields=['cancelled', 'cancelled_at', 'offset_id', 'red_flush_id', 'updated_at'])
reverse_record.offset_to = original_balance_record.id
reverse_record.offset_at = now
reverse_record.red_flush_id = current_red_flush_id
reverse_record.save(update_fields=['offset_to', 'offset_at', 'red_flush_id', 'updated_at'])
if stock_source_type is not None:
_red_flush_stock_records_for_order(
order=locked_order,
stock_source_type=stock_source_type,
red_flush_id=current_red_flush_id,
red_flushed_by=red_flushed_by,
reason=reason_text,
error_label=error_label,
)
locked_order.is_red_flushed = True
locked_order.red_flush_id = current_red_flush_id
locked_order.red_flushed_at = now
locked_order.save(update_fields=['is_red_flushed', 'red_flush_id', 'red_flushed_at', 'updated_at'])
locked_order.refresh_from_db()
return locked_order
def _ensure_order_not_external_source(order, error_label: str) -> None:
if getattr(order, 'is_external_source', False) or getattr(order, 'external_source_id', None):
raise ValueError(f'外部来源{error_label}不允许红冲')
def _get_original_balance_change_record(
*,
merchant_id: int,
source_type: models.BalanceChangeSourceEnum,
source_id: int,
error_label: str,
) -> models.BalanceChangeRecord:
qs = models.BalanceChangeRecord.objects.select_for_update().filter(
merchant_id=merchant_id,
source_type=source_type,
source_id=source_id,
offset_to__isnull=True,
cancelled=False,
)
try:
return qs.get()
except models.BalanceChangeRecord.DoesNotExist as exc:
raise ValueError(f'{error_label}缺少可红冲的余额变动记录') from exc
except models.BalanceChangeRecord.MultipleObjectsReturned as exc:
raise ValueError(f'{error_label}存在多条可红冲的余额变动记录') from exc
def _create_reverse_balance_change_record(
*,
original_record: models.BalanceChangeRecord,
order,
counterparty_field: str,
red_flush_id,
reason: str,
) -> models.BalanceChangeRecord:
reverse_delta = -original_record.delta
remarks = f'红冲余额记录 {original_record.id}: {reason}'
extra_meta = {
'red_flush_of_balance_change_record': original_record.id,
}
if counterparty_field == 'supplier':
return BalanceService.adjust_supplier_balance(
merchant=order.merchant,
supplier=order.supplier,
delta=reverse_delta,
source_type=original_record.source_type,
source_id=original_record.source_id,
remarks=remarks,
extra_meta=extra_meta,
red_flush_id=red_flush_id,
)
if counterparty_field == 'customer':
return BalanceService.adjust_customer_balance(
merchant=order.merchant,
customer=order.customer,
delta=reverse_delta,
source_type=original_record.source_type,
source_id=original_record.source_id,
remarks=remarks,
extra_meta=extra_meta,
red_flush_id=red_flush_id,
)
raise ValueError('未知的业务主体类型')
def _red_flush_stock_records_for_order(
*,
order,
stock_source_type: stock_models.StockChangeSourceEnum,
red_flush_id,
red_flushed_by,
reason: str,
error_label: str,
) -> None:
stock_records = list(
stock_models.StockChangeRecord.objects.select_for_update().filter(
merchant_id=order.merchant_id,
source_type=stock_source_type,
source_id=order.id,
).order_by('id')
)
if not stock_records:
raise ValueError(f'{error_label}缺少可红冲的库存记录')
stock_service = StockFlowService(merchant=order.merchant, created_by=red_flushed_by)
for stock_record in stock_records:
stock_service.offset_stock_change(
source_record_id=stock_record.id,
reason=reason,
red_flush_id=red_flush_id,
extra_meta={
'business_order_type': order.__class__.__name__,
'business_order_id': order.id,
},
)
def _normalize_order_items(
*,
merchant: basic_info_models.Merchant,
@@ -2468,4 +2786,3 @@ def _normalize_statement_amount(value) -> Decimal:
def _decimal_to_string(value: Decimal) -> str:
normalized = _normalize_statement_amount(value)
return format(normalized, 'f')