187 lines
11 KiB
Python
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]
|