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

394 lines
16 KiB
Python

from decimal import Decimal
from io import StringIO
from unittest.mock import patch
from django.core.management import call_command
from django.test import TestCase, override_settings
from basic_info import models as basic_models
from business import models as business_models
from business.external_finance_sync import sync_customer_finance, sync_customer_finance_batch
from business.services import build_customer_statement, build_supplier_statement
from .fixtures import create_basic_fixtures
class FakeHaoBuYeClient:
def __init__(self, *, finance_payloads=None, customers_payload=None, i_sale_customer_payloads=None):
self.finance_payloads = finance_payloads or {}
self.customers_payload = customers_payload or {
'customers': [],
'cursor_id': None,
'next_cursor_id': None,
}
self.i_sale_customer_payloads = i_sale_customer_payloads or {}
def fetch_customer_finance(self, *, customer_name, include_adjustments=True, include_cash_movement=True):
return self.finance_payloads[customer_name]
def list_customers(self, *, cursor_id=None):
return self.customers_payload
def fetch_i_sale_by_customer(self, *, customer_id, category):
return self.i_sale_customer_payloads.get(
(customer_id, category),
{
'status': 'not_found',
'records': [],
},
)
class ExternalFinanceSyncTestCase(TestCase):
def setUp(self):
(
self.merchant,
self.supplier,
self.warehouse_strict,
self.warehouse_relaxed,
self.product,
self.operator,
) = create_basic_fixtures()
def test_sync_customer_finance_creates_receipt_and_refund_orders(self):
client = FakeHaoBuYeClient(
finance_payloads={
'张晓鹏': {
'status': 'active',
'customer_name': '张晓鹏',
'customer_ids': ['KH00308'],
'receipts': [
{
'BianHaoID': 'SK20225267',
'KhID': 'KH00308',
'RiQi': '2026-05-07T00:00:00Z',
'FkJinE': '198708.00',
'ZkJinE': '12.00',
'JieSunFS': '月结',
'ZhaiYao': '正常收款',
'BeiZhu': '回款备注',
},
{
'BianHaoID': 'SK20208849',
'KhID': 'KH00308',
'RiQi': '2022-05-31T00:00:00Z',
'FkJinE': '0.00',
'ZkJinE': '151.00',
'JieSunFS': '月结',
'ZhaiYao': '纯折扣收款',
'BeiZhu': '',
},
],
'refunds': [
{
'BianHaoID': 'XT20203479',
'KhID': 'KH00308',
'RiQi': '2025-11-25T00:00:00Z',
'YfJinE': '-490.00',
'FkJinE': '0.00',
'JieSunFS': '欠款',
'ZhaiYao': '成品销售退货单XT20203479[欠款]',
'BeiZhu': '',
}
],
}
},
i_sale_customer_payloads={
(
'KH00308',
business_models.ExternalCustomerStatementCategoryEnum.SALE,
): {
'status': 'active',
'records': [
{
'SubID': 1,
'BianHaoID': 'XS20367431',
'DanType': '成品销售单',
'KhID': 'KH00308',
'RiQi': '2026-05-14T00:00:00Z',
'KdRiQi': '2026-05-14T19:08:23Z',
'JieSunFS': '欠款',
'HpID': 'HP00001',
'JiJiaDW': '',
'JianShu': '1.00',
'ShuLiang': '20.00',
'DanJia': '32.000000',
'JinE': '640.000000',
'BeiZhu': '外部销售备注',
'BeiZhuC': '',
'BeiZhuD': '',
'MeoD': '普通单据',
}
],
},
(
'KH00308',
business_models.ExternalCustomerStatementCategoryEnum.SALE_RETURN,
): {
'status': 'active',
'records': [
{
'SubID': 2,
'BianHaoID': 'XT20203479',
'DanType': '客户退货单',
'KhID': 'KH00308',
'RiQi': '2025-11-25T00:00:00Z',
'KdRiQi': '2025-11-25T18:08:23Z',
'JieSunFS': '欠款',
'HpID': 'HP00001',
'JiJiaDW': '',
'JianShu': '1.00',
'ShuLiang': '5.00',
'DanJia': '98.000000',
'JinE': '490.000000',
'BeiZhu': '',
'BeiZhuC': '',
'BeiZhuD': '',
'MeoD': '退货单据',
}
],
},
},
)
basic_models.Product.objects.create(
merchant=self.merchant,
category=basic_models.ProductCategory.objects.get(merchant=self.merchant),
name='外部面料',
human_id='HP00001',
unit=basic_models.ProductUnitEnum.METER,
)
payload = sync_customer_finance(
customer_name='张晓鹏',
operator=self.operator,
client=client,
allow_create_customer=True,
)
self.assertEqual(payload['created_count'], 3)
self.assertEqual(payload['skipped_existing_count'], 0)
self.assertEqual(payload['skipped_zero_settlement_count'], 0)
self.assertEqual(payload['external_business_created_count'], 2)
self.assertEqual(payload['sales_seen'], 1)
self.assertEqual(payload['sale_returns_seen'], 1)
customer = basic_models.Customer.objects.get(merchant=self.merchant, name='张晓鹏')
orders = list(
business_models.ReceiptOrder.objects.filter(merchant=self.merchant, customer=customer).order_by('external_source_id')
)
self.assertEqual(len(orders), 3)
by_external_id = {order.external_source_id: order for order in orders}
self.assertEqual(by_external_id['SK20225267'].amount, Decimal('198708.00'))
self.assertEqual(by_external_id['SK20225267'].discount_amount, Decimal('12.00'))
self.assertEqual(by_external_id['SK20225267'].status, business_models.ReceiptOrderStatusEnum.APPROVED)
self.assertEqual(by_external_id['SK20208849'].amount, Decimal('0.00'))
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'))
external_orders = list(
business_models.ExternalCustomerStatementOrder.objects.filter(
merchant=self.merchant,
customer=customer,
).order_by('category', 'external_source_id')
)
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'))
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回款备注')
self.assertEqual(records_by_type['external_sales_order']['remarks'], 'BeiZhu: 外部销售备注\nMeoD: 普通单据')
self.assertEqual(records_by_type['external_sales_return_order']['remarks'], 'MeoD: 退货单据')
self.assertEqual(statement_payload['records'][0]['current_balance'], '-198231.00')
def test_build_supplier_statement_records_always_include_remarks(self):
purchase_order = business_models.PurchaseOrder.objects.create(
merchant=self.merchant,
supplier=self.supplier,
purchase_date='2026-05-01',
warehouse=self.warehouse_relaxed,
operator=self.operator,
status=business_models.PurchaseOrderStatusEnum.APPROVED,
remarks='采购备注',
)
purchase_return_order = business_models.PurchaseReturnOrder.objects.create(
merchant=self.merchant,
supplier=self.supplier,
return_date='2026-05-02',
warehouse=self.warehouse_relaxed,
purchase_order=purchase_order,
status=business_models.PurchaseReturnStatusEnum.APPROVED,
remarks='退货备注',
)
business_models.PaymentOrder.objects.create(
merchant=self.merchant,
supplier=self.supplier,
payment_date='2026-05-03',
amount=Decimal('100.00'),
discount_amount=Decimal('0.00'),
operator=self.operator,
status=business_models.PaymentOrderStatusEnum.APPROVED,
remarks='付款备注',
)
statement_payload = build_supplier_statement(merchant=self.merchant, supplier=self.supplier)
records_by_type = {record['source_type']: record for record in statement_payload['records']}
self.assertEqual(records_by_type['purchase_order']['remarks'], '采购备注')
self.assertEqual(records_by_type['purchase_return_order']['remarks'], '退货备注')
self.assertEqual(records_by_type['payment_order']['remarks'], '付款备注')
self.assertTrue(all('remarks' in record for record in statement_payload['records']))
def test_sync_customer_finance_is_idempotent_for_same_external_source_id(self):
customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='宏洋',
created_by=self.operator,
)
business_models.ReceiptOrder.objects.create(
merchant=self.merchant,
customer=customer,
receipt_date='2026-05-09',
amount=Decimal('37839.00'),
discount_amount=Decimal('0.00'),
operator=self.operator,
status=business_models.ReceiptOrderStatusEnum.APPROVED,
is_external_source=True,
external_source_id='SK20225323',
remarks='existing',
)
client = FakeHaoBuYeClient(
finance_payloads={
'宏洋': {
'status': 'active',
'customer_name': '宏洋',
'customer_ids': ['KH01474'],
'receipts': [
{
'BianHaoID': 'SK20225323',
'KhID': 'KH01474',
'RiQi': '2026-05-09T00:00:00Z',
'FkJinE': '37839.00',
'ZkJinE': '0.00',
'JieSunFS': '月结',
'ZhaiYao': '',
'BeiZhu': '',
}
],
'refunds': [],
}
}
)
payload = sync_customer_finance(
customer_name='宏洋',
operator=self.operator,
client=client,
allow_create_customer=False,
)
self.assertEqual(payload['created_count'], 0)
self.assertEqual(payload['skipped_existing_count'], 1)
self.assertEqual(payload['external_business_created_count'], 0)
self.assertEqual(
business_models.ReceiptOrder.objects.filter(merchant=self.merchant, external_source_id='SK20225323').count(),
1,
)
def test_sync_customer_finance_batch_reuses_single_customer_sync_flow(self):
client = FakeHaoBuYeClient(
customers_payload={
'cursor_id': 'KH00001',
'next_cursor_id': 'KH00011',
'customers': [
{'customer_id': 'KH00002', 'customer_name': '客户甲'},
{'customer_id': 'KH00003', 'customer_name': '客户乙'},
],
},
finance_payloads={
'客户甲': {
'status': 'active',
'customer_name': '客户甲',
'customer_ids': ['KH00002'],
'receipts': [],
'refunds': [],
},
'客户乙': {
'status': 'active',
'customer_name': '客户乙',
'customer_ids': ['KH00003'],
'receipts': [
{
'BianHaoID': 'SK-TEST-1',
'KhID': 'KH00003',
'RiQi': '2026-05-01T00:00:00Z',
'FkJinE': '100.00',
'ZkJinE': '0.00',
'JieSunFS': '现金',
'ZhaiYao': '',
'BeiZhu': '',
}
],
'refunds': [],
},
},
)
payload = sync_customer_finance_batch(
operator=self.operator,
cursor_id='KH00001',
client=client,
allow_create_customer=True,
)
self.assertEqual(payload['count'], 2)
self.assertEqual(payload['next_cursor_id'], 'KH00011')
self.assertEqual(payload['results'][0]['customer_name'], '客户甲')
self.assertEqual(payload['results'][1]['created_count'], 1)
class ExternalFinanceSyncCommandTestCase(TestCase):
def setUp(self):
(
self.merchant,
self.supplier,
self.warehouse_strict,
self.warehouse_relaxed,
self.product,
self.operator,
) = create_basic_fixtures()
@override_settings(HAOBUYE_FINANCE_SYNC_OPERATOR_ID=0)
def test_command_sync_external_customer_finance(self):
stdout = StringIO()
with patch(
'business.management.commands.sync_external_customer_finance.sync_customer_finance',
return_value={'customer_name': '宏洋', 'created_count': 2},
) as mock_sync:
call_command(
'sync_external_customer_finance',
'宏洋',
'--operator-id',
str(self.operator.id),
'--allow-create-customer',
stdout=stdout,
)
mock_sync.assert_called_once()
self.assertIn('created_count', stdout.getvalue())