1
0
forked from erp-dev/erp

fea: business completed

This commit is contained in:
2026-06-06 14:14:50 +08:00
parent b97e86257e
commit 3b2aacaa5c
12 changed files with 1270 additions and 92 deletions

View File

@@ -127,6 +127,10 @@ class HaoBuYeFinanceClient:
payload = response.json()
except ValueError:
payload = {}
# 404 且返回了 status=not_found 的 JSON 视为正常业务响应(如客户无退货单),
# 由调用方根据 status 字段决定后续逻辑,而不是在此处抛异常中断整个同步。
if response.status_code == 404 and isinstance(payload, dict) and payload.get('status') == 'not_found':
return payload
message = payload.get('message') or payload.get('error') or f'外部接口异常状态码: {response.status_code}'
raise ExternalFinanceSyncError(str(message))
try:
@@ -775,12 +779,18 @@ def _apply_sale_discount_zk(
progress_callback: Callable[[str], None] | None = None,
) -> int:
"""
从 sale_discount 数据F_Skd 的 XS% 行)中提取 ZkJinE
更新到对应的 ExternalCustomerStatementOrder.zk_amount。
从 sale_discount 数据F_Skd 的 XS% 行)中提取 ZkJinE 和 YfJinE
更新到对应的 ExternalCustomerStatementOrder.zk_amount 和 total_amount
关键修正I_Sale.SfJinE 始终为 0数据源缺陷导致从 I_Sale.JinE 计算的
total_amount 未扣除实付部分。F_Skd XS% 的 YfJinE 已经扣除了实付,是正确的
欠款基数,因此用它覆盖 total_amount。
返回更新的记录数。
"""
# 按 BianHaoID 聚合折扣(一个 BianHaoID 可能有多条 F_Skd 记录)
# 按 BianHaoID 聚合折扣和应付金额
zk_by_source: dict[str, Decimal] = {}
yf_by_source: dict[str, Decimal] = {}
for record in sale_discounts:
if not isinstance(record, dict):
continue
@@ -788,13 +798,31 @@ def _apply_sale_discount_zk(
if not source_id:
continue
zk_value = abs(_to_decimal(record.get('ZkJinE'), field_name='ZkJinE', default=Decimal('0')))
# YfJinE 不取 abs():负值代表退款/冲减,应保留原始正负
yf_value = _to_decimal(record.get('YfJinE'), field_name='YfJinE', default=Decimal('0'))
if zk_value:
zk_by_source[source_id] = zk_by_source.get(source_id, Decimal('0')) + zk_value
# YfJinE 是每个销售单的正确欠款金额(已扣除实付 SfJinE
# 一个 BianHaoID 通常只有一条 F_Skd XS% 记录,但为安全起见做聚合
# 注意:不过滤零值,因为 YfJinE=0 也是有效数据(表示全额实付)
yf_by_source[source_id] = yf_by_source.get(source_id, Decimal('0')) + yf_value
if not zk_by_source:
if not zk_by_source and not yf_by_source:
return 0
updated_count = 0
# 更新 total_amount用 F_Skd XS%.YfJinE 覆盖从 I_Sale.JinE 计算的值
for source_id, yf_total in yf_by_source.items():
updated = business_models.ExternalCustomerStatementOrder.objects.filter(
merchant=merchant,
customer=customer,
category=business_models.ExternalCustomerStatementCategoryEnum.SALE,
external_source_id=source_id,
).exclude(total_amount=yf_total).update(total_amount=yf_total)
updated_count += updated
# 更新 zk_amount
for source_id, zk_total in zk_by_source.items():
updated = business_models.ExternalCustomerStatementOrder.objects.filter(
merchant=merchant,
@@ -807,7 +835,7 @@ def _apply_sale_discount_zk(
if updated_count:
_emit_progress(
progress_callback,
f'销售折扣更新: {updated_count} 条记录的 zk_amount 已从 sale_discount 数据更新',
f'销售折扣/应付更新: {updated_count} 条记录已从 sale_discount 数据更新 (total_amount via YfJinE, zk_amount via ZkJinE)',
)
return updated_count

View File

@@ -1,9 +1,12 @@
import logging
from typing import Any, Dict, List
from celery import shared_task
from business import services as business_services
logger = logging.getLogger(__name__)
@shared_task(bind=True)
def create_purchase_order_stock_entries(
@@ -24,6 +27,80 @@ def create_purchase_order_stock_entries(
return payload
@shared_task(bind=True)
def sync_external_customer_finance_scheduled(self) -> Dict[str, Any]:
"""
定时任务:同步指定客户列表的外部财务(对账)数据。
每天凌晨 5 点由 celery beat 触发,逐个客户调用 sync_customer_finance()。
单个客户失败时跳过并记录错误,不影响其他客户。
"""
from django.conf import settings
from basic_info.models import Employee
from business.external_finance_sync import sync_customer_finance
customer_names = getattr(settings, 'FINANCE_SYNC_CUSTOMER_NAMES', [])
operator_id = getattr(settings, 'HAOBUYE_FINANCE_SYNC_OPERATOR_ID', 0)
if not customer_names:
logger.warning('[finance_sync_scheduled] FINANCE_SYNC_CUSTOMER_NAMES 为空,跳过')
return {'status': 'skipped', 'reason': 'empty_customer_list'}
if not operator_id:
logger.error('[finance_sync_scheduled] HAOBUYE_FINANCE_SYNC_OPERATOR_ID 未配置')
return {'status': 'error', 'reason': 'missing_operator_id'}
try:
operator = Employee.objects.select_related('merchant').get(id=operator_id)
except Employee.DoesNotExist:
logger.error('[finance_sync_scheduled] 经办人不存在: id=%s', operator_id)
return {'status': 'error', 'reason': f'operator_not_found:{operator_id}'}
results: List[Dict[str, Any]] = []
errors: List[Dict[str, str]] = []
for customer_name in customer_names:
try:
logger.info('[finance_sync_scheduled] 开始同步客户: %s', customer_name)
result = sync_customer_finance(
customer_name=customer_name,
operator=operator,
allow_create_customer=True,
force_update=True,
)
results.append({
'customer_name': customer_name,
'created_count': result.get('created_count', 0),
'skipped_existing_count': result.get('skipped_existing_count', 0),
'external_business_created_count': result.get('external_business_created_count', 0),
})
logger.info(
'[finance_sync_scheduled] 客户 %s 同步完成: created=%s, skipped=%s',
customer_name,
result.get('created_count', 0),
result.get('skipped_existing_count', 0),
)
except Exception as exc:
logger.exception('[finance_sync_scheduled] 客户 %s 同步失败', customer_name)
errors.append({'customer_name': customer_name, 'error': str(exc)})
summary = {
'status': 'completed',
'total_customers': len(customer_names),
'success_count': len(results),
'error_count': len(errors),
'results': results,
'errors': errors,
}
logger.info(
'[finance_sync_scheduled] 全部完成: success=%s, errors=%s',
len(results),
len(errors),
)
return summary
@shared_task(bind=True)
def create_sales_order_stock_entries(
self,