Prepare ERP source for Gitea deployment

This commit is contained in:
A R R R Associates
2026-06-20 15:01:44 +05:30
commit 5c75eb6bd9
450 changed files with 67698 additions and 0 deletions
+228
View File
@@ -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