206 lines
12 KiB
Python
206 lines
12 KiB
Python
"""只读额度统计:共享缓存与授权合同分配分开展示,不重建历史余额。"""
|
|
from decimal import Decimal
|
|
from sqlalchemy import and_, or_, literal, func
|
|
from fastapi import HTTPException
|
|
from app.crud.crm.data_scope_helpers import apply_crm_owner_scope
|
|
from app.models.crm_models import CrmContract, CrmContractProduct
|
|
from app.models.pl_models import PlPlan
|
|
from app.models.pl_quota_models import (PlContractCertLink as Link, PlContractCertProduct as Scope,
|
|
PlPlanCertUsage as Usage, PlPlanCertUsageItem as Item, PlCertQtyLog as Log)
|
|
from app.services.pl_data_scope import apply_pl_scope
|
|
from app.services.pl_quota_cert_service import MODELS, _get_row
|
|
|
|
ZERO = Decimal('0')
|
|
|
|
|
|
def _active(db, user, model):
|
|
return db.query(model).filter(model.tenant_no == user.tenant_no, model.del_flag == 0)
|
|
|
|
|
|
def _contracts(db, user):
|
|
return apply_crm_owner_scope(_active(db, user, CrmContract), CrmContract,
|
|
user.to_user_info(), db, user.tenant_no)
|
|
|
|
|
|
def _certificates(db, user, keyword=None, cert_type=None, cert_no=None):
|
|
queries = []
|
|
for kind, model in MODELS.items():
|
|
if cert_type and kind != cert_type:
|
|
continue
|
|
query = apply_pl_scope(_active(db, user, model), model, user, db, user.tenant_no)
|
|
if keyword:
|
|
query = query.filter(or_(model.cert_no.contains(keyword, autoescape=True),
|
|
model.product_name_on_cert.contains(keyword, autoescape=True)))
|
|
if cert_no:
|
|
query = query.filter(model.cert_no.contains(cert_no, autoescape=True))
|
|
queries.append(query.with_entities(literal(kind).label('cert_type'), model.id.label('cert_id'),
|
|
*[getattr(model, field).label(field) for field in ('cert_no', 'product_name_on_cert',
|
|
'quantity_unit', 'total_qty', 'reserved_qty', 'used_qty', 'status',
|
|
'valid_from', 'valid_to', 'create_time')]))
|
|
return (queries[0].union_all(*queries[1:]) if len(queries) > 1 else queries[0]).subquery()
|
|
|
|
|
|
def _cert_summary(row):
|
|
return {'id': row.cert_id, 'certId': row.cert_id, 'certType': row.cert_type, 'certNo': row.cert_no,
|
|
'productNameOnCert': row.product_name_on_cert, 'quantityUnit': row.quantity_unit,
|
|
'totalQty': row.total_qty, 'reservedQty': row.reserved_qty, 'usedQty': row.used_qty,
|
|
'availableQty': row.total_qty - row.reserved_qty - row.used_qty, 'status': row.status,
|
|
'validFrom': row.valid_from, 'validTo': row.valid_to}
|
|
|
|
|
|
def _contract_summary(row):
|
|
return {'id': row.id, 'contractNo': row.contract_no, 'contractName': row.contract_name,
|
|
'contractType': row.contract_type}
|
|
|
|
|
|
def _key_filter(model, keys):
|
|
return or_(*[and_(model.cert_type == kind, model.cert_id.in_(
|
|
[ident for k, ident in keys if k == kind])) for kind in MODELS])
|
|
|
|
|
|
def _details(db, user, keys, contract_ids=None):
|
|
"""有限证件/合同范围一次读取维护身份、一次读取授权使用;不连接明细造成乘积。"""
|
|
if not keys:
|
|
return {}, []
|
|
contracts = _contracts(db, user).with_entities(CrmContract.id)
|
|
if contract_ids is not None:
|
|
contracts = contracts.filter(CrmContract.id.in_(contract_ids))
|
|
links = _active(db, user, Link).filter(_key_filter(Link, keys), Link.contract_id.in_(contracts)).all()
|
|
groups = {}
|
|
for link in links:
|
|
groups[(link.cert_type, link.cert_id, link.contract_id)] = {
|
|
'bindingId': link.id, 'linkStatus': link.link_status, 'scopeRevision': link.scope_revision,
|
|
'contractReservedQty': ZERO, 'contractUsedQty': ZERO, 'products': []}
|
|
products = {}
|
|
if links:
|
|
rows = db.query(Scope, CrmContractProduct).join(CrmContractProduct,
|
|
Scope.contract_product_id == CrmContractProduct.id).filter(
|
|
Scope.tenant_no == user.tenant_no, Scope.del_flag == 0,
|
|
Scope.contract_cert_link_id.in_([l.id for l in links]),
|
|
CrmContractProduct.tenant_no == user.tenant_no, CrmContractProduct.del_flag == 0)
|
|
by_link = {l.id: l for l in links}
|
|
for scope, product in rows:
|
|
link = by_link[scope.contract_cert_link_id]
|
|
if product.contract_id != link.contract_id:
|
|
continue
|
|
key = (link.cert_type, link.cert_id, link.contract_id)
|
|
value = {'id': product.id, 'contractProductId': product.id, 'productId': product.product_id,
|
|
'productName': product.product_name, 'model': product.model, 'unit': product.unit,
|
|
'bindingId': link.id, 'scopeId': scope.id, 'scopeStatus': scope.scope_status,
|
|
'linkStatus': link.link_status, 'reservedQty': ZERO, 'usedQty': ZERO}
|
|
groups[key]['products'].append(value)
|
|
products[(*key, product.id)] = value
|
|
plans = apply_pl_scope(_active(db, user, PlPlan), PlPlan, user, db, user.tenant_no).with_entities(PlPlan.id)
|
|
rows = db.query(Item, Usage, PlPlan, CrmContractProduct).join(Usage, Item.usage_id == Usage.id).join(
|
|
PlPlan, Usage.plan_id == PlPlan.id).join(CrmContractProduct, and_(
|
|
Item.contract_product_id == CrmContractProduct.id, Item.contract_id == CrmContractProduct.contract_id)
|
|
).filter(Item.tenant_no == user.tenant_no, Item.del_flag == 0,
|
|
Usage.tenant_no == user.tenant_no, Usage.del_flag == 0, _key_filter(Usage, keys),
|
|
Item.contract_id.in_(contracts), PlPlan.id.in_(plans),
|
|
CrmContractProduct.tenant_no == user.tenant_no, CrmContractProduct.del_flag == 0
|
|
).order_by(Item.create_time.desc(), Item.id).all()
|
|
usage_items = []
|
|
for item, usage, plan, product in rows:
|
|
key = (usage.cert_type, usage.cert_id, item.contract_id)
|
|
group = groups.setdefault(key, {'bindingId': None, 'linkStatus': None, 'scopeRevision': None,
|
|
'contractReservedQty': ZERO, 'contractUsedQty': ZERO, 'products': []})
|
|
# 证件单位不可变;异常历史单位不混入当前单位的聚合,明细仍保留追溯。
|
|
unit = keys[key[:2]]
|
|
if usage.quantity_unit == unit:
|
|
group['contractReservedQty'] += item.reserved_qty
|
|
group['contractUsedQty'] += item.used_qty
|
|
value = products.get((*key, product.id))
|
|
if value is None:
|
|
value = {'id': product.id, 'contractProductId': product.id, 'productId': product.product_id,
|
|
'productName': product.product_name, 'model': product.model, 'unit': product.unit,
|
|
'bindingId': None, 'scopeId': None, 'scopeStatus': None, 'linkStatus': None,
|
|
'reservedQty': ZERO, 'usedQty': ZERO}
|
|
products[(*key, product.id)] = value
|
|
group['products'].append(value)
|
|
value['reservedQty'] += item.reserved_qty
|
|
value['usedQty'] += item.used_qty
|
|
source_stop = next((stop for stop in (item.source_snapshot or {}).get('stops', [])
|
|
if stop.get('id') == item.source_stop_id), {})
|
|
party_name = source_stop.get('supplier_name' if item.source_stop_type == 'load' else 'customer_name')
|
|
usage_items.append({'id': item.id, 'usageId': usage.id, 'certType': usage.cert_type,
|
|
'certId': usage.cert_id, 'planId': plan.id, 'planNo': plan.plan_no,
|
|
'stopType': item.source_stop_type, 'stopId': item.source_stop_id,
|
|
'stopProductId': item.source_stop_product_id, 'planProductId': item.plan_product_id,
|
|
'contractId': item.contract_id, 'contractProductId': item.contract_product_id,
|
|
'productName': product.product_name, 'quantityUnit': usage.quantity_unit,
|
|
'partyName': party_name or '—', 'address': source_stop.get('address') or '—',
|
|
'reservedQty': item.reserved_qty, 'usedQty': item.used_qty, 'itemStatus': item.item_status})
|
|
return groups, usage_items
|
|
|
|
|
|
def list_contracts(db, user, keyword=None, page=1, page_size=20, contract_no=None):
|
|
certs = _certificates(db, user)
|
|
linked = _active(db, user, Link).join(certs, and_(Link.cert_type == certs.c.cert_type,
|
|
Link.cert_id == certs.c.cert_id)).with_entities(Link.contract_id)
|
|
query = _contracts(db, user).filter(CrmContract.id.in_(linked))
|
|
if keyword:
|
|
query = query.filter(or_(CrmContract.contract_no.contains(keyword, autoescape=True),
|
|
CrmContract.contract_name.contains(keyword, autoescape=True)))
|
|
if contract_no:
|
|
query = query.filter(CrmContract.contract_no.contains(contract_no, autoescape=True))
|
|
total = query.count()
|
|
rows = query.order_by(CrmContract.create_time.desc(), CrmContract.id).offset((page-1)*page_size).limit(page_size).all()
|
|
counts = dict(db.query(Link.contract_id, func.count(Link.id)).join(certs,
|
|
and_(Link.cert_type == certs.c.cert_type, Link.cert_id == certs.c.cert_id)).filter(
|
|
Link.tenant_no == user.tenant_no, Link.del_flag == 0, Link.contract_id.in_([r.id for r in rows])
|
|
).group_by(Link.contract_id).all()) if rows else {}
|
|
return {'list': [{**_contract_summary(r), 'certCount': counts.get(r.id, 0)} for r in rows],
|
|
'total': total, 'page': page, 'pageSize': page_size}
|
|
|
|
|
|
def list_certificates(db, user, keyword=None, cert_type=None, page=1, page_size=20, cert_no=None):
|
|
certs = _certificates(db, user, keyword, cert_type, cert_no)
|
|
query = db.query(certs)
|
|
total = query.count()
|
|
rows = query.order_by(certs.c.create_time.desc(), certs.c.cert_type, certs.c.cert_id).offset(
|
|
(page-1)*page_size).limit(page_size).all()
|
|
return {'list': [_cert_summary(r) for r in rows], 'total': total, 'page': page, 'pageSize': page_size}
|
|
|
|
|
|
def contract_statistics(db, user, contract_id):
|
|
contract = _contracts(db, user).filter(CrmContract.id == contract_id).first()
|
|
if contract is None:
|
|
raise HTTPException(404, '合同不存在或无权访问')
|
|
certs = _certificates(db, user)
|
|
linked = _active(db, user, Link).filter(Link.contract_id == contract_id)
|
|
# 历史使用即使已停止绑定也可追溯,但证件本身仍须有效且授权。
|
|
used = _active(db, user, Usage).join(Item, Item.usage_id == Usage.id).filter(
|
|
Item.tenant_no == user.tenant_no, Item.del_flag == 0, Item.contract_id == contract_id)
|
|
rows = db.query(certs).filter(or_(linked.filter(Link.cert_type == certs.c.cert_type,
|
|
Link.cert_id == certs.c.cert_id).exists(), used.filter(Usage.cert_type == certs.c.cert_type,
|
|
Usage.cert_id == certs.c.cert_id).exists())).order_by(certs.c.create_time.desc()).all()
|
|
keys = {(r.cert_type, r.cert_id): r.quantity_unit for r in rows}
|
|
groups, _ = _details(db, user, keys, [contract_id])
|
|
result = []
|
|
for row in rows:
|
|
group = groups.get((row.cert_type, row.cert_id, contract_id))
|
|
if group is not None:
|
|
result.append({**_cert_summary(row), **group,
|
|
'productNames': list(dict.fromkeys(p['productName'] for p in group['products']))})
|
|
return {**_contract_summary(contract), 'certificates': result}
|
|
|
|
|
|
def certificate_statistics(db, user, cert_type, cert_id):
|
|
_get_row(db, user, cert_type, cert_id)
|
|
certs = _certificates(db, user, cert_type=cert_type)
|
|
row = db.query(certs).filter(certs.c.cert_id == cert_id).one()
|
|
groups, items = _details(db, user, {(cert_type, cert_id): row.quantity_unit})
|
|
ids = [key[2] for key in groups]
|
|
contracts = _contracts(db, user).filter(CrmContract.id.in_(ids)).order_by(CrmContract.create_time.desc()).all() if ids else []
|
|
logs = _active(db, user, Log).filter(Log.cert_type == cert_type, Log.cert_id == cert_id,
|
|
Log.plan_id.is_(None), Log.usage_id.is_(None), Log.quantity_unit == row.quantity_unit)
|
|
opening = unattributed = ZERO
|
|
for event, delta in logs.with_entities(Log.event_type, func.sum(Log.used_delta)).group_by(Log.event_type):
|
|
if event == 'opening':
|
|
opening += delta
|
|
else:
|
|
unattributed += delta
|
|
return {**_cert_summary(row), 'contracts': [{**_contract_summary(c),
|
|
**groups[(cert_type, cert_id, c.id)]} for c in contracts], 'usageItems': items,
|
|
'openingUsedQty': opening, 'unattributedUsedQty': unattributed}
|