Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from sqlalchemy import func, or_, select
|
||||
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.consultants.models import ConsultantProfile
|
||||
from app.modules.core.iam.models import User
|
||||
from app.modules.core.tenancy.models import Branch, Tenant
|
||||
from app.modules.marketplace.models import MarketplaceLead, MarketplaceLeadAssignment
|
||||
|
||||
LEAD_STATUSES = ["NEW", "CONTACTED", "QUALIFIED", "ASSIGNED", "ACCEPTED", "REJECTED", "CONVERTED", "LOST"]
|
||||
LEAD_PRIORITIES = ["LOW", "NORMAL", "HIGH", "URGENT"]
|
||||
LEAD_SOURCES = ["manual", "public_website", "consultant", "referral", "campaign", "other"]
|
||||
SERVICE_CATEGORIES = ["GST", "Income Tax", "ROC", "Audit", "Accounting", "Payroll", "Registration", "Advisory", "Other"]
|
||||
|
||||
|
||||
def money(value) -> Decimal:
|
||||
if value in (None, ""):
|
||||
return Decimal("0.00")
|
||||
return Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def _role_set(role_names: list[str] | None) -> set[str]:
|
||||
return set(role_names or [])
|
||||
|
||||
|
||||
def is_system_admin(role_names: list[str] | None) -> bool:
|
||||
return "System Admin" in _role_set(role_names)
|
||||
|
||||
|
||||
def is_firm_admin(role_names: list[str] | None) -> bool:
|
||||
return "Firm Admin" in _role_set(role_names)
|
||||
|
||||
|
||||
def is_partner(role_names: list[str] | None) -> bool:
|
||||
return "Partner" in _role_set(role_names)
|
||||
|
||||
|
||||
def generate_lead_no(db) -> str:
|
||||
year = datetime.now().year
|
||||
count = db.execute(select(func.count(MarketplaceLead.id))).scalar_one() or 0
|
||||
return f"ML-{year}-{count + 1:05d}"
|
||||
|
||||
|
||||
def list_reference_audit_firms(db) -> list[Tenant]:
|
||||
return list(db.execute(select(Tenant).order_by(Tenant.name.asc())).scalars().all())
|
||||
|
||||
|
||||
def list_reference_branches(db, tenant_id: int | None = None) -> list[Branch]:
|
||||
stmt = select(Branch).order_by(Branch.name.asc())
|
||||
if tenant_id:
|
||||
stmt = stmt.where(Branch.tenant_id == tenant_id)
|
||||
return list(db.execute(stmt).scalars().all())
|
||||
|
||||
|
||||
def list_reference_partners(db, tenant_id: int | None = None) -> list[User]:
|
||||
stmt = select(User).order_by(User.full_name.asc(), User.email.asc())
|
||||
if tenant_id:
|
||||
stmt = stmt.where(User.tenant_id == tenant_id)
|
||||
return list(db.execute(stmt).scalars().all())
|
||||
|
||||
|
||||
def list_marketplace_leads(db, *, q: str = "", status: str = "", user=None, role_names: list[str] | None = None) -> list[MarketplaceLead]:
|
||||
stmt = select(MarketplaceLead).order_by(MarketplaceLead.created_at_utc.desc(), MarketplaceLead.id.desc())
|
||||
roles = _role_set(role_names)
|
||||
if not is_system_admin(role_names):
|
||||
tenant_id = getattr(user, "tenant_id", None)
|
||||
if tenant_id:
|
||||
stmt = stmt.where(MarketplaceLead.assigned_tenant_id == tenant_id)
|
||||
if is_partner(role_names):
|
||||
stmt = stmt.where(MarketplaceLead.assigned_partner_user_id == getattr(user, "id", None))
|
||||
if status:
|
||||
stmt = stmt.where(MarketplaceLead.status == status)
|
||||
if q:
|
||||
like = f"%{q}%"
|
||||
stmt = stmt.where(or_(MarketplaceLead.lead_no.ilike(like), MarketplaceLead.lead_name.ilike(like), MarketplaceLead.business_name.ilike(like), MarketplaceLead.mobile.ilike(like), MarketplaceLead.email.ilike(like), MarketplaceLead.service_requested.ilike(like)))
|
||||
return list(db.execute(stmt).scalars().all())
|
||||
|
||||
|
||||
def get_marketplace_lead(db, lead_id: int) -> MarketplaceLead | None:
|
||||
return db.get(MarketplaceLead, int(lead_id))
|
||||
|
||||
|
||||
def create_marketplace_lead(db, *, lead_name: str, business_name: str | None, email: str | None, mobile: str | None, city: str | None, state: str | None, service_category: str | None, service_requested: str, message: str | None, source: str = "manual", priority: str = "NORMAL", estimated_value=0, created_by_user_id: int | None = None) -> MarketplaceLead:
|
||||
lead = MarketplaceLead(
|
||||
lead_no=generate_lead_no(db),
|
||||
source=source or "manual",
|
||||
service_category=service_category or None,
|
||||
service_requested=(service_requested or "General enquiry").strip(),
|
||||
lead_name=lead_name.strip(),
|
||||
business_name=(business_name or None),
|
||||
email=(email or None),
|
||||
mobile=(mobile or None),
|
||||
city=(city or None),
|
||||
state=(state or None),
|
||||
message=(message or None),
|
||||
priority=priority if priority in LEAD_PRIORITIES else "NORMAL",
|
||||
estimated_value=money(estimated_value),
|
||||
created_by_user_id=created_by_user_id,
|
||||
)
|
||||
db.add(lead)
|
||||
db.commit()
|
||||
db.refresh(lead)
|
||||
return lead
|
||||
|
||||
|
||||
def assign_marketplace_lead(db, *, lead: MarketplaceLead, tenant_id: int, branch_id: int | None, partner_user_id: int | None, notes: str | None, assigned_by_user_id: int | None) -> MarketplaceLead:
|
||||
now = datetime.now(timezone.utc)
|
||||
lead.assigned_tenant_id = tenant_id
|
||||
lead.assigned_branch_id = branch_id or None
|
||||
lead.assigned_partner_user_id = partner_user_id or None
|
||||
lead.assigned_by_user_id = assigned_by_user_id
|
||||
lead.assigned_at_utc = now
|
||||
lead.status = "ASSIGNED"
|
||||
lead.updated_by_user_id = assigned_by_user_id
|
||||
db.add(MarketplaceLeadAssignment(lead_id=lead.id, tenant_id=tenant_id, branch_id=branch_id or None, partner_user_id=partner_user_id or None, notes=notes or None, assigned_by_user_id=assigned_by_user_id, assigned_at_utc=now))
|
||||
db.commit()
|
||||
db.refresh(lead)
|
||||
return lead
|
||||
|
||||
|
||||
def update_lead_status(db, *, lead: MarketplaceLead, status: str, user_id: int | None) -> MarketplaceLead:
|
||||
if status not in LEAD_STATUSES:
|
||||
raise ValueError("Invalid lead status")
|
||||
lead.status = status
|
||||
lead.updated_by_user_id = user_id
|
||||
db.commit()
|
||||
db.refresh(lead)
|
||||
return lead
|
||||
|
||||
|
||||
def convert_lead_to_client(db, *, lead: MarketplaceLead, tenant_id: int, branch_id: int, partner_user_id: int | None, client_code: str | None, user_id: int | None) -> Client:
|
||||
if lead.converted_client_id:
|
||||
existing = db.get(Client, lead.converted_client_id)
|
||||
if existing:
|
||||
return existing
|
||||
code = (client_code or f"LEAD-{lead.id:05d}").strip().upper()
|
||||
name = (lead.business_name or lead.lead_name).strip()
|
||||
client = Client(
|
||||
tenant_id=tenant_id,
|
||||
branch_id=branch_id,
|
||||
partner_id=partner_user_id or None,
|
||||
engagement_mode="internal_managed",
|
||||
client_code=code,
|
||||
client_name=name,
|
||||
trade_name=lead.business_name or None,
|
||||
client_type="Other",
|
||||
contact_person_name=lead.lead_name,
|
||||
mobile=lead.mobile,
|
||||
email=lead.email,
|
||||
city=lead.city,
|
||||
state=lead.state,
|
||||
status="active",
|
||||
is_active=True,
|
||||
is_archived=False,
|
||||
notes=f"Converted from marketplace lead {lead.lead_no}.\n\n{lead.message or ''}".strip(),
|
||||
created_by_user_id=user_id,
|
||||
updated_by_user_id=user_id,
|
||||
)
|
||||
db.add(client)
|
||||
db.flush()
|
||||
lead.converted_client_id = client.id
|
||||
lead.converted_by_user_id = user_id
|
||||
lead.converted_at_utc = datetime.now(timezone.utc)
|
||||
lead.status = "CONVERTED"
|
||||
lead.updated_by_user_id = user_id
|
||||
db.commit()
|
||||
db.refresh(client)
|
||||
return client
|
||||
|
||||
|
||||
def get_marketplace_overview_counts(db) -> dict[str, int]:
|
||||
"""Return lightweight public marketplace counts.
|
||||
|
||||
This is intentionally read-only and tolerant of partially configured data so the
|
||||
public marketplace page never breaks the ERP login/runtime flow.
|
||||
"""
|
||||
try:
|
||||
firm_count = db.execute(select(func.count(Tenant.id)).where(Tenant.is_active.is_(True))).scalar_one() or 0
|
||||
except Exception:
|
||||
firm_count = 0
|
||||
try:
|
||||
consultant_count = db.execute(select(func.count(ConsultantProfile.id)).where(ConsultantProfile.is_active.is_(True))).scalar_one() or 0
|
||||
except Exception:
|
||||
consultant_count = 0
|
||||
try:
|
||||
lead_count = db.execute(select(func.count(MarketplaceLead.id))).scalar_one() or 0
|
||||
except Exception:
|
||||
lead_count = 0
|
||||
return {"audit_firms": int(firm_count), "consultants": int(consultant_count), "service_requests": int(lead_count)}
|
||||
|
||||
|
||||
def list_public_marketplace_audit_firms(db, limit: int = 12) -> list[Tenant]:
|
||||
"""List active audit firms for public marketplace display.
|
||||
|
||||
More advanced publication controls can be added later. For Phase 7T.4 this uses
|
||||
only active tenants and does not expose private firm records beyond basic name/type.
|
||||
"""
|
||||
try:
|
||||
stmt = select(Tenant).where(Tenant.is_active.is_(True)).order_by(Tenant.name.asc()).limit(int(limit or 12))
|
||||
return list(db.execute(stmt).scalars().all())
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def list_public_marketplace_consultants(db, limit: int = 12) -> list[ConsultantProfile]:
|
||||
"""List active consultants for public marketplace display."""
|
||||
try:
|
||||
stmt = (
|
||||
select(ConsultantProfile)
|
||||
.where(ConsultantProfile.is_active.is_(True))
|
||||
.order_by(ConsultantProfile.contact_person.asc(), ConsultantProfile.firm_name.asc())
|
||||
.limit(int(limit or 12))
|
||||
)
|
||||
return list(db.execute(stmt).scalars().all())
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def is_marketplace_domain_request(request) -> bool:
|
||||
"""True when Phase 7T.2 resolved the host as a marketplace domain."""
|
||||
try:
|
||||
return getattr(request.state, "domain_type", None) == "marketplace"
|
||||
except Exception:
|
||||
return False
|
||||
Reference in New Issue
Block a user