4377 lines
159 KiB
Python
4377 lines
159 KiB
Python
"""中试服务管理 CRUD(细胞产品类型/规格/价格体系/项目价格、渠道信息、体检机构)"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import math
|
||
from datetime import datetime
|
||
from typing import Any, Dict, List, Optional, Tuple
|
||
|
||
from sqlalchemy import and_, func, or_
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.utils import generate_uuid
|
||
from app.crud.collection_protocol import urls_to_attachment_list
|
||
from app.models.models import (
|
||
BizCellProductType,
|
||
BizCellProductSpec,
|
||
BizCellProductPrice,
|
||
BizCellProductProjectSystem,
|
||
BizCellProductProjectSystemItem,
|
||
BizChannel,
|
||
BizChannelAccount,
|
||
BizChannelAgreement,
|
||
BizChannelAgreementProductPrice,
|
||
BizChannelInvoice,
|
||
BizChannelMailAddress,
|
||
BizChannelProductQuota,
|
||
BizChannelServiceOrder,
|
||
BizChannelServiceOrderProduct,
|
||
BizChannelServiceOrderTxn,
|
||
BizCheckupInstitution,
|
||
BizCheckupInstitutionInvoice,
|
||
BizCheckupInstitutionProject,
|
||
BizCheckupInstitutionPackage,
|
||
BizCheckupInstitutionPackageItem,
|
||
SysUser,
|
||
)
|
||
|
||
|
||
def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
|
||
val = (value or "").strip()
|
||
if len(val) == 10:
|
||
return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
|
||
return val
|
||
|
||
|
||
def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
|
||
ids = list({x for x in user_ids if x})
|
||
if not ids:
|
||
return {}
|
||
rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
|
||
return {u.id: (u.nick_name or "").strip() for u in rows}
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 1: BizCellProductType 细胞产品类型
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_cell_product_type(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"productTypeCode": row.product_type_code or "",
|
||
"productTypeName": row.product_type_name or "",
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_cell_product_types(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
|
||
def _filtered():
|
||
q0 = db.query(BizCellProductType).filter(BizCellProductType.del_flag == 0)
|
||
if tenant_no:
|
||
q0 = q0.filter(BizCellProductType.tenant_no == tenant_no)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q0 = q0.filter(
|
||
or_(
|
||
BizCellProductType.product_type_code.like(like),
|
||
BizCellProductType.product_type_name.like(like),
|
||
)
|
||
)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q0 = apply_data_scope(q0, BizCellProductType, user_info, db, tenant_no)
|
||
return q0
|
||
|
||
total = _filtered().order_by(None).with_entities(func.count(BizCellProductType.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = _filtered().order_by(BizCellProductType.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||
return [_row_to_dict_cell_product_type(x) for x in rows], int(total)
|
||
|
||
|
||
def get_cell_product_type(db, type_id, tenant_no=None):
|
||
q = db.query(BizCellProductType).filter(and_(BizCellProductType.id == type_id, BizCellProductType.del_flag == 0))
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductType.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def cell_product_type_code_exists(db, code, tenant_no, exclude_id=None):
|
||
q = db.query(BizCellProductType).filter(
|
||
and_(BizCellProductType.product_type_code == code, BizCellProductType.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductType.tenant_no == tenant_no)
|
||
if exclude_id:
|
||
q = q.filter(BizCellProductType.id != exclude_id)
|
||
return db.query(q.exists()).scalar()
|
||
|
||
|
||
def create_cell_product_type(db, *, product_type_code, product_type_name, tenant_no="zkxw", order_num=None, remark=None, user_id=None):
|
||
if cell_product_type_code_exists(db, product_type_code, tenant_no):
|
||
raise ValueError(f"产品类型编码 {product_type_code} 已存在")
|
||
obj = BizCellProductType(
|
||
id=generate_uuid(),
|
||
product_type_code=product_type_code,
|
||
product_type_name=product_type_name,
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_cell_product_type(db, type_id, *, product_type_code=None, product_type_name=None, order_num=None, remark=None, user_id=None, tenant_no=None):
|
||
obj = get_cell_product_type(db, type_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if product_type_code is not None and product_type_code != obj.product_type_code:
|
||
if cell_product_type_code_exists(db, product_type_code, tenant_no, exclude_id=type_id):
|
||
raise ValueError(f"产品类型编码 {product_type_code} 已存在")
|
||
obj.product_type_code = product_type_code
|
||
if product_type_name is not None:
|
||
obj.product_type_name = product_type_name
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_cell_product_type(db, type_id, tenant_no=None):
|
||
obj = get_cell_product_type(db, type_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 2: BizCellProductSpec 细胞产品规格
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_cell_product_spec(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"specCode": row.spec_code or "",
|
||
"specName": row.spec_name or "",
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_cell_product_specs(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
|
||
def _filtered():
|
||
q0 = db.query(BizCellProductSpec).filter(BizCellProductSpec.del_flag == 0)
|
||
if tenant_no:
|
||
q0 = q0.filter(BizCellProductSpec.tenant_no == tenant_no)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q0 = q0.filter(
|
||
or_(
|
||
BizCellProductSpec.spec_code.like(like),
|
||
BizCellProductSpec.spec_name.like(like),
|
||
)
|
||
)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q0 = apply_data_scope(q0, BizCellProductSpec, user_info, db, tenant_no)
|
||
return q0
|
||
|
||
total = _filtered().order_by(None).with_entities(func.count(BizCellProductSpec.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = _filtered().order_by(BizCellProductSpec.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||
return [_row_to_dict_cell_product_spec(x) for x in rows], int(total)
|
||
|
||
|
||
def get_cell_product_spec(db, spec_id, tenant_no=None):
|
||
q = db.query(BizCellProductSpec).filter(and_(BizCellProductSpec.id == spec_id, BizCellProductSpec.del_flag == 0))
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductSpec.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def cell_product_spec_code_exists(db, code, tenant_no, exclude_id=None):
|
||
q = db.query(BizCellProductSpec).filter(
|
||
and_(BizCellProductSpec.spec_code == code, BizCellProductSpec.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductSpec.tenant_no == tenant_no)
|
||
if exclude_id:
|
||
q = q.filter(BizCellProductSpec.id != exclude_id)
|
||
return db.query(q.exists()).scalar()
|
||
|
||
|
||
def create_cell_product_spec(db, *, spec_code, spec_name, tenant_no="zkxw", order_num=None, remark=None, user_id=None):
|
||
if cell_product_spec_code_exists(db, spec_code, tenant_no):
|
||
raise ValueError(f"规格编码 {spec_code} 已存在")
|
||
obj = BizCellProductSpec(
|
||
id=generate_uuid(),
|
||
spec_code=spec_code,
|
||
spec_name=spec_name,
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_cell_product_spec(db, spec_id, *, spec_code=None, spec_name=None, order_num=None, remark=None, user_id=None, tenant_no=None):
|
||
obj = get_cell_product_spec(db, spec_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if spec_code is not None and spec_code != obj.spec_code:
|
||
if cell_product_spec_code_exists(db, spec_code, tenant_no, exclude_id=spec_id):
|
||
raise ValueError(f"规格编码 {spec_code} 已存在")
|
||
obj.spec_code = spec_code
|
||
if spec_name is not None:
|
||
obj.spec_name = spec_name
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_cell_product_spec(db, spec_id, tenant_no=None):
|
||
obj = get_cell_product_spec(db, spec_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 3: BizCellProductPrice 细胞产品价格
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_cell_product_price(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"projectName": row.project_name or "",
|
||
"projectCode": row.project_code or "",
|
||
"cellTypeCode": row.cell_type_code or "",
|
||
"cellTypeName": row.cell_type_name or "",
|
||
"specText": row.spec_text or "",
|
||
"specId": row.spec_id or "",
|
||
"cellProductTypeId": row.cell_product_type_id or "",
|
||
"unitPrice": float(row.unit_price) if row.unit_price is not None else None,
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_cell_product_prices(db, *, page=1, page_size=10, keyword=None, tenant_no=None, cell_product_type_id=None, user_info=None):
|
||
def _filtered():
|
||
q0 = db.query(BizCellProductPrice).filter(BizCellProductPrice.del_flag == 0)
|
||
if tenant_no:
|
||
q0 = q0.filter(BizCellProductPrice.tenant_no == tenant_no)
|
||
if cell_product_type_id:
|
||
q0 = q0.filter(BizCellProductPrice.cell_product_type_id == cell_product_type_id)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q0 = q0.filter(
|
||
or_(
|
||
BizCellProductPrice.project_name.like(like),
|
||
BizCellProductPrice.project_code.like(like),
|
||
BizCellProductPrice.cell_type_name.like(like),
|
||
)
|
||
)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q0 = apply_data_scope(q0, BizCellProductPrice, user_info, db, tenant_no)
|
||
return q0
|
||
|
||
total = _filtered().order_by(None).with_entities(func.count(BizCellProductPrice.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = _filtered().order_by(BizCellProductPrice.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||
return [_row_to_dict_cell_product_price(x) for x in rows], int(total)
|
||
|
||
|
||
def get_cell_product_price(db, price_id, tenant_no=None):
|
||
q = db.query(BizCellProductPrice).filter(and_(BizCellProductPrice.id == price_id, BizCellProductPrice.del_flag == 0))
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductPrice.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def cell_product_price_code_exists(db, code, tenant_no, exclude_id=None):
|
||
q = db.query(BizCellProductPrice).filter(
|
||
and_(BizCellProductPrice.project_code == code, BizCellProductPrice.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductPrice.tenant_no == tenant_no)
|
||
if exclude_id:
|
||
q = q.filter(BizCellProductPrice.id != exclude_id)
|
||
return db.query(q.exists()).scalar()
|
||
|
||
|
||
def create_cell_product_price(
|
||
db,
|
||
*,
|
||
project_name,
|
||
project_code,
|
||
cell_type_code,
|
||
cell_type_name,
|
||
spec_text,
|
||
spec_id,
|
||
cell_product_type_id,
|
||
unit_price,
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
if cell_product_price_code_exists(db, project_code, tenant_no):
|
||
raise ValueError(f"项目编码 {project_code} 已存在")
|
||
obj = BizCellProductPrice(
|
||
id=generate_uuid(),
|
||
project_name=project_name,
|
||
project_code=project_code,
|
||
cell_type_code=cell_type_code,
|
||
cell_type_name=cell_type_name,
|
||
spec_text=spec_text or "",
|
||
spec_id=spec_id or "",
|
||
cell_product_type_id=cell_product_type_id or "",
|
||
unit_price=unit_price,
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_cell_product_price(
|
||
db,
|
||
price_id,
|
||
*,
|
||
project_name=None,
|
||
project_code=None,
|
||
cell_type_code=None,
|
||
cell_type_name=None,
|
||
spec_text=None,
|
||
spec_id=None,
|
||
cell_product_type_id=None,
|
||
unit_price=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
obj = get_cell_product_price(db, price_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if project_code is not None and project_code != obj.project_code:
|
||
if cell_product_price_code_exists(db, project_code, tenant_no, exclude_id=price_id):
|
||
raise ValueError(f"项目编码 {project_code} 已存在")
|
||
obj.project_code = project_code
|
||
if project_name is not None:
|
||
obj.project_name = project_name
|
||
if cell_type_code is not None:
|
||
obj.cell_type_code = cell_type_code
|
||
if cell_type_name is not None:
|
||
obj.cell_type_name = cell_type_name
|
||
if spec_text is not None:
|
||
obj.spec_text = spec_text
|
||
if spec_id is not None:
|
||
obj.spec_id = spec_id
|
||
if cell_product_type_id is not None:
|
||
obj.cell_product_type_id = cell_product_type_id
|
||
if unit_price is not None:
|
||
obj.unit_price = unit_price
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_cell_product_price(db, price_id, tenant_no=None):
|
||
obj = get_cell_product_price(db, price_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 4: BizCellProductProjectSystem 项目价格体系(主)
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_project_system(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"systemName": row.system_name or "",
|
||
"systemCode": row.system_code or "",
|
||
"status": row.status or "1",
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_project_systems(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
|
||
def _filtered():
|
||
q0 = db.query(BizCellProductProjectSystem).filter(BizCellProductProjectSystem.del_flag == 0)
|
||
if tenant_no:
|
||
q0 = q0.filter(BizCellProductProjectSystem.tenant_no == tenant_no)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q0 = q0.filter(
|
||
or_(
|
||
BizCellProductProjectSystem.system_code.like(like),
|
||
BizCellProductProjectSystem.system_name.like(like),
|
||
)
|
||
)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q0 = apply_data_scope(q0, BizCellProductProjectSystem, user_info, db, tenant_no)
|
||
return q0
|
||
|
||
total = _filtered().order_by(None).with_entities(func.count(BizCellProductProjectSystem.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = _filtered().order_by(BizCellProductProjectSystem.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||
return [_row_to_dict_project_system(x) for x in rows], int(total)
|
||
|
||
|
||
def get_project_system(db, system_id, tenant_no=None):
|
||
q = db.query(BizCellProductProjectSystem).filter(
|
||
and_(BizCellProductProjectSystem.id == system_id, BizCellProductProjectSystem.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductProjectSystem.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def project_system_code_exists(db, code, tenant_no, exclude_id=None):
|
||
q = db.query(BizCellProductProjectSystem).filter(
|
||
and_(BizCellProductProjectSystem.system_code == code, BizCellProductProjectSystem.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductProjectSystem.tenant_no == tenant_no)
|
||
if exclude_id:
|
||
q = q.filter(BizCellProductProjectSystem.id != exclude_id)
|
||
return db.query(q.exists()).scalar()
|
||
|
||
|
||
def create_project_system(db, *, system_name, system_code, status="1", tenant_no="zkxw", order_num=None, remark=None, user_id=None):
|
||
if project_system_code_exists(db, system_code, tenant_no):
|
||
raise ValueError(f"体系编码 {system_code} 已存在")
|
||
obj = BizCellProductProjectSystem(
|
||
id=generate_uuid(),
|
||
system_name=system_name,
|
||
system_code=system_code,
|
||
status=status or "1",
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_project_system(db, system_id, *, system_name=None, system_code=None, status=None, order_num=None, remark=None, user_id=None, tenant_no=None):
|
||
obj = get_project_system(db, system_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if system_code is not None and system_code != obj.system_code:
|
||
if project_system_code_exists(db, system_code, tenant_no, exclude_id=system_id):
|
||
raise ValueError(f"体系编码 {system_code} 已存在")
|
||
obj.system_code = system_code
|
||
if system_name is not None:
|
||
obj.system_name = system_name
|
||
if status is not None:
|
||
obj.status = status
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_project_system(db, system_id, tenant_no=None):
|
||
obj = get_project_system(db, system_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 5: BizCellProductProjectSystemItem 项目价格体系明细
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_project_system_item(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"projectSystemId": row.project_system_id or "",
|
||
"sourcePriceId": row.source_price_id or "",
|
||
"projectName": row.project_name or "",
|
||
"projectCode": row.project_code or "",
|
||
"cellTypeCode": row.cell_type_code or "",
|
||
"cellTypeName": row.cell_type_name or "",
|
||
"specText": row.spec_text or "",
|
||
"specId": row.spec_id or "",
|
||
"cellProductTypeId": row.cell_product_type_id or "",
|
||
"unitPrice": float(row.unit_price) if row.unit_price is not None else None,
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_project_system_items(db, project_system_id, tenant_no=None, user_info=None):
|
||
q = db.query(BizCellProductProjectSystemItem).filter(
|
||
and_(
|
||
BizCellProductProjectSystemItem.project_system_id == project_system_id,
|
||
BizCellProductProjectSystemItem.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductProjectSystemItem.tenant_no == tenant_no)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q = apply_data_scope(q, BizCellProductProjectSystemItem, user_info, db, tenant_no)
|
||
rows = q.order_by(
|
||
BizCellProductProjectSystemItem.order_num.asc(),
|
||
BizCellProductProjectSystemItem.create_time.asc(),
|
||
).all()
|
||
return [_row_to_dict_project_system_item(x) for x in rows]
|
||
|
||
|
||
def create_project_system_item(
|
||
db,
|
||
*,
|
||
project_system_id,
|
||
source_price_id="",
|
||
project_name,
|
||
project_code,
|
||
cell_type_code,
|
||
cell_type_name,
|
||
spec_text="",
|
||
spec_id="",
|
||
cell_product_type_id="",
|
||
unit_price,
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
obj = BizCellProductProjectSystemItem(
|
||
id=generate_uuid(),
|
||
project_system_id=project_system_id,
|
||
source_price_id=source_price_id,
|
||
project_name=project_name,
|
||
project_code=project_code,
|
||
cell_type_code=cell_type_code,
|
||
cell_type_name=cell_type_name,
|
||
spec_text=spec_text or "",
|
||
spec_id=spec_id or "",
|
||
cell_product_type_id=cell_product_type_id or "",
|
||
unit_price=unit_price,
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_project_system_item(
|
||
db,
|
||
item_id,
|
||
*,
|
||
source_price_id=None,
|
||
project_name=None,
|
||
project_code=None,
|
||
cell_type_code=None,
|
||
cell_type_name=None,
|
||
spec_text=None,
|
||
spec_id=None,
|
||
cell_product_type_id=None,
|
||
unit_price=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
q = db.query(BizCellProductProjectSystemItem).filter(
|
||
and_(BizCellProductProjectSystemItem.id == item_id, BizCellProductProjectSystemItem.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductProjectSystemItem.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if source_price_id is not None:
|
||
obj.source_price_id = source_price_id
|
||
if project_name is not None:
|
||
obj.project_name = project_name
|
||
if project_code is not None:
|
||
obj.project_code = project_code
|
||
if cell_type_code is not None:
|
||
obj.cell_type_code = cell_type_code
|
||
if cell_type_name is not None:
|
||
obj.cell_type_name = cell_type_name
|
||
if spec_text is not None:
|
||
obj.spec_text = spec_text
|
||
if spec_id is not None:
|
||
obj.spec_id = spec_id
|
||
if cell_product_type_id is not None:
|
||
obj.cell_product_type_id = cell_product_type_id
|
||
if unit_price is not None:
|
||
obj.unit_price = unit_price
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_project_system_item(db, item_id, tenant_no=None):
|
||
q = db.query(BizCellProductProjectSystemItem).filter(
|
||
and_(BizCellProductProjectSystemItem.id == item_id, BizCellProductProjectSystemItem.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCellProductProjectSystemItem.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 6: BizChannel 渠道信息
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_channel(
|
||
row,
|
||
db: Optional[Session] = None,
|
||
tenant_no: Optional[str] = None,
|
||
) -> Dict[str, Any]:
|
||
tn = tenant_no or row.tenant_no
|
||
return {
|
||
"id": row.id,
|
||
"channelName": row.channel_name or "",
|
||
"creditCode": row.credit_code or "",
|
||
"channelType": row.channel_type or "",
|
||
"contactName": row.contact_name or "",
|
||
"contactPhone": row.contact_phone or "",
|
||
"contactEmail": row.contact_email or "",
|
||
"protocolAttachments": row.protocol_attachments or "",
|
||
"attachmentFiles": urls_to_attachment_list(row.protocol_attachments, db, tn) if db else [],
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_channels(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
|
||
def _filtered():
|
||
q0 = db.query(BizChannel).filter(BizChannel.del_flag == 0)
|
||
if tenant_no:
|
||
q0 = q0.filter(BizChannel.tenant_no == tenant_no)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q0 = q0.filter(
|
||
or_(
|
||
BizChannel.channel_name.like(like),
|
||
BizChannel.credit_code.like(like),
|
||
BizChannel.contact_name.like(like),
|
||
)
|
||
)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q0 = apply_data_scope(q0, BizChannel, user_info, db, tenant_no)
|
||
return q0
|
||
|
||
total = _filtered().order_by(None).with_entities(func.count(BizChannel.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = _filtered().order_by(BizChannel.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||
return [_row_to_dict_channel(x, db, tenant_no) for x in rows], int(total)
|
||
|
||
|
||
def get_channel(db, channel_id, tenant_no=None):
|
||
q = db.query(BizChannel).filter(and_(BizChannel.id == channel_id, BizChannel.del_flag == 0))
|
||
if tenant_no:
|
||
q = q.filter(BizChannel.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def create_channel(
|
||
db,
|
||
*,
|
||
channel_name,
|
||
credit_code=None,
|
||
channel_type=None,
|
||
contact_name=None,
|
||
contact_phone=None,
|
||
contact_email=None,
|
||
protocol_attachments=None,
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
obj = BizChannel(
|
||
id=generate_uuid(),
|
||
channel_name=channel_name,
|
||
credit_code=credit_code or "",
|
||
channel_type=channel_type or "",
|
||
contact_name=contact_name or "",
|
||
contact_phone=contact_phone or "",
|
||
contact_email=contact_email or "",
|
||
protocol_attachments=protocol_attachments or "",
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_channel(
|
||
db,
|
||
channel_id,
|
||
*,
|
||
channel_name=None,
|
||
credit_code=None,
|
||
channel_type=None,
|
||
contact_name=None,
|
||
contact_phone=None,
|
||
contact_email=None,
|
||
protocol_attachments=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
obj = get_channel(db, channel_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if channel_name is not None:
|
||
obj.channel_name = channel_name
|
||
if credit_code is not None:
|
||
obj.credit_code = credit_code
|
||
if channel_type is not None:
|
||
obj.channel_type = channel_type
|
||
if contact_name is not None:
|
||
obj.contact_name = contact_name
|
||
if contact_phone is not None:
|
||
obj.contact_phone = contact_phone
|
||
if contact_email is not None:
|
||
obj.contact_email = contact_email
|
||
if protocol_attachments is not None:
|
||
obj.protocol_attachments = protocol_attachments
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_channel(db, channel_id, tenant_no=None):
|
||
obj = get_channel(db, channel_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 7: BizChannelInvoice 渠道开票信息
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_channel_invoice(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"channelId": row.channel_id or "",
|
||
"companyName": row.company_name or "",
|
||
"taxNo": row.tax_no or "",
|
||
"companyAddress": row.company_address or "",
|
||
"companyPhone": row.company_phone or "",
|
||
"bankName": row.bank_name or "",
|
||
"bankAccount": row.bank_account or "",
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_channel_invoices(db, channel_id, tenant_no=None, user_info=None):
|
||
q = db.query(BizChannelInvoice).filter(
|
||
and_(
|
||
BizChannelInvoice.channel_id == channel_id,
|
||
BizChannelInvoice.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelInvoice.tenant_no == tenant_no)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q = apply_data_scope(q, BizChannelInvoice, user_info, db, tenant_no)
|
||
rows = q.order_by(BizChannelInvoice.create_time.desc()).all()
|
||
return [_row_to_dict_channel_invoice(x) for x in rows]
|
||
|
||
|
||
def create_channel_invoice(
|
||
db,
|
||
*,
|
||
channel_id,
|
||
company_name,
|
||
tax_no=None,
|
||
company_address=None,
|
||
company_phone=None,
|
||
bank_name=None,
|
||
bank_account=None,
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
obj = BizChannelInvoice(
|
||
id=generate_uuid(),
|
||
channel_id=channel_id,
|
||
company_name=company_name,
|
||
tax_no=tax_no or "",
|
||
company_address=company_address or "",
|
||
company_phone=company_phone or "",
|
||
bank_name=bank_name or "",
|
||
bank_account=bank_account or "",
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_channel_invoice(
|
||
db,
|
||
invoice_id,
|
||
*,
|
||
company_name=None,
|
||
tax_no=None,
|
||
company_address=None,
|
||
company_phone=None,
|
||
bank_name=None,
|
||
bank_account=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
q = db.query(BizChannelInvoice).filter(
|
||
and_(BizChannelInvoice.id == invoice_id, BizChannelInvoice.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelInvoice.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if company_name is not None:
|
||
obj.company_name = company_name
|
||
if tax_no is not None:
|
||
obj.tax_no = tax_no
|
||
if company_address is not None:
|
||
obj.company_address = company_address
|
||
if company_phone is not None:
|
||
obj.company_phone = company_phone
|
||
if bank_name is not None:
|
||
obj.bank_name = bank_name
|
||
if bank_account is not None:
|
||
obj.bank_account = bank_account
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_channel_invoice(db, invoice_id, tenant_no=None):
|
||
q = db.query(BizChannelInvoice).filter(
|
||
and_(BizChannelInvoice.id == invoice_id, BizChannelInvoice.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelInvoice.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 8: BizChannelMailAddress 渠道邮寄地址
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_channel_mail_address(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"channelId": row.channel_id or "",
|
||
"mailAddress": row.mail_address or "",
|
||
"mailContact": row.mail_contact or "",
|
||
"mailPhone": row.mail_phone or "",
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_channel_mail_addresses(db, channel_id, tenant_no=None, user_info=None):
|
||
q = db.query(BizChannelMailAddress).filter(
|
||
and_(
|
||
BizChannelMailAddress.channel_id == channel_id,
|
||
BizChannelMailAddress.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelMailAddress.tenant_no == tenant_no)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q = apply_data_scope(q, BizChannelMailAddress, user_info, db, tenant_no)
|
||
rows = q.order_by(BizChannelMailAddress.create_time.desc()).all()
|
||
return [_row_to_dict_channel_mail_address(x) for x in rows]
|
||
|
||
|
||
def create_channel_mail_address(
|
||
db,
|
||
*,
|
||
channel_id,
|
||
mail_address,
|
||
mail_contact=None,
|
||
mail_phone=None,
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
obj = BizChannelMailAddress(
|
||
id=generate_uuid(),
|
||
channel_id=channel_id,
|
||
mail_address=mail_address,
|
||
mail_contact=mail_contact or "",
|
||
mail_phone=mail_phone or "",
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_channel_mail_address(
|
||
db,
|
||
address_id,
|
||
*,
|
||
mail_address=None,
|
||
mail_contact=None,
|
||
mail_phone=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
q = db.query(BizChannelMailAddress).filter(
|
||
and_(BizChannelMailAddress.id == address_id, BizChannelMailAddress.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelMailAddress.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if mail_address is not None:
|
||
obj.mail_address = mail_address
|
||
if mail_contact is not None:
|
||
obj.mail_contact = mail_contact
|
||
if mail_phone is not None:
|
||
obj.mail_phone = mail_phone
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_channel_mail_address(db, address_id, tenant_no=None):
|
||
q = db.query(BizChannelMailAddress).filter(
|
||
and_(BizChannelMailAddress.id == address_id, BizChannelMailAddress.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelMailAddress.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 9: BizChannelAgreement 渠道协议
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_channel_agreement(row) -> Dict[str, Any]:
|
||
def _fmt_date(v):
|
||
"""CHAR(8) YYYYMMDD → YYYY-MM-DD"""
|
||
if not v or len(v) != 8:
|
||
return v or ""
|
||
return f"{v[:4]}-{v[4:6]}-{v[6:8]}"
|
||
|
||
return {
|
||
"id": row.id,
|
||
"channelId": row.channel_id or "",
|
||
"agreementType": row.agreement_type or "",
|
||
"businessType": row.business_type or "",
|
||
"settleMode": row.settle_mode or "",
|
||
"creditLimitAmount": float(row.credit_limit_amount) if row.credit_limit_amount is not None else None,
|
||
"effectiveDate": _fmt_date(row.effective_date),
|
||
"expireDate": _fmt_date(row.expire_date),
|
||
"agreementStatus": row.agreement_status or "",
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_channel_agreements(db, channel_id, tenant_no=None, user_info=None):
|
||
q = db.query(BizChannelAgreement).filter(
|
||
and_(
|
||
BizChannelAgreement.channel_id == channel_id,
|
||
BizChannelAgreement.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q = apply_data_scope(q, BizChannelAgreement, user_info, db, tenant_no)
|
||
rows = q.order_by(BizChannelAgreement.create_time.desc()).all()
|
||
return [_row_to_dict_channel_agreement(x) for x in rows]
|
||
|
||
|
||
def create_channel_agreement(
|
||
db,
|
||
*,
|
||
channel_id,
|
||
agreement_type,
|
||
business_type=None,
|
||
settle_mode=None,
|
||
credit_limit_amount=None,
|
||
effective_date=None,
|
||
expire_date=None,
|
||
agreement_status="dsx",
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
_ed = (effective_date or "").replace("-", "") if effective_date else ""
|
||
_xd = (expire_date or "").replace("-", "") if expire_date else ""
|
||
obj = BizChannelAgreement(
|
||
id=generate_uuid(),
|
||
channel_id=channel_id,
|
||
agreement_type=agreement_type,
|
||
business_type=business_type or "",
|
||
settle_mode=settle_mode or "",
|
||
credit_limit_amount=credit_limit_amount,
|
||
effective_date=_ed,
|
||
expire_date=_xd,
|
||
agreement_status=agreement_status or "dsx",
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_channel_agreement(
|
||
db,
|
||
agreement_id,
|
||
*,
|
||
agreement_type=None,
|
||
business_type=None,
|
||
settle_mode=None,
|
||
credit_limit_amount=None,
|
||
effective_date=None,
|
||
expire_date=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
q = db.query(BizChannelAgreement).filter(
|
||
and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if agreement_type is not None:
|
||
obj.agreement_type = agreement_type
|
||
if business_type is not None:
|
||
obj.business_type = business_type
|
||
if settle_mode is not None:
|
||
obj.settle_mode = settle_mode
|
||
if credit_limit_amount is not None:
|
||
obj.credit_limit_amount = credit_limit_amount
|
||
if effective_date is not None:
|
||
obj.effective_date = effective_date.replace("-", "") if effective_date else ""
|
||
if expire_date is not None:
|
||
obj.expire_date = expire_date.replace("-", "") if expire_date else ""
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_channel_agreement(db, agreement_id, tenant_no=None):
|
||
q = db.query(BizChannelAgreement).filter(
|
||
and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
def activate_channel_agreement(db, agreement_id, tenant_no=None, user_id=None):
|
||
"""生效协议:仅待生效(dsx)可变为生效中(sxz)"""
|
||
q = db.query(BizChannelAgreement).filter(
|
||
and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if (obj.agreement_status or "").strip() != "dsx":
|
||
raise ValueError("仅待生效状态的协议可执行生效操作")
|
||
obj.agreement_status = "sxz"
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def void_channel_agreement(db, agreement_id, tenant_no=None, user_id=None):
|
||
"""作废协议:设置 agreement_status='yzf'"""
|
||
q = db.query(BizChannelAgreement).filter(
|
||
and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.agreement_status = "yzf"
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 10: BizChannelAgreementProductPrice 渠道协议产品价格
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_channel_agreement_price(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"channelAgreementId": row.channel_agreement_id or "",
|
||
"projectName": row.project_name or "",
|
||
"projectCode": row.project_code or "",
|
||
"cellTypeCode": row.cell_type_code or "",
|
||
"cellTypeName": row.cell_type_name or "",
|
||
"specText": row.spec_text or "",
|
||
"originalPrice": float(row.original_price) if row.original_price is not None else None,
|
||
"agreementPrice": float(row.agreement_price) if row.agreement_price is not None else None,
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def _channel_agreement_price_item_key(
|
||
project_code=None,
|
||
project_name=None,
|
||
cell_type_code=None,
|
||
cell_type_name=None,
|
||
spec_text=None,
|
||
) -> str:
|
||
code = (project_code or project_name or "").strip().lower()
|
||
cell = (cell_type_code or cell_type_name or "").strip().lower()
|
||
spec = (spec_text or "").strip().lower()
|
||
return f"{code}|{cell}|{spec}"
|
||
|
||
|
||
def _channel_agreement_price_exists(
|
||
db,
|
||
channel_agreement_id,
|
||
*,
|
||
project_code=None,
|
||
project_name=None,
|
||
cell_type_code=None,
|
||
cell_type_name=None,
|
||
spec_text=None,
|
||
tenant_no=None,
|
||
exclude_price_id=None,
|
||
):
|
||
target_key = _channel_agreement_price_item_key(
|
||
project_code=project_code,
|
||
project_name=project_name,
|
||
cell_type_code=cell_type_code,
|
||
cell_type_name=cell_type_name,
|
||
spec_text=spec_text,
|
||
)
|
||
q = db.query(BizChannelAgreementProductPrice).filter(
|
||
and_(
|
||
BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
|
||
BizChannelAgreementProductPrice.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
|
||
if exclude_price_id:
|
||
q = q.filter(BizChannelAgreementProductPrice.id != exclude_price_id)
|
||
for row in q.all():
|
||
if _channel_agreement_price_item_key(
|
||
project_code=row.project_code,
|
||
project_name=row.project_name,
|
||
cell_type_code=row.cell_type_code,
|
||
cell_type_name=row.cell_type_name,
|
||
spec_text=row.spec_text,
|
||
) == target_key:
|
||
return True
|
||
return False
|
||
|
||
|
||
def list_channel_agreement_prices(db, channel_agreement_id, tenant_no=None, user_info=None):
|
||
q = db.query(BizChannelAgreementProductPrice).filter(
|
||
and_(
|
||
BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
|
||
BizChannelAgreementProductPrice.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q = apply_data_scope(q, BizChannelAgreementProductPrice, user_info, db, tenant_no)
|
||
rows = q.order_by(BizChannelAgreementProductPrice.create_time.desc()).all()
|
||
return [_row_to_dict_channel_agreement_price(x) for x in rows]
|
||
|
||
|
||
def create_channel_agreement_price(
|
||
db,
|
||
*,
|
||
channel_agreement_id,
|
||
project_name=None,
|
||
project_code=None,
|
||
cell_type_code=None,
|
||
cell_type_name=None,
|
||
spec_text=None,
|
||
original_price=None,
|
||
agreement_price=None,
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
if _channel_agreement_price_exists(
|
||
db,
|
||
channel_agreement_id,
|
||
project_code=project_code,
|
||
project_name=project_name,
|
||
cell_type_code=cell_type_code,
|
||
cell_type_name=cell_type_name,
|
||
spec_text=spec_text,
|
||
tenant_no=tenant_no,
|
||
):
|
||
raise ValueError("该项目信息已存在,不可重复添加")
|
||
obj = BizChannelAgreementProductPrice(
|
||
id=generate_uuid(),
|
||
channel_agreement_id=channel_agreement_id,
|
||
project_name=project_name or "",
|
||
project_code=project_code or "",
|
||
cell_type_code=cell_type_code or "",
|
||
cell_type_name=cell_type_name or "",
|
||
spec_text=spec_text or "",
|
||
original_price=original_price,
|
||
agreement_price=agreement_price,
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_channel_agreement_price(
|
||
db,
|
||
price_id,
|
||
*,
|
||
project_name=None,
|
||
project_code=None,
|
||
cell_type_code=None,
|
||
cell_type_name=None,
|
||
spec_text=None,
|
||
original_price=None,
|
||
agreement_price=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
q = db.query(BizChannelAgreementProductPrice).filter(
|
||
and_(BizChannelAgreementProductPrice.id == price_id, BizChannelAgreementProductPrice.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
next_project_name = project_name if project_name is not None else obj.project_name
|
||
next_project_code = project_code if project_code is not None else obj.project_code
|
||
next_cell_type_code = cell_type_code if cell_type_code is not None else obj.cell_type_code
|
||
next_cell_type_name = cell_type_name if cell_type_name is not None else obj.cell_type_name
|
||
next_spec_text = spec_text if spec_text is not None else obj.spec_text
|
||
if _channel_agreement_price_exists(
|
||
db,
|
||
obj.channel_agreement_id,
|
||
project_code=next_project_code,
|
||
project_name=next_project_name,
|
||
cell_type_code=next_cell_type_code,
|
||
cell_type_name=next_cell_type_name,
|
||
spec_text=next_spec_text,
|
||
tenant_no=tenant_no,
|
||
exclude_price_id=price_id,
|
||
):
|
||
raise ValueError("该项目信息已存在,不可重复添加")
|
||
if project_name is not None:
|
||
obj.project_name = project_name
|
||
if project_code is not None:
|
||
obj.project_code = project_code
|
||
if cell_type_code is not None:
|
||
obj.cell_type_code = cell_type_code
|
||
if cell_type_name is not None:
|
||
obj.cell_type_name = cell_type_name
|
||
if spec_text is not None:
|
||
obj.spec_text = spec_text
|
||
if original_price is not None:
|
||
obj.original_price = original_price
|
||
if agreement_price is not None:
|
||
obj.agreement_price = agreement_price
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_channel_agreement_price(db, price_id, tenant_no=None):
|
||
q = db.query(BizChannelAgreementProductPrice).filter(
|
||
and_(BizChannelAgreementProductPrice.id == price_id, BizChannelAgreementProductPrice.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 11: BizCheckupInstitution 体检机构
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_checkup_institution(row, projects: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
||
data = {
|
||
"id": row.id,
|
||
"institutionName": row.institution_name or "",
|
||
"institutionAddress": row.institution_address or "",
|
||
"contactName": row.contact_name or "",
|
||
"contactPhone": row.contact_phone or "",
|
||
"institutionLogo": row.institution_logo or "",
|
||
"status": row.status or "1",
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
if projects is not None:
|
||
data["projects"] = projects
|
||
return data
|
||
|
||
|
||
def list_checkup_institutions(db, *, page=1, page_size=10, keyword=None, status=None, tenant_no=None, user_info=None):
|
||
def _filtered():
|
||
q0 = db.query(BizCheckupInstitution).filter(BizCheckupInstitution.del_flag == 0)
|
||
if tenant_no:
|
||
q0 = q0.filter(BizCheckupInstitution.tenant_no == tenant_no)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q0 = q0.filter(
|
||
or_(
|
||
BizCheckupInstitution.institution_name.like(like),
|
||
BizCheckupInstitution.contact_name.like(like),
|
||
)
|
||
)
|
||
st = (status or "").strip()
|
||
if st in ("0", "1"):
|
||
q0 = q0.filter(BizCheckupInstitution.status == st)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q0 = apply_data_scope(q0, BizCheckupInstitution, user_info, db, tenant_no)
|
||
return q0
|
||
|
||
total = _filtered().order_by(None).with_entities(func.count(BizCheckupInstitution.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = _filtered().order_by(BizCheckupInstitution.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||
institution_ids = [x.id for x in rows]
|
||
projects_by_inst: Dict[str, List[Dict[str, Any]]] = {iid: [] for iid in institution_ids}
|
||
if institution_ids:
|
||
pq = db.query(BizCheckupInstitutionProject).filter(
|
||
BizCheckupInstitutionProject.checkup_institution_id.in_(institution_ids),
|
||
BizCheckupInstitutionProject.del_flag == 0,
|
||
)
|
||
if tenant_no:
|
||
pq = pq.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
pq = apply_data_scope(pq, BizCheckupInstitutionProject, user_info, db, tenant_no)
|
||
for pr in pq.order_by(BizCheckupInstitutionProject.create_time.desc()).all():
|
||
projects_by_inst.setdefault(pr.checkup_institution_id, []).append(
|
||
_row_to_dict_checkup_institution_project(pr)
|
||
)
|
||
return [
|
||
_row_to_dict_checkup_institution(x, projects_by_inst.get(x.id, [])) for x in rows
|
||
], int(total)
|
||
|
||
|
||
def get_checkup_institution(db, institution_id, tenant_no=None):
|
||
q = db.query(BizCheckupInstitution).filter(
|
||
and_(BizCheckupInstitution.id == institution_id, BizCheckupInstitution.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitution.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def create_checkup_institution(
|
||
db,
|
||
*,
|
||
institution_name,
|
||
institution_address=None,
|
||
contact_name=None,
|
||
contact_phone=None,
|
||
institution_logo=None,
|
||
status="1",
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
obj = BizCheckupInstitution(
|
||
id=generate_uuid(),
|
||
institution_name=institution_name,
|
||
institution_address=institution_address or "",
|
||
contact_name=contact_name or "",
|
||
contact_phone=contact_phone or "",
|
||
institution_logo=(institution_logo or "").strip() or None,
|
||
status=status or "1",
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_checkup_institution(
|
||
db,
|
||
institution_id,
|
||
*,
|
||
institution_name=None,
|
||
institution_address=None,
|
||
contact_name=None,
|
||
contact_phone=None,
|
||
institution_logo=None,
|
||
status=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
obj = get_checkup_institution(db, institution_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if institution_name is not None:
|
||
obj.institution_name = institution_name
|
||
if institution_address is not None:
|
||
obj.institution_address = institution_address
|
||
if contact_name is not None:
|
||
obj.contact_name = contact_name
|
||
if contact_phone is not None:
|
||
obj.contact_phone = contact_phone
|
||
if institution_logo is not None:
|
||
obj.institution_logo = (institution_logo or "").strip() or None
|
||
if status is not None:
|
||
obj.status = status
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_checkup_institution(db, institution_id, tenant_no=None):
|
||
obj = get_checkup_institution(db, institution_id, tenant_no)
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 12: BizCheckupInstitutionInvoice 体检机构开票信息
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_checkup_institution_invoice(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"checkupInstitutionId": row.checkup_institution_id or "",
|
||
"companyName": row.company_name or "",
|
||
"taxNo": row.tax_no or "",
|
||
"companyAddress": row.company_address or "",
|
||
"companyPhone": row.company_phone or "",
|
||
"bankName": row.bank_name or "",
|
||
"bankAccount": row.bank_account or "",
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_checkup_institution_invoices(db, checkup_institution_id, tenant_no=None, user_info=None):
|
||
q = db.query(BizCheckupInstitutionInvoice).filter(
|
||
and_(
|
||
BizCheckupInstitutionInvoice.checkup_institution_id == checkup_institution_id,
|
||
BizCheckupInstitutionInvoice.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionInvoice.tenant_no == tenant_no)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q = apply_data_scope(q, BizCheckupInstitutionInvoice, user_info, db, tenant_no)
|
||
rows = q.order_by(BizCheckupInstitutionInvoice.create_time.desc()).all()
|
||
return [_row_to_dict_checkup_institution_invoice(x) for x in rows]
|
||
|
||
|
||
def create_checkup_institution_invoice(
|
||
db,
|
||
*,
|
||
checkup_institution_id,
|
||
company_name,
|
||
tax_no=None,
|
||
company_address=None,
|
||
company_phone=None,
|
||
bank_name=None,
|
||
bank_account=None,
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
obj = BizCheckupInstitutionInvoice(
|
||
id=generate_uuid(),
|
||
checkup_institution_id=checkup_institution_id,
|
||
company_name=company_name,
|
||
tax_no=tax_no or "",
|
||
company_address=company_address or "",
|
||
company_phone=company_phone or "",
|
||
bank_name=bank_name or "",
|
||
bank_account=bank_account or "",
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_checkup_institution_invoice(
|
||
db,
|
||
invoice_id,
|
||
*,
|
||
company_name=None,
|
||
tax_no=None,
|
||
company_address=None,
|
||
company_phone=None,
|
||
bank_name=None,
|
||
bank_account=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
q = db.query(BizCheckupInstitutionInvoice).filter(
|
||
and_(BizCheckupInstitutionInvoice.id == invoice_id, BizCheckupInstitutionInvoice.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionInvoice.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if company_name is not None:
|
||
obj.company_name = company_name
|
||
if tax_no is not None:
|
||
obj.tax_no = tax_no
|
||
if company_address is not None:
|
||
obj.company_address = company_address
|
||
if company_phone is not None:
|
||
obj.company_phone = company_phone
|
||
if bank_name is not None:
|
||
obj.bank_name = bank_name
|
||
if bank_account is not None:
|
||
obj.bank_account = bank_account
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_checkup_institution_invoice(db, invoice_id, tenant_no=None):
|
||
q = db.query(BizCheckupInstitutionInvoice).filter(
|
||
and_(BizCheckupInstitutionInvoice.id == invoice_id, BizCheckupInstitutionInvoice.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionInvoice.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 13: BizCheckupInstitutionProject 体检机构项目及价格
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_checkup_institution_project(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"checkupInstitutionId": row.checkup_institution_id or "",
|
||
"projectName": row.project_name or "",
|
||
"projectFee": float(row.project_fee) if row.project_fee is not None else None,
|
||
"status": row.status or "1",
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def list_checkup_institution_projects(db, checkup_institution_id, tenant_no=None, user_info=None):
|
||
q = db.query(BizCheckupInstitutionProject).filter(
|
||
and_(
|
||
BizCheckupInstitutionProject.checkup_institution_id == checkup_institution_id,
|
||
BizCheckupInstitutionProject.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q = apply_data_scope(q, BizCheckupInstitutionProject, user_info, db, tenant_no)
|
||
rows = q.order_by(BizCheckupInstitutionProject.create_time.desc()).all()
|
||
return [_row_to_dict_checkup_institution_project(x) for x in rows]
|
||
|
||
|
||
def create_checkup_institution_project(
|
||
db,
|
||
*,
|
||
checkup_institution_id,
|
||
project_name,
|
||
project_fee=None,
|
||
status="1",
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
obj = BizCheckupInstitutionProject(
|
||
id=generate_uuid(),
|
||
checkup_institution_id=checkup_institution_id,
|
||
project_name=project_name,
|
||
project_fee=project_fee,
|
||
status=status or "1",
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def update_checkup_institution_project(
|
||
db,
|
||
project_id,
|
||
*,
|
||
project_name=None,
|
||
project_fee=None,
|
||
status=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
q = db.query(BizCheckupInstitutionProject).filter(
|
||
and_(BizCheckupInstitutionProject.id == project_id, BizCheckupInstitutionProject.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
if project_name is not None:
|
||
obj.project_name = project_name
|
||
if project_fee is not None:
|
||
obj.project_fee = project_fee
|
||
if status is not None:
|
||
obj.status = status
|
||
if order_num is not None:
|
||
obj.order_num = order_num
|
||
if remark is not None:
|
||
obj.remark = remark
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
def soft_delete_checkup_institution_project(db, project_id, tenant_no=None):
|
||
q = db.query(BizCheckupInstitutionProject).filter(
|
||
and_(BizCheckupInstitutionProject.id == project_id, BizCheckupInstitutionProject.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("记录不存在")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 14: BizCheckupInstitutionPackage 体检机构项目套餐
|
||
# ────────────────────────────────────────────
|
||
|
||
def _sum_package_item_fees(items: List[Dict[str, Any]]) -> float:
|
||
total = 0.0
|
||
for it in items:
|
||
fee = it.get("project_fee")
|
||
if fee is None and "projectFee" in it:
|
||
fee = it.get("projectFee")
|
||
total += float(fee) if fee is not None else 0.0
|
||
return total
|
||
|
||
|
||
def _row_to_dict_checkup_institution_package(
|
||
row,
|
||
*,
|
||
total_fee: Optional[float] = None,
|
||
item_count: Optional[int] = None,
|
||
) -> Dict[str, Any]:
|
||
d = {
|
||
"id": row.id,
|
||
"checkupInstitutionId": row.checkup_institution_id or "",
|
||
"packageName": row.package_name or "",
|
||
"packageCode": row.package_code or "",
|
||
"status": row.status or "1",
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
if total_fee is not None:
|
||
d["totalFee"] = total_fee
|
||
if item_count is not None:
|
||
d["itemCount"] = item_count
|
||
return d
|
||
|
||
|
||
def _row_to_dict_checkup_institution_package_item(
|
||
row,
|
||
*,
|
||
source_project_deleted: bool = False,
|
||
source_project_status: Optional[str] = None,
|
||
) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"packageId": row.package_id or "",
|
||
"checkupInstitutionProjectId": row.checkup_institution_project_id or "",
|
||
"projectName": row.project_name or "",
|
||
"projectFee": float(row.project_fee) if row.project_fee is not None else None,
|
||
"status": source_project_status if source_project_status is not None else "1",
|
||
"sourceProjectDeleted": source_project_deleted,
|
||
"tenantNo": row.tenant_no or "",
|
||
"orderNum": row.order_num if row.order_num is not None else 0,
|
||
"remark": row.remark or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def checkup_package_code_exists(db, institution_id, code, tenant_no, exclude_id=None):
|
||
q = db.query(BizCheckupInstitutionPackage).filter(
|
||
and_(
|
||
BizCheckupInstitutionPackage.checkup_institution_id == institution_id,
|
||
BizCheckupInstitutionPackage.package_code == code,
|
||
BizCheckupInstitutionPackage.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionPackage.tenant_no == tenant_no)
|
||
if exclude_id:
|
||
q = q.filter(BizCheckupInstitutionPackage.id != exclude_id)
|
||
return db.query(q.exists()).scalar()
|
||
|
||
|
||
def _get_checkup_project_for_package(db, project_id, institution_id, tenant_no=None):
|
||
q = db.query(BizCheckupInstitutionProject).filter(
|
||
BizCheckupInstitutionProject.id == project_id,
|
||
BizCheckupInstitutionProject.checkup_institution_id == institution_id,
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def _validate_package_items_for_save(
|
||
db,
|
||
institution_id,
|
||
items,
|
||
tenant_no=None,
|
||
existing_item_project_ids=None,
|
||
):
|
||
if not items:
|
||
raise ValueError("套餐至少包含一个体检项目")
|
||
existing_ids = set(existing_item_project_ids or [])
|
||
seen = set()
|
||
for it in items:
|
||
pid = it.get("checkup_institution_project_id") or it.get("checkupInstitutionProjectId")
|
||
if not pid:
|
||
raise ValueError("套餐明细缺少机构项目")
|
||
if pid in seen:
|
||
raise ValueError("套餐内不能重复添加同一体检项目")
|
||
seen.add(pid)
|
||
proj = _get_checkup_project_for_package(db, pid, institution_id, tenant_no)
|
||
if not proj:
|
||
raise ValueError("存在无效的体检项目")
|
||
if pid in existing_ids:
|
||
continue
|
||
if proj.del_flag != 0:
|
||
raise ValueError("不能新添加已删除的体检项目")
|
||
if (proj.status or "1") != "1":
|
||
raise ValueError("套餐不能包含已停用的体检项目")
|
||
|
||
|
||
def _list_package_items_raw(db, package_id, tenant_no=None):
|
||
q = db.query(BizCheckupInstitutionPackageItem).filter(
|
||
and_(
|
||
BizCheckupInstitutionPackageItem.package_id == package_id,
|
||
BizCheckupInstitutionPackageItem.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionPackageItem.tenant_no == tenant_no)
|
||
return q.order_by(BizCheckupInstitutionPackageItem.order_num.asc(), BizCheckupInstitutionPackageItem.create_time.asc()).all()
|
||
|
||
|
||
def _package_items_to_dicts(db, institution_id, item_rows, tenant_no=None):
|
||
result = []
|
||
for row in item_rows:
|
||
proj = _get_checkup_project_for_package(
|
||
db, row.checkup_institution_project_id, institution_id, tenant_no
|
||
)
|
||
deleted = proj is None or proj.del_flag != 0
|
||
proj_status = (proj.status or "1") if proj else "1"
|
||
result.append(
|
||
_row_to_dict_checkup_institution_package_item(
|
||
row,
|
||
source_project_deleted=deleted,
|
||
source_project_status=proj_status,
|
||
)
|
||
)
|
||
return result
|
||
|
||
|
||
def _insert_package_items(
|
||
db,
|
||
*,
|
||
package_id,
|
||
institution_id,
|
||
items,
|
||
tenant_no,
|
||
user_id=None,
|
||
):
|
||
for idx, it in enumerate(items):
|
||
pid = it.get("checkup_institution_project_id") or it.get("checkupInstitutionProjectId")
|
||
proj = _get_checkup_project_for_package(db, pid, institution_id, tenant_no)
|
||
name = (it.get("project_name") or it.get("projectName") or "").strip()
|
||
if not name and proj:
|
||
name = proj.project_name or ""
|
||
fee = it.get("project_fee")
|
||
if fee is None:
|
||
fee = it.get("projectFee")
|
||
obj = BizCheckupInstitutionPackageItem(
|
||
id=generate_uuid(),
|
||
package_id=package_id,
|
||
checkup_institution_project_id=pid,
|
||
project_name=name,
|
||
project_fee=fee,
|
||
tenant_no=tenant_no,
|
||
order_num=it.get("order_num") if it.get("order_num") is not None else it.get("orderNum") or (idx + 1),
|
||
remark=it.get("remark") or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
|
||
|
||
def list_checkup_institution_packages(db, checkup_institution_id, tenant_no=None, user_info=None):
|
||
q = db.query(BizCheckupInstitutionPackage).filter(
|
||
and_(
|
||
BizCheckupInstitutionPackage.checkup_institution_id == checkup_institution_id,
|
||
BizCheckupInstitutionPackage.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionPackage.tenant_no == tenant_no)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q = apply_data_scope(q, BizCheckupInstitutionPackage, user_info, db, tenant_no)
|
||
rows = q.order_by(BizCheckupInstitutionPackage.create_time.desc()).all()
|
||
out = []
|
||
for row in rows:
|
||
item_rows = _list_package_items_raw(db, row.id, tenant_no)
|
||
fees = [float(x.project_fee) if x.project_fee is not None else 0.0 for x in item_rows]
|
||
out.append(
|
||
_row_to_dict_checkup_institution_package(
|
||
row,
|
||
total_fee=sum(fees),
|
||
item_count=len(item_rows),
|
||
)
|
||
)
|
||
return out
|
||
|
||
|
||
def get_checkup_institution_package(db, package_id, tenant_no=None):
|
||
q = db.query(BizCheckupInstitutionPackage).filter(
|
||
and_(BizCheckupInstitutionPackage.id == package_id, BizCheckupInstitutionPackage.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizCheckupInstitutionPackage.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def get_checkup_institution_package_detail(db, package_id, tenant_no=None):
|
||
pkg = get_checkup_institution_package(db, package_id, tenant_no)
|
||
if not pkg:
|
||
return None
|
||
item_rows = _list_package_items_raw(db, package_id, tenant_no)
|
||
items = _package_items_to_dicts(db, pkg.checkup_institution_id, item_rows, tenant_no)
|
||
fees = [float(x.project_fee) if x.project_fee is not None else 0.0 for x in item_rows]
|
||
d = _row_to_dict_checkup_institution_package(pkg, total_fee=sum(fees), item_count=len(item_rows))
|
||
d["items"] = items
|
||
return d
|
||
|
||
|
||
def create_checkup_institution_package(
|
||
db,
|
||
*,
|
||
checkup_institution_id,
|
||
package_name,
|
||
package_code,
|
||
status="1",
|
||
items,
|
||
tenant_no="zkxw",
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
code = (package_code or "").strip()
|
||
if not code:
|
||
raise ValueError("套餐编码不能为空")
|
||
if checkup_package_code_exists(db, checkup_institution_id, code, tenant_no):
|
||
raise ValueError(f"套餐编码 {code} 已存在")
|
||
_validate_package_items_for_save(db, checkup_institution_id, items, tenant_no)
|
||
pkg = BizCheckupInstitutionPackage(
|
||
id=generate_uuid(),
|
||
checkup_institution_id=checkup_institution_id,
|
||
package_name=package_name,
|
||
package_code=code,
|
||
status=status or "1",
|
||
tenant_no=tenant_no,
|
||
order_num=order_num or 0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(pkg)
|
||
db.flush()
|
||
_insert_package_items(
|
||
db,
|
||
package_id=pkg.id,
|
||
institution_id=checkup_institution_id,
|
||
items=items,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
)
|
||
db.commit()
|
||
db.refresh(pkg)
|
||
return get_checkup_institution_package_detail(db, pkg.id, tenant_no)
|
||
|
||
|
||
def update_checkup_institution_package(
|
||
db,
|
||
package_id,
|
||
*,
|
||
package_name=None,
|
||
package_code=None,
|
||
status=None,
|
||
items=None,
|
||
order_num=None,
|
||
remark=None,
|
||
user_id=None,
|
||
tenant_no=None,
|
||
):
|
||
pkg = get_checkup_institution_package(db, package_id, tenant_no)
|
||
if not pkg:
|
||
raise ValueError("记录不存在")
|
||
institution_id = pkg.checkup_institution_id
|
||
if package_code is not None:
|
||
code = package_code.strip()
|
||
if not code:
|
||
raise ValueError("套餐编码不能为空")
|
||
if code != pkg.package_code and checkup_package_code_exists(
|
||
db, institution_id, code, tenant_no, exclude_id=package_id
|
||
):
|
||
raise ValueError(f"套餐编码 {code} 已存在")
|
||
pkg.package_code = code
|
||
if package_name is not None:
|
||
pkg.package_name = package_name
|
||
if status is not None:
|
||
pkg.status = status
|
||
if order_num is not None:
|
||
pkg.order_num = order_num
|
||
if remark is not None:
|
||
pkg.remark = remark
|
||
if items is not None:
|
||
old_rows = _list_package_items_raw(db, package_id, tenant_no)
|
||
existing_pids = {r.checkup_institution_project_id for r in old_rows}
|
||
_validate_package_items_for_save(
|
||
db,
|
||
institution_id,
|
||
items,
|
||
tenant_no,
|
||
existing_item_project_ids=existing_pids,
|
||
)
|
||
# 全量替换:物理删除旧明细,避免 uk(package_id, project_id) 与软删行冲突
|
||
q_items = db.query(BizCheckupInstitutionPackageItem).filter(
|
||
BizCheckupInstitutionPackageItem.package_id == package_id
|
||
)
|
||
if tenant_no:
|
||
q_items = q_items.filter(BizCheckupInstitutionPackageItem.tenant_no == tenant_no)
|
||
for row in q_items.all():
|
||
db.delete(row)
|
||
db.flush()
|
||
_insert_package_items(
|
||
db,
|
||
package_id=package_id,
|
||
institution_id=institution_id,
|
||
items=items,
|
||
tenant_no=tenant_no or pkg.tenant_no,
|
||
user_id=user_id,
|
||
)
|
||
pkg.update_by = user_id
|
||
pkg.update_time = datetime.now()
|
||
db.add(pkg)
|
||
db.commit()
|
||
return get_checkup_institution_package_detail(db, package_id, tenant_no)
|
||
|
||
|
||
def soft_delete_checkup_institution_package(db, package_id, tenant_no=None):
|
||
pkg = get_checkup_institution_package(db, package_id, tenant_no)
|
||
if not pkg:
|
||
raise ValueError("记录不存在")
|
||
now = datetime.now()
|
||
pkg.del_flag = 1
|
||
pkg.update_time = now
|
||
db.add(pkg)
|
||
for row in _list_package_items_raw(db, package_id, tenant_no):
|
||
row.del_flag = 1
|
||
row.update_time = now
|
||
db.add(row)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Shared utility
|
||
# ────────────────────────────────────────────
|
||
|
||
def total_pages(total, page_size):
|
||
if total <= 0:
|
||
return 0
|
||
return int(math.ceil(total / float(page_size)))
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 15: BizChannelAccount 渠道账户
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_channel_account(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"channelId": row.channel_id or "",
|
||
"balanceAmount": float(row.balance_amount) if row.balance_amount else 0,
|
||
"tenantNo": row.tenant_no or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def get_channel_account(db, channel_id, tenant_no=None):
|
||
"""只读查询渠道账户,不加行锁"""
|
||
q = db.query(BizChannelAccount).filter(
|
||
and_(BizChannelAccount.channel_id == channel_id, BizChannelAccount.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAccount.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def _get_or_create_channel_account(db, channel_id, tenant_no, user_id):
|
||
"""获取或创建渠道账户(带行锁 with_for_update)"""
|
||
q = db.query(BizChannelAccount).filter(
|
||
and_(BizChannelAccount.channel_id == channel_id, BizChannelAccount.del_flag == 0)
|
||
).with_for_update()
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAccount.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if obj:
|
||
return obj
|
||
obj = BizChannelAccount(
|
||
id=generate_uuid(),
|
||
channel_id=channel_id,
|
||
balance_amount=0,
|
||
tenant_no=tenant_no or "zkxw",
|
||
order_num=0,
|
||
remark="",
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.flush()
|
||
return obj
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 16: BizChannelProductQuota 渠道产品份数
|
||
# ────────────────────────────────────────────
|
||
|
||
def _row_to_dict_channel_product_quota(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"channelId": row.channel_id or "",
|
||
"projectCode": row.project_code or "",
|
||
"projectName": row.project_name or "",
|
||
"cellTypeCode": row.cell_type_code or "",
|
||
"cellTypeName": row.cell_type_name or "",
|
||
"specText": row.spec_text or "",
|
||
"totalQty": float(row.total_qty) if row.total_qty else 0,
|
||
"usedQty": float(row.used_qty) if row.used_qty else 0,
|
||
"remainingQty": float(row.remaining_qty) if row.remaining_qty else 0,
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
|
||
|
||
def _upsert_channel_product_quota(
|
||
db,
|
||
channel_id,
|
||
project_code,
|
||
project_name,
|
||
cell_type_code,
|
||
cell_type_name,
|
||
spec_text,
|
||
add_qty,
|
||
tenant_no,
|
||
user_id,
|
||
):
|
||
"""增加份数:已存在则累加,不存在则新建"""
|
||
q = db.query(BizChannelProductQuota).filter(
|
||
and_(
|
||
BizChannelProductQuota.channel_id == channel_id,
|
||
BizChannelProductQuota.project_code == (project_code or ""),
|
||
BizChannelProductQuota.cell_type_code == (cell_type_code or ""),
|
||
BizChannelProductQuota.spec_text == (spec_text or ""),
|
||
BizChannelProductQuota.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelProductQuota.tenant_no == tenant_no)
|
||
obj = q.first()
|
||
if obj:
|
||
obj.total_qty = (float(obj.total_qty) if obj.total_qty else 0) + add_qty
|
||
obj.remaining_qty = (float(obj.remaining_qty) if obj.remaining_qty else 0) + add_qty
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.flush()
|
||
return obj
|
||
obj = BizChannelProductQuota(
|
||
id=generate_uuid(),
|
||
channel_id=channel_id,
|
||
project_code=project_code or "",
|
||
project_name=project_name or "",
|
||
cell_type_code=cell_type_code or "",
|
||
cell_type_name=cell_type_name or "",
|
||
spec_text=spec_text or "",
|
||
total_qty=add_qty,
|
||
used_qty=0,
|
||
remaining_qty=add_qty,
|
||
tenant_no=tenant_no or "zkxw",
|
||
order_num=0,
|
||
remark="",
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=datetime.now(),
|
||
update_by=user_id,
|
||
update_time=datetime.now(),
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.flush()
|
||
return obj
|
||
|
||
|
||
def _reduce_channel_product_quota(db, channel_id, project_code, cell_type_code, spec_text, reduce_qty, user_id):
|
||
"""扣减份数:验证 remaining_qty >= reduce_qty"""
|
||
q = db.query(BizChannelProductQuota).filter(
|
||
and_(
|
||
BizChannelProductQuota.channel_id == channel_id,
|
||
BizChannelProductQuota.project_code == (project_code or ""),
|
||
BizChannelProductQuota.cell_type_code == (cell_type_code or ""),
|
||
BizChannelProductQuota.spec_text == (spec_text or ""),
|
||
BizChannelProductQuota.del_flag == 0,
|
||
)
|
||
)
|
||
obj = q.first()
|
||
if not obj:
|
||
raise ValueError("份数记录不存在")
|
||
remaining = float(obj.remaining_qty) if obj.remaining_qty else 0
|
||
if remaining < reduce_qty:
|
||
raise ValueError(f"剩余份数不足,当前剩余 {remaining},需扣减 {reduce_qty}")
|
||
obj.total_qty = (float(obj.total_qty) if obj.total_qty else 0) - reduce_qty
|
||
obj.remaining_qty = remaining - reduce_qty
|
||
obj.update_by = user_id
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.flush()
|
||
return obj
|
||
|
||
|
||
def list_channel_product_quotas(db, channel_id, tenant_no=None):
|
||
"""查询渠道下所有产品份数"""
|
||
q = db.query(BizChannelProductQuota).filter(
|
||
and_(
|
||
BizChannelProductQuota.channel_id == channel_id,
|
||
BizChannelProductQuota.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelProductQuota.tenant_no == tenant_no)
|
||
rows = q.order_by(BizChannelProductQuota.create_time.desc()).all()
|
||
return [_row_to_dict_channel_product_quota(x) for x in rows]
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Entity 17: BizChannelServiceOrder 渠道服务订单
|
||
# ────────────────────────────────────────────
|
||
|
||
def _calc_gmcp_purchase_total_from_db(db, order_id, tenant_no=None) -> float:
|
||
"""从订单产品持仓汇总购买总额(协议单价 × 累计购买份数)。"""
|
||
q = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
|
||
total = 0.0
|
||
for prod in q.all():
|
||
price = float(prod.agreement_unit_price or 0)
|
||
qty = float(prod.total_buy_qty or 0)
|
||
total += price * qty
|
||
return total
|
||
|
||
|
||
def _batch_gmcp_purchase_totals(db, order_ids, tenant_no=None) -> Dict[str, float]:
|
||
if not order_ids:
|
||
return {}
|
||
amount_expr = BizChannelServiceOrderProduct.agreement_unit_price * BizChannelServiceOrderProduct.total_buy_qty
|
||
q = db.query(
|
||
BizChannelServiceOrderProduct.service_order_id,
|
||
func.coalesce(func.sum(amount_expr), 0),
|
||
).filter(
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
BizChannelServiceOrderProduct.service_order_id.in_(order_ids),
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
|
||
return {row[0]: float(row[1] or 0) for row in q.group_by(BizChannelServiceOrderProduct.service_order_id).all()}
|
||
|
||
|
||
def _calc_gmcp_current_holding_amount_from_db(db, order_id, tenant_no=None) -> float:
|
||
"""从订单产品持仓汇总当前余额(协议单价 × 当前持有份数)。"""
|
||
q = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
|
||
total = 0.0
|
||
for prod in q.all():
|
||
price = float(prod.agreement_unit_price or 0)
|
||
qty = float(prod.current_qty or 0)
|
||
total += price * qty
|
||
return total
|
||
|
||
|
||
def _batch_gmcp_current_holding_amounts(db, order_ids, tenant_no=None) -> Dict[str, float]:
|
||
if not order_ids:
|
||
return {}
|
||
amount_expr = BizChannelServiceOrderProduct.agreement_unit_price * BizChannelServiceOrderProduct.current_qty
|
||
q = db.query(
|
||
BizChannelServiceOrderProduct.service_order_id,
|
||
func.coalesce(func.sum(amount_expr), 0),
|
||
).filter(
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
BizChannelServiceOrderProduct.service_order_id.in_(order_ids),
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
|
||
return {row[0]: float(row[1] or 0) for row in q.group_by(BizChannelServiceOrderProduct.service_order_id).all()}
|
||
|
||
|
||
def _resolve_service_order_purchase_total(row, db=None, tenant_no=None, purchase_total_override=None) -> float:
|
||
stored = float(row.purchase_total_amount) if row.purchase_total_amount else 0
|
||
if (row.order_type or "") != "gmcp":
|
||
return stored
|
||
if purchase_total_override is not None:
|
||
return float(purchase_total_override)
|
||
if db:
|
||
computed = _calc_gmcp_purchase_total_from_db(db, row.id, tenant_no or row.tenant_no)
|
||
if computed > 0 or not row.purchase_total_amount:
|
||
return computed
|
||
return stored
|
||
|
||
|
||
def _resolve_service_order_current_balance(
|
||
row, db=None, tenant_no=None, current_balance_override=None
|
||
) -> float:
|
||
if (row.order_type or "") == "gmcp":
|
||
if current_balance_override is not None:
|
||
return float(current_balance_override)
|
||
if db:
|
||
return _calc_gmcp_current_holding_amount_from_db(db, row.id, tenant_no or row.tenant_no)
|
||
return 0.0
|
||
return float(row.current_balance) if row.current_balance else 0
|
||
|
||
|
||
def _row_to_dict_service_order(
|
||
row,
|
||
db=None,
|
||
tenant_no=None,
|
||
*,
|
||
purchase_total_override=None,
|
||
current_balance_override=None,
|
||
) -> Dict[str, Any]:
|
||
tn = tenant_no or row.tenant_no
|
||
purchase_total = _resolve_service_order_purchase_total(
|
||
row, db, tn, purchase_total_override=purchase_total_override
|
||
)
|
||
current_balance = _resolve_service_order_current_balance(
|
||
row, db, tn, current_balance_override=current_balance_override
|
||
)
|
||
d = {
|
||
"id": row.id,
|
||
"channelId": row.channel_id or "",
|
||
"channelName": row.channel_name or "",
|
||
"orderName": row.order_name or "",
|
||
"deptName": row.dept_name or "",
|
||
"orderType": row.order_type or "",
|
||
"prepayAmount": float(row.prepay_amount) if row.prepay_amount else 0,
|
||
"purchaseTotalAmount": purchase_total,
|
||
"orderStatus": row.order_status or "",
|
||
"channelAgreementId": row.channel_agreement_id or "",
|
||
"totalRechargeAmount": float(row.total_recharge_amount) if row.total_recharge_amount else 0,
|
||
"totalRefundAmount": float(row.total_refund_amount) if row.total_refund_amount else 0,
|
||
"currentBalance": current_balance,
|
||
"balanceBefore": float(row.balance_before) if row.balance_before else 0,
|
||
"balanceAfter": float(row.balance_after) if row.balance_after else 0,
|
||
"financeSubmitBy": row.finance_submit_by or "",
|
||
"financeSubmitTime": row.finance_submit_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_submit_time else "",
|
||
"financeSubmitRemark": row.finance_submit_remark or "",
|
||
"financeSubmitAttachments": row.finance_submit_attachments or "",
|
||
"financeSubmitAttachmentFiles": urls_to_attachment_list(row.finance_submit_attachments, db, tn) if db and row.finance_submit_attachments else [],
|
||
"financeConfirmTime": row.finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_confirm_time else "",
|
||
"financeConfirmAttachments": row.finance_confirm_attachments or "",
|
||
"financeConfirmAttachmentFiles": urls_to_attachment_list(row.finance_confirm_attachments, db, tn) if db and row.finance_confirm_attachments else [],
|
||
"financeRemark": row.finance_remark or "",
|
||
"refundApplyBy": row.refund_apply_by or "",
|
||
"refundApplyTime": row.refund_apply_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_apply_time else "",
|
||
"refundAmount": float(row.refund_amount) if row.refund_amount else 0,
|
||
"refundRemark": row.refund_remark or "",
|
||
"refundAttachments": row.refund_attachments or "",
|
||
"refundAttachmentFiles": urls_to_attachment_list(row.refund_attachments, db, tn) if db and row.refund_attachments else [],
|
||
"refundFinanceConfirmTime": row.refund_finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_finance_confirm_time else "",
|
||
"refundFinanceConfirmAttachments": row.refund_finance_confirm_attachments or "",
|
||
"refundFinanceConfirmAttachmentFiles": urls_to_attachment_list(row.refund_finance_confirm_attachments, db, tn) if db and row.refund_finance_confirm_attachments else [],
|
||
"refundFinanceRemark": row.refund_finance_remark or "",
|
||
"tenantNo": row.tenant_no or "",
|
||
"remark": row.remark or "",
|
||
"createBy": row.create_by or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
return d
|
||
|
||
|
||
def _row_to_dict_service_order_product(row) -> Dict[str, Any]:
|
||
return {
|
||
"id": row.id,
|
||
"serviceOrderId": row.service_order_id or "",
|
||
"projectCode": row.project_code or "",
|
||
"projectName": row.project_name or "",
|
||
"cellType": row.cell_type or "",
|
||
"specText": row.spec_text or "",
|
||
"agreementUnitPrice": float(row.agreement_unit_price) if row.agreement_unit_price else 0,
|
||
"buyQty": float(row.buy_qty) if row.buy_qty else 0,
|
||
"buyAmount": float(row.buy_amount) if row.buy_amount else 0,
|
||
"totalBuyQty": float(row.total_buy_qty) if row.total_buy_qty else 0,
|
||
"totalUnsubscribeQty": float(row.total_unsubscribe_qty) if row.total_unsubscribe_qty else 0,
|
||
"currentQty": float(row.current_qty) if row.current_qty else 0,
|
||
"totalUnsubscribeAmount": float(row.total_unsubscribe_amount) if row.total_unsubscribe_amount else 0,
|
||
}
|
||
|
||
|
||
def _normalize_txn_product_item(p: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""流水 products_json 项统一为 camelCase(入库时为 snake_case)。"""
|
||
qty = p.get("qty")
|
||
if qty is None:
|
||
qty = p.get("buyQty") or p.get("buy_qty")
|
||
unit_price = p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0
|
||
qty_val = float(qty or 0)
|
||
amount = p.get("amount")
|
||
if amount is None and qty_val:
|
||
amount = float(unit_price or 0) * qty_val
|
||
return {
|
||
"projectCode": p.get("projectCode") or p.get("project_code") or "",
|
||
"projectName": p.get("projectName") or p.get("project_name") or "",
|
||
"cellType": p.get("cellType") or p.get("cell_type") or "",
|
||
"specText": p.get("specText") or p.get("spec_text") or "",
|
||
"agreementUnitPrice": float(unit_price or 0),
|
||
"qty": qty_val,
|
||
"amount": float(amount or 0),
|
||
}
|
||
|
||
|
||
def _enrich_txn_products_from_order(
|
||
db,
|
||
service_order_id: str,
|
||
products: List[Dict[str, Any]],
|
||
tenant_no=None,
|
||
) -> List[Dict[str, Any]]:
|
||
"""退订等流水可能仅存 projectCode,从订单持仓补全项目名与规格。"""
|
||
if not db or not service_order_id or not products:
|
||
return products
|
||
holdings = get_service_order_products(db, service_order_id, tenant_no)
|
||
if not holdings:
|
||
return products
|
||
by_code_spec = {
|
||
(h.get("projectCode") or "", h.get("specText") or ""): h for h in holdings
|
||
}
|
||
by_code: Dict[str, List[Dict[str, Any]]] = {}
|
||
for h in holdings:
|
||
code = h.get("projectCode") or ""
|
||
by_code.setdefault(code, []).append(h)
|
||
enriched: List[Dict[str, Any]] = []
|
||
for p in products:
|
||
item = dict(p)
|
||
code = item.get("projectCode") or ""
|
||
spec = item.get("specText") or ""
|
||
holding = by_code_spec.get((code, spec))
|
||
if not holding and code:
|
||
matches = by_code.get(code, [])
|
||
if len(matches) == 1:
|
||
holding = matches[0]
|
||
if holding:
|
||
if not (item.get("projectName") or "").strip():
|
||
item["projectName"] = holding.get("projectName") or ""
|
||
if not (item.get("specText") or "").strip():
|
||
item["specText"] = holding.get("specText") or ""
|
||
if not (item.get("cellType") or "").strip():
|
||
item["cellType"] = holding.get("cellType") or ""
|
||
enriched.append(item)
|
||
return enriched
|
||
|
||
|
||
def _row_to_dict_service_order_txn(row, db=None, tenant_no=None) -> Dict[str, Any]:
|
||
tn = tenant_no or row.tenant_no
|
||
d = {
|
||
"id": row.id,
|
||
"serviceOrderId": row.service_order_id or "",
|
||
"txnType": row.txn_type or "",
|
||
"amount": float(row.amount) if row.amount else 0,
|
||
"txnStatus": row.txn_status or "",
|
||
"productsJson": row.products_json or "",
|
||
"remark": row.remark or "",
|
||
"refundAttachments": row.refund_attachments or "",
|
||
"refundAttachmentFiles": urls_to_attachment_list(row.refund_attachments, db, tn) if db and row.refund_attachments else [],
|
||
"financeSubmitBy": row.finance_submit_by or "",
|
||
"financeSubmitTime": row.finance_submit_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_submit_time else "",
|
||
"financeSubmitRemark": row.finance_submit_remark or "",
|
||
"financeSubmitAttachments": row.finance_submit_attachments or "",
|
||
"financeSubmitAttachmentFiles": urls_to_attachment_list(row.finance_submit_attachments, db, tn) if db and row.finance_submit_attachments else [],
|
||
"financeConfirmTime": row.finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_confirm_time else "",
|
||
"financeConfirmBy": row.finance_confirm_by or "",
|
||
"financeConfirmAttachments": row.finance_confirm_attachments or "",
|
||
"financeConfirmAttachmentFiles": urls_to_attachment_list(row.finance_confirm_attachments, db, tn) if db and row.finance_confirm_attachments else [],
|
||
"financeRemark": row.finance_remark or "",
|
||
"voidBy": row.void_by or "",
|
||
"voidTime": row.void_time.strftime("%Y-%m-%d %H:%M:%S") if row.void_time else "",
|
||
"balanceBefore": float(row.balance_before) if row.balance_before else 0,
|
||
"balanceAfter": float(row.balance_after) if row.balance_after else 0,
|
||
"tenantNo": row.tenant_no or "",
|
||
"createBy": row.create_by or "",
|
||
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||
}
|
||
if row.products_json:
|
||
try:
|
||
raw_products = json.loads(row.products_json)
|
||
products = [_normalize_txn_product_item(x) for x in raw_products]
|
||
if db and row.service_order_id:
|
||
products = _enrich_txn_products_from_order(
|
||
db, row.service_order_id, products, tenant_no
|
||
)
|
||
d["products"] = products
|
||
except Exception:
|
||
d["products"] = []
|
||
else:
|
||
d["products"] = []
|
||
return d
|
||
|
||
|
||
def _service_order_txn_to_response_dict(
|
||
db,
|
||
txn,
|
||
tenant_no=None,
|
||
*,
|
||
order=None,
|
||
name_map=None,
|
||
) -> Dict[str, Any]:
|
||
"""流水响应字典:含订单渠道信息与提交人/创建人昵称。"""
|
||
d = _row_to_dict_service_order_txn(txn, db, tenant_no)
|
||
if order is None and txn.service_order_id:
|
||
order = get_service_order(db, txn.service_order_id, tenant_no)
|
||
if order:
|
||
d["channelName"] = order.channel_name or ""
|
||
d["orderName"] = order.order_name or ""
|
||
d["deptName"] = order.dept_name or ""
|
||
d["orderType"] = order.order_type or ""
|
||
confirm_uid = txn.finance_confirm_by or (
|
||
txn.update_by
|
||
if (txn.txn_status or "") == "yqr" and txn.finance_confirm_time and not txn.finance_confirm_by
|
||
else None
|
||
)
|
||
void_uid = txn.void_by or (
|
||
txn.update_by
|
||
if (txn.txn_status or "") == "yzf"
|
||
and txn.finance_submit_time
|
||
and not txn.void_by
|
||
else None
|
||
)
|
||
if name_map is None:
|
||
user_ids = [x for x in (txn.create_by, txn.finance_submit_by, confirm_uid, void_uid) if x]
|
||
name_map = _user_nick_name_map(db, user_ids) if db and user_ids else {}
|
||
d["createByName"] = name_map.get(txn.create_by or "", "") or (txn.create_by or "")
|
||
d["financeSubmitByName"] = name_map.get(txn.finance_submit_by or "", "") or (
|
||
txn.finance_submit_by or ""
|
||
)
|
||
d["financeConfirmByName"] = name_map.get(confirm_uid or "", "") or (confirm_uid or "")
|
||
if not d.get("voidTime") and (txn.txn_status or "") == "yzf" and txn.update_time:
|
||
d["voidTime"] = txn.update_time.strftime("%Y-%m-%d %H:%M:%S")
|
||
d["voidByName"] = name_map.get(void_uid or "", "") or (void_uid or "")
|
||
if not (d.get("financeSubmitRemark") or "").strip() and (d.get("remark") or "").strip():
|
||
d["financeSubmitRemark"] = (d.get("remark") or "").strip()
|
||
_enrich_finance_operate_fields(d, txn)
|
||
return d
|
||
|
||
|
||
def _enrich_finance_operate_fields(d: Dict[str, Any], txn) -> None:
|
||
"""财务页「已处理」展示:操作类型、操作人、操作时间。"""
|
||
status = txn.txn_status or ""
|
||
if status == "yqr" and txn.finance_confirm_time:
|
||
d["financeOperateType"] = "confirm"
|
||
d["financeOperateTypeLabel"] = "财务确认"
|
||
d["financeOperateByName"] = d.get("financeConfirmByName") or d.get("financeConfirmBy") or ""
|
||
d["financeOperateTime"] = d.get("financeConfirmTime") or ""
|
||
elif status == "yzf" and txn.finance_submit_time:
|
||
d["financeOperateType"] = "void"
|
||
d["financeOperateTypeLabel"] = "财务作废"
|
||
d["financeOperateByName"] = d.get("voidByName") or d.get("voidBy") or ""
|
||
d["financeOperateTime"] = d.get("voidTime") or ""
|
||
else:
|
||
d["financeOperateType"] = ""
|
||
d["financeOperateTypeLabel"] = ""
|
||
d["financeOperateByName"] = ""
|
||
d["financeOperateTime"] = ""
|
||
|
||
|
||
def list_channel_service_orders(
|
||
db,
|
||
*,
|
||
page=1,
|
||
page_size=10,
|
||
order_type=None,
|
||
order_status=None,
|
||
channel_id=None,
|
||
keyword=None,
|
||
create_time_start=None,
|
||
create_time_end=None,
|
||
tenant_no=None,
|
||
user_info=None,
|
||
):
|
||
def _filtered():
|
||
q0 = db.query(BizChannelServiceOrder).filter(BizChannelServiceOrder.del_flag == 0)
|
||
if tenant_no:
|
||
q0 = q0.filter(BizChannelServiceOrder.tenant_no == tenant_no)
|
||
ot = (order_type or "").strip()
|
||
if ot:
|
||
q0 = q0.filter(BizChannelServiceOrder.order_type == ot)
|
||
# Support comma-separated order_status
|
||
os_raw = (order_status or "").strip()
|
||
if os_raw:
|
||
statuses = [s.strip() for s in os_raw.split(",") if s.strip()]
|
||
if statuses:
|
||
q0 = q0.filter(BizChannelServiceOrder.order_status.in_(statuses))
|
||
if channel_id:
|
||
q0 = q0.filter(BizChannelServiceOrder.channel_id == channel_id)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q0 = q0.filter(
|
||
or_(
|
||
BizChannelServiceOrder.channel_name.like(like),
|
||
BizChannelServiceOrder.order_name.like(like),
|
||
BizChannelServiceOrder.dept_name.like(like),
|
||
)
|
||
)
|
||
if create_time_start and create_time_start.strip():
|
||
q0 = q0.filter(
|
||
BizChannelServiceOrder.create_time
|
||
>= _normalize_date_bound(create_time_start.strip())
|
||
)
|
||
if create_time_end and create_time_end.strip():
|
||
q0 = q0.filter(
|
||
BizChannelServiceOrder.create_time
|
||
<= _normalize_date_bound(create_time_end.strip(), end_of_day=True)
|
||
)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q0 = apply_data_scope(q0, BizChannelServiceOrder, user_info, db, tenant_no)
|
||
return q0
|
||
|
||
total = _filtered().order_by(None).with_entities(func.count(BizChannelServiceOrder.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = (
|
||
_filtered()
|
||
.order_by(BizChannelServiceOrder.create_time.desc())
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
user_ids: List[str] = []
|
||
for x in rows:
|
||
if x.create_by:
|
||
user_ids.append(x.create_by)
|
||
if x.finance_submit_by:
|
||
user_ids.append(x.finance_submit_by)
|
||
name_map = _user_nick_name_map(db, user_ids) if db else {}
|
||
gmcp_ids = [x.id for x in rows if (x.order_type or "") == "gmcp"]
|
||
purchase_total_map = _batch_gmcp_purchase_totals(db, gmcp_ids, tenant_no) if db and gmcp_ids else {}
|
||
holding_amount_map = (
|
||
_batch_gmcp_current_holding_amounts(db, gmcp_ids, tenant_no) if db and gmcp_ids else {}
|
||
)
|
||
return [
|
||
{
|
||
**_row_to_dict_service_order(
|
||
x,
|
||
db,
|
||
tenant_no,
|
||
purchase_total_override=purchase_total_map.get(x.id),
|
||
current_balance_override=holding_amount_map.get(x.id),
|
||
),
|
||
"createByName": name_map.get(x.create_by or "", "") or (x.create_by or ""),
|
||
"financeSubmitByName": name_map.get(x.finance_submit_by or "", "") or (x.finance_submit_by or ""),
|
||
}
|
||
for x in rows
|
||
], int(total)
|
||
|
||
|
||
def get_service_order(db, order_id, tenant_no=None):
|
||
q = db.query(BizChannelServiceOrder).filter(
|
||
and_(BizChannelServiceOrder.id == order_id, BizChannelServiceOrder.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelServiceOrder.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def get_service_order_products(db, service_order_id, tenant_no=None):
|
||
q = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == service_order_id,
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
|
||
rows = q.order_by(BizChannelServiceOrderProduct.create_time.asc()).all()
|
||
return [_row_to_dict_service_order_product(x) for x in rows]
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Service Order: Create / Update / Delete
|
||
# ────────────────────────────────────────────
|
||
|
||
def _calc_gmcp_purchase_total(products):
|
||
total = 0.0
|
||
for p in products:
|
||
price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
|
||
qty = float(p.get("buyQty") or p.get("buy_qty") or 0)
|
||
total += price * qty
|
||
return total
|
||
|
||
|
||
def _gmcp_has_purchase_lock(db, order_id, tenant_no=None) -> bool:
|
||
"""是否存在购买记录(未作废 purchase 流水,或历史建单写入的产品行)。"""
|
||
q = db.query(BizChannelServiceOrderTxn).filter(
|
||
and_(
|
||
BizChannelServiceOrderTxn.service_order_id == order_id,
|
||
BizChannelServiceOrderTxn.txn_type == "purchase",
|
||
BizChannelServiceOrderTxn.txn_status != "yzf",
|
||
BizChannelServiceOrderTxn.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelServiceOrderTxn.tenant_no == tenant_no)
|
||
if q.first():
|
||
return True
|
||
pq = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
pq = pq.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
|
||
for row in pq.all():
|
||
if float(row.buy_qty or 0) > 0 or float(row.total_buy_qty or 0) > 0:
|
||
return True
|
||
return False
|
||
|
||
|
||
def gmcp_agreement_editable(db, order, tenant_no=None) -> bool:
|
||
if order.order_type != "gmcp" or order.order_status == "yzf":
|
||
return False
|
||
return not _gmcp_has_purchase_lock(db, order.id, tenant_no or order.tenant_no)
|
||
|
||
|
||
def _resolve_agreement_price_cell_type(db, channel_agreement_id, project_code, spec_text, cell_type_fallback=""):
|
||
price_row = (
|
||
db.query(BizChannelAgreementProductPrice)
|
||
.filter(
|
||
and_(
|
||
BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
|
||
BizChannelAgreementProductPrice.project_code == (project_code or ""),
|
||
BizChannelAgreementProductPrice.spec_text == (spec_text or ""),
|
||
BizChannelAgreementProductPrice.del_flag == 0,
|
||
)
|
||
)
|
||
).first()
|
||
if price_row:
|
||
return price_row.cell_type_code or "", price_row.cell_type_name or cell_type_fallback
|
||
return "", cell_type_fallback
|
||
|
||
|
||
def _save_gmcp_product_rows(
|
||
db,
|
||
order_id,
|
||
products,
|
||
tenant_no,
|
||
user_id,
|
||
now=None,
|
||
):
|
||
"""仅保存购买订单产品明细,不调整份数库存"""
|
||
now = now or datetime.now()
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
project_name = p.get("projectName") or p.get("project_name") or ""
|
||
cell_type = p.get("cellType") or p.get("cell_type") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
|
||
buy_qty = float(p.get("buyQty") or p.get("buy_qty") or 0)
|
||
buy_amount = float(p.get("buyAmount") or p.get("buy_amount") or agreement_unit_price * buy_qty)
|
||
|
||
prod_row = BizChannelServiceOrderProduct(
|
||
id=generate_uuid(),
|
||
service_order_id=order_id,
|
||
project_code=project_code,
|
||
project_name=project_name,
|
||
cell_type=cell_type,
|
||
spec_text=spec_text,
|
||
agreement_unit_price=agreement_unit_price,
|
||
buy_qty=buy_qty,
|
||
buy_amount=buy_amount,
|
||
tenant_no=tenant_no or "zkxw",
|
||
order_num=0,
|
||
remark="",
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(prod_row)
|
||
db.flush()
|
||
|
||
|
||
def _replace_gmcp_product_rows(db, order_id, products, tenant_no, user_id):
|
||
now = datetime.now()
|
||
db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
|
||
_save_gmcp_product_rows(db, order_id, products, tenant_no, user_id, now=now)
|
||
|
||
|
||
def _apply_gmcp_order_quotas(db, order, tenant_no, user_id):
|
||
"""财务确认收款后,按订单明细增加渠道产品份数"""
|
||
products = get_service_order_products(db, order.id, tenant_no)
|
||
for prod in products:
|
||
project_code = prod.get("projectCode") or ""
|
||
spec_text = prod.get("specText") or ""
|
||
cell_type_code, cell_type_name = _resolve_agreement_price_cell_type(
|
||
db,
|
||
order.channel_agreement_id,
|
||
project_code,
|
||
spec_text,
|
||
prod.get("cellType") or "",
|
||
)
|
||
_upsert_channel_product_quota(
|
||
db,
|
||
channel_id=order.channel_id,
|
||
project_code=project_code,
|
||
project_name=prod.get("projectName") or "",
|
||
cell_type_code=cell_type_code,
|
||
cell_type_name=cell_type_name,
|
||
spec_text=spec_text,
|
||
add_qty=float(prod.get("buyQty") or 0),
|
||
tenant_no=tenant_no or order.tenant_no,
|
||
user_id=user_id,
|
||
)
|
||
|
||
|
||
def _get_valid_channel_agreement(db, channel_agreement_id, channel_id, tenant_no=None):
|
||
"""校验渠道协议存在、属于当前渠道且已生效"""
|
||
aid = (channel_agreement_id or "").strip()
|
||
if not aid:
|
||
raise ValueError("请选择渠道协议")
|
||
q = db.query(BizChannelAgreement).filter(
|
||
and_(
|
||
BizChannelAgreement.id == aid,
|
||
BizChannelAgreement.channel_id == channel_id,
|
||
BizChannelAgreement.del_flag == 0,
|
||
)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
|
||
agreement = q.first()
|
||
if not agreement:
|
||
raise ValueError("渠道协议不存在或不属于当前渠道")
|
||
if (agreement.agreement_status or "").strip() != "sxz":
|
||
raise ValueError("协议未生效,无法使用")
|
||
return agreement
|
||
|
||
|
||
def create_channel_service_order(
|
||
db,
|
||
*,
|
||
channel_id,
|
||
order_type,
|
||
channel_name=None,
|
||
order_name=None,
|
||
dept_name=None,
|
||
prepay_amount=None,
|
||
channel_agreement_id=None,
|
||
products=None,
|
||
tenant_no=None,
|
||
remark=None,
|
||
user_id=None,
|
||
):
|
||
# Validate channel exists
|
||
channel = get_channel(db, channel_id, tenant_no)
|
||
if not channel:
|
||
raise ValueError("渠道不存在")
|
||
|
||
now = datetime.now()
|
||
balance_before = None
|
||
balance_after = None
|
||
purchase_total_amount = None
|
||
order_status = "dsx"
|
||
|
||
if order_type == "yfk":
|
||
# 预存款订单:创建后为待生效,充值金额通过「发起充值」流水录入
|
||
_get_valid_channel_agreement(db, channel_agreement_id, channel_id, tenant_no)
|
||
order_status = "dsx"
|
||
prepay_amount = None
|
||
|
||
elif order_type == "gmcp":
|
||
# 购买型:建单仅绑定渠道与协议;产品通过 purchase 流水录入并经财务确认
|
||
if not channel_agreement_id:
|
||
raise ValueError("请选择渠道协议")
|
||
_get_valid_channel_agreement(db, channel_agreement_id, channel_id, tenant_no)
|
||
purchase_total_amount = _calc_gmcp_purchase_total(products) if products else None
|
||
order_status = "dsx"
|
||
else:
|
||
raise ValueError(f"不支持的订单类型: {order_type}")
|
||
|
||
order_id = generate_uuid()
|
||
order = BizChannelServiceOrder(
|
||
id=order_id,
|
||
channel_id=channel_id,
|
||
channel_name=channel_name or channel.channel_name or "",
|
||
order_name=(order_name or "").strip() or None,
|
||
dept_name=dept_name or "",
|
||
order_type=order_type,
|
||
prepay_amount=None,
|
||
purchase_total_amount=purchase_total_amount,
|
||
order_status=order_status,
|
||
channel_agreement_id=channel_agreement_id,
|
||
balance_before=balance_before,
|
||
balance_after=balance_after,
|
||
tenant_no=tenant_no or "zkxw",
|
||
order_num=0,
|
||
remark=remark or "",
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(order)
|
||
db.flush()
|
||
|
||
db.commit()
|
||
db.refresh(order)
|
||
return order
|
||
|
||
|
||
def update_channel_service_order(db, order_id, *, tenant_no=None, user_id=None, products=None, **kwargs):
|
||
"""更新订单基本信息;gmcp 产品仅通过 purchase 流水维护"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_status == "yzf":
|
||
raise ValueError("已作废订单不可编辑")
|
||
if kwargs.get("channel_id"):
|
||
raise ValueError("渠道不可修改")
|
||
if products is not None and order.order_type == "gmcp":
|
||
raise ValueError("请通过「购买产品」操作录入产品明细")
|
||
|
||
if order.order_type in ("yfk", "gmcp"):
|
||
if order.order_status not in ("dsx", "sxz"):
|
||
raise ValueError("当前状态不允许编辑")
|
||
else:
|
||
raise ValueError("不支持的订单类型")
|
||
|
||
if "dept_name" in kwargs and kwargs["dept_name"] is not None:
|
||
order.dept_name = kwargs["dept_name"]
|
||
if "order_name" in kwargs and kwargs["order_name"] is not None:
|
||
order.order_name = (kwargs["order_name"] or "").strip() or None
|
||
if "remark" in kwargs and kwargs["remark"] is not None:
|
||
order.remark = kwargs["remark"]
|
||
if order.order_type == "yfk":
|
||
if "channel_agreement_id" in kwargs and kwargs["channel_agreement_id"] is not None:
|
||
new_agreement_id = kwargs["channel_agreement_id"]
|
||
if new_agreement_id != (order.channel_agreement_id or ""):
|
||
if order.order_status != "dsx":
|
||
raise ValueError("已有业务流水,不可修改渠道协议")
|
||
_get_valid_channel_agreement(
|
||
db, new_agreement_id, order.channel_id, tenant_no
|
||
)
|
||
order.channel_agreement_id = new_agreement_id
|
||
elif order.order_type == "gmcp":
|
||
if "channel_agreement_id" in kwargs and kwargs["channel_agreement_id"] is not None:
|
||
new_agreement_id = kwargs["channel_agreement_id"]
|
||
if new_agreement_id != (order.channel_agreement_id or ""):
|
||
if _gmcp_has_purchase_lock(db, order_id, tenant_no):
|
||
raise ValueError("已有购买记录,不可修改渠道协议")
|
||
_get_valid_channel_agreement(
|
||
db, new_agreement_id, order.channel_id, tenant_no
|
||
)
|
||
order.channel_agreement_id = new_agreement_id
|
||
order.update_by = user_id
|
||
order.update_time = datetime.now()
|
||
db.add(order)
|
||
db.commit()
|
||
db.refresh(order)
|
||
return order
|
||
|
||
|
||
def soft_delete_service_order(db, order_id, tenant_no=None):
|
||
"""仅允许待生效状态下删除订单"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_status != "dsx":
|
||
raise ValueError("仅待生效订单可删除")
|
||
order.del_flag = 1
|
||
order.update_time = datetime.now()
|
||
db.add(order)
|
||
db.commit()
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Service Order: Status Transitions
|
||
# ────────────────────────────────────────────
|
||
|
||
def submit_finance_confirm(
|
||
db,
|
||
order_id,
|
||
*,
|
||
finance_submit_remark=None,
|
||
finance_submit_attachments=None,
|
||
tenant_no=None,
|
||
user_id=None,
|
||
):
|
||
"""发起财务确认:type=yfk/gmcp + status=dsx → dqr"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_type not in ("yfk", "gmcp"):
|
||
raise ValueError("仅预存款或购买产品订单可发起财务确认")
|
||
if order.order_status != "dsx":
|
||
raise ValueError("仅待生效状态可发起财务确认")
|
||
|
||
now = datetime.now()
|
||
order.order_status = "dqr"
|
||
order.finance_submit_remark = finance_submit_remark or ""
|
||
order.finance_submit_attachments = finance_submit_attachments or ""
|
||
order.finance_submit_time = now
|
||
order.finance_submit_by = user_id
|
||
order.update_by = user_id
|
||
order.update_time = now
|
||
db.add(order)
|
||
db.commit()
|
||
db.refresh(order)
|
||
return order
|
||
|
||
|
||
def confirm_receipt(db, order_id, *, finance_remark=None, finance_confirm_attachments=None, tenant_no=None, user_id=None):
|
||
"""确认到账:yfk dqr→yqr 增加余额;gmcp dqr→sxz 增加产品份数"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_type not in ("yfk", "gmcp"):
|
||
raise ValueError("仅预存款或购买产品订单可确认到账")
|
||
if order.order_status != "dqr":
|
||
raise ValueError("仅待确认状态可确认到账")
|
||
|
||
now = datetime.now()
|
||
|
||
if order.order_type == "yfk":
|
||
account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
|
||
balance_before = float(account.balance_amount) if account.balance_amount else 0
|
||
add_amount = float(order.prepay_amount) if order.prepay_amount else 0
|
||
account.balance_amount = balance_before + add_amount
|
||
balance_after = float(account.balance_amount)
|
||
account.update_by = user_id
|
||
account.update_time = now
|
||
db.add(account)
|
||
db.flush()
|
||
|
||
order.order_status = "yqr"
|
||
order.balance_before = balance_before
|
||
order.balance_after = balance_after
|
||
else:
|
||
_apply_gmcp_order_quotas(db, order, tenant_no, user_id)
|
||
order.order_status = "sxz"
|
||
|
||
order.finance_confirm_time = now
|
||
order.finance_confirm_attachments = finance_confirm_attachments or ""
|
||
order.finance_remark = finance_remark or ""
|
||
order.update_by = user_id
|
||
order.update_time = now
|
||
db.add(order)
|
||
db.commit()
|
||
db.refresh(order)
|
||
return order
|
||
|
||
|
||
def submit_refund(db, order_id, *, refund_amount=None, refund_remark=None, refund_attachments=None, tenant_no=None, user_id=None):
|
||
"""申请退款:type=yfk + status=yqr → tfqrz"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_type != "yfk":
|
||
raise ValueError("仅预存款订单可申请退款")
|
||
if order.order_status != "yqr":
|
||
raise ValueError("仅已确认状态可申请退款")
|
||
|
||
now = datetime.now()
|
||
# Default refund_amount to prepay_amount
|
||
if refund_amount is None:
|
||
refund_amount = float(order.prepay_amount) if order.prepay_amount else 0
|
||
else:
|
||
refund_amount = float(refund_amount)
|
||
|
||
prepay = float(order.prepay_amount) if order.prepay_amount else 0
|
||
if refund_amount > prepay:
|
||
raise ValueError(f"退款金额不能大于预存金额 {prepay}")
|
||
|
||
order.order_status = "tfqrz"
|
||
order.refund_apply_by = user_id
|
||
order.refund_apply_time = now
|
||
order.refund_amount = refund_amount
|
||
order.refund_remark = refund_remark or ""
|
||
order.refund_attachments = refund_attachments or ""
|
||
order.update_by = user_id
|
||
order.update_time = now
|
||
db.add(order)
|
||
db.commit()
|
||
db.refresh(order)
|
||
return order
|
||
|
||
|
||
def confirm_refund(db, order_id, *, refund_finance_remark=None, refund_finance_confirm_attachments=None, tenant_no=None, user_id=None):
|
||
"""确认退款:status=tfqrz → ytf,扣减余额"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_status != "tfqrz":
|
||
raise ValueError("仅退款待确认状态可确认退款")
|
||
|
||
now = datetime.now()
|
||
account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
|
||
balance_before = float(account.balance_amount) if account.balance_amount else 0
|
||
deduct = float(order.refund_amount) if order.refund_amount else 0
|
||
account.balance_amount = balance_before - deduct
|
||
balance_after = float(account.balance_amount)
|
||
account.update_by = user_id
|
||
account.update_time = now
|
||
db.add(account)
|
||
db.flush()
|
||
|
||
order.order_status = "ytf"
|
||
order.refund_finance_confirm_time = now
|
||
order.refund_finance_confirm_attachments = refund_finance_confirm_attachments or ""
|
||
order.refund_finance_remark = refund_finance_remark or ""
|
||
order.balance_before = balance_before
|
||
order.balance_after = balance_after
|
||
order.update_by = user_id
|
||
order.update_time = now
|
||
db.add(order)
|
||
db.commit()
|
||
db.refresh(order)
|
||
return order
|
||
|
||
|
||
def void_service_order(db, order_id, *, tenant_no=None, user_id=None):
|
||
"""作废订单:type=gmcp + status=sxz → yzf,扣减已生效的产品份数"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_type != "gmcp":
|
||
raise ValueError("仅购买产品订单可作废")
|
||
if order.order_status != "sxz":
|
||
raise ValueError("仅生效中状态可作废")
|
||
|
||
now = datetime.now()
|
||
|
||
# Reduce product quotas
|
||
products = get_service_order_products(db, order_id, tenant_no)
|
||
for prod in products:
|
||
cell_type_code, _ = _resolve_agreement_price_cell_type(
|
||
db,
|
||
order.channel_agreement_id,
|
||
prod.get("projectCode") or "",
|
||
prod.get("specText") or "",
|
||
prod.get("cellType") or "",
|
||
)
|
||
|
||
_reduce_channel_product_quota(
|
||
db,
|
||
channel_id=order.channel_id,
|
||
project_code=prod.get("projectCode") or "",
|
||
cell_type_code=cell_type_code,
|
||
spec_text=prod.get("specText") or "",
|
||
reduce_qty=prod.get("buyQty") or 0,
|
||
user_id=user_id,
|
||
)
|
||
|
||
order.order_status = "yzf"
|
||
order.update_by = user_id
|
||
order.update_time = now
|
||
db.add(order)
|
||
db.commit()
|
||
db.refresh(order)
|
||
return order
|
||
|
||
|
||
# ────────────────────────────────────────────
|
||
# Service Order Txn: 操作流水 CRUD
|
||
# ────────────────────────────────────────────
|
||
|
||
def list_service_order_txns(
|
||
db,
|
||
*,
|
||
service_order_id=None,
|
||
channel_id=None,
|
||
txn_status=None,
|
||
txn_type=None,
|
||
keyword=None,
|
||
create_time_start=None,
|
||
create_time_end=None,
|
||
finance_done_only=False,
|
||
page=1,
|
||
page_size=20,
|
||
tenant_no=None,
|
||
):
|
||
"""查询操作流水列表。finance_done_only:财务已处理(已确认或财务侧作废)。"""
|
||
q = db.query(BizChannelServiceOrderTxn).filter(BizChannelServiceOrderTxn.del_flag == 0)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelServiceOrderTxn.tenant_no == tenant_no)
|
||
if service_order_id:
|
||
q = q.filter(BizChannelServiceOrderTxn.service_order_id == service_order_id)
|
||
cid = (channel_id or "").strip()
|
||
kw = (keyword or "").strip()
|
||
if cid:
|
||
q = q.join(
|
||
BizChannelServiceOrder,
|
||
and_(
|
||
BizChannelServiceOrder.id == BizChannelServiceOrderTxn.service_order_id,
|
||
BizChannelServiceOrder.del_flag == 0,
|
||
),
|
||
).filter(BizChannelServiceOrder.channel_id == cid)
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q = q.filter(
|
||
or_(
|
||
BizChannelServiceOrder.channel_name.like(like),
|
||
BizChannelServiceOrder.order_name.like(like),
|
||
BizChannelServiceOrder.dept_name.like(like),
|
||
)
|
||
)
|
||
elif kw:
|
||
like = f"%{kw}%"
|
||
q = q.outerjoin(
|
||
BizChannelServiceOrder,
|
||
and_(
|
||
BizChannelServiceOrder.id == BizChannelServiceOrderTxn.service_order_id,
|
||
BizChannelServiceOrder.del_flag == 0,
|
||
),
|
||
).filter(
|
||
or_(
|
||
BizChannelServiceOrder.channel_name.like(like),
|
||
BizChannelServiceOrder.order_name.like(like),
|
||
BizChannelServiceOrder.dept_name.like(like),
|
||
)
|
||
)
|
||
if finance_done_only:
|
||
q = q.filter(
|
||
or_(
|
||
BizChannelServiceOrderTxn.txn_status == "yqr",
|
||
and_(
|
||
BizChannelServiceOrderTxn.txn_status == "yzf",
|
||
BizChannelServiceOrderTxn.finance_submit_time.isnot(None),
|
||
),
|
||
)
|
||
)
|
||
else:
|
||
ss = (txn_status or "").strip()
|
||
if ss:
|
||
statuses = [s.strip() for s in ss.split(",") if s.strip()]
|
||
if statuses:
|
||
q = q.filter(BizChannelServiceOrderTxn.txn_status.in_(statuses))
|
||
tt = (txn_type or "").strip()
|
||
if tt:
|
||
types = [t.strip() for t in tt.split(",") if t.strip()]
|
||
if types:
|
||
q = q.filter(BizChannelServiceOrderTxn.txn_type.in_(types))
|
||
if create_time_start and create_time_start.strip():
|
||
q = q.filter(
|
||
BizChannelServiceOrderTxn.create_time
|
||
>= _normalize_date_bound(create_time_start.strip())
|
||
)
|
||
if create_time_end and create_time_end.strip():
|
||
q = q.filter(
|
||
BizChannelServiceOrderTxn.create_time
|
||
<= _normalize_date_bound(create_time_end.strip(), end_of_day=True)
|
||
)
|
||
|
||
total = q.order_by(None).with_entities(func.count(BizChannelServiceOrderTxn.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
if finance_done_only:
|
||
operate_time = func.coalesce(
|
||
BizChannelServiceOrderTxn.finance_confirm_time,
|
||
BizChannelServiceOrderTxn.void_time,
|
||
)
|
||
rows = (
|
||
q.order_by(operate_time.desc(), BizChannelServiceOrderTxn.create_time.desc())
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
else:
|
||
rows = (
|
||
q.order_by(BizChannelServiceOrderTxn.create_time.desc())
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
if not rows:
|
||
return [], int(total)
|
||
|
||
order_ids = list({r.service_order_id for r in rows if r.service_order_id})
|
||
orders: Dict[str, Any] = {}
|
||
if order_ids:
|
||
oq = db.query(BizChannelServiceOrder).filter(
|
||
BizChannelServiceOrder.id.in_(order_ids),
|
||
BizChannelServiceOrder.del_flag == 0,
|
||
)
|
||
if tenant_no:
|
||
oq = oq.filter(BizChannelServiceOrder.tenant_no == tenant_no)
|
||
for o in oq.all():
|
||
orders[o.id] = o
|
||
|
||
user_ids: List[str] = []
|
||
for r in rows:
|
||
if r.create_by:
|
||
user_ids.append(r.create_by)
|
||
if r.finance_submit_by:
|
||
user_ids.append(r.finance_submit_by)
|
||
if r.finance_confirm_by:
|
||
user_ids.append(r.finance_confirm_by)
|
||
elif (r.txn_status or "") == "yqr" and r.update_by:
|
||
user_ids.append(r.update_by)
|
||
if r.void_by:
|
||
user_ids.append(r.void_by)
|
||
elif (r.txn_status or "") == "yzf" and r.finance_submit_time and r.update_by:
|
||
user_ids.append(r.update_by)
|
||
name_map = _user_nick_name_map(db, user_ids) if db and user_ids else {}
|
||
|
||
items = [
|
||
_service_order_txn_to_response_dict(
|
||
db,
|
||
r,
|
||
tenant_no,
|
||
order=orders.get(r.service_order_id),
|
||
name_map=name_map,
|
||
)
|
||
for r in rows
|
||
]
|
||
return items, int(total)
|
||
|
||
|
||
def get_service_order_txn(db, txn_id, *, tenant_no=None):
|
||
"""获取单条流水"""
|
||
q = db.query(BizChannelServiceOrderTxn).filter(
|
||
and_(BizChannelServiceOrderTxn.id == txn_id, BizChannelServiceOrderTxn.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizChannelServiceOrderTxn.tenant_no == tenant_no)
|
||
return q.first()
|
||
|
||
|
||
def create_recharge_txn(
|
||
db,
|
||
order_id,
|
||
*,
|
||
amount,
|
||
remark=None,
|
||
finance_submit_attachments=None,
|
||
tenant_no=None,
|
||
user_id=None,
|
||
):
|
||
"""创建充值流水(yfk订单),状态 dsx;附件预存于 finance_submit_attachments 供后续财务确认"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_type != "yfk":
|
||
raise ValueError("仅充值型订单可发起充值")
|
||
if order.order_status not in ("dsx", "sxz"):
|
||
raise ValueError("订单当前状态不允许充值")
|
||
|
||
amount = float(amount)
|
||
if amount <= 0:
|
||
raise ValueError("充值金额必须大于0")
|
||
|
||
now = datetime.now()
|
||
txn = BizChannelServiceOrderTxn(
|
||
id=generate_uuid(),
|
||
service_order_id=order_id,
|
||
txn_type="recharge",
|
||
amount=amount,
|
||
txn_status="dsx",
|
||
remark=remark or "",
|
||
finance_submit_attachments=(finance_submit_attachments or "").strip() or None,
|
||
tenant_no=tenant_no or order.tenant_no,
|
||
order_num=0,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(txn)
|
||
db.commit()
|
||
db.refresh(txn)
|
||
return txn
|
||
|
||
|
||
def create_refund_txn(db, order_id, *, amount, remark=None, attachments=None, tenant_no=None, user_id=None):
|
||
"""创建退款流水(yfk订单),amount <= current_balance"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_type != "yfk":
|
||
raise ValueError("仅充值型订单可申请退款")
|
||
if order.order_status != "sxz":
|
||
raise ValueError("仅生效中订单可申请退款")
|
||
|
||
amount = float(amount)
|
||
if amount <= 0:
|
||
raise ValueError("退款金额必须大于0")
|
||
current_balance = float(order.current_balance) if order.current_balance else 0
|
||
if amount > current_balance:
|
||
raise ValueError(f"退款金额不能大于当前余额 {current_balance}")
|
||
|
||
now = datetime.now()
|
||
txn = BizChannelServiceOrderTxn(
|
||
id=generate_uuid(),
|
||
service_order_id=order_id,
|
||
txn_type="refund",
|
||
amount=amount,
|
||
txn_status="dsx",
|
||
remark=remark or "",
|
||
refund_attachments=attachments or "",
|
||
tenant_no=tenant_no or order.tenant_no,
|
||
order_num=0,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(txn)
|
||
db.commit()
|
||
db.refresh(txn)
|
||
return txn
|
||
|
||
|
||
def create_purchase_txn(db, order_id, *, products, remark=None, tenant_no=None, user_id=None):
|
||
"""创建购买产品流水(gmcp订单),同时创建/更新产品持仓"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_type != "gmcp":
|
||
raise ValueError("仅购买型订单可购买产品")
|
||
if order.order_status not in ("dsx", "sxz"):
|
||
raise ValueError("订单当前状态不允许购买")
|
||
if not products:
|
||
raise ValueError("产品明细不能为空")
|
||
|
||
now = datetime.now()
|
||
tn = tenant_no or order.tenant_no
|
||
total_amount = 0.0
|
||
|
||
# Upsert product holding rows
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
project_name = p.get("projectName") or p.get("project_name") or ""
|
||
cell_type = p.get("cellType") or p.get("cell_type") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
|
||
qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
|
||
amt = float(p.get("amount") or agreement_unit_price * qty)
|
||
total_amount += amt
|
||
|
||
# Find or create product holding row
|
||
existing = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
||
BizChannelServiceOrderProduct.project_code == project_code,
|
||
BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
).first()
|
||
|
||
if existing:
|
||
existing.total_buy_qty = float(existing.total_buy_qty or 0) + qty
|
||
existing.current_qty = float(existing.current_qty or 0) + qty
|
||
existing.update_by = user_id
|
||
existing.update_time = now
|
||
db.add(existing)
|
||
else:
|
||
prod_row = BizChannelServiceOrderProduct(
|
||
id=generate_uuid(),
|
||
service_order_id=order_id,
|
||
project_code=project_code,
|
||
project_name=project_name,
|
||
cell_type=cell_type,
|
||
spec_text=spec_text,
|
||
agreement_unit_price=agreement_unit_price,
|
||
buy_qty=qty,
|
||
buy_amount=amt,
|
||
total_buy_qty=qty,
|
||
total_unsubscribe_qty=0,
|
||
current_qty=qty,
|
||
total_unsubscribe_amount=0,
|
||
tenant_no=tn,
|
||
order_num=0,
|
||
remark="",
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(prod_row)
|
||
|
||
db.flush()
|
||
|
||
products_json = json.dumps(products, ensure_ascii=False)
|
||
txn = BizChannelServiceOrderTxn(
|
||
id=generate_uuid(),
|
||
service_order_id=order_id,
|
||
txn_type="purchase",
|
||
amount=total_amount,
|
||
txn_status="dsx",
|
||
products_json=products_json,
|
||
remark=remark or "",
|
||
tenant_no=tn,
|
||
order_num=0,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
order.purchase_total_amount = float(order.purchase_total_amount or 0) + total_amount
|
||
order.update_by = user_id
|
||
order.update_time = now
|
||
db.add(order)
|
||
db.add(txn)
|
||
db.commit()
|
||
db.refresh(txn)
|
||
return txn
|
||
|
||
|
||
def create_unsubscribe_txn(db, order_id, *, products, remark=None, tenant_no=None, user_id=None):
|
||
"""创建退订产品流水(gmcp订单),校验退订份数 <= current_qty"""
|
||
order = get_service_order(db, order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("订单不存在")
|
||
if order.order_type != "gmcp":
|
||
raise ValueError("仅购买型订单可退订产品")
|
||
if order.order_status != "sxz":
|
||
raise ValueError("仅生效中订单可退订")
|
||
if not products:
|
||
raise ValueError("退订产品明细不能为空")
|
||
|
||
now = datetime.now()
|
||
tn = tenant_no or order.tenant_no
|
||
total_amount = 0.0
|
||
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
qty = float(p.get("qty") or 0)
|
||
if qty <= 0:
|
||
raise ValueError("退订数量必须大于0")
|
||
|
||
# Find existing product holding
|
||
existing = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
||
BizChannelServiceOrderProduct.project_code == project_code,
|
||
BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
).first()
|
||
if not existing:
|
||
raise ValueError(f"产品 {project_code} 不存在于持仓中")
|
||
|
||
current_qty = float(existing.current_qty or 0)
|
||
if qty > current_qty:
|
||
raise ValueError(f"退订数量 {qty} 超过当前持有数量 {current_qty}")
|
||
|
||
unit_price = float(existing.agreement_unit_price or 0)
|
||
amt = qty * unit_price
|
||
total_amount += amt
|
||
|
||
existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) + qty
|
||
existing.current_qty = current_qty - qty
|
||
existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) + amt
|
||
existing.update_by = user_id
|
||
existing.update_time = now
|
||
db.add(existing)
|
||
|
||
db.flush()
|
||
|
||
products_json = json.dumps(products, ensure_ascii=False)
|
||
txn = BizChannelServiceOrderTxn(
|
||
id=generate_uuid(),
|
||
service_order_id=order_id,
|
||
txn_type="unsubscribe",
|
||
amount=total_amount,
|
||
txn_status="dsx",
|
||
products_json=products_json,
|
||
remark=remark or "",
|
||
tenant_no=tn,
|
||
order_num=0,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(txn)
|
||
db.commit()
|
||
db.refresh(txn)
|
||
return txn
|
||
|
||
|
||
def _rollback_purchase_txn_holdings(db, order_id, products, user_id, now=None):
|
||
now = now or datetime.now()
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
|
||
existing = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
||
BizChannelServiceOrderProduct.project_code == project_code,
|
||
BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
).first()
|
||
if existing:
|
||
existing.total_buy_qty = float(existing.total_buy_qty or 0) - qty
|
||
existing.current_qty = float(existing.current_qty or 0) - qty
|
||
existing.update_by = user_id
|
||
existing.update_time = now
|
||
db.add(existing)
|
||
|
||
|
||
def _apply_purchase_txn_holdings(db, order_id, products, tenant_no, user_id, now=None):
|
||
now = now or datetime.now()
|
||
tn = tenant_no
|
||
total_amount = 0.0
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
project_name = p.get("projectName") or p.get("project_name") or ""
|
||
cell_type = p.get("cellType") or p.get("cell_type") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
|
||
qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
|
||
if qty <= 0:
|
||
raise ValueError("购买份数必须大于0")
|
||
amt = float(p.get("amount") or agreement_unit_price * qty)
|
||
total_amount += amt
|
||
|
||
existing = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
||
BizChannelServiceOrderProduct.project_code == project_code,
|
||
BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
).first()
|
||
|
||
if existing:
|
||
existing.total_buy_qty = float(existing.total_buy_qty or 0) + qty
|
||
existing.current_qty = float(existing.current_qty or 0) + qty
|
||
existing.update_by = user_id
|
||
existing.update_time = now
|
||
db.add(existing)
|
||
else:
|
||
prod_row = BizChannelServiceOrderProduct(
|
||
id=generate_uuid(),
|
||
service_order_id=order_id,
|
||
project_code=project_code,
|
||
project_name=project_name,
|
||
cell_type=cell_type,
|
||
spec_text=spec_text,
|
||
agreement_unit_price=agreement_unit_price,
|
||
buy_qty=qty,
|
||
buy_amount=amt,
|
||
total_buy_qty=qty,
|
||
total_unsubscribe_qty=0,
|
||
current_qty=qty,
|
||
total_unsubscribe_amount=0,
|
||
tenant_no=tn,
|
||
order_num=0,
|
||
remark="",
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(prod_row)
|
||
db.flush()
|
||
return total_amount
|
||
|
||
|
||
def _rollback_unsubscribe_txn_holdings(db, order_id, products, user_id, now=None):
|
||
now = now or datetime.now()
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
qty = float(p.get("qty") or 0)
|
||
existing = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
||
BizChannelServiceOrderProduct.project_code == project_code,
|
||
BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
).first()
|
||
if existing:
|
||
unit_price = float(existing.agreement_unit_price or 0)
|
||
existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) - qty
|
||
existing.current_qty = float(existing.current_qty or 0) + qty
|
||
existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) - (qty * unit_price)
|
||
existing.update_by = user_id
|
||
existing.update_time = now
|
||
db.add(existing)
|
||
|
||
|
||
def _apply_unsubscribe_txn_holdings(db, order_id, products, user_id, now=None):
|
||
now = now or datetime.now()
|
||
total_amount = 0.0
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
qty = float(p.get("qty") or 0)
|
||
if qty <= 0:
|
||
raise ValueError("退订数量必须大于0")
|
||
|
||
existing = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
||
BizChannelServiceOrderProduct.project_code == project_code,
|
||
BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
).first()
|
||
if not existing:
|
||
raise ValueError(f"产品 {project_code} 不存在于持仓中")
|
||
|
||
current_qty = float(existing.current_qty or 0)
|
||
if qty > current_qty:
|
||
raise ValueError(f"退订数量 {qty} 超过当前持有数量 {current_qty}")
|
||
|
||
unit_price = float(existing.agreement_unit_price or 0)
|
||
amt = qty * unit_price
|
||
total_amount += amt
|
||
|
||
existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) + qty
|
||
existing.current_qty = current_qty - qty
|
||
existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) + amt
|
||
existing.update_by = user_id
|
||
existing.update_time = now
|
||
db.add(existing)
|
||
db.flush()
|
||
return total_amount
|
||
|
||
|
||
def update_service_order_txn(
|
||
db,
|
||
txn_id,
|
||
*,
|
||
amount=None,
|
||
remark=None,
|
||
finance_submit_attachments=None,
|
||
refund_attachments=None,
|
||
products=None,
|
||
tenant_no=None,
|
||
user_id=None,
|
||
):
|
||
"""更新待生效流水:仅 dsx;充值/退款可改金额与附件,购买/退订可改产品明细与备注"""
|
||
txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
|
||
if not txn:
|
||
raise ValueError("流水不存在")
|
||
if txn.txn_status != "dsx":
|
||
raise ValueError("仅待生效流水可编辑")
|
||
|
||
now = datetime.now()
|
||
txn_type = (txn.txn_type or "").strip()
|
||
|
||
if amount is not None:
|
||
amt = float(amount)
|
||
if amt <= 0:
|
||
raise ValueError("金额必须大于0")
|
||
if txn_type in ("recharge", "refund"):
|
||
if txn_type == "refund":
|
||
order = get_service_order(db, txn.service_order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("关联订单不存在")
|
||
current_balance = float(order.current_balance) if order.current_balance else 0
|
||
if amt > current_balance:
|
||
raise ValueError(f"退款金额不能大于当前余额 {current_balance}")
|
||
txn.amount = amt
|
||
else:
|
||
raise ValueError("购买/退订流水不可直接修改金额,请调整产品明细")
|
||
|
||
if products is not None:
|
||
if txn_type not in ("purchase", "unsubscribe"):
|
||
raise ValueError("仅购买/退订流水可修改产品明细")
|
||
if not products:
|
||
raise ValueError("产品明细不能为空")
|
||
order = get_service_order(db, txn.service_order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("关联订单不存在")
|
||
old_products = json.loads(txn.products_json) if txn.products_json else []
|
||
if txn_type == "purchase":
|
||
_rollback_purchase_txn_holdings(db, order.id, old_products, user_id, now)
|
||
total_amount = _apply_purchase_txn_holdings(
|
||
db, order.id, products, tenant_no or order.tenant_no, user_id, now
|
||
)
|
||
else:
|
||
_rollback_unsubscribe_txn_holdings(db, order.id, old_products, user_id, now)
|
||
total_amount = _apply_unsubscribe_txn_holdings(db, order.id, products, user_id, now)
|
||
txn.amount = total_amount
|
||
txn.products_json = json.dumps(products, ensure_ascii=False)
|
||
|
||
if remark is not None:
|
||
txn.remark = remark or ""
|
||
|
||
if finance_submit_attachments is not None:
|
||
txn.finance_submit_attachments = (finance_submit_attachments or "").strip() or None
|
||
|
||
if refund_attachments is not None:
|
||
if txn_type != "refund":
|
||
raise ValueError("仅退款流水可修改退款附件")
|
||
txn.refund_attachments = (refund_attachments or "").strip() or None
|
||
|
||
txn.update_by = user_id
|
||
txn.update_time = now
|
||
db.add(txn)
|
||
db.commit()
|
||
db.refresh(txn)
|
||
return txn
|
||
|
||
|
||
def submit_txn_finance(db, txn_id, *, finance_submit_remark=None, finance_submit_attachments=None, tenant_no=None, user_id=None):
|
||
"""发起流水财务确认:dsx → dqr"""
|
||
txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
|
||
if not txn:
|
||
raise ValueError("流水不存在")
|
||
if txn.txn_status != "dsx":
|
||
raise ValueError("仅待生效流水可发起确认")
|
||
|
||
now = datetime.now()
|
||
txn.txn_status = "dqr"
|
||
txn.finance_submit_by = user_id
|
||
txn.finance_submit_time = now
|
||
submit_remark = (finance_submit_remark or "").strip()
|
||
if not submit_remark:
|
||
submit_remark = (txn.remark or "").strip()
|
||
txn.finance_submit_remark = submit_remark
|
||
if finance_submit_attachments:
|
||
txn.finance_submit_attachments = finance_submit_attachments
|
||
elif not (txn.finance_submit_attachments or "").strip():
|
||
if (txn.txn_type or "").strip() == "refund" and (txn.refund_attachments or "").strip():
|
||
txn.finance_submit_attachments = txn.refund_attachments
|
||
else:
|
||
txn.finance_submit_attachments = ""
|
||
txn.update_by = user_id
|
||
txn.update_time = now
|
||
db.add(txn)
|
||
db.commit()
|
||
db.refresh(txn)
|
||
return txn
|
||
|
||
|
||
def confirm_txn(db, txn_id, *, finance_remark=None, finance_confirm_attachments=None, tenant_no=None, user_id=None):
|
||
"""财务确认流水:dqr → yqr,更新主表汇总和产品持仓,更新渠道账户余额"""
|
||
txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
|
||
if not txn:
|
||
raise ValueError("流水不存在")
|
||
if txn.txn_status != "dqr":
|
||
raise ValueError("仅待确认流水可进行财务确认")
|
||
|
||
order = get_service_order(db, txn.service_order_id, tenant_no)
|
||
if not order:
|
||
raise ValueError("关联订单不存在")
|
||
|
||
now = datetime.now()
|
||
|
||
if txn.txn_type == "recharge":
|
||
# Increase channel account balance
|
||
account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
|
||
balance_before = float(account.balance_amount) if account.balance_amount else 0
|
||
add_amount = float(txn.amount) if txn.amount else 0
|
||
account.balance_amount = balance_before + add_amount
|
||
balance_after = float(account.balance_amount)
|
||
account.update_by = user_id
|
||
account.update_time = now
|
||
db.add(account)
|
||
db.flush()
|
||
|
||
# Update order summary
|
||
order.total_recharge_amount = float(order.total_recharge_amount or 0) + add_amount
|
||
order.current_balance = float(order.current_balance or 0) + add_amount
|
||
txn.balance_before = balance_before
|
||
txn.balance_after = balance_after
|
||
|
||
elif txn.txn_type == "refund":
|
||
# Deduct channel account balance
|
||
account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
|
||
balance_before = float(account.balance_amount) if account.balance_amount else 0
|
||
deduct_amount = float(txn.amount) if txn.amount else 0
|
||
account.balance_amount = balance_before - deduct_amount
|
||
balance_after = float(account.balance_amount)
|
||
account.update_by = user_id
|
||
account.update_time = now
|
||
db.add(account)
|
||
db.flush()
|
||
|
||
# Update order summary
|
||
order.total_refund_amount = float(order.total_refund_amount or 0) + deduct_amount
|
||
order.current_balance = float(order.current_balance or 0) - deduct_amount
|
||
txn.balance_before = balance_before
|
||
txn.balance_after = balance_after
|
||
|
||
elif txn.txn_type == "purchase":
|
||
# Products already updated at create time, just update channel quotas
|
||
products = json.loads(txn.products_json) if txn.products_json else []
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
|
||
cell_type_code, _ = _resolve_agreement_price_cell_type(
|
||
db, order.channel_agreement_id, project_code, spec_text, p.get("cellType") or p.get("cell_type") or ""
|
||
)
|
||
_upsert_channel_product_quota(
|
||
db,
|
||
channel_id=order.channel_id,
|
||
project_code=project_code,
|
||
project_name=p.get("projectName") or p.get("project_name") or "",
|
||
cell_type_code=cell_type_code,
|
||
cell_type_name=p.get("cellType") or p.get("cell_type") or "",
|
||
spec_text=spec_text,
|
||
add_qty=qty,
|
||
tenant_no=tenant_no or order.tenant_no,
|
||
user_id=user_id,
|
||
)
|
||
|
||
elif txn.txn_type == "unsubscribe":
|
||
# Products already updated at create time, just reduce channel quotas
|
||
products = json.loads(txn.products_json) if txn.products_json else []
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
qty = float(p.get("qty") or 0)
|
||
cell_type_code, _ = _resolve_agreement_price_cell_type(
|
||
db, order.channel_agreement_id, project_code, spec_text, p.get("cellType") or p.get("cell_type") or ""
|
||
)
|
||
_reduce_channel_product_quota(
|
||
db,
|
||
channel_id=order.channel_id,
|
||
project_code=project_code,
|
||
cell_type_code=cell_type_code,
|
||
spec_text=spec_text,
|
||
reduce_qty=qty,
|
||
user_id=user_id,
|
||
)
|
||
|
||
# Update txn status
|
||
txn.txn_status = "yqr"
|
||
txn.finance_confirm_time = now
|
||
txn.finance_confirm_by = user_id
|
||
txn.finance_confirm_attachments = finance_confirm_attachments or ""
|
||
txn.finance_remark = finance_remark or ""
|
||
txn.update_by = user_id
|
||
txn.update_time = now
|
||
|
||
# Transition order to sxz if currently dsx
|
||
if order.order_status == "dsx":
|
||
order.order_status = "sxz"
|
||
order.update_by = user_id
|
||
order.update_time = now
|
||
|
||
db.add(txn)
|
||
db.add(order)
|
||
db.commit()
|
||
db.refresh(txn)
|
||
return txn
|
||
|
||
|
||
def void_txn(db, txn_id, *, tenant_no=None, user_id=None):
|
||
"""作废流水:dsx/dqr → yzf。如果是 purchase/unsubscribe 还需要回滚产品持仓"""
|
||
txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
|
||
if not txn:
|
||
raise ValueError("流水不存在")
|
||
if txn.txn_status not in ("dsx", "dqr"):
|
||
raise ValueError("仅待生效或待财务确认流水可作废")
|
||
if (txn.txn_type or "").strip() == "settlement":
|
||
raise ValueError("输注计划结算流水不可作废")
|
||
|
||
now = datetime.now()
|
||
order = get_service_order(db, txn.service_order_id, tenant_no)
|
||
|
||
if txn.txn_type == "purchase" and order:
|
||
# Rollback product holdings
|
||
products = json.loads(txn.products_json) if txn.products_json else []
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
|
||
existing = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order.id,
|
||
BizChannelServiceOrderProduct.project_code == project_code,
|
||
BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
).first()
|
||
if existing:
|
||
existing.total_buy_qty = float(existing.total_buy_qty or 0) - qty
|
||
existing.current_qty = float(existing.current_qty or 0) - qty
|
||
existing.update_by = user_id
|
||
existing.update_time = now
|
||
db.add(existing)
|
||
txn_amount = float(txn.amount or 0)
|
||
if txn_amount > 0:
|
||
order.purchase_total_amount = max(
|
||
0.0,
|
||
float(order.purchase_total_amount or 0) - txn_amount,
|
||
)
|
||
order.update_by = user_id
|
||
order.update_time = now
|
||
db.add(order)
|
||
|
||
elif txn.txn_type == "unsubscribe" and order:
|
||
# Rollback unsubscribe: restore product holdings
|
||
products = json.loads(txn.products_json) if txn.products_json else []
|
||
for p in products:
|
||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
||
qty = float(p.get("qty") or 0)
|
||
unit_price = 0
|
||
existing = db.query(BizChannelServiceOrderProduct).filter(
|
||
and_(
|
||
BizChannelServiceOrderProduct.service_order_id == order.id,
|
||
BizChannelServiceOrderProduct.project_code == project_code,
|
||
BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
|
||
BizChannelServiceOrderProduct.del_flag == 0,
|
||
)
|
||
).first()
|
||
if existing:
|
||
unit_price = float(existing.agreement_unit_price or 0)
|
||
existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) - qty
|
||
existing.current_qty = float(existing.current_qty or 0) + qty
|
||
existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) - (qty * unit_price)
|
||
existing.update_by = user_id
|
||
existing.update_time = now
|
||
db.add(existing)
|
||
|
||
txn.txn_status = "yzf"
|
||
txn.void_by = user_id
|
||
txn.void_time = now
|
||
txn.update_by = user_id
|
||
txn.update_time = now
|
||
db.add(txn)
|
||
db.commit()
|
||
db.refresh(txn)
|
||
return txn
|