1
0
forked from erp-dev/erp
Files
erpnew/business/services.py
2026-05-19 23:41:34 +08:00

2392 lines
91 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import logging
from collections import OrderedDict
from datetime import date, datetime
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from typing import Any, Dict, Iterable, List, Tuple, Optional
from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone
from basic_info import models as basic_info_models
from stock import models as stock_models
from stock.services import StockFlowService
from basic_info.services import MerchantSettingService
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
def adjust_supplier_balance(
*,
merchant: basic_info_models.Merchant,
supplier: basic_info_models.Supplier,
delta: Decimal,
source_type: models.BalanceChangeSourceEnum,
source_id: int,
request_id: str | None = None,
remarks: str | None = '',
extra_meta: Dict[str, Any] | None = None,
):
BalanceService._adjust_balance(
merchant=merchant,
counterparty=supplier,
balance_model=models.SupplierBalance,
balance_field='supplier',
delta=delta,
target_type=models.BalanceChangeTargetEnum.SUPPLIER,
source_type=source_type,
source_id=source_id,
request_id=request_id,
remarks=remarks,
extra_meta=extra_meta,
)
@staticmethod
def adjust_customer_balance(
*,
merchant: basic_info_models.Merchant,
customer: basic_info_models.Customer,
delta: Decimal,
source_type: models.BalanceChangeSourceEnum,
source_id: int,
request_id: str | None = None,
remarks: str | None = '',
extra_meta: Dict[str, Any] | None = None,
):
BalanceService._adjust_balance(
merchant=merchant,
counterparty=customer,
balance_model=models.CustomerBalance,
balance_field='customer',
delta=delta,
target_type=models.BalanceChangeTargetEnum.CUSTOMER,
source_type=source_type,
source_id=source_id,
request_id=request_id,
remarks=remarks,
extra_meta=extra_meta,
)
@staticmethod
def _adjust_balance(
*,
merchant: basic_info_models.Merchant,
counterparty,
balance_model,
balance_field: str,
delta: Decimal,
target_type: models.BalanceChangeTargetEnum,
source_type: models.BalanceChangeSourceEnum,
source_id: int,
request_id: str | None,
remarks: str | None,
extra_meta: Dict[str, Any] | None,
):
meta_payload = extra_meta or {}
remarks_value = remarks or ''
with transaction.atomic():
balance, _ = balance_model.objects.select_for_update().get_or_create(
merchant=merchant,
defaults={'balance': Decimal('0')},
**{balance_field: counterparty},
)
before = balance.balance
balance.balance += delta
balance.save(update_fields=['balance', 'updated_at'])
record_kwargs = {
'merchant': merchant,
'target_type': target_type,
'source_type': source_type,
'source_id': source_id,
'delta': delta,
'direction': (
models.BalanceChangeDirectionEnum.INCREASE
if delta >= 0
else models.BalanceChangeDirectionEnum.DECREASE
),
'balance_before': before,
'balance_after': balance.balance,
'request_id': request_id,
'remarks': remarks_value,
'extra_meta': meta_payload,
}
if target_type == models.BalanceChangeTargetEnum.SUPPLIER:
record_kwargs['supplier'] = counterparty
else:
record_kwargs['customer'] = counterparty
models.BalanceChangeRecord.objects.create(**record_kwargs)
@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__)
def _normalize_order_date(value) -> date:
if isinstance(value, date):
return value
if isinstance(value, datetime):
return value.date()
if isinstance(value, str):
try:
return date.fromisoformat(value)
except ValueError as exc:
raise ValueError('order_date 格式不正确,应为 YYYY-MM-DD') from exc
raise ValueError('order_date 格式不正确')
def create_purchase_order(
*,
merchant: basic_info_models.Merchant,
supplier: basic_info_models.Supplier,
order_date,
warehouse: basic_info_models.WareHouse,
operator: basic_info_models.Employee,
items: List[Dict[str, Any]],
remarks: str | None = '',
created_by=None,
from_pre_purchase_order_id: int | None = None,
) -> models.PurchaseOrder:
"""
创建采购订单并触发异步创建入库单任务。
Args:
merchant: 采购单所属商户
supplier: 供应商
order_date: 订单日期 (date)
warehouse: 入库仓库实例
operator: 经办人
items: 产品明细,字段会根据仓库模式校验
remarks: 备注
created_by: 创建者用户(可选,用于 stock 记录中的 created_by
"""
if not items:
raise ValueError('items 不能为空')
normalized_date = _normalize_order_date(order_date)
purchase_items, stock_flow_items = _normalize_order_items(
merchant=merchant,
warehouse=warehouse,
items=items,
is_outgoing=False,
)
with transaction.atomic():
purchase_order = models.PurchaseOrder.objects.create(
merchant=merchant,
supplier=supplier,
purchase_date=normalized_date,
operator=operator,
warehouse=warehouse,
remarks=remarks,
from_pre_purchase_order_id=from_pre_purchase_order_id,
)
bulk_objects = [
models.PurchaseOrderItem(
purchase_order=purchase_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'],
batch_number=item_data.get('batch_number'),
remarks=item_data.get('remarks'),
)
for item_data in purchase_items
]
models.PurchaseOrderItem.objects.bulk_create(bulk_objects)
purchase_order.refresh_from_db()
return purchase_order
def update_purchase_order(
*,
purchase_order: models.PurchaseOrder,
supplier: basic_info_models.Supplier | None = None,
order_date=None,
warehouse: basic_info_models.WareHouse | None = None,
items: List[Dict[str, Any]] | None = None,
remarks: str | None = '',
from_pre_purchase_order_id: int | None = None,
) -> models.PurchaseOrder:
"""
更新采购订单(仅限审批中状态)。
"""
_ensure_order_pending(purchase_order, models.PurchaseOrderStatusEnum.PENDING, '采购单')
merchant = purchase_order.merchant
new_supplier = supplier or purchase_order.supplier
new_warehouse = warehouse or purchase_order.warehouse
new_order_date = _normalize_order_date(order_date or purchase_order.purchase_date)
remarks = remarks if remarks is not None else purchase_order.remarks
if not items:
raise ValueError('items 需要为非空数组')
normalized_items, _ = _normalize_order_items(
merchant=merchant,
warehouse=new_warehouse,
items=items,
is_outgoing=False,
)
with transaction.atomic():
purchase_order.supplier = new_supplier
purchase_order.purchase_date = new_order_date
purchase_order.warehouse = new_warehouse
purchase_order.remarks = remarks
if from_pre_purchase_order_id is not None:
purchase_order.from_pre_purchase_order_id = from_pre_purchase_order_id
purchase_order.save(
update_fields=[
'supplier',
'purchase_date',
'warehouse',
'remarks',
'from_pre_purchase_order_id',
'updated_at',
]
)
purchase_order.items.all().delete()
bulk_objects = [
models.PurchaseOrderItem(
purchase_order=purchase_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'],
batch_number=item_data.get('batch_number'),
remarks=item_data.get('remarks'),
)
for item_data in normalized_items
]
models.PurchaseOrderItem.objects.bulk_create(bulk_objects)
purchase_order.refresh_from_db()
return purchase_order
def create_sales_order(
*,
merchant: basic_info_models.Merchant,
customer: basic_info_models.Customer,
order_date,
warehouse: basic_info_models.WareHouse,
operator: basic_info_models.Employee,
items: List[Dict[str, Any]],
remarks: str | None = '',
created_by=None,
from_pre_sales_order_id: int | None = None,
) -> models.SalesOrder:
"""
创建销售订单,后续审批通过后会触发出库任务。
"""
if not items:
raise ValueError('items 不能为空')
normalized_date = _normalize_order_date(order_date)
sales_items, stock_flow_items = _normalize_order_items(
merchant=merchant,
warehouse=warehouse,
items=items,
is_outgoing=True,
customer=customer,
)
with transaction.atomic():
sales_order = models.SalesOrder.objects.create(
merchant=merchant,
customer=customer,
sales_date=normalized_date,
operator=operator,
warehouse=warehouse,
remarks=remarks,
from_pre_sales_order_id=from_pre_sales_order_id,
)
bulk_objects = [
models.SalesOrderItem(
sales_order=sales_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'),
order_quantity=item_data.get('order_quantity'),
remarks=item_data.get('remarks'),
printing_job=item_data.get('printing_job'),
)
for item_data in sales_items
]
models.SalesOrderItem.objects.bulk_create(bulk_objects)
sales_order.refresh_from_db()
return sales_order
def update_sales_order(
*,
sales_order: models.SalesOrder,
customer: basic_info_models.Customer | None = None,
order_date=None,
warehouse: basic_info_models.WareHouse | None = None,
operator: basic_info_models.Employee | None = None,
items: List[Dict[str, Any]] | None = None,
remarks: str | None = '',
) -> models.SalesOrder:
"""
更新销售订单(仅限审批中状态)。
"""
_ensure_order_pending(sales_order, models.SalesOrderStatusEnum.PENDING, '销售单')
merchant = sales_order.merchant
new_customer = customer or sales_order.customer
new_warehouse = warehouse or sales_order.warehouse
new_operator = operator or sales_order.operator
new_order_date = _normalize_order_date(order_date or sales_order.sales_date)
remarks = remarks if remarks is not None else sales_order.remarks
if not items:
raise ValueError('items 需要为非空数组')
sales_items, stock_flow_items = _normalize_order_items(
merchant=merchant,
warehouse=new_warehouse,
items=items,
is_outgoing=True,
customer=new_customer,
)
with transaction.atomic():
sales_order.customer = new_customer
sales_order.sales_date = new_order_date
sales_order.warehouse = new_warehouse
sales_order.operator = new_operator
sales_order.remarks = remarks
sales_order.save(
update_fields=['customer', 'sales_date', 'warehouse', 'operator', 'remarks', 'updated_at']
)
sales_order.items.all().delete()
bulk_objects = [
models.SalesOrderItem(
sales_order=sales_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'),
order_quantity=item_data.get('order_quantity'),
remarks=item_data.get('remarks'),
printing_job=item_data.get('printing_job'),
)
for item_data in sales_items
]
models.SalesOrderItem.objects.bulk_create(bulk_objects)
sales_order.refresh_from_db()
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 update_purchase_return_order(
*,
purchase_return_order: models.PurchaseReturnOrder,
supplier: basic_info_models.Supplier | None = None,
return_date=None,
warehouse: basic_info_models.WareHouse | None = None,
items: List[Dict[str, Any]] | None = None,
remarks: str | None = '',
purchase_order: models.PurchaseOrder | None = None,
purchase_order_id: int | None = None,
) -> models.PurchaseReturnOrder:
"""
更新采购退货单(仅限审批中状态)。
"""
_ensure_order_pending(purchase_return_order, models.PurchaseReturnStatusEnum.PENDING, '采购退货单')
merchant = purchase_return_order.merchant
new_supplier = supplier or purchase_return_order.supplier
new_warehouse = warehouse or purchase_return_order.warehouse
new_return_date = _normalize_order_date(return_date or purchase_return_order.return_date)
remarks = remarks if remarks is not None else purchase_return_order.remarks
if not items:
raise ValueError('items 需要为非空数组')
resolved_purchase_order = purchase_order or purchase_return_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 != new_supplier.id:
raise ValueError('采购退货单的供应商必须与原采购单一致')
normalized_items, _ = _normalize_order_items(
merchant=merchant,
warehouse=new_warehouse,
items=items,
is_outgoing=True,
)
with transaction.atomic():
purchase_return_order.supplier = new_supplier
purchase_return_order.return_date = new_return_date
purchase_return_order.warehouse = new_warehouse
purchase_return_order.remarks = remarks
purchase_return_order.purchase_order = resolved_purchase_order
purchase_return_order.save(
update_fields=['supplier', 'return_date', 'warehouse', 'remarks', 'purchase_order', 'updated_at']
)
purchase_return_order.items.all().delete()
bulk_objects = [
models.PurchaseReturnOrderItem(
purchase_return_order=purchase_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)
purchase_return_order.refresh_from_db()
return purchase_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 update_sales_return_order(
*,
sales_return_order: models.SalesReturnOrder,
customer: basic_info_models.Customer | None = None,
return_date=None,
warehouse: basic_info_models.WareHouse | None = None,
items: List[Dict[str, Any]] | None = None,
remarks: str | None = '',
sales_order: models.SalesOrder | None = None,
sales_order_id: int | None = None,
) -> models.SalesReturnOrder:
"""
更新销售退货单(仅限审批中状态)。
"""
_ensure_order_pending(sales_return_order, models.SalesReturnStatusEnum.PENDING, '销售退货单')
merchant = sales_return_order.merchant
new_customer = customer or sales_return_order.customer
new_warehouse = warehouse or sales_return_order.warehouse
new_return_date = _normalize_order_date(return_date or sales_return_order.return_date)
remarks = remarks if remarks is not None else sales_return_order.remarks
if not items:
raise ValueError('items 需要为非空数组')
resolved_sales_order = sales_order or sales_return_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 != new_customer.id:
raise ValueError('销售退货单的客户必须与原销售单一致')
normalized_items, _ = _normalize_order_items(
merchant=merchant,
warehouse=new_warehouse,
items=items,
is_outgoing=False,
)
with transaction.atomic():
sales_return_order.customer = new_customer
sales_return_order.return_date = new_return_date
sales_return_order.warehouse = new_warehouse
sales_return_order.remarks = remarks
sales_return_order.sales_order = resolved_sales_order
sales_return_order.save(
update_fields=['customer', 'return_date', 'warehouse', 'remarks', 'sales_order', 'updated_at']
)
sales_return_order.items.all().delete()
bulk_objects = [
models.SalesReturnOrderItem(
sales_return_order=sales_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)
sales_return_order.refresh_from_db()
return sales_return_order
def create_payment_order(
*,
merchant: basic_info_models.Merchant,
supplier: basic_info_models.Supplier,
payment_date,
amount,
operator: basic_info_models.Employee,
remarks: str | None = '',
bank_account: basic_info_models.BankAccount | None = None,
markup: str | None = None,
discount_amount=None,
) -> models.PaymentOrder:
"""
创建付款单(资金流出)。
"""
normalized_date = _normalize_order_date(payment_date)
normalized_amount = _ensure_non_zero_amount(amount, 'amount')
normalized_discount = _ensure_non_negative_amount(discount_amount, 'discount_amount')
if bank_account and bank_account.merchant_id != merchant.id:
raise ValueError('银行账户不属于当前商户')
with transaction.atomic():
payment_order = models.PaymentOrder.objects.create(
merchant=merchant,
supplier=supplier,
payment_date=normalized_date,
amount=normalized_amount,
operator=operator,
remarks=remarks,
bank_account=bank_account,
markup=markup or None,
discount_amount=normalized_discount,
)
payment_order.refresh_from_db()
return payment_order
def create_receipt_order(
*,
merchant: basic_info_models.Merchant,
customer: basic_info_models.Customer,
receipt_date,
amount,
operator: basic_info_models.Employee,
remarks: str | None = '',
bank_account: basic_info_models.BankAccount | None = None,
markup: str | None = None,
discount_amount=None,
) -> models.ReceiptOrder:
"""
创建收款单(资金流入)。
"""
normalized_date = _normalize_order_date(receipt_date)
normalized_amount = _ensure_non_zero_amount(amount, 'amount')
normalized_discount = _ensure_non_negative_amount(discount_amount, 'discount_amount')
if bank_account and bank_account.merchant_id != merchant.id:
raise ValueError('银行账户不属于当前商户')
with transaction.atomic():
receipt_order = models.ReceiptOrder.objects.create(
merchant=merchant,
customer=customer,
receipt_date=normalized_date,
amount=normalized_amount,
operator=operator,
remarks=remarks,
bank_account=bank_account,
markup=markup or None,
discount_amount=normalized_discount,
)
receipt_order.refresh_from_db()
return receipt_order
def review_purchase_order(
*,
purchase_order: models.PurchaseOrder | None = None,
purchase_order_id: int | None = None,
target_status: models.PurchaseOrderStatusEnum,
reviewed_by=None,
) -> models.PurchaseOrder:
"""
审批或作废采购单。
当目标状态为 APPROVED 且开启自动入库任务时,将触发入库 Celery 任务;
当目标状态为 CANCELLED 时,会在更新状态前确认未生成任何出入库单。
"""
order = _resolve_purchase_order_instance(purchase_order, purchase_order_id)
if target_status not in {
models.PurchaseOrderStatusEnum.APPROVED,
models.PurchaseOrderStatusEnum.CANCELLED,
}:
raise ValueError('target_status 只能是 APPROVED 或 CANCELLED')
if order.status == target_status:
return order
if target_status == models.PurchaseOrderStatusEnum.APPROVED:
if order.status == models.PurchaseOrderStatusEnum.CANCELLED:
raise ValueError('作废状态的采购单无法再次审批')
return _approve_purchase_order(order, reviewed_by)
return _cancel_purchase_order(order)
def review_sales_order(
*,
sales_order: models.SalesOrder | None = None,
sales_order_id: int | None = None,
target_status: models.SalesOrderStatusEnum,
reviewed_by=None,
) -> models.SalesOrder:
"""
审批或作废销售单。
"""
order = _resolve_sales_order_instance(sales_order, sales_order_id)
if target_status not in {
models.SalesOrderStatusEnum.APPROVED,
models.SalesOrderStatusEnum.CANCELLED,
}:
raise ValueError('target_status 只能是 APPROVED 或 CANCELLED')
if order.status == target_status:
return order
if target_status == models.SalesOrderStatusEnum.APPROVED:
if order.status == models.SalesOrderStatusEnum.CANCELLED:
raise ValueError('作废状态的销售单无法再次审批')
return _approve_sales_order(order, reviewed_by)
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,
payment_order_id: int | None = None,
target_status: models.PaymentOrderStatusEnum,
reviewed_by=None,
) -> models.PaymentOrder:
order = _resolve_payment_order_instance(payment_order, payment_order_id)
if target_status not in {
models.PaymentOrderStatusEnum.APPROVED,
models.PaymentOrderStatusEnum.CANCELLED,
}:
raise ValueError('target_status 只能是 APPROVED 或 CANCELLED')
if order.status == target_status:
return order
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.settlement_amount,
source_type=models.BalanceChangeSourceEnum.PAYMENT_ORDER,
source_id=locked.id,
)
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
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(
*,
receipt_order: models.ReceiptOrder | None = None,
receipt_order_id: int | None = None,
target_status: models.ReceiptOrderStatusEnum,
reviewed_by=None,
) -> models.ReceiptOrder:
order = _resolve_receipt_order_instance(receipt_order, receipt_order_id)
if target_status not in {
models.ReceiptOrderStatusEnum.APPROVED,
models.ReceiptOrderStatusEnum.CANCELLED,
}:
raise ValueError('target_status 只能是 APPROVED 或 CANCELLED')
if order.status == target_status:
return order
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.settlement_amount,
source_type=models.BalanceChangeSourceEnum.RECEIPT_ORDER,
source_id=locked.id,
)
locked.refresh_from_db(fields=['status', 'updated_at'])
return locked
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(
*,
merchant: basic_info_models.Merchant,
warehouse: basic_info_models.WareHouse,
items: List[Dict[str, Any]],
is_outgoing: bool,
customer: Optional[basic_info_models.Customer] = None,
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
"""
根据仓库模式校验订单明细,并返回:
- normalized_items: 用于创建订单明细模型
- stock_flow_items: 传递给 StockFlowService 的 items 结构
is_outgoing=True 表示出库(销售等),需要额外校验 consume_detail_ids。
"""
normalized_items: List[Dict[str, Any]] = []
stock_flow_items: List[Dict[str, Any]] = []
warehouse_mode = warehouse.mode
for index, raw_item in enumerate(items):
product_id = raw_item.get('product_id')
if not product_id:
raise ValueError(f'items[{index}].product_id 不能为空')
try:
product = basic_info_models.Product.objects.get(id=product_id, merchant=merchant)
except basic_info_models.Product.DoesNotExist as exc:
raise ValueError(f'产品 {product_id} 不存在或不属于当前商户') from exc
price = _to_decimal(raw_item.get('price', '0'), f'items[{index}].price')
empty_diff_percent = _to_decimal(raw_item.get('empty_diff_percent', '0'), f'items[{index}].empty_diff_percent')
color = raw_item.get('color')
batch_number = raw_item.get('batch_number')
remarks = raw_item.get('remarks')
spec = raw_item.get('spec')
order_quantity = _to_non_negative_int_or_none(
raw_item.get('order_quantity'),
f'items[{index}].order_quantity',
)
unit = raw_item.get('unit') or product.get_unit_display() or ''
printing_job = None
raw_printing_job_id = raw_item.get('printing_job') or raw_item.get('printing_job_id')
if raw_printing_job_id is not None:
try:
printing_job_id = int(raw_printing_job_id)
except (TypeError, ValueError):
raise ValueError(f'items[{index}].printing_job 必须为数字')
from printing import models as printing_models
try:
printing_job = printing_models.PrintingJob.objects.select_related(
'product', 'printing_order__customer'
).get(id=printing_job_id)
except printing_models.PrintingJob.DoesNotExist as exc:
raise ValueError(f'items[{index}].printing_job 不存在或已删除') from exc
if printing_job.product_id != product.id:
raise ValueError(f'items[{index}].printing_job 对应的产品与当前明细不一致')
if customer and printing_job.printing_order and printing_job.printing_order.customer_id != customer.id:
raise ValueError(f'items[{index}].printing_job 客户不匹配')
quantity = 0
num_of_rolls = 0
quantity_of_rolls = None
consume_detail_ids_str = None
if warehouse_mode == basic_info_models.WareHouseModeEnum.UNRESTRICTED:
if 'numbers' in raw_item and raw_item['numbers']:
raise ValueError(f'仓库为宽进/宽出模式items[{index}] 不应提供 numbers')
quantity = _to_positive_int(raw_item.get('quantity'), f'items[{index}].quantity')
num_of_rolls = _to_positive_int(raw_item.get('num_of_rolls'), f'items[{index}].num_of_rolls')
stock_flow_items.append({
'product_id': product.id,
'value': str(quantity),
'num_of_rolls': num_of_rolls,
})
elif warehouse_mode == basic_info_models.WareHouseModeEnum.RESTRICT_IN:
numbers = raw_item.get('numbers')
if not numbers or not isinstance(numbers, list):
raise ValueError(f'仓库为严进宽出模式items[{index}] 需要提供 numbers 数组')
normalized_numbers = [
str(_to_positive_int(value, f'items[{index}].numbers[{pos}]'))
for pos, value in enumerate(numbers)
]
num_of_rolls = len(normalized_numbers)
quantity = sum(int(val) for val in normalized_numbers)
quantity_of_rolls = ','.join(normalized_numbers)
stock_flow_items.append({
'product_id': product.id,
'quantities': normalized_numbers,
})
else: # WareHouseModeEnum.RESTRICT_IN_OUT
if is_outgoing:
consume_ids = raw_item.get('consume_detail_ids')
if not consume_ids or not isinstance(consume_ids, list):
raise ValueError(f'仓库为严进严出模式items[{index}] 需要提供 consume_detail_ids 数组')
normalized_ids = [
_to_positive_int(value, f'items[{index}].consume_detail_ids[{pos}]')
for pos, value in enumerate(consume_ids)
]
consume_detail_ids_str = ','.join(str(value) for value in normalized_ids)
quantity = _to_positive_int(raw_item.get('quantity'), f'items[{index}].quantity')
num_of_rolls = len(normalized_ids)
details_by_id = stock_models.StockChangeDetail.objects.select_related(
'stock_change_record'
).filter(
id__in=normalized_ids,
merchant=merchant,
).in_bulk()
if len(details_by_id) == len(normalized_ids):
quantities: List[str] = []
for pos, detail_id in enumerate(normalized_ids):
detail = details_by_id[detail_id]
if detail.product_id != product.id:
raise ValueError(
f'items[{index}].consume_detail_ids[{pos}] 产品不匹配'
)
if detail.stock_change_record.warehouse_id != warehouse.id:
raise ValueError(
f'items[{index}].consume_detail_ids[{pos}] 仓库不匹配'
)
quantities.append(format(detail.quantity.normalize(), 'f'))
quantity_of_rolls = ','.join(quantities)
stock_flow_items.append({
'product_id': product.id,
'consume_detail_ids': normalized_ids,
})
else:
numbers = raw_item.get('numbers')
if not numbers or not isinstance(numbers, list):
raise ValueError(f'仓库为严进严出模式items[{index}] 需要提供 numbers 数组')
normalized_numbers = [
str(_to_positive_int(value, f'items[{index}].numbers[{pos}]'))
for pos, value in enumerate(numbers)
]
num_of_rolls = len(normalized_numbers)
quantity = sum(int(val) for val in normalized_numbers)
quantity_of_rolls = ','.join(normalized_numbers)
stock_flow_items.append({
'product_id': product.id,
'quantities': normalized_numbers,
})
normalized_items.append({
'product': product,
'price': price,
'color': color,
'quantity': quantity,
'unit': unit,
'empty_diff_percent': empty_diff_percent,
'quantity_of_rolls': quantity_of_rolls,
'num_of_rolls': num_of_rolls,
'batch_number': batch_number,
'remarks': remarks,
'spec': spec,
'order_quantity': order_quantity,
'consume_detail_ids': consume_detail_ids_str,
'printing_job': printing_job,
})
return normalized_items, stock_flow_items
def _approve_purchase_order(
purchase_order: models.PurchaseOrder,
reviewed_by,
) -> models.PurchaseOrder:
with transaction.atomic():
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(),
source_type=models.BalanceChangeSourceEnum.PURCHASE_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_order_stock_entries.delay(
purchase_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_order(purchase_order: models.PurchaseOrder) -> models.PurchaseOrder:
with transaction.atomic():
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:
with transaction.atomic():
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(),
source_type=models.BalanceChangeSourceEnum.SALES_ORDER,
source_id=locked_order.id,
)
consume_detail_ids: List[int] = []
for item in locked_order.items.all():
raw_ids = (item.consume_detail_ids or '').strip()
if not raw_ids:
continue
consume_detail_ids.extend(
int(value)
for value in raw_ids.split(',')
if value.strip()
)
if consume_detail_ids:
stock_models.StockFreeze.objects.filter(
merchant=locked_order.merchant,
stock_detail_id__in=consume_detail_ids,
status=stock_models.StockFreezeStatusEnum.FROZEN,
).update(
status=stock_models.StockFreezeStatusEnum.COMPLETED,
completed_by=reviewed_by,
completed_at=timezone.now(),
completed_with=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_order_stock_entries.delay(
sales_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_order(sales_order: models.SalesOrder) -> models.SalesOrder:
with transaction.atomic():
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('销售单已生成出入库记录,无法作废')
locked.status = models.SalesOrderStatusEnum.CANCELLED
locked.save(update_fields=['status', 'updated_at'])
locked.refresh_from_db(fields=['status', 'updated_at'])
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,
warehouse_id: int,
items: List[Dict[str, Any]],
created_by_id: int | None = None,
) -> Dict[str, Any]:
"""
根据采购单生成入库记录。
"""
try:
purchase_order = models.PurchaseOrder.objects.select_related('merchant').get(id=purchase_order_id)
except models.PurchaseOrder.DoesNotExist:
logger.error('PurchaseOrder %s 不存在,无法创建入库单', purchase_order_id)
return {'error': 'purchase_order_not_found', 'purchase_order_id': purchase_order_id}
merchant = purchase_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.PURCHASE,
source_id=purchase_order.id,
items=items,
)
payload = {
'purchase_order_id': purchase_order.id,
'stock_change_record_id': getattr(record, 'id', None),
'created_details_count': created_count,
}
logger.info('采购单 %s 入库任务完成: %s', purchase_order.id, payload)
return payload
def create_sales_order_stock_entries_sync(
*,
sales_order_id: int,
warehouse_id: int,
items: List[Dict[str, Any]],
created_by_id: int | None = None,
) -> Dict[str, Any]:
"""
根据销售单生成出库记录。
"""
try:
sales_order = models.SalesOrder.objects.select_related('merchant').get(id=sales_order_id)
except models.SalesOrder.DoesNotExist:
logger.error('SalesOrder %s 不存在,无法创建出库单', sales_order_id)
return {'error': 'sales_order_not_found', 'sales_order_id': sales_order_id}
merchant = sales_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.SALES,
source_id=sales_order.id,
items=items,
)
payload = {
'sales_order_id': sales_order.id,
'stock_change_record_id': getattr(record, 'id', None),
'created_details_count': created_count,
}
logger.info('销售单 %s 出库任务完成: %s', sales_order.id, payload)
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 结构。
"""
warehouse_mode = order.warehouse.mode
items_payload: List[Dict[str, Any]] = []
order_items = order.items.all()
if not order_items:
raise ValueError('订单没有任何明细,无法生成库存记录')
if warehouse_mode == basic_info_models.WareHouseModeEnum.UNRESTRICTED:
for item in order_items:
items_payload.append({
'product_id': item.product_id,
'value': str(item.quantity),
'num_of_rolls': item.num_of_rolls or 1,
})
return items_payload
# 严进 / 严进严出模式
for item in order_items:
consume_ids_raw = getattr(item, 'consume_detail_ids', None)
if consume_ids_raw:
normalized_ids = [value.strip() for value in consume_ids_raw.split(',') if value.strip()]
if not normalized_ids:
raise ValueError('严进严出订单缺少 consume_detail_ids 数据,无法生成库存记录')
items_payload.append({
'product_id': item.product_id,
'consume_detail_ids': [int(value) for value in normalized_ids],
})
continue
raw_numbers = (item.quantity_of_rolls or '').split(',')
normalized_numbers = [value.strip() for value in raw_numbers if value.strip()]
if not normalized_numbers:
raise ValueError('严进仓订单缺少 numbers 数据,无法生成库存记录')
items_payload.append({
'product_id': item.product_id,
'quantities': normalized_numbers,
})
return items_payload
def can_bind_source_id(record: stock_models.StockChangeRecord) -> bool:
"""
判断库存记录是否允许绑定 source_id。
目前默认允许,后续可按业务需要扩展。
"""
return True
def bind_purchase_order_stock_change_record(
*,
purchase_order_id: int,
stock_change_record_id: int,
operator=None,
) -> stock_models.StockChangeRecord:
purchase_order = models.PurchaseOrder.objects.select_related(
'merchant', 'warehouse'
).get(id=purchase_order_id)
if _auto_stock_task_enabled(purchase_order.merchant):
raise ValueError('自动入库任务已开启,无法手动绑定库存记录')
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():
record = stock_models.StockChangeRecord.objects.select_for_update().get(
id=stock_change_record_id
)
if record.source_id is not None:
raise ValueError('库存记录已绑定业务单据')
if record.merchant_id != purchase_order.merchant_id:
raise ValueError('库存记录不属于当前商户')
if record.warehouse_id != purchase_order.warehouse_id:
raise ValueError('库存记录仓库不匹配')
if record.type != stock_models.StockChangeTypeEnum.ADD:
raise ValueError('仅允许绑定入库类型库存记录')
if record.source_type != stock_models.StockChangeSourceEnum.PURCHASE:
raise ValueError('仅允许绑定采购来源的库存记录')
if not can_bind_source_id(record):
raise ValueError('库存记录当前状态不允许绑定')
record.source_id = purchase_order.id
record.save(update_fields=['source_id', 'updated_at'])
operator_id = getattr(operator, 'id', None)
logger.info(
'采购单 %s 绑定库存记录 %s, operator=%s',
purchase_order.id,
record.id,
operator_id,
)
return record
def _auto_stock_task_enabled(merchant: basic_info_models.Merchant) -> bool:
try:
setting = MerchantSettingService.get_setting(
merchant,
basic_info_models.MerchantSettingKeyEnum.AUTO_CREATE_STOCK_CHANGE_TASKS,
)
except basic_info_models.MerchantSetting.DoesNotExist:
logger.warning('商户 %s 未配置 auto_create_stock_change_tasks默认关闭', merchant.id)
return False
return setting.value is True
def _order_has_stock_records(
*,
merchant_id: int,
source_type: stock_models.StockChangeSourceEnum,
source_id: int,
) -> bool:
return stock_models.StockChangeRecord.objects.filter(
merchant_id=merchant_id,
source_type=source_type,
source_id=source_id,
).exists()
def _resolve_purchase_order_instance(
purchase_order: models.PurchaseOrder | None,
purchase_order_id: int | None,
) -> models.PurchaseOrder:
if purchase_order is None and purchase_order_id is None:
raise ValueError('必须提供 purchase_order 或 purchase_order_id')
if purchase_order is not None:
purchase_order_id = purchase_order.id
return models.PurchaseOrder.objects.select_related('merchant', 'warehouse').prefetch_related('items').get(
id=purchase_order_id
)
def _resolve_sales_order_instance(
sales_order: models.SalesOrder | None,
sales_order_id: int | None,
) -> models.SalesOrder:
if sales_order is None and sales_order_id is None:
raise ValueError('必须提供 sales_order 或 sales_order_id')
if sales_order is not None:
sales_order_id = sales_order.id
return models.SalesOrder.objects.select_related('merchant', 'warehouse').prefetch_related('items').get(
id=sales_order_id
)
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,
) -> models.PaymentOrder:
if payment_order is None and payment_order_id is None:
raise ValueError('必须提供 payment_order 或 payment_order_id')
if payment_order is not None:
payment_order_id = payment_order.id
return models.PaymentOrder.objects.select_related('merchant', 'supplier').get(id=payment_order_id)
def _resolve_receipt_order_instance(
receipt_order: models.ReceiptOrder | None,
receipt_order_id: int | None,
) -> models.ReceiptOrder:
if receipt_order is None and receipt_order_id is None:
raise ValueError('必须提供 receipt_order 或 receipt_order_id')
if receipt_order is not None:
receipt_order_id = receipt_order.id
return models.ReceiptOrder.objects.select_related('merchant', 'customer').get(id=receipt_order_id)
def _to_decimal(value, field_name: str) -> Decimal:
try:
return Decimal(str(value))
except (InvalidOperation, TypeError) as exc:
raise ValueError(f'{field_name} 必须是合法数值') from exc
def _ensure_non_zero_amount(value, field_name: str) -> Decimal:
amount = _to_decimal(value, field_name)
# 金额类字段要求非零(允许负数用于处理退款场景)
# 付款单负金额 = 供应商退款,收款单负金额 = 退款给客户
if amount == 0:
raise ValueError(f'{field_name} 不能为 0')
return amount
def _ensure_order_pending(order, pending_status, entity_name: str):
if getattr(order, 'status', None) != pending_status:
raise ValueError(f'{entity_name}当前状态不可修改')
def _ensure_non_negative_amount(value, field_name: str) -> Decimal:
if value in (None, ''):
return Decimal('0')
amount = _to_decimal(value, field_name)
if amount < 0:
raise ValueError(f'{field_name} 不能小于 0')
return amount
def _to_positive_int(value, field_name: str) -> int:
if value is None:
raise ValueError(f'{field_name} 不能为空')
decimal_value = _to_decimal(value, field_name)
if decimal_value <= 0:
raise ValueError(f'{field_name} 必须大于 0')
if decimal_value != decimal_value.to_integral_value():
raise ValueError(f'{field_name} 必须为整数')
return int(decimal_value)
def _to_non_negative_int_or_none(value, field_name: str) -> int | None:
if value in (None, ''):
return None
decimal_value = _to_decimal(value, field_name)
if decimal_value < 0:
raise ValueError(f'{field_name} 不能小于 0')
if decimal_value != decimal_value.to_integral_value():
raise ValueError(f'{field_name} 必须为整数')
return int(decimal_value)
# ==================== Statement Builders ====================
_STATEMENT_TWO_PLACES = Decimal('0.01')
_STATEMENT_ZERO = Decimal('0')
STATEMENT_COUNTERPARTY_CHOICES = (
('customer', '客户'),
('supplier', '供应商'),
)
STATEMENT_ORDER_TYPE_CHOICES = (
('sales_order', '销售单'),
('sales_return_order', '销售退货单'),
('receipt_order', '收款单'),
('external_sales_order', '外部销售单'),
('external_sales_return_order', '外部销售退货单'),
('purchase_order', '采购单'),
('purchase_return_order', '采购退货单'),
('payment_order', '付款单'),
)
def build_customer_statement(
*,
merchant: basic_info_models.Merchant,
customer: basic_info_models.Customer,
) -> Dict[str, Any]:
"""
根据客户历史单据生成对账记录,供多个 API 复用。
"""
local_balance = BalanceService.get_customer_balance(merchant=merchant, customer=customer)
builder = _CustomerStatementBuilder(merchant=merchant, current_balance=local_balance)
records = builder.collect_records(customer)
builder.adjust_current_balance(builder.external_balance_adjustment)
return builder.build_payload(
counterparty_id=customer.id,
counterparty_name=customer.name,
records=records,
)
def build_supplier_statement(
*,
merchant: basic_info_models.Merchant,
supplier: basic_info_models.Supplier,
) -> Dict[str, Any]:
"""
根据供应商历史单据生成对账记录,供多个 API 复用。
"""
balance = BalanceService.get_supplier_balance(merchant=merchant, supplier=supplier)
builder = _SupplierStatementBuilder(merchant=merchant, current_balance=balance)
records = builder.collect_records(supplier)
return builder.build_payload(
counterparty_id=supplier.id,
counterparty_name=supplier.name,
records=records,
)
def build_statement_summary(payload: Dict[str, Any]) -> Dict[str, str]:
"""
通用汇总函数,配合 StatementResponseSerializer 的 summary 字段。
"""
records = payload.get('records', []) or []
total_positive = sum(
(record.get('positive_amount', _STATEMENT_ZERO) for record in records),
_STATEMENT_ZERO,
)
total_negative = sum(
(record.get('negative_amount', _STATEMENT_ZERO) for record in records),
_STATEMENT_ZERO,
)
return {
'positive_total': _decimal_to_string(total_positive),
'negative_total': _decimal_to_string(total_negative),
}
class _StatementBuilder:
def __init__(
self,
*,
merchant: basic_info_models.Merchant,
current_balance: Decimal,
):
self.merchant = merchant
self._current_balance_value = _normalize_statement_amount(current_balance)
self._current_balance_display = _decimal_to_string(self._current_balance_value)
def adjust_current_balance(self, delta: Decimal) -> None:
self._current_balance_value += _normalize_statement_amount(delta)
self._current_balance_display = _decimal_to_string(self._current_balance_value)
def collect_records(self, counterparty) -> List[dict]: # pragma: no cover - interface only
raise NotImplementedError
def build_payload(
self,
*,
counterparty_id: int,
counterparty_name: str,
records: List[dict],
) -> Dict[str, Any]:
sorted_records = self._sort_records(records)
processed_records = self._attach_running_totals(sorted_records)
return {
'counterparty': counterparty_id,
'counterparty_name': counterparty_name,
'records': processed_records,
}
def _build_record(
self,
*,
counterparty_id: int,
counterparty_name: str,
source_type: str,
source_label: str,
source_id: int,
occurred_at,
recorded_at,
status: int,
status_label: str,
warehouse: basic_info_models.WareHouse | None = None,
positive_amount,
negative_amount,
remarks: str | None = '',
items: List[dict] | None = None,
extra: dict | None = None,
) -> dict:
items = items or []
record = {
'counterparty': counterparty_id,
'counterparty_name': counterparty_name,
'source_type': source_type,
'source_label': source_label,
'source_id': source_id,
'occurred_at': occurred_at,
'recorded_at': recorded_at,
'status': status,
'status_label': status_label,
# 注意:收款单/付款单等资金类单据无仓库概念warehouse 将为 None
'warehouse': warehouse,
'positive_amount': _normalize_statement_amount(positive_amount),
'negative_amount': _normalize_statement_amount(negative_amount),
'remarks': remarks or '',
'items': items,
}
if extra:
record['extra'] = extra
return record
def _aggregate_items(self, order_items) -> List[dict]:
aggregated: OrderedDict[tuple, dict] = OrderedDict()
for item in order_items:
product = getattr(item, 'product', None)
product_id = getattr(item, 'product_id', None)
product_name = getattr(product, 'name', '')
unit = getattr(item, 'unit', '')
price = getattr(item, 'price', Decimal('0'))
# 这些字段均来自订单明细(*OrderItem用于在对账单中对 items 进行合理聚合:
# - 同一产品不同颜色/规格应拆分展示,避免合并后信息丢失
color = getattr(item, 'color', None) or ''
spec = getattr(item, 'spec', None) or ''
key = (product_id, product_name, unit, price, color, spec)
if key not in aggregated:
aggregated[key] = {
'product_id': product_id,
'product_name': product_name,
'quantity': Decimal('0'),
'price': price,
'unit': unit,
'color': color,
'spec': spec,
# 统一输出各条数数量(严格模式下为每条数量列表;非严格模式/未填则为空列表)
# 来源于各 *OrderItem 模型新增的 split_quantity_of_rolls() 方法
'quantity_of_rolls': [],
# 条数:来自各 *OrderItem.num_of_rolls。聚合时对相同产品/单价的多行明细累加。
'num_of_rolls': 0,
}
quantity_value = getattr(item, 'quantity', 0) or 0
aggregated[key]['quantity'] += Decimal(str(quantity_value))
num_of_rolls_value = getattr(item, 'num_of_rolls', None)
try:
num_of_rolls_int = int(num_of_rolls_value) if num_of_rolls_value is not None else 0
except (TypeError, ValueError): # pragma: no cover - defensive
num_of_rolls_int = 0
split_quantity_of_rolls = getattr(item, 'split_quantity_of_rolls', None)
if callable(split_quantity_of_rolls):
try:
rolls = split_quantity_of_rolls() or []
except Exception: # pragma: no cover - defensive
rolls = []
if rolls:
aggregated[key]['quantity_of_rolls'].extend(list(rolls))
# 若无 num_of_rolls或为 0兜底使用 rolls 长度
if not num_of_rolls_int:
num_of_rolls_int = len(rolls)
aggregated[key]['num_of_rolls'] += num_of_rolls_int
return list(aggregated.values())
def _sort_records(self, records: Iterable[dict]) -> List[dict]:
return sorted(
records,
key=lambda item: (item['occurred_at'], item['recorded_at'], item['source_id']),
reverse=True,
)
def _attach_running_totals(self, records: List[dict]) -> List[dict]:
running_total = _STATEMENT_ZERO
processed: List[dict] = []
for record in records:
record_copy = dict(record)
record_copy['cumulative_amount'] = _decimal_to_string(running_total)
record_copy['current_balance'] = self._current_balance_display
arrears_amount = self._current_balance_value - running_total
record_copy['arrears_amount'] = _decimal_to_string(arrears_amount)
delta = record_copy['positive_amount'] - record_copy['negative_amount']
running_total += delta
processed.append(record_copy)
return processed
class _CustomerStatementBuilder(_StatementBuilder):
def __init__(self, *, merchant: basic_info_models.Merchant, current_balance: Decimal):
super().__init__(merchant=merchant, current_balance=current_balance)
self.external_balance_adjustment = Decimal('0')
def collect_records(self, customer: basic_info_models.Customer) -> List[dict]:
records: List[dict] = []
records.extend(self._build_sales_records(customer))
records.extend(self._build_sales_return_records(customer))
records.extend(self._build_receipt_records(customer))
records.extend(self._build_external_statement_records(customer))
return records
def _build_sales_records(self, customer: basic_info_models.Customer) -> List[dict]:
qs = (
models.SalesOrder.objects.filter(
merchant=self.merchant,
customer=customer,
status=models.SalesOrderStatusEnum.APPROVED,
)
.select_related('customer', 'warehouse')
.prefetch_related('items__product')
)
records = []
for order in qs:
items = self._aggregate_items(order.items.all())
records.append(
self._build_record(
counterparty_id=order.customer_id,
counterparty_name=order.customer.name,
source_type='sales_order',
source_label='销售单',
source_id=order.id,
occurred_at=order.sales_date,
recorded_at=order.created_at,
status=order.status,
status_label=order.get_status_display(),
warehouse=order.warehouse,
positive_amount=order.get_total_amount(),
negative_amount=_STATEMENT_ZERO,
remarks=order.remarks,
items=items,
)
)
return records
def _build_sales_return_records(self, customer: basic_info_models.Customer) -> List[dict]:
qs = (
models.SalesReturnOrder.objects.filter(
merchant=self.merchant,
customer=customer,
status=models.SalesReturnStatusEnum.APPROVED,
)
.select_related('customer', 'warehouse')
.prefetch_related('items__product')
)
records = []
for order in qs:
items = self._aggregate_items(order.items.all())
records.append(
self._build_record(
counterparty_id=order.customer_id,
counterparty_name=order.customer.name,
source_type='sales_return_order',
source_label='销售退货单',
source_id=order.id,
occurred_at=order.return_date,
recorded_at=order.created_at,
status=order.status,
status_label=order.get_status_display(),
warehouse=order.warehouse,
positive_amount=_STATEMENT_ZERO,
negative_amount=order.get_total_amount(),
remarks=order.remarks,
items=items,
)
)
return records
def _build_receipt_records(self, customer: basic_info_models.Customer) -> List[dict]:
qs = (
models.ReceiptOrder.objects.filter(
merchant=self.merchant,
customer=customer,
status=models.ReceiptOrderStatusEnum.APPROVED,
)
.select_related('customer')
)
records = []
for order in qs:
# ERP 口径:收款单的 ZkJinE 从应收侧扣减(负的 positive_amount
# 已收只展示 FkJinEorder.amount。净效果与 settlement_amount 一致。
discount = order.discount_amount or Decimal('0')
records.append(
self._build_record(
counterparty_id=order.customer_id,
counterparty_name=order.customer.name,
source_type='receipt_order',
source_label='收款单',
source_id=order.id,
occurred_at=order.receipt_date,
recorded_at=order.created_at,
status=order.status,
status_label=order.get_status_display(),
positive_amount=-discount,
negative_amount=order.amount,
remarks=order.remarks,
)
)
return records
def _build_external_statement_records(self, customer: basic_info_models.Customer) -> List[dict]:
qs = models.ExternalCustomerStatementOrder.objects.filter(
merchant=self.merchant,
customer=customer,
).order_by('occurred_at', 'recorded_at', 'id')
records = []
for order in qs:
sf = order.sf_amount or Decimal('0')
zk = order.zk_amount or Decimal('0')
if order.category == models.ExternalCustomerStatementCategoryEnum.SALE:
source_type = 'external_sales_order'
source_label = '外部销售单'
# 净应收 = 毛额 - 折扣;现场收款作为 negative_amount 减少欠款
positive_amount = order.total_amount - zk
negative_amount = sf
else:
source_type = 'external_sales_return_order'
source_label = '外部销售退货单'
# 退货减少应收:退货金额 + 退货折扣都减少欠款
positive_amount = _STATEMENT_ZERO
negative_amount = order.total_amount + zk
self.external_balance_adjustment += positive_amount - negative_amount
records.append(
self._build_record(
counterparty_id=order.customer_id,
counterparty_name=order.customer.name,
source_type=source_type,
source_label=source_label,
source_id=order.id,
occurred_at=order.occurred_at,
recorded_at=order.recorded_at or order.created_at,
status=models.ReceiptOrderStatusEnum.APPROVED,
status_label='已同步',
positive_amount=positive_amount,
negative_amount=negative_amount,
remarks=order.remarks,
items=list(order.items_payload or []),
extra={
'external_source_id': order.external_source_id,
'external_customer_id': order.external_customer_id,
'settlement_method': order.settlement_method,
},
)
)
return records
class _SupplierStatementBuilder(_StatementBuilder):
def collect_records(self, supplier: basic_info_models.Supplier) -> List[dict]:
records: List[dict] = []
records.extend(self._build_purchase_records(supplier))
records.extend(self._build_purchase_return_records(supplier))
records.extend(self._build_payment_records(supplier))
return records
def _build_purchase_records(self, supplier: basic_info_models.Supplier) -> List[dict]:
qs = (
models.PurchaseOrder.objects.filter(
merchant=self.merchant,
supplier=supplier,
status=models.PurchaseOrderStatusEnum.APPROVED,
)
.select_related('supplier', 'warehouse')
.prefetch_related('items__product')
)
records = []
for order in qs:
items = self._aggregate_items(order.items.all())
records.append(
self._build_record(
counterparty_id=order.supplier_id,
counterparty_name=order.supplier.name,
source_type='purchase_order',
source_label='采购单',
source_id=order.id,
occurred_at=order.purchase_date,
recorded_at=order.created_at,
status=order.status,
status_label=order.get_status_display(),
warehouse=order.warehouse,
positive_amount=order.get_total_amount(),
negative_amount=_STATEMENT_ZERO,
remarks=order.remarks,
items=items,
)
)
return records
def _build_purchase_return_records(self, supplier: basic_info_models.Supplier) -> List[dict]:
qs = (
models.PurchaseReturnOrder.objects.filter(
merchant=self.merchant,
supplier=supplier,
status=models.PurchaseReturnStatusEnum.APPROVED,
)
.select_related('supplier', 'warehouse')
.prefetch_related('items__product')
)
records = []
for order in qs:
items = self._aggregate_items(order.items.all())
records.append(
self._build_record(
counterparty_id=order.supplier_id,
counterparty_name=order.supplier.name,
source_type='purchase_return_order',
source_label='采购退货单',
source_id=order.id,
occurred_at=order.return_date,
recorded_at=order.created_at,
status=order.status,
status_label=order.get_status_display(),
warehouse=order.warehouse,
positive_amount=_STATEMENT_ZERO,
negative_amount=order.get_total_amount(),
remarks=order.remarks,
items=items,
)
)
return records
def _build_payment_records(self, supplier: basic_info_models.Supplier) -> List[dict]:
qs = (
models.PaymentOrder.objects.filter(
merchant=self.merchant,
supplier=supplier,
status=models.PaymentOrderStatusEnum.APPROVED,
)
.select_related('supplier')
)
records = []
for order in qs:
records.append(
self._build_record(
counterparty_id=order.supplier_id,
counterparty_name=order.supplier.name,
source_type='payment_order',
source_label='付款单',
source_id=order.id,
occurred_at=order.payment_date,
recorded_at=order.created_at,
status=order.status,
status_label=order.get_status_display(),
positive_amount=_STATEMENT_ZERO,
negative_amount=order.get_total_amount(),
remarks=order.remarks,
)
)
return records
def _normalize_statement_amount(value) -> Decimal:
if isinstance(value, Decimal):
decimal_value = value
else:
decimal_value = Decimal(str(value))
return decimal_value.quantize(_STATEMENT_TWO_PLACES, rounding=ROUND_HALF_UP)
def _decimal_to_string(value: Decimal) -> str:
normalized = _normalize_statement_amount(value)
return format(normalized, 'f')