小程序检索沿用原有关键字,在编号和品名之外匹配证件主体以及计划供方、需方。 Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
7.1 KiB
Python
128 lines
7.1 KiB
Python
"""计划用证列表:按真实占用/结转状态分组,余额可用性由只读预览实时计算。"""
|
|
from datetime import date
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import case, or_, cast, String
|
|
from app.models.pl_models import PlPlan, PlPlanProduct
|
|
from app.models.pl_quota_models import PlPlanCertUsage
|
|
from app.services.pl_data_scope import apply_pl_scope
|
|
|
|
STATUSES = ('not_required', 'needs_configuration', 'needs_validation', 'reserved', 'settled', 'exception')
|
|
FOLLOW_STATUSES = {'drafting', 'wait_notice', 'pending_arrange', 'arranged', 'completed', 'cancelled'}
|
|
|
|
|
|
def _filter_date(value):
|
|
if value in (None, ''):
|
|
return None
|
|
if isinstance(value, date):
|
|
return value
|
|
try:
|
|
return date.fromisoformat(value)
|
|
except (TypeError, ValueError):
|
|
raise HTTPException(400, '计划日期格式无效') from None
|
|
|
|
|
|
def _text(value):
|
|
text = str(value or '').strip()
|
|
return text or None
|
|
|
|
|
|
def _hazard_product_names(db, user, plan_ids):
|
|
grouped = {plan_id: [] for plan_id in plan_ids}
|
|
if not plan_ids:
|
|
return {}
|
|
rows = db.query(PlPlanProduct.plan_id, PlPlanProduct.product_name).filter(
|
|
PlPlanProduct.tenant_no == user.tenant_no, PlPlanProduct.del_flag == 0,
|
|
PlPlanProduct.plan_id.in_(plan_ids),
|
|
or_(PlPlanProduct.is_toxic == 1, PlPlanProduct.is_explosive == 1),
|
|
).order_by(PlPlanProduct.product_name, PlPlanProduct.id).all()
|
|
for plan_id, name in rows:
|
|
text = (name or '').strip()
|
|
if text and text not in grouped[plan_id]:
|
|
grouped[plan_id].append(text)
|
|
return {plan_id: '、'.join(names) for plan_id, names in grouped.items() if names}
|
|
|
|
|
|
def list_plans(db, user, keyword='', status=None, page=1, page_size=20, *,
|
|
plan_no=None, license_plate=None, plan_date_from=None, plan_date_to=None,
|
|
quota_status_in=None, supplier_name=None, customer_name=None, product_name=None,
|
|
follow_status_in=None, conversion_only=False):
|
|
if status and status not in STATUSES:
|
|
raise HTTPException(400, '未知用证状态')
|
|
selected_states = [value.strip() for value in (quota_status_in or '').split(',') if value.strip()]
|
|
if any(value not in STATUSES for value in selected_states):
|
|
raise HTTPException(400, '未知用证状态')
|
|
date_from, date_to = _filter_date(plan_date_from), _filter_date(plan_date_to)
|
|
if date_from and date_to and date_from > date_to:
|
|
raise HTTPException(400, '计划日期起始不能晚于结束')
|
|
hazard_product = db.query(PlPlanProduct.id).filter(PlPlanProduct.tenant_no == user.tenant_no,
|
|
PlPlanProduct.plan_id == PlPlan.id, PlPlanProduct.del_flag == 0,
|
|
or_(PlPlanProduct.is_toxic == 1, PlPlanProduct.is_explosive == 1)).exists()
|
|
toxic = db.query(PlPlanProduct.id).filter(PlPlanProduct.tenant_no == user.tenant_no,
|
|
PlPlanProduct.plan_id == PlPlan.id, PlPlanProduct.del_flag == 0, PlPlanProduct.is_toxic == 1).exists()
|
|
def usage(*states):
|
|
return db.query(PlPlanCertUsage.id).filter(PlPlanCertUsage.tenant_no == user.tenant_no,
|
|
PlPlanCertUsage.plan_id == PlPlan.id, PlPlanCertUsage.del_flag == 0,
|
|
PlPlanCertUsage.usage_status.in_(states)).exists()
|
|
error = db.query(PlPlanCertUsage.id).filter(PlPlanCertUsage.tenant_no == user.tenant_no,
|
|
PlPlanCertUsage.plan_id == PlPlan.id, PlPlanCertUsage.del_flag == 0,
|
|
PlPlanCertUsage.confirmation_snapshot['settlementError'].as_string().isnot(None),
|
|
PlPlanCertUsage.confirmation_snapshot['settlementError'].as_string() != '').exists()
|
|
state = case((~toxic, 'not_required'),
|
|
(or_(PlPlan.archive_status == 'exception', usage('exception'), error), 'exception'),
|
|
(usage('reserved', 'in_transit'), 'reserved'), (usage('settled'), 'settled'),
|
|
(or_(PlPlan.cert_configuration.is_(None), cast(PlPlan.cert_configuration, String).in_(['null', '{}'])), 'needs_configuration'),
|
|
else_='needs_validation')
|
|
query = db.query(PlPlan, state.label('quota_state')).filter(PlPlan.tenant_no == user.tenant_no,
|
|
PlPlan.del_flag == 0, PlPlan.follow_status != 'cancelled', hazard_product)
|
|
query = apply_pl_scope(query, PlPlan, user, db, user.tenant_no)
|
|
if keyword:
|
|
query = query.filter(or_(
|
|
PlPlan.plan_no.contains(keyword, autoescape=True),
|
|
PlPlan.supplier_name.contains(keyword, autoescape=True),
|
|
PlPlan.customer_name.contains(keyword, autoescape=True),
|
|
))
|
|
if status:
|
|
query = query.filter(state == status)
|
|
# 表头条件必须先于统计与分页,不能只过滤已加载的一页。
|
|
if plan_no:
|
|
query = query.filter(PlPlan.plan_no.contains(plan_no, autoescape=True))
|
|
if license_plate:
|
|
query = query.filter(PlPlan.license_plate.contains(license_plate, autoescape=True))
|
|
if date_from:
|
|
query = query.filter(PlPlan.plan_date >= date_from)
|
|
if date_to:
|
|
query = query.filter(PlPlan.plan_date <= date_to)
|
|
if selected_states:
|
|
query = query.filter(state.in_(selected_states))
|
|
if _text(supplier_name):
|
|
query = query.filter(PlPlan.supplier_name.contains(_text(supplier_name), autoescape=True))
|
|
if _text(customer_name):
|
|
query = query.filter(PlPlan.customer_name.contains(_text(customer_name), autoescape=True))
|
|
if _text(product_name):
|
|
query = query.filter(db.query(PlPlanProduct.id).filter(
|
|
PlPlanProduct.tenant_no == user.tenant_no, PlPlanProduct.plan_id == PlPlan.id,
|
|
PlPlanProduct.del_flag == 0, or_(PlPlanProduct.is_toxic == 1, PlPlanProduct.is_explosive == 1),
|
|
PlPlanProduct.product_name.contains(_text(product_name), autoescape=True)).exists())
|
|
follow_states = [value.strip() for value in (follow_status_in or '').split(',') if value.strip()]
|
|
if any(value not in FOLLOW_STATUSES for value in follow_states):
|
|
raise HTTPException(400, '未知跟进状态')
|
|
if follow_states:
|
|
query = query.filter(PlPlan.follow_status.in_(follow_states))
|
|
if conversion_only:
|
|
from sqlalchemy import false as sql_false
|
|
from app.services.pl_quota_conversion import plans_needing_unit_conversion
|
|
candidate_ids = [row[0] for row in query.with_entities(PlPlan.id).all()]
|
|
needing = plans_needing_unit_conversion(db, user.tenant_no, candidate_ids)
|
|
query = query.filter(PlPlan.id.in_(needing) if needing else sql_false())
|
|
total = query.count()
|
|
rows = query.order_by(PlPlan.create_time.desc(), PlPlan.id).offset((page-1)*page_size).limit(page_size).all()
|
|
plan_ids = [plan.id for plan, _value in rows]
|
|
names = _hazard_product_names(db, user, plan_ids)
|
|
from app.services.pl_quota_conversion import plans_needing_unit_conversion
|
|
needing = plans_needing_unit_conversion(db, user.tenant_no, plan_ids)
|
|
return {'total': total, 'list': [{'id': p.id, 'planNo': p.plan_no, 'planDate': p.plan_date,
|
|
'licensePlate': p.license_plate, 'supplierName': p.supplier_name or '',
|
|
'customerName': p.customer_name or '', 'productNames': names.get(p.id, ''),
|
|
'followStatus': p.follow_status, 'quotaStatus': value,
|
|
'needsUnitConversion': p.id in needing} for p, value in rows]}
|