1
0
forked from erp-dev/erp

feat: balance api

This commit is contained in:
2025-11-30 23:04:21 +08:00
parent 9006a530d1
commit 9acc4e14fc
21 changed files with 714 additions and 64 deletions

View File

@@ -18,6 +18,66 @@ from .tasks import (
create_purchase_order_stock_entries,
create_sales_order_stock_entries,
)
class BalanceService:
@staticmethod
def adjust_supplier_balance(
*,
merchant: basic_info_models.Merchant,
supplier: basic_info_models.Supplier,
delta: Decimal,
):
with transaction.atomic():
balance, _ = models.SupplierBalance.objects.select_for_update().get_or_create(
merchant=merchant,
supplier=supplier,
defaults={'balance': Decimal('0')},
)
balance.balance += delta
balance.save(update_fields=['balance', 'updated_at'])
@staticmethod
def adjust_customer_balance(
*,
merchant: basic_info_models.Merchant,
customer: basic_info_models.Customer,
delta: Decimal,
):
with transaction.atomic():
balance, _ = models.CustomerBalance.objects.select_for_update().get_or_create(
merchant=merchant,
customer=customer,
defaults={'balance': Decimal('0')},
)
balance.balance += delta
balance.save(update_fields=['balance', 'updated_at'])
@staticmethod
def get_customer_balance(
*,
merchant: basic_info_models.Merchant,
customer: basic_info_models.Customer,
) -> Decimal:
balance = models.CustomerBalance.objects.filter(
merchant=merchant,
customer=customer,
).first()
if balance is None:
return Decimal('0')
return balance.balance
@staticmethod
def get_supplier_balance(
*,
merchant: basic_info_models.Merchant,
supplier: basic_info_models.Supplier,
) -> Decimal:
balance = models.SupplierBalance.objects.filter(
merchant=merchant,
supplier=supplier,
).first()
if balance is None:
return Decimal('0')
return balance.balance
logger = logging.getLogger(__name__)
@@ -296,12 +356,37 @@ def review_payment_order(
if order.status == target_status:
return order
with transaction.atomic():
order.status = target_status
order.save(update_fields=['status', 'updated_at'])
if target_status == models.PaymentOrderStatusEnum.APPROVED:
if order.status == models.PaymentOrderStatusEnum.CANCELLED:
raise ValueError('作废状态的付款单无法再次审批')
with transaction.atomic():
locked = models.PaymentOrder.objects.select_related(
'merchant', 'supplier'
).select_for_update().get(id=order.id)
if locked.status == models.PaymentOrderStatusEnum.APPROVED:
return locked
if locked.status == models.PaymentOrderStatusEnum.CANCELLED:
raise ValueError('作废状态的付款单无法再次审批')
locked.status = models.PaymentOrderStatusEnum.APPROVED
locked.save(update_fields=['status', 'updated_at'])
BalanceService.adjust_supplier_balance(
merchant=locked.merchant,
supplier=locked.supplier,
delta=-locked.amount,
)
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
order.refresh_from_db(fields=['status', 'updated_at'])
return order
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
def review_receipt_order(
@@ -322,12 +407,37 @@ def review_receipt_order(
if order.status == target_status:
return order
with transaction.atomic():
order.status = target_status
order.save(update_fields=['status', 'updated_at'])
if target_status == models.ReceiptOrderStatusEnum.APPROVED:
if order.status == models.ReceiptOrderStatusEnum.CANCELLED:
raise ValueError('作废状态的收款单无法再次审批')
with transaction.atomic():
locked = models.ReceiptOrder.objects.select_related(
'merchant', 'customer'
).select_for_update().get(id=order.id)
if locked.status == models.ReceiptOrderStatusEnum.APPROVED:
return locked
if locked.status == models.ReceiptOrderStatusEnum.CANCELLED:
raise ValueError('作废状态的收款单无法再次审批')
locked.status = models.ReceiptOrderStatusEnum.APPROVED
locked.save(update_fields=['status', 'updated_at'])
BalanceService.adjust_customer_balance(
merchant=locked.merchant,
customer=locked.customer,
delta=-locked.amount,
)
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
order.refresh_from_db(fields=['status', 'updated_at'])
return order
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
def _normalize_order_items(
@@ -449,80 +559,112 @@ def _approve_purchase_order(
purchase_order: models.PurchaseOrder,
reviewed_by,
) -> models.PurchaseOrder:
stock_flow_items = _build_stock_flow_items_from_order(purchase_order)
with transaction.atomic():
purchase_order.status = models.PurchaseOrderStatusEnum.APPROVED
purchase_order.save(update_fields=['status', 'updated_at'])
locked_order = models.PurchaseOrder.objects.select_related(
'merchant', 'warehouse', 'supplier'
).prefetch_related('items').select_for_update().get(id=purchase_order.id)
if locked_order.status == models.PurchaseOrderStatusEnum.APPROVED:
return locked_order
if locked_order.status == models.PurchaseOrderStatusEnum.CANCELLED:
raise ValueError('作废状态的采购单无法再次审批')
stock_flow_items = _build_stock_flow_items_from_order(locked_order)
locked_order.status = models.PurchaseOrderStatusEnum.APPROVED
locked_order.save(update_fields=['status', 'updated_at'])
BalanceService.adjust_supplier_balance(
merchant=locked_order.merchant,
supplier=locked_order.supplier,
delta=locked_order.get_total_amount(),
)
created_by_id = getattr(reviewed_by, 'id', None)
if _auto_stock_task_enabled(purchase_order.merchant):
logger.info('审批通过采购单 %s,触发入库任务', purchase_order.id)
if _auto_stock_task_enabled(locked_order.merchant):
logger.info('审批通过采购单 %s,触发入库任务', locked_order.id)
create_purchase_order_stock_entries.delay(
purchase_order_id=purchase_order.id,
warehouse_id=purchase_order.warehouse_id,
purchase_order_id=locked_order.id,
warehouse_id=locked_order.warehouse_id,
items=stock_flow_items,
created_by_id=created_by_id,
)
purchase_order.refresh_from_db(fields=['status', 'updated_at'])
return purchase_order
locked_order.refresh_from_db(fields=['status', 'updated_at'])
return locked_order
def _cancel_purchase_order(purchase_order: models.PurchaseOrder) -> models.PurchaseOrder:
if _order_has_stock_records(
merchant_id=purchase_order.merchant_id,
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
source_id=purchase_order.id,
):
raise ValueError('采购单已生成出入库记录,无法作废')
with transaction.atomic():
purchase_order.status = models.PurchaseOrderStatusEnum.CANCELLED
purchase_order.save(update_fields=['status', 'updated_at'])
purchase_order.refresh_from_db(fields=['status', 'updated_at'])
return purchase_order
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(
merchant_id=locked.merchant_id,
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
source_id=locked.id,
):
raise ValueError('采购单已生成出入库记录,无法作废')
locked.status = models.PurchaseOrderStatusEnum.CANCELLED
locked.save(update_fields=['status', 'updated_at'])
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
def _approve_sales_order(
sales_order: models.SalesOrder,
reviewed_by,
) -> models.SalesOrder:
stock_flow_items = _build_stock_flow_items_from_order(sales_order)
with transaction.atomic():
sales_order.status = models.SalesOrderStatusEnum.APPROVED
sales_order.save(update_fields=['status', 'updated_at'])
locked_order = models.SalesOrder.objects.select_related(
'merchant', 'warehouse', 'customer'
).prefetch_related('items').select_for_update().get(id=sales_order.id)
if locked_order.status == models.SalesOrderStatusEnum.APPROVED:
return locked_order
if locked_order.status == models.SalesOrderStatusEnum.CANCELLED:
raise ValueError('作废状态的销售单无法再次审批')
stock_flow_items = _build_stock_flow_items_from_order(locked_order)
locked_order.status = models.SalesOrderStatusEnum.APPROVED
locked_order.save(update_fields=['status', 'updated_at'])
BalanceService.adjust_customer_balance(
merchant=locked_order.merchant,
customer=locked_order.customer,
delta=locked_order.get_total_amount(),
)
created_by_id = getattr(reviewed_by, 'id', None)
if _auto_stock_task_enabled(sales_order.merchant):
logger.info('审批通过销售单 %s,触发出库任务', sales_order.id)
if _auto_stock_task_enabled(locked_order.merchant):
logger.info('审批通过销售单 %s,触发出库任务', locked_order.id)
create_sales_order_stock_entries.delay(
sales_order_id=sales_order.id,
warehouse_id=sales_order.warehouse_id,
sales_order_id=locked_order.id,
warehouse_id=locked_order.warehouse_id,
items=stock_flow_items,
created_by_id=created_by_id,
)
sales_order.refresh_from_db(fields=['status', 'updated_at'])
return sales_order
locked_order.refresh_from_db(fields=['status', 'updated_at'])
return locked_order
def _cancel_sales_order(sales_order: models.SalesOrder) -> models.SalesOrder:
if _order_has_stock_records(
merchant_id=sales_order.merchant_id,
source_type=stock_models.StockChangeSourceEnum.SALES,
source_id=sales_order.id,
):
raise ValueError('销售单已生成出入库记录,无法作废')
with transaction.atomic():
sales_order.status = models.SalesOrderStatusEnum.CANCELLED
sales_order.save(update_fields=['status', 'updated_at'])
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('销售单已生成出入库记录,无法作废')
sales_order.refresh_from_db(fields=['status', 'updated_at'])
return sales_order
locked.status = models.SalesOrderStatusEnum.CANCELLED
locked.save(update_fields=['status', 'updated_at'])
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
def create_purchase_order_stock_entries_sync(