forked from erp-dev/erp
feat: sales return order and purchase return order
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user