forked from erp-dev/erp
feat: red flush
This commit is contained in:
@@ -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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user