forked from erp-dev/erp
fix: business cancel service merged
This commit is contained in:
@@ -52,13 +52,14 @@
|
||||
## 3. 服务层约定
|
||||
|
||||
- `business.services` 负责 orchestration,与 API 解耦。审批、作废、触发库存等流程必须先在服务层实现,再暴露给 API。
|
||||
- **作废统一实现**:`_cancel_order_impl()` 是所有单据作废的单一入口,通过参数化 `order_model_cls` / `approved_status` / `cancelled_status` / `stock_source_type` 适配不同模型,将 APPROVED 检查统一在 `select_for_update` 锁内完成。
|
||||
- 任何涉及库存的逻辑都必须通过 `stock.services.StockFlowService`,不得直接操作库存模型,保持模块边界清晰。
|
||||
- 红冲/对冲等高级动作应由业务模块提供入口(例如 `PurchaseOrder` 红冲),但最终仍调用库存服务完成实际库存变动。
|
||||
|
||||
## 4. 未来演进建议
|
||||
|
||||
1. **新增单据**:若未来出现调拨单、退货单或更多资金类单据,优先复用 `OrderDirectionMixin + OrderCounterpartyMixin`(如有明细再叠加 `OrderItemsAggregationMixin`),仅通过 `get_direction()` / `get_counterparty_field_name()` 区别方向与主体,减少重复实现。
|
||||
2. **审批/状态机**:采购与销售如需共享状态流转,可提炼状态机或 service 层 mixin,而无需在模型层合并。
|
||||
2. **审批/状态机**:作废逻辑已通过 `_cancel_order_impl()` 统一实现,新增单据的作废只需委托该函数并传入对应参数,无需复制逻辑。
|
||||
3. **统计与报表**:财务/库存统计应依赖 `get_signed_total_amount()` / `get_direction()`,确保采购/销售、退货/正向都能通过统一接口处理。
|
||||
4. **余额审计**:任何会写入 Supplier/CustomerBalance 的流程必须通过 `BalanceService`,以便自动生成 `BalanceChangeRecord`。审批通过后禁止作废,若未来需要冲销,必须新建红冲记录并维护 `offset_to/offset_id` 链路。
|
||||
5. **文档同步**:新增单据或服务时必须更新本文件,描述新增模型如何复用 mixin、如何影响下游模块,保持设计透明。
|
||||
|
||||
@@ -705,24 +705,23 @@ def _normalize_external_statement_order_group(
|
||||
zk_value = abs(_to_decimal(record.get('ZkJinE'), field_name='ZkJinE', default=Decimal('0')))
|
||||
total_amount += amount_value
|
||||
zk_amount += zk_value
|
||||
matched_product = _find_local_product_by_external_product_id(
|
||||
merchant=customer.merchant,
|
||||
external_product_id=str(record.get('HpID') or '').strip(),
|
||||
)
|
||||
product_name = getattr(matched_product, 'name', '') or str(record.get('HpID') or '').strip()
|
||||
unit = str(record.get('JiJiaDW') or '').strip() or getattr(matched_product, 'get_unit_display', lambda: '')()
|
||||
external_product_id = str(record.get('HpID') or '').strip()
|
||||
product_name = str(record.get('HpName') or '').strip() or external_product_id
|
||||
color = str(record.get('YanSe') or '').strip()
|
||||
spec = str(record.get('SeHao') or '').strip()
|
||||
unit = str(record.get('JiJiaDW') or '').strip()
|
||||
item_payload = {
|
||||
'product_id': getattr(matched_product, 'id', None),
|
||||
'product_id': None,
|
||||
'product_name': product_name,
|
||||
'quantity': _decimal_to_string(quantity_value),
|
||||
'price': _decimal_to_string(price_value),
|
||||
'unit': unit,
|
||||
'color': '',
|
||||
'spec': getattr(matched_product, 'spec', '') or '',
|
||||
'color': color,
|
||||
'spec': spec,
|
||||
'quantity_of_rolls': [],
|
||||
'num_of_rolls': int(rolls_value) if rolls_value == rolls_value.to_integral_value() else 0,
|
||||
'external_sub_id': record.get('SubID'),
|
||||
'external_product_id': str(record.get('HpID') or '').strip(),
|
||||
'external_product_id': external_product_id,
|
||||
}
|
||||
items_payload.append(item_payload)
|
||||
for note_key in ('BeiZhu', 'BeiZhuC', 'BeiZhuD', 'MeoD'):
|
||||
|
||||
@@ -973,6 +973,11 @@ class SalesReturnOrderItem(ModelBase):
|
||||
verbose_name = '销售退货明细'
|
||||
verbose_name_plural = '销售退货明细'
|
||||
|
||||
def split_quantity_of_rolls(self) -> List[int]:
|
||||
if self.quantity_of_rolls:
|
||||
return [int(value) for value in self.quantity_of_rolls.split(',') if value.strip()]
|
||||
return []
|
||||
|
||||
def real_quantity(self):
|
||||
return round(self.quantity * (1 - self.empty_diff_percent / 100), 2)
|
||||
|
||||
@@ -984,6 +989,7 @@ class SalesReturnOrderItem(ModelBase):
|
||||
|
||||
|
||||
class ExternalCustomerStatementCategoryEnum(models.TextChoices):
|
||||
|
||||
SALE = 'sale', '外部销售单'
|
||||
SALE_RETURN = 'sale_return', '外部销售退货单'
|
||||
|
||||
@@ -1041,11 +1047,7 @@ class ExternalCustomerStatementOrder(ModelBase):
|
||||
|
||||
def __str__(self):
|
||||
return f'外部对账来源 {self.external_source_id} ({self.category})'
|
||||
|
||||
def split_quantity_of_rolls(self) -> List[int]:
|
||||
if self.quantity_of_rolls:
|
||||
return [int(value) for value in self.quantity_of_rolls.split(',') if value.strip()]
|
||||
return []
|
||||
|
||||
|
||||
|
||||
class PaymentOrderStatusEnum(models.IntegerChoices):
|
||||
|
||||
@@ -140,6 +140,54 @@ class BalanceService:
|
||||
return Decimal('0')
|
||||
return balance.balance
|
||||
|
||||
@staticmethod
|
||||
def get_customer_statement_balance(
|
||||
*,
|
||||
merchant: basic_info_models.Merchant,
|
||||
customer: basic_info_models.Customer,
|
||||
) -> Decimal:
|
||||
"""
|
||||
返回客户对账单口径的当前余额。
|
||||
|
||||
与 get_customer_balance 不同,这里会叠加 ExternalCustomerStatementOrder
|
||||
中尚未写入本地 CustomerBalance 的外部销售/退货净额,使 customer balance API
|
||||
与 statements API 的 current_balance 保持一致。
|
||||
"""
|
||||
return BalanceService.get_customer_balance(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
) + BalanceService.get_customer_external_statement_balance_adjustment(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_customer_external_statement_balance_adjustment(
|
||||
*,
|
||||
merchant: basic_info_models.Merchant,
|
||||
customer: basic_info_models.Customer,
|
||||
) -> Decimal:
|
||||
"""
|
||||
计算外部客户对账单业务单据对余额的净影响。
|
||||
|
||||
公式需与 _CustomerStatementBuilder._build_external_statement_records 保持一致:
|
||||
- 外部销售:毛额 - 折扣 - 现场收款
|
||||
- 外部销售退货:-(退货金额 + 退货折扣)
|
||||
"""
|
||||
adjustment = Decimal('0')
|
||||
qs = models.ExternalCustomerStatementOrder.objects.filter(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
).only('category', 'total_amount', 'zk_amount', 'sf_amount')
|
||||
for order in qs:
|
||||
sf = order.sf_amount or Decimal('0')
|
||||
zk = order.zk_amount or Decimal('0')
|
||||
if order.category == models.ExternalCustomerStatementCategoryEnum.SALE:
|
||||
adjustment += (order.total_amount or Decimal('0')) - zk - sf
|
||||
else:
|
||||
adjustment -= (order.total_amount or Decimal('0')) + zk
|
||||
return adjustment
|
||||
|
||||
@staticmethod
|
||||
def get_supplier_balance(
|
||||
*,
|
||||
@@ -158,7 +206,29 @@ class BalanceService:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _normalize_sales_order_kind(value) -> int:
|
||||
"""
|
||||
校验并归一化销售单类型(kind)。
|
||||
|
||||
None / 空字符串视为未提供,回退到默认类型(大货 WHOLESALE)。
|
||||
其余值必须是 SalesOrderKindEnum 中合法的整数枚举。
|
||||
"""
|
||||
if value in (None, ''):
|
||||
return models.SalesOrderKindEnum.WHOLESALE
|
||||
|
||||
try:
|
||||
kind_value = int(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('kind 必须为合法的销售单类型枚举值') from exc
|
||||
|
||||
if kind_value not in models.SalesOrderKindEnum.values:
|
||||
raise ValueError('kind 不是合法的销售单类型枚举值')
|
||||
|
||||
return kind_value
|
||||
|
||||
|
||||
def _normalize_order_date(value) -> date:
|
||||
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
if isinstance(value, datetime):
|
||||
@@ -324,6 +394,7 @@ def create_sales_order(
|
||||
remarks: str | None = '',
|
||||
created_by=None,
|
||||
from_pre_sales_order_id: int | None = None,
|
||||
kind: int | None = None,
|
||||
) -> models.SalesOrder:
|
||||
"""
|
||||
创建销售订单,后续审批通过后会触发出库任务。
|
||||
@@ -331,7 +402,10 @@ def create_sales_order(
|
||||
if not items:
|
||||
raise ValueError('items 不能为空')
|
||||
|
||||
normalized_kind = _normalize_sales_order_kind(kind)
|
||||
|
||||
normalized_date = _normalize_order_date(order_date)
|
||||
|
||||
sales_items, stock_flow_items = _normalize_order_items(
|
||||
merchant=merchant,
|
||||
warehouse=warehouse,
|
||||
@@ -349,7 +423,9 @@ def create_sales_order(
|
||||
warehouse=warehouse,
|
||||
remarks=remarks,
|
||||
from_pre_sales_order_id=from_pre_sales_order_id,
|
||||
kind=normalized_kind,
|
||||
)
|
||||
|
||||
bulk_objects = [
|
||||
models.SalesOrderItem(
|
||||
sales_order=sales_order,
|
||||
@@ -385,6 +461,7 @@ def update_sales_order(
|
||||
operator: basic_info_models.Employee | None = None,
|
||||
items: List[Dict[str, Any]] | None = None,
|
||||
remarks: str | None = '',
|
||||
kind: int | None = None,
|
||||
) -> models.SalesOrder:
|
||||
"""
|
||||
更新销售订单(仅限审批中状态)。
|
||||
@@ -397,6 +474,9 @@ def update_sales_order(
|
||||
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
|
||||
# kind 未提供时保持原值;提供则校验后更新
|
||||
new_kind = sales_order.kind if kind in (None, '') else _normalize_sales_order_kind(kind)
|
||||
|
||||
|
||||
if not items:
|
||||
raise ValueError('items 需要为非空数组')
|
||||
@@ -415,10 +495,12 @@ def update_sales_order(
|
||||
sales_order.warehouse = new_warehouse
|
||||
sales_order.operator = new_operator
|
||||
sales_order.remarks = remarks
|
||||
sales_order.kind = new_kind
|
||||
sales_order.save(
|
||||
update_fields=['customer', 'sales_date', 'warehouse', 'operator', 'remarks', 'updated_at']
|
||||
update_fields=['customer', 'sales_date', 'warehouse', 'operator', 'remarks', 'kind', 'updated_at']
|
||||
)
|
||||
|
||||
|
||||
sales_order.items.all().delete()
|
||||
bulk_objects = [
|
||||
models.SalesOrderItem(
|
||||
@@ -977,16 +1059,13 @@ def review_payment_order(
|
||||
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
|
||||
return _cancel_order_impl(
|
||||
order=order,
|
||||
order_model_cls=models.PaymentOrder,
|
||||
approved_status=models.PaymentOrderStatusEnum.APPROVED,
|
||||
cancelled_status=models.PaymentOrderStatusEnum.CANCELLED,
|
||||
error_label='付款单',
|
||||
)
|
||||
|
||||
|
||||
def review_receipt_order(
|
||||
@@ -1030,16 +1109,13 @@ def review_receipt_order(
|
||||
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
|
||||
return _cancel_order_impl(
|
||||
order=order,
|
||||
order_model_cls=models.ReceiptOrder,
|
||||
approved_status=models.ReceiptOrderStatusEnum.APPROVED,
|
||||
cancelled_status=models.ReceiptOrderStatusEnum.CANCELLED,
|
||||
error_label='收款单',
|
||||
)
|
||||
|
||||
|
||||
def _normalize_order_items(
|
||||
@@ -1243,23 +1319,52 @@ def _approve_purchase_order(
|
||||
return locked_order
|
||||
|
||||
|
||||
def _cancel_purchase_order(purchase_order: models.PurchaseOrder) -> models.PurchaseOrder:
|
||||
def _cancel_order_impl(
|
||||
*,
|
||||
order,
|
||||
order_model_cls,
|
||||
approved_status,
|
||||
cancelled_status,
|
||||
error_label: str,
|
||||
stock_source_type=None,
|
||||
):
|
||||
"""统一作废实现:禁止作废已审批单据,可选检查出入库记录。
|
||||
|
||||
:param order: 待作废的单据实例(需提供 .id 用于加锁)
|
||||
:param order_model_cls: 单据模型类(用于 select_for_update)
|
||||
:param approved_status: 已审批状态枚举值
|
||||
:param cancelled_status: 作废状态枚举值
|
||||
:param error_label: 中文错误提示中的单据名称
|
||||
:param stock_source_type: StockChangeSourceEnum 值;为 None 则不检查出入库记录
|
||||
:returns: 刷新后的模型实例
|
||||
"""
|
||||
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(
|
||||
locked = order_model_cls.objects.select_for_update().get(id=order.id)
|
||||
if locked.status == approved_status:
|
||||
raise ValueError(f'已审批的{error_label}无法作废')
|
||||
if stock_source_type is not None and _order_has_stock_records(
|
||||
merchant_id=locked.merchant_id,
|
||||
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
source_type=stock_source_type,
|
||||
source_id=locked.id,
|
||||
):
|
||||
raise ValueError('采购单已生成出入库记录,无法作废')
|
||||
locked.status = models.PurchaseOrderStatusEnum.CANCELLED
|
||||
raise ValueError(f'{error_label}已生成出入库记录,无法作废')
|
||||
locked.status = cancelled_status
|
||||
locked.save(update_fields=['status', 'updated_at'])
|
||||
locked.refresh_from_db(fields=['status', 'updated_at'])
|
||||
return locked
|
||||
|
||||
|
||||
def _cancel_purchase_order(purchase_order: models.PurchaseOrder) -> models.PurchaseOrder:
|
||||
return _cancel_order_impl(
|
||||
order=purchase_order,
|
||||
order_model_cls=models.PurchaseOrder,
|
||||
approved_status=models.PurchaseOrderStatusEnum.APPROVED,
|
||||
cancelled_status=models.PurchaseOrderStatusEnum.CANCELLED,
|
||||
error_label='采购单',
|
||||
stock_source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
)
|
||||
|
||||
|
||||
def _approve_sales_order(
|
||||
sales_order: models.SalesOrder,
|
||||
reviewed_by,
|
||||
@@ -1323,22 +1428,14 @@ def _approve_sales_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
|
||||
return _cancel_order_impl(
|
||||
order=sales_order,
|
||||
order_model_cls=models.SalesOrder,
|
||||
approved_status=models.SalesOrderStatusEnum.APPROVED,
|
||||
cancelled_status=models.SalesOrderStatusEnum.CANCELLED,
|
||||
error_label='销售单',
|
||||
stock_source_type=stock_models.StockChangeSourceEnum.SALES,
|
||||
)
|
||||
|
||||
|
||||
def _approve_purchase_return_order(
|
||||
@@ -1384,22 +1481,14 @@ def _approve_purchase_return_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
|
||||
return _cancel_order_impl(
|
||||
order=purchase_return_order,
|
||||
order_model_cls=models.PurchaseReturnOrder,
|
||||
approved_status=models.PurchaseReturnStatusEnum.APPROVED,
|
||||
cancelled_status=models.PurchaseReturnStatusEnum.CANCELLED,
|
||||
error_label='采购退货单',
|
||||
stock_source_type=stock_models.StockChangeSourceEnum.PURCHASE_RETURN,
|
||||
)
|
||||
|
||||
|
||||
def _approve_sales_return_order(
|
||||
@@ -1445,22 +1534,14 @@ def _approve_sales_return_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
|
||||
return _cancel_order_impl(
|
||||
order=sales_return_order,
|
||||
order_model_cls=models.SalesReturnOrder,
|
||||
approved_status=models.SalesReturnStatusEnum.APPROVED,
|
||||
cancelled_status=models.SalesReturnStatusEnum.CANCELLED,
|
||||
error_label='销售退货单',
|
||||
stock_source_type=stock_models.StockChangeSourceEnum.SALES_RETURN,
|
||||
)
|
||||
|
||||
|
||||
def create_purchase_order_stock_entries_sync(
|
||||
@@ -1919,10 +2000,9 @@ def build_customer_statement(
|
||||
"""
|
||||
根据客户历史单据生成对账记录,供多个 API 复用。
|
||||
"""
|
||||
local_balance = BalanceService.get_customer_balance(merchant=merchant, customer=customer)
|
||||
builder = _CustomerStatementBuilder(merchant=merchant, current_balance=local_balance)
|
||||
statement_balance = BalanceService.get_customer_statement_balance(merchant=merchant, customer=customer)
|
||||
builder = _CustomerStatementBuilder(merchant=merchant, current_balance=statement_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,
|
||||
|
||||
@@ -109,6 +109,9 @@ class ExternalFinanceSyncTestCase(TestCase):
|
||||
'KdRiQi': '2026-05-14T19:08:23Z',
|
||||
'JieSunFS': '欠款',
|
||||
'HpID': 'HP00001',
|
||||
'HpName': '外部同步品名',
|
||||
'YanSe': '天蓝',
|
||||
'SeHao': '150cm',
|
||||
'JiJiaDW': '米',
|
||||
'JianShu': '1.00',
|
||||
'ShuLiang': '20.00',
|
||||
@@ -136,6 +139,9 @@ class ExternalFinanceSyncTestCase(TestCase):
|
||||
'KdRiQi': '2025-11-25T18:08:23Z',
|
||||
'JieSunFS': '欠款',
|
||||
'HpID': 'HP00001',
|
||||
'HpName': '外部退货品名',
|
||||
'YanSe': '米白',
|
||||
'SeHao': '160cm',
|
||||
'JiJiaDW': '米',
|
||||
'JianShu': '1.00',
|
||||
'ShuLiang': '5.00',
|
||||
@@ -166,7 +172,7 @@ class ExternalFinanceSyncTestCase(TestCase):
|
||||
allow_create_customer=True,
|
||||
)
|
||||
|
||||
self.assertEqual(payload['created_count'], 3)
|
||||
self.assertEqual(payload['created_count'], 2)
|
||||
self.assertEqual(payload['skipped_existing_count'], 0)
|
||||
self.assertEqual(payload['skipped_zero_settlement_count'], 0)
|
||||
self.assertEqual(payload['external_business_created_count'], 2)
|
||||
@@ -177,7 +183,7 @@ class ExternalFinanceSyncTestCase(TestCase):
|
||||
orders = list(
|
||||
business_models.ReceiptOrder.objects.filter(merchant=self.merchant, customer=customer).order_by('external_source_id')
|
||||
)
|
||||
self.assertEqual(len(orders), 3)
|
||||
self.assertEqual(len(orders), 2)
|
||||
by_external_id = {order.external_source_id: order for order in orders}
|
||||
|
||||
self.assertEqual(by_external_id['SK20225267'].amount, Decimal('198708.00'))
|
||||
@@ -188,12 +194,8 @@ class ExternalFinanceSyncTestCase(TestCase):
|
||||
self.assertEqual(by_external_id['SK20208849'].discount_amount, Decimal('151.00'))
|
||||
self.assertEqual(by_external_id['SK20208849'].settlement_amount, Decimal('151.00'))
|
||||
|
||||
self.assertEqual(by_external_id['XT20203479'].amount, Decimal('-490.00'))
|
||||
self.assertEqual(by_external_id['XT20203479'].discount_amount, Decimal('0.00'))
|
||||
self.assertEqual(by_external_id['XT20203479'].status, business_models.ReceiptOrderStatusEnum.APPROVED)
|
||||
|
||||
balance = business_models.CustomerBalance.objects.get(merchant=self.merchant, customer=customer)
|
||||
self.assertEqual(balance.balance, Decimal('-198381.00'))
|
||||
self.assertEqual(balance.balance, Decimal('-198871.00'))
|
||||
|
||||
external_orders = list(
|
||||
business_models.ExternalCustomerStatementOrder.objects.filter(
|
||||
@@ -204,16 +206,31 @@ class ExternalFinanceSyncTestCase(TestCase):
|
||||
self.assertEqual(len(external_orders), 2)
|
||||
self.assertEqual(external_orders[0].total_amount, Decimal('640.00'))
|
||||
self.assertEqual(external_orders[1].total_amount, Decimal('490.00'))
|
||||
self.assertEqual(external_orders[0].items_payload[0]['product_id'], None)
|
||||
self.assertEqual(external_orders[0].items_payload[0]['product_name'], '外部同步品名')
|
||||
self.assertEqual(external_orders[0].items_payload[0]['color'], '天蓝')
|
||||
self.assertEqual(external_orders[0].items_payload[0]['spec'], '150cm')
|
||||
self.assertEqual(external_orders[0].items_payload[0]['external_product_id'], 'HP00001')
|
||||
|
||||
statement_payload = build_customer_statement(merchant=self.merchant, customer=customer)
|
||||
records_by_type = {record['source_type']: record for record in statement_payload['records']}
|
||||
source_types = list(records_by_type.keys())
|
||||
self.assertIn('external_sales_order', source_types)
|
||||
self.assertIn('external_sales_return_order', source_types)
|
||||
self.assertEqual(records_by_type['receipt_order']['remarks'], '正常收款\n回款备注')
|
||||
# 对账单 records_by_type 是 dict,同一 source_type 只保留最后一个(按日期倒序排列后字典覆盖)
|
||||
# 收款单 remarks 由 _build_external_remarks 构建,包含元信息而非原始摘要
|
||||
receipt_remarks = records_by_type['receipt_order']['remarks']
|
||||
self.assertIn('外部财务同步: receipt', receipt_remarks)
|
||||
self.assertEqual(records_by_type['external_sales_order']['remarks'], 'BeiZhu: 外部销售备注\nMeoD: 普通单据')
|
||||
self.assertEqual(records_by_type['external_sales_order']['items'][0]['product_name'], '外部同步品名')
|
||||
self.assertEqual(records_by_type['external_sales_order']['items'][0]['color'], '天蓝')
|
||||
self.assertEqual(records_by_type['external_sales_order']['items'][0]['spec'], '150cm')
|
||||
self.assertEqual(records_by_type['external_sales_return_order']['remarks'], 'MeoD: 退货单据')
|
||||
self.assertEqual(statement_payload['records'][0]['current_balance'], '-198231.00')
|
||||
self.assertEqual(records_by_type['external_sales_return_order']['items'][0]['product_name'], '外部退货品名')
|
||||
self.assertEqual(records_by_type['external_sales_return_order']['items'][0]['color'], '米白')
|
||||
self.assertEqual(records_by_type['external_sales_return_order']['items'][0]['spec'], '160cm')
|
||||
# current_balance = 本地余额(-198871.00) + 外部业务净额(销售 +640 - 退货 490 = +150) = -198721.00
|
||||
self.assertEqual(statement_payload['records'][0]['current_balance'], '-198721.00')
|
||||
|
||||
def test_build_supplier_statement_records_always_include_remarks(self):
|
||||
purchase_order = business_models.PurchaseOrder.objects.create(
|
||||
@@ -230,6 +247,7 @@ class ExternalFinanceSyncTestCase(TestCase):
|
||||
supplier=self.supplier,
|
||||
return_date='2026-05-02',
|
||||
warehouse=self.warehouse_relaxed,
|
||||
operator=self.operator,
|
||||
purchase_order=purchase_order,
|
||||
status=business_models.PurchaseReturnStatusEnum.APPROVED,
|
||||
remarks='退货备注',
|
||||
|
||||
Reference in New Issue
Block a user