1
0
forked from erp-dev/erp

feat: sales return order and purchase return order

This commit is contained in:
2025-12-01 16:52:59 +08:00
parent a3b4af2602
commit fae33809ac
17 changed files with 1745 additions and 6 deletions

View File

@@ -17,6 +17,8 @@ from . import models
from .tasks import (
create_purchase_order_stock_entries,
create_sales_order_stock_entries,
create_purchase_return_order_stock_entries,
create_sales_return_order_stock_entries,
)
class BalanceService:
@staticmethod
@@ -292,6 +294,150 @@ def create_sales_order(
return sales_order
def create_purchase_return_order(
*,
merchant: basic_info_models.Merchant,
supplier: basic_info_models.Supplier,
return_date,
warehouse: basic_info_models.WareHouse,
operator: basic_info_models.Employee,
items: List[Dict[str, Any]],
remarks: str | None = '',
purchase_order: models.PurchaseOrder | None = None,
purchase_order_id: int | None = None,
created_by=None,
) -> models.PurchaseReturnOrder:
"""
创建采购退货单,出库并冲减供应商欠款。
"""
if not items:
raise ValueError('items 不能为空')
normalized_date = _normalize_order_date(return_date)
normalized_items, _ = _normalize_order_items(
merchant=merchant,
warehouse=warehouse,
items=items,
is_outgoing=True,
)
resolved_purchase_order = purchase_order
if resolved_purchase_order is None and purchase_order_id:
resolved_purchase_order = models.PurchaseOrder.objects.filter(
id=purchase_order_id,
merchant=merchant,
).first()
if resolved_purchase_order is None:
raise ValueError('关联的采购单不存在或不属于当前商户')
if resolved_purchase_order and resolved_purchase_order.supplier_id != supplier.id:
raise ValueError('采购退货单的供应商必须与原采购单一致')
with transaction.atomic():
return_order = models.PurchaseReturnOrder.objects.create(
merchant=merchant,
supplier=supplier,
purchase_order=resolved_purchase_order,
return_date=normalized_date,
operator=operator,
warehouse=warehouse,
remarks=remarks,
)
bulk_objects = [
models.PurchaseReturnOrderItem(
purchase_return_order=return_order,
product=item_data['product'],
price=item_data['price'],
color=item_data.get('color'),
quantity=item_data['quantity'],
unit=item_data['unit'],
spec=item_data.get('spec'),
empty_diff_percent=item_data['empty_diff_percent'],
quantity_of_rolls=item_data.get('quantity_of_rolls'),
num_of_rolls=item_data['num_of_rolls'],
consume_detail_ids=item_data.get('consume_detail_ids'),
batch_number=item_data.get('batch_number'),
remarks=item_data.get('remarks'),
)
for item_data in normalized_items
]
models.PurchaseReturnOrderItem.objects.bulk_create(bulk_objects)
return_order.refresh_from_db()
return return_order
def create_sales_return_order(
*,
merchant: basic_info_models.Merchant,
customer: basic_info_models.Customer,
return_date,
warehouse: basic_info_models.WareHouse,
operator: basic_info_models.Employee,
items: List[Dict[str, Any]],
remarks: str | None = '',
sales_order: models.SalesOrder | None = None,
sales_order_id: int | None = None,
created_by=None,
) -> models.SalesReturnOrder:
"""
创建销售退货单,入库并冲减客户欠款。
"""
if not items:
raise ValueError('items 不能为空')
normalized_date = _normalize_order_date(return_date)
normalized_items, _ = _normalize_order_items(
merchant=merchant,
warehouse=warehouse,
items=items,
is_outgoing=False,
)
resolved_sales_order = sales_order
if resolved_sales_order is None and sales_order_id:
resolved_sales_order = models.SalesOrder.objects.filter(
id=sales_order_id,
merchant=merchant,
).first()
if resolved_sales_order is None:
raise ValueError('关联的销售单不存在或不属于当前商户')
if resolved_sales_order and resolved_sales_order.customer_id != customer.id:
raise ValueError('销售退货单的客户必须与原销售单一致')
with transaction.atomic():
return_order = models.SalesReturnOrder.objects.create(
merchant=merchant,
customer=customer,
sales_order=resolved_sales_order,
return_date=normalized_date,
operator=operator,
warehouse=warehouse,
remarks=remarks,
)
bulk_objects = [
models.SalesReturnOrderItem(
sales_return_order=return_order,
product=item_data['product'],
price=item_data['price'],
color=item_data.get('color'),
quantity=item_data['quantity'],
unit=item_data['unit'],
spec=item_data.get('spec'),
empty_diff_percent=item_data['empty_diff_percent'],
quantity_of_rolls=item_data.get('quantity_of_rolls'),
num_of_rolls=item_data['num_of_rolls'],
consume_detail_ids=item_data.get('consume_detail_ids'),
batch_number=item_data.get('batch_number'),
remarks=item_data.get('remarks'),
)
for item_data in normalized_items
]
models.SalesReturnOrderItem.objects.bulk_create(bulk_objects)
return_order.refresh_from_db()
return return_order
def create_payment_order(
*,
merchant: basic_info_models.Merchant,
@@ -409,6 +555,58 @@ def review_sales_order(
return _cancel_sales_order(order)
def review_purchase_return_order(
*,
purchase_return_order: models.PurchaseReturnOrder | None = None,
purchase_return_order_id: int | None = None,
target_status: models.PurchaseReturnStatusEnum,
reviewed_by=None,
) -> models.PurchaseReturnOrder:
order = _resolve_purchase_return_order_instance(purchase_return_order, purchase_return_order_id)
if target_status not in {
models.PurchaseReturnStatusEnum.APPROVED,
models.PurchaseReturnStatusEnum.CANCELLED,
}:
raise ValueError('target_status 只能是 APPROVED 或 CANCELLED')
if order.status == target_status:
return order
if target_status == models.PurchaseReturnStatusEnum.APPROVED:
if order.status == models.PurchaseReturnStatusEnum.CANCELLED:
raise ValueError('作废状态的采购退货单无法再次审批')
return _approve_purchase_return_order(order, reviewed_by)
return _cancel_purchase_return_order(order)
def review_sales_return_order(
*,
sales_return_order: models.SalesReturnOrder | None = None,
sales_return_order_id: int | None = None,
target_status: models.SalesReturnStatusEnum,
reviewed_by=None,
) -> models.SalesReturnOrder:
order = _resolve_sales_return_order_instance(sales_return_order, sales_return_order_id)
if target_status not in {
models.SalesReturnStatusEnum.APPROVED,
models.SalesReturnStatusEnum.CANCELLED,
}:
raise ValueError('target_status 只能是 APPROVED 或 CANCELLED')
if order.status == target_status:
return order
if target_status == models.SalesReturnStatusEnum.APPROVED:
if order.status == models.SalesReturnStatusEnum.CANCELLED:
raise ValueError('作废状态的销售退货单无法再次审批')
return _approve_sales_return_order(order, reviewed_by)
return _cancel_sales_return_order(order)
def review_payment_order(
*,
payment_order: models.PaymentOrder | None = None,
@@ -746,6 +944,128 @@ def _cancel_sales_order(sales_order: models.SalesOrder) -> models.SalesOrder:
return locked
def _approve_purchase_return_order(
purchase_return_order: models.PurchaseReturnOrder,
reviewed_by,
) -> models.PurchaseReturnOrder:
with transaction.atomic():
locked_order = models.PurchaseReturnOrder.objects.select_related(
'merchant', 'warehouse', 'supplier'
).prefetch_related('items').select_for_update().get(id=purchase_return_order.id)
if locked_order.status == models.PurchaseReturnStatusEnum.APPROVED:
return locked_order
if locked_order.status == models.PurchaseReturnStatusEnum.CANCELLED:
raise ValueError('作废状态的采购退货单无法再次审批')
stock_flow_items = _build_stock_flow_items_from_order(locked_order)
locked_order.status = models.PurchaseReturnStatusEnum.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(),
source_type=models.BalanceChangeSourceEnum.PURCHASE_RETURN_ORDER,
source_id=locked_order.id,
)
created_by_id = getattr(reviewed_by, 'id', None)
if _auto_stock_task_enabled(locked_order.merchant):
logger.info('审批通过采购退货单 %s,触发出库任务', locked_order.id)
create_purchase_return_order_stock_entries.delay(
purchase_return_order_id=locked_order.id,
warehouse_id=locked_order.warehouse_id,
items=stock_flow_items,
created_by_id=created_by_id,
)
locked_order.refresh_from_db(fields=['status', 'updated_at'])
return locked_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
def _approve_sales_return_order(
sales_return_order: models.SalesReturnOrder,
reviewed_by,
) -> models.SalesReturnOrder:
with transaction.atomic():
locked_order = models.SalesReturnOrder.objects.select_related(
'merchant', 'warehouse', 'customer'
).prefetch_related('items').select_for_update().get(id=sales_return_order.id)
if locked_order.status == models.SalesReturnStatusEnum.APPROVED:
return locked_order
if locked_order.status == models.SalesReturnStatusEnum.CANCELLED:
raise ValueError('作废状态的销售退货单无法再次审批')
stock_flow_items = _build_stock_flow_items_from_order(locked_order)
locked_order.status = models.SalesReturnStatusEnum.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(),
source_type=models.BalanceChangeSourceEnum.SALES_RETURN_ORDER,
source_id=locked_order.id,
)
created_by_id = getattr(reviewed_by, 'id', None)
if _auto_stock_task_enabled(locked_order.merchant):
logger.info('审批通过销售退货单 %s,触发入库任务', locked_order.id)
create_sales_return_order_stock_entries.delay(
sales_return_order_id=locked_order.id,
warehouse_id=locked_order.warehouse_id,
items=stock_flow_items,
created_by_id=created_by_id,
)
locked_order.refresh_from_db(fields=['status', 'updated_at'])
return locked_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
def create_purchase_order_stock_entries_sync(
*,
purchase_order_id: int,
@@ -826,6 +1146,90 @@ def create_sales_order_stock_entries_sync(
return payload
def create_purchase_return_order_stock_entries_sync(
*,
purchase_return_order_id: int,
warehouse_id: int,
items: List[Dict[str, Any]],
created_by_id: int | None = None,
) -> Dict[str, Any]:
"""
根据采购退货单生成出库记录。
"""
try:
purchase_return_order = models.PurchaseReturnOrder.objects.select_related('merchant').get(
id=purchase_return_order_id
)
except models.PurchaseReturnOrder.DoesNotExist:
logger.error('PurchaseReturnOrder %s 不存在,无法创建出库单', purchase_return_order_id)
return {'error': 'purchase_return_order_not_found', 'purchase_return_order_id': purchase_return_order_id}
merchant = purchase_return_order.merchant
created_by = None
if created_by_id:
UserModel = get_user_model()
created_by = UserModel.objects.filter(id=created_by_id).first()
service = StockFlowService(merchant=merchant, created_by=created_by)
record, details, created_count = service.stock_out(
warehouse_id=warehouse_id,
source_type=stock_models.StockChangeSourceEnum.PURCHASE_RETURN,
source_id=purchase_return_order.id,
items=items,
)
payload = {
'purchase_return_order_id': purchase_return_order.id,
'stock_change_record_id': getattr(record, 'id', None),
'created_details_count': created_count,
}
logger.info('采购退货单 %s 出库任务完成: %s', purchase_return_order.id, payload)
return payload
def create_sales_return_order_stock_entries_sync(
*,
sales_return_order_id: int,
warehouse_id: int,
items: List[Dict[str, Any]],
created_by_id: int | None = None,
) -> Dict[str, Any]:
"""
根据销售退货单生成入库记录。
"""
try:
sales_return_order = models.SalesReturnOrder.objects.select_related('merchant').get(
id=sales_return_order_id
)
except models.SalesReturnOrder.DoesNotExist:
logger.error('SalesReturnOrder %s 不存在,无法创建入库单', sales_return_order_id)
return {'error': 'sales_return_order_not_found', 'sales_return_order_id': sales_return_order_id}
merchant = sales_return_order.merchant
created_by = None
if created_by_id:
UserModel = get_user_model()
created_by = UserModel.objects.filter(id=created_by_id).first()
service = StockFlowService(merchant=merchant, created_by=created_by)
record, details, created_count = service.stock_in(
warehouse_id=warehouse_id,
source_type=stock_models.StockChangeSourceEnum.SALES_RETURN,
source_id=sales_return_order.id,
items=items,
)
payload = {
'sales_return_order_id': sales_return_order.id,
'stock_change_record_id': getattr(record, 'id', None),
'created_details_count': created_count,
}
logger.info('销售退货单 %s 入库任务完成: %s', sales_return_order.id, payload)
return payload
def _build_stock_flow_items_from_order(order) -> List[Dict[str, Any]]:
"""
根据订单明细还原 StockFlowService 所需的 items 结构。
@@ -925,6 +1329,36 @@ def _resolve_sales_order_instance(
)
def _resolve_purchase_return_order_instance(
purchase_return_order: models.PurchaseReturnOrder | None,
purchase_return_order_id: int | None,
) -> models.PurchaseReturnOrder:
if purchase_return_order is None and purchase_return_order_id is None:
raise ValueError('必须提供 purchase_return_order 或 purchase_return_order_id')
if purchase_return_order is not None:
purchase_return_order_id = purchase_return_order.id
return models.PurchaseReturnOrder.objects.select_related('merchant', 'warehouse').prefetch_related('items').get(
id=purchase_return_order_id
)
def _resolve_sales_return_order_instance(
sales_return_order: models.SalesReturnOrder | None,
sales_return_order_id: int | None,
) -> models.SalesReturnOrder:
if sales_return_order is None and sales_return_order_id is None:
raise ValueError('必须提供 sales_return_order 或 sales_return_order_id')
if sales_return_order is not None:
sales_return_order_id = sales_return_order.id
return models.SalesReturnOrder.objects.select_related('merchant', 'warehouse').prefetch_related('items').get(
id=sales_return_order_id
)
def _resolve_payment_order_instance(
payment_order: models.PaymentOrder | None,
payment_order_id: int | None,