Files
shanxia2.0/backend/app/services/pl_stop_legal_document.py
T

187 lines
11 KiB
Python

"""逐装送段产品维护证明;权限筛选先于汇总、分页,不参与额度或归档。"""
from datetime import datetime
from uuid import uuid4
from fastapi import HTTPException
from pydantic import BaseModel
from sqlalchemy import and_, or_, func, case
from app.crud.crm.data_scope_helpers import apply_crm_owner_scope
from app.models.crm_models import CrmContract
from app.models.pl_models import PlPlan, PlPlanProduct, PlPlanLoad, PlPlanLoadProduct, PlPlanDelivery, PlPlanDeliveryProduct
from app.models.pl_quota_models import PlStopLegalDocument as Document
from app.schemas.pl.legal_use_document import StopLegalDocumentsBody
from app.services.pl_data_scope import apply_pl_scope
from app.services.pl_legal_use_document import _plan
from app.services.pl_quota_contract_service import _contract
from app.services.pl_plan_cert_perm import user_can_upload_plan_cert
from app.services.pl_quota_attachments import authorize_attachment, storage_base_url
SIDES = (('load', PlPlanLoadProduct, PlPlanLoad, 'load_id'),
('delivery', PlPlanDeliveryProduct, PlPlanDelivery, 'delivery_id'))
def _query(db, user, side, model, parent, field, contract_id=None):
contracts = db.query(CrmContract.id).filter_by(tenant_no=user.tenant_no, del_flag=0)
contracts = apply_crm_owner_scope(contracts, CrmContract, user.to_user_info(), db, user.tenant_no)
query = db.query(model, parent, PlPlanProduct, CrmContract, Document).join(
parent, and_(getattr(model, field) == parent.id, model.plan_id == parent.plan_id,
parent.tenant_no == user.tenant_no, parent.del_flag == 0)).join(
PlPlanProduct, and_(model.plan_product_id == PlPlanProduct.id, model.plan_id == PlPlanProduct.plan_id,
model.product_id == PlPlanProduct.product_id,
PlPlanProduct.tenant_no == user.tenant_no, PlPlanProduct.del_flag == 0,
PlPlanProduct.is_explosive == 1)).outerjoin(
CrmContract, and_(model.contract_id == CrmContract.id, CrmContract.tenant_no == user.tenant_no,
CrmContract.del_flag == 0)).outerjoin(
Document, and_(Document.plan_id == model.plan_id, Document.stop_type == side,
Document.stop_product_id == model.id, Document.tenant_no == user.tenant_no,
Document.del_flag == 0)).filter(
model.tenant_no == user.tenant_no, model.del_flag == 0,
or_(model.contract_id.is_(None), model.contract_id == '', model.contract_id.in_(contracts)))
if contract_id:
query = query.filter(model.contract_id == contract_id)
return query
def _unlinked_legacy(db, user):
"""无有效段的旧附件只在全局计划范围展示,不猜测合同归属。"""
query = db.query(PlPlanProduct).filter_by(tenant_no=user.tenant_no, del_flag=0, is_explosive=1).filter(
PlPlanProduct.legal_use_cert_url.isnot(None), PlPlanProduct.legal_use_cert_url != '')
for _, model, parent, field in SIDES:
linked = db.query(model.id).join(parent, getattr(model, field) == parent.id).filter(
model.plan_product_id == PlPlanProduct.id, model.plan_id == PlPlanProduct.plan_id,
model.tenant_no == user.tenant_no, model.del_flag == 0,
parent.tenant_no == user.tenant_no, parent.del_flag == 0, parent.plan_id == model.plan_id)
query = query.filter(~linked.exists())
return query
def _legacy_row(product):
return {'id': product.id, 'productId': product.product_id, 'productName': product.product_name,
'model': product.model, 'legalUseCertUrl': product.legal_use_cert_url}
def _details(db, user, plans, contract_id=None, lock=False):
result = {p.id: {'id': p.id, 'planNo': p.plan_no, 'planDate': p.plan_date,
'licensePlate': p.license_plate, 'transportStatus': p.transport_status,
'archiveStatus': p.archive_status, 'items': [], 'legacyProducts': []} for p in plans}
legacy = {p.id: {} for p in plans}
for side, model, parent, field in SIDES:
query = _query(db, user, side, model, parent, field, contract_id).filter(model.plan_id.in_(result))
if lock:
query = query.with_for_update().populate_existing()
for row, stop, product, contract, document in query.order_by(parent.order_num, parent.id, model.order_num, model.id).all():
url = document.legal_use_cert_url if document else None
result[row.plan_id]['items'].append({
'stopType': side, 'stopId': stop.id, 'stopOrder': getattr(stop, 'seq', None) or stop.order_num or 1,
'stopProductId': row.id, 'productId': row.product_id, 'productName': row.product_name,
'model': row.model, 'unit': row.unit, 'contractId': row.contract_id,
'contractNo': contract.contract_no if contract else None, 'contractProductId': row.contract_product_id,
'partyName': stop.customer_name, 'address': stop.address, 'legalUseCertUrl': url,
'documentStatus': 'uploaded' if url else 'pending',
'uploadedBy': document.uploaded_by if document else None,
'uploadedTime': document.uploaded_time if document else None})
if product.legal_use_cert_url:
legacy[row.plan_id][product.id] = _legacy_row(product)
if not contract_id:
for product in _unlinked_legacy(db, user).filter(PlPlanProduct.plan_id.in_(result)).all():
legacy[product.plan_id][product.id] = _legacy_row(product)
for pid, value in result.items():
value['requiredCount'] = len(value['items'])
value['uploadedCount'] = sum(r['documentStatus'] == 'uploaded' for r in value['items'])
value['legacyProducts'] = list(legacy[pid].values())
value['documentStatus'] = ('not_required' if not value['requiredCount'] else
'uploaded' if value['uploadedCount'] == value['requiredCount'] else 'pending')
return result
def get_documents(db, user, plan_id, contract_id=None):
plan = _plan(db, user, plan_id)
if contract_id:
_contract(db, user, contract_id)
return _details(db, user, [plan], contract_id)[plan_id]
def list_documents(db, user, keyword=None, status=None, contract_id=None, page=1, page_size=20):
if status not in (None, 'pending', 'uploaded') or page < 1 or not 1 <= page_size <= 100:
raise HTTPException(400, '无效资料状态或分页参数')
if contract_id:
_contract(db, user, contract_id)
queries = [_query(db, user, side, model, parent, field, contract_id).with_entities(
model.plan_id.label('plan_id'), case((and_(Document.legal_use_cert_url.isnot(None),
Document.legal_use_cert_url != ''), 1), else_=0).label('uploaded'))
for side, model, parent, field in SIDES]
sources = queries[0].union_all(queries[1]).subquery()
counts = db.query(sources.c.plan_id, func.count().label('required'),
func.sum(sources.c.uploaded).label('uploaded')).group_by(sources.c.plan_id).subquery()
query = db.query(PlPlan).outerjoin(counts, counts.c.plan_id == PlPlan.id).filter(
PlPlan.tenant_no == user.tenant_no, PlPlan.del_flag == 0)
if contract_id:
query = query.filter(counts.c.required > 0)
else:
query = query.filter(or_(counts.c.required > 0, PlPlan.id.in_(
_unlinked_legacy(db, user).with_entities(PlPlanProduct.plan_id))))
query = apply_pl_scope(query, PlPlan, user, db, user.tenant_no)
if keyword:
query = query.filter(PlPlan.plan_no.contains(keyword))
if status == 'pending':
query = query.filter(counts.c.uploaded < counts.c.required)
elif status == 'uploaded':
query = query.filter(counts.c.uploaded == counts.c.required)
total = query.count()
plans = query.order_by(PlPlan.create_time.desc(), PlPlan.id).offset((page - 1) * page_size).limit(page_size).all()
details = _details(db, user, plans, contract_id) if plans else {}
return {'list': [details[p.id] for p in plans], 'total': total, 'page': page, 'pageSize': page_size}
def save_documents(db, user, plan_id, data, contract_id=None):
if not user_can_upload_plan_cert(db, user):
raise HTTPException(403, '无权维护合法使用证明')
body = StopLegalDocumentsBody(**(data.model_dump(exclude_unset=True) if isinstance(data, BaseModel) else data))
plan = _plan(db, user, plan_id, lock=True)
if contract_id:
_contract(db, user, contract_id, lock=True)
if 'items' in body.model_fields_set and body.items is None:
raise HTTPException(400, '段产品集合不可为null')
details = _details(db, user, [plan], contract_id, lock=True)[plan_id]
valid = {(r['stopType'], r['stopProductId']) for r in details['items']}
# 包含软删除行以复用稳定唯一键;清空不会删除记录或创建信息。
records = {(r.stop_type, r.stop_product_id): r for r in db.query(Document).filter_by(
tenant_no=user.tenant_no, plan_id=plan_id).with_for_update().populate_existing().all()}
seen, changes = set(), []
for item in body.items or []:
key = (item.stop_type, item.stop_product_id)
if key in seen or key not in valid:
raise HTTPException(400, '段产品重复、不适用或不在可访问计划合同范围内')
seen.add(key)
if 'legal_use_cert_url' not in item.model_fields_set:
continue
row = records.get(key)
old = row.legal_use_cert_url if row and row.del_flag == 0 else ''
urls = list(dict.fromkeys(v.strip() for v in (item.legal_use_cert_url or '').split(',') if v.strip()))
for url in urls:
if url not in (old or '').split(','):
authorize_attachment(db, user, url, storage_base_url())
value = ','.join(urls)
if value != (old or ''):
changes.append((key, row, value))
for key, row, value in changes:
if row is None:
row = Document(id=str(uuid4()), tenant_no=user.tenant_no, plan_id=plan_id,
stop_type=key[0], stop_product_id=key[1], create_by=user.id,
create_dept_id=user.dept_id or '')
db.add(row)
row.del_flag = 0
row.legal_use_cert_url = value
row.update_by = user.id
row.uploaded_by = user.id if value else None
row.uploaded_time = datetime.now() if value else None
if changes:
from app.services.pl_plan_service import record_plan_change
record_plan_change(db, tenant_no=user.tenant_no, plan_id=plan_id, change_type='legal_use_document',
change_content='维护逐段合法使用证明:' + '、'.join(k[0] + ':' + k[1] for k, _, _ in changes),
user_id=user.id, user_name=getattr(user, 'name', None), dept_id=user.dept_id or '')
db.flush()
# 响应同样读取锁后的最新段证明,避免退回等待计划锁之前的事务快照。
return _details(db, user, [plan], contract_id, lock=True)[plan_id]