1
0
forked from erp-dev/erp
Files
erpnew/business/tests/test_external_finance_sync.py

412 lines
18 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 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',
'HpName': '外部同步品名',
'YanSe': '天蓝',
'SeHao': '150cm',
'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',
'HpName': '外部退货品名',
'YanSe': '米白',
'SeHao': '160cm',
'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'], 2)
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), 2)
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'))
balance = business_models.CustomerBalance.objects.get(merchant=self.merchant, customer=customer)
self.assertEqual(balance.balance, Decimal('-198871.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'))
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)
# 对账单 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(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(
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,
operator=self.operator,
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())