|
|
@@ -12,12 +12,15 @@ from sqlalchemy.orm import Session, aliased
|
|
|
from app.core.utils import parse_datetime
|
|
|
from app.crud import protocol_sample_payment as payment_crud
|
|
|
from app.models.models import (
|
|
|
+ BizCellSampleType,
|
|
|
BizCollectionProtocol,
|
|
|
BizProtocolCollectionInfo,
|
|
|
BizProtocolPartyCustomer,
|
|
|
BizProtocolPaymentSchedule,
|
|
|
BizProtocolPaymentTransfer,
|
|
|
+ BizProtocolSample,
|
|
|
BizProtocolSamplePayment,
|
|
|
+ SysUser,
|
|
|
)
|
|
|
from app.services.market_field_scope import (
|
|
|
MarketFieldContext,
|
|
|
@@ -54,6 +57,61 @@ def _money(value: Any) -> Optional[str]:
|
|
|
return format(Decimal(str(value)).quantize(Decimal('0.01')), 'f')
|
|
|
|
|
|
|
|
|
+def _display_ymd(value: Any) -> str:
|
|
|
+ text = str(value or '').strip()
|
|
|
+ if len(text) == 8 and text.isdigit():
|
|
|
+ return f'{text[:4]}-{text[4:6]}-{text[6:8]}'
|
|
|
+ return text
|
|
|
+
|
|
|
+
|
|
|
+def _load_sample_meta(
|
|
|
+ db: Session,
|
|
|
+ payments: List[BizProtocolSamplePayment],
|
|
|
+ tenant_no: str,
|
|
|
+) -> Dict[str, Dict[str, Any]]:
|
|
|
+ sample_codes = list({str(row.sample_code or '').strip() for row in payments if row.sample_code})
|
|
|
+ if not sample_codes:
|
|
|
+ return {}
|
|
|
+ rows = (
|
|
|
+ db.query(BizProtocolSample, BizCellSampleType)
|
|
|
+ .outerjoin(
|
|
|
+ BizCellSampleType,
|
|
|
+ and_(
|
|
|
+ BizCellSampleType.id == BizProtocolSample.sample_type_id,
|
|
|
+ BizCellSampleType.tenant_no == tenant_no,
|
|
|
+ BizCellSampleType.del_flag == 0,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .filter(
|
|
|
+ BizProtocolSample.sample_code.in_(sample_codes),
|
|
|
+ BizProtocolSample.tenant_no == tenant_no,
|
|
|
+ BizProtocolSample.del_flag == 0,
|
|
|
+ )
|
|
|
+ .order_by(
|
|
|
+ BizProtocolSample.update_time.desc(),
|
|
|
+ BizProtocolSample.create_time.desc(),
|
|
|
+ BizProtocolSample.id.desc(),
|
|
|
+ )
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ result: Dict[str, Dict[str, Any]] = {}
|
|
|
+ for sample, sample_type in rows:
|
|
|
+ original = sample.original_price
|
|
|
+ current = sample.price
|
|
|
+ discount = None
|
|
|
+ if original is not None and current is not None and original > current:
|
|
|
+ discount = original - current
|
|
|
+ result.setdefault(
|
|
|
+ sample.sample_code or '',
|
|
|
+ {
|
|
|
+ 'storageYears': sample.storage_years,
|
|
|
+ 'sampleTypeName': sample_type.type_name if sample_type else '',
|
|
|
+ 'discountAmount': _money(discount),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
def _transfer_dict(row: BizProtocolPaymentTransfer) -> Dict[str, Any]:
|
|
|
return {
|
|
|
'id': row.id,
|
|
|
@@ -101,7 +159,43 @@ def _payment_dict(
|
|
|
party: Optional[BizProtocolPartyCustomer],
|
|
|
schedules: List[BizProtocolPaymentSchedule],
|
|
|
transfers_by_schedule: Dict[str, List[BizProtocolPaymentTransfer]],
|
|
|
+ sample_meta: Optional[Dict[str, Any]] = None,
|
|
|
) -> Dict[str, Any]:
|
|
|
+ meta = sample_meta or {}
|
|
|
+ schedule_rows = [
|
|
|
+ _period_dict(row, transfers_by_schedule.get(row.id, []))
|
|
|
+ for row in schedules
|
|
|
+ ]
|
|
|
+ due_total = sum(
|
|
|
+ (Decimal(row['dueAmount'] or '0') for row in schedule_rows),
|
|
|
+ Decimal('0'),
|
|
|
+ )
|
|
|
+ paid_total = sum(
|
|
|
+ (Decimal(row['paidAmount'] or '0') for row in schedule_rows),
|
|
|
+ Decimal('0'),
|
|
|
+ )
|
|
|
+ remain_total = max(due_total - paid_total, Decimal('0'))
|
|
|
+ receivable_total = sum(
|
|
|
+ (
|
|
|
+ Decimal(str(row.due_amount or '0'))
|
|
|
+ for row in schedules
|
|
|
+ if (row.schedule_status or '') != 'void'
|
|
|
+ ),
|
|
|
+ Decimal('0'),
|
|
|
+ )
|
|
|
+ transfers = [
|
|
|
+ transfer
|
|
|
+ for schedule in schedules
|
|
|
+ for transfer in transfers_by_schedule.get(schedule.id, [])
|
|
|
+ ]
|
|
|
+ cleared_count = sum(
|
|
|
+ 1 for row in schedule_rows if Decimal(row['arrearsAmount'] or '0') <= 0
|
|
|
+ )
|
|
|
+ progress = (
|
|
|
+ '已缴清' if schedule_rows and remain_total <= 0
|
|
|
+ else '部分缴费' if paid_total > 0
|
|
|
+ else '待缴费'
|
|
|
+ )
|
|
|
return {
|
|
|
'id': payment.id,
|
|
|
'collectionProtocolId': protocol.id,
|
|
|
@@ -113,11 +207,20 @@ def _payment_dict(
|
|
|
'sampleCode': payment.sample_code or '',
|
|
|
'payMode': payment.pay_mode or '',
|
|
|
'contractAmount': _money(payment.contract_amount),
|
|
|
+ 'signDate': _display_ymd(getattr(protocol, 'register_date', None)),
|
|
|
+ 'storageYears': meta.get('storageYears'),
|
|
|
+ 'sampleTypeName': meta.get('sampleTypeName') or '',
|
|
|
+ 'discountAmount': meta.get('discountAmount'),
|
|
|
+ 'receivableAmount': _money(receivable_total),
|
|
|
'remark': payment.remark or '',
|
|
|
- 'schedules': [
|
|
|
- _period_dict(row, transfers_by_schedule.get(row.id, []))
|
|
|
- for row in schedules
|
|
|
- ],
|
|
|
+ 'paidAmount': _money(paid_total),
|
|
|
+ 'remainAmount': _money(remain_total),
|
|
|
+ 'dueRemainAmount': _money(remain_total),
|
|
|
+ 'clearedScheduleCount': cleared_count,
|
|
|
+ 'activeScheduleCount': len(schedule_rows),
|
|
|
+ 'paymentProgress': progress,
|
|
|
+ 'periods': payment_crud._build_periods_array(schedules, transfers),
|
|
|
+ 'schedules': schedule_rows,
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -253,6 +356,7 @@ def list_payments(
|
|
|
schedules, transfers = _load_children(
|
|
|
db, [row[0].id for row in rows], ctx.tenant_no
|
|
|
)
|
|
|
+ sample_meta = _load_sample_meta(db, [row[0] for row in rows], ctx.tenant_no)
|
|
|
return [
|
|
|
_payment_dict(
|
|
|
payment,
|
|
|
@@ -260,6 +364,7 @@ def list_payments(
|
|
|
party,
|
|
|
schedules.get(payment.id, []),
|
|
|
transfers,
|
|
|
+ sample_meta.get(payment.sample_code or ''),
|
|
|
)
|
|
|
for payment, protocol, party in rows
|
|
|
], int(total)
|
|
|
@@ -286,12 +391,14 @@ def get_payment_detail(
|
|
|
detail='旧版协议缴费模型不支持移动端办理,请前往管理端查看',
|
|
|
)
|
|
|
schedules, transfers = _load_children(db, [payment.id], ctx.tenant_no)
|
|
|
+ sample_meta = _load_sample_meta(db, [payment], ctx.tenant_no)
|
|
|
return _payment_dict(
|
|
|
payment,
|
|
|
protocol,
|
|
|
party,
|
|
|
schedules.get(payment.id, []),
|
|
|
transfers,
|
|
|
+ sample_meta.get(payment.sample_code or ''),
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -347,9 +454,7 @@ def list_payment_chase(
|
|
|
func.sum(
|
|
|
case(
|
|
|
(
|
|
|
- BizProtocolPaymentTransfer.pay_status.in_(
|
|
|
- ('dfj', 'jfqrz', 'yjf')
|
|
|
- ),
|
|
|
+ BizProtocolPaymentTransfer.pay_status == 'yjf',
|
|
|
BizProtocolPaymentTransfer.transfer_amount,
|
|
|
),
|
|
|
else_=Decimal('0'),
|
|
|
@@ -473,6 +578,18 @@ def list_payment_chase(
|
|
|
.limit(page_size)
|
|
|
.all()
|
|
|
)
|
|
|
+ sales_user_ids = {
|
|
|
+ collection.sales_user_id
|
|
|
+ for _, _, _, _, collection, _ in candidates
|
|
|
+ if collection.sales_user_id
|
|
|
+ }
|
|
|
+ sales_user_names = {
|
|
|
+ row.id: row.nick_name or ''
|
|
|
+ for row in db.query(SysUser).filter(
|
|
|
+ SysUser.id.in_(sales_user_ids or ['']),
|
|
|
+ SysUser.tenant_no == ctx.tenant_no,
|
|
|
+ )
|
|
|
+ }
|
|
|
rows: List[Dict[str, Any]] = []
|
|
|
for schedule, payment, protocol, party, collection, paid in candidates:
|
|
|
due_date = parse_due_date_ymd(schedule.due_date)
|
|
|
@@ -488,10 +605,14 @@ def list_payment_chase(
|
|
|
'samplePaymentId': payment.id,
|
|
|
'protocolId': protocol.id,
|
|
|
'protocolNo': protocol.protocol_no or '',
|
|
|
+ 'protocolStatus': protocol.protocol_status or '',
|
|
|
'motherName': (party.mother_name if party else '') or '',
|
|
|
'motherPhone': (party.mother_phone if party else '') or '',
|
|
|
'sampleCode': payment.sample_code or schedule.sample_code or '',
|
|
|
'salesUserId': collection.sales_user_id or '',
|
|
|
+ 'salesUserName': sales_user_names.get(
|
|
|
+ collection.sales_user_id or '', ''
|
|
|
+ ),
|
|
|
'payMode': payment.pay_mode or '',
|
|
|
'scheduleNo': schedule.schedule_no,
|
|
|
'dueDate': due_date.strftime('%Y-%m-%d'),
|
|
|
@@ -792,7 +913,7 @@ def withdraw_transfer(
|
|
|
db, transfer_id=transfer_id, ctx=ctx, user_info=user_info
|
|
|
)
|
|
|
if (row.pay_status or '') != 'jfqrz':
|
|
|
- raise ValueError('仅缴费确认中可撤回')
|
|
|
+ raise ValueError('仅付款确认中可撤回')
|
|
|
now = datetime.now()
|
|
|
changed = (
|
|
|
db.query(BizProtocolPaymentTransfer)
|