Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, field_validator, model_validator
|
||||
|
||||
from app.modules.clients.constants import (
|
||||
CLIENT_CATEGORY_OPTIONS,
|
||||
CLIENT_SORT_FIELDS,
|
||||
CLIENT_STATUS,
|
||||
CLIENT_TYPES,
|
||||
ENGAGEMENT_MODES,
|
||||
RISK_CATEGORIES,
|
||||
)
|
||||
from app.modules.clients.utils import GSTIN_RE, MOBILE_RE, PAN_RE, PIN_RE, TAN_RE, normalize_text, normalize_upper
|
||||
|
||||
|
||||
class ClientBase(BaseModel):
|
||||
tenant_id: int
|
||||
branch_id: int
|
||||
partner_id: Optional[int] = None
|
||||
default_review_partner_user_id: Optional[int] = None
|
||||
engagement_mode: str = "internal_managed"
|
||||
client_code: str
|
||||
client_name: str
|
||||
trade_name: Optional[str] = None
|
||||
client_type: str = "Other"
|
||||
pan: Optional[str] = None
|
||||
gstin: Optional[str] = None
|
||||
tan: Optional[str] = None
|
||||
cin_llpin: Optional[str] = None
|
||||
msme_no: Optional[str] = None
|
||||
iec_code: Optional[str] = None
|
||||
contact_person_name: Optional[str] = None
|
||||
contact_person_designation: Optional[str] = None
|
||||
mobile: Optional[str] = None
|
||||
alternate_mobile: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
alternate_email: Optional[EmailStr] = None
|
||||
address_line_1: Optional[str] = None
|
||||
address_line_2: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
pincode: Optional[str] = None
|
||||
country: Optional[str] = "India"
|
||||
status: str = "active"
|
||||
client_category: Optional[str] = None
|
||||
risk_category: Optional[str] = None
|
||||
onboarding_date: Optional[date] = None
|
||||
closing_date: Optional[date] = None
|
||||
notes: Optional[str] = None
|
||||
gst_applicable: bool = False
|
||||
income_tax_applicable: bool = False
|
||||
tds_applicable: bool = False
|
||||
roc_applicable: bool = False
|
||||
audit_applicable: bool = False
|
||||
pf_applicable: bool = False
|
||||
esi_applicable: bool = False
|
||||
professional_tax_applicable: bool = False
|
||||
payroll_applicable: bool = False
|
||||
msme_applicable: bool = False
|
||||
import_export_applicable: bool = False
|
||||
|
||||
@field_validator("client_code", "client_name", mode="before")
|
||||
@classmethod
|
||||
def required_text(cls, value):
|
||||
value = normalize_text(value)
|
||||
if not value:
|
||||
raise ValueError("This field is required.")
|
||||
return value
|
||||
|
||||
@field_validator(
|
||||
"trade_name", "cin_llpin", "msme_no", "iec_code", "contact_person_name", "contact_person_designation",
|
||||
"address_line_1", "address_line_2", "city", "state", "country", "client_category", "risk_category", "notes",
|
||||
mode="before",
|
||||
)
|
||||
@classmethod
|
||||
def clean_text(cls, value):
|
||||
return normalize_text(value)
|
||||
|
||||
@field_validator("pan", "gstin", "tan", mode="before")
|
||||
@classmethod
|
||||
def uppercase_codes(cls, value):
|
||||
return normalize_upper(value)
|
||||
|
||||
@field_validator("engagement_mode", mode="before")
|
||||
@classmethod
|
||||
def clean_engagement_mode(cls, value):
|
||||
value = normalize_text(value) or "internal_managed"
|
||||
return value.lower()
|
||||
|
||||
@field_validator("engagement_mode")
|
||||
@classmethod
|
||||
def validate_engagement_mode(cls, value):
|
||||
if value not in ENGAGEMENT_MODES:
|
||||
raise ValueError("Invalid engagement mode.")
|
||||
return value
|
||||
|
||||
@field_validator("mobile", "alternate_mobile", mode="before")
|
||||
@classmethod
|
||||
def clean_mobile(cls, value):
|
||||
value = normalize_text(value)
|
||||
if value is None:
|
||||
return None
|
||||
value = value.replace(" ", "").replace("-", "")
|
||||
if value.startswith("+91"):
|
||||
value = value[3:]
|
||||
return value
|
||||
|
||||
@field_validator("pan")
|
||||
@classmethod
|
||||
def validate_pan(cls, value):
|
||||
if value and not PAN_RE.match(value):
|
||||
raise ValueError("Invalid PAN format.")
|
||||
return value
|
||||
|
||||
@field_validator("gstin")
|
||||
@classmethod
|
||||
def validate_gstin(cls, value):
|
||||
if value and not GSTIN_RE.match(value):
|
||||
raise ValueError("Invalid GSTIN format.")
|
||||
return value
|
||||
|
||||
@field_validator("tan")
|
||||
@classmethod
|
||||
def validate_tan(cls, value):
|
||||
if value and not TAN_RE.match(value):
|
||||
raise ValueError("Invalid TAN format.")
|
||||
return value
|
||||
|
||||
@field_validator("mobile", "alternate_mobile")
|
||||
@classmethod
|
||||
def validate_mobile(cls, value):
|
||||
if value and not MOBILE_RE.match(value):
|
||||
raise ValueError("Mobile number must be a valid 10-digit Indian mobile.")
|
||||
return value
|
||||
|
||||
@field_validator("pincode", mode="before")
|
||||
@classmethod
|
||||
def clean_pincode(cls, value):
|
||||
return normalize_text(value)
|
||||
|
||||
@field_validator("pincode")
|
||||
@classmethod
|
||||
def validate_pincode(cls, value):
|
||||
if value and not PIN_RE.match(value):
|
||||
raise ValueError("Pincode must be a valid 6-digit code.")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_dates_and_assignment(self):
|
||||
if self.onboarding_date and self.closing_date and self.closing_date < self.onboarding_date:
|
||||
raise ValueError("Closing date cannot be earlier than onboarding date.")
|
||||
if self.engagement_mode == "internal_managed" and not self.partner_id:
|
||||
raise ValueError("Partner is required for internal managed clients.")
|
||||
return self
|
||||
|
||||
|
||||
class ClientCreate(ClientBase):
|
||||
pass
|
||||
|
||||
|
||||
class ClientUpdate(BaseModel):
|
||||
tenant_id: Optional[int] = None
|
||||
branch_id: Optional[int] = None
|
||||
partner_id: Optional[int] = None
|
||||
default_review_partner_user_id: Optional[int] = None
|
||||
engagement_mode: Optional[str] = None
|
||||
client_name: Optional[str] = None
|
||||
trade_name: Optional[str] = None
|
||||
client_type: Optional[str] = None
|
||||
pan: Optional[str] = None
|
||||
gstin: Optional[str] = None
|
||||
tan: Optional[str] = None
|
||||
cin_llpin: Optional[str] = None
|
||||
msme_no: Optional[str] = None
|
||||
iec_code: Optional[str] = None
|
||||
contact_person_name: Optional[str] = None
|
||||
contact_person_designation: Optional[str] = None
|
||||
mobile: Optional[str] = None
|
||||
alternate_mobile: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
alternate_email: Optional[EmailStr] = None
|
||||
address_line_1: Optional[str] = None
|
||||
address_line_2: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
pincode: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
client_category: Optional[str] = None
|
||||
risk_category: Optional[str] = None
|
||||
onboarding_date: Optional[date] = None
|
||||
closing_date: Optional[date] = None
|
||||
notes: Optional[str] = None
|
||||
gst_applicable: Optional[bool] = None
|
||||
income_tax_applicable: Optional[bool] = None
|
||||
tds_applicable: Optional[bool] = None
|
||||
roc_applicable: Optional[bool] = None
|
||||
audit_applicable: Optional[bool] = None
|
||||
pf_applicable: Optional[bool] = None
|
||||
esi_applicable: Optional[bool] = None
|
||||
professional_tax_applicable: Optional[bool] = None
|
||||
payroll_applicable: Optional[bool] = None
|
||||
msme_applicable: Optional[bool] = None
|
||||
import_export_applicable: Optional[bool] = None
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@field_validator(
|
||||
"client_name", "trade_name", "cin_llpin", "msme_no", "iec_code", "contact_person_name", "contact_person_designation",
|
||||
"address_line_1", "address_line_2", "city", "state", "country", "client_category", "risk_category", "notes",
|
||||
mode="before",
|
||||
)
|
||||
@classmethod
|
||||
def clean_text(cls, value):
|
||||
return normalize_text(value)
|
||||
|
||||
@field_validator("pan", "gstin", "tan", mode="before")
|
||||
@classmethod
|
||||
def uppercase_codes(cls, value):
|
||||
return normalize_upper(value)
|
||||
|
||||
@field_validator("engagement_mode", mode="before")
|
||||
@classmethod
|
||||
def clean_engagement_mode(cls, value):
|
||||
if value is None:
|
||||
return None
|
||||
value = normalize_text(value) or None
|
||||
return value.lower() if value else None
|
||||
|
||||
@field_validator("engagement_mode")
|
||||
@classmethod
|
||||
def validate_engagement_mode(cls, value):
|
||||
if value is not None and value not in ENGAGEMENT_MODES:
|
||||
raise ValueError("Invalid engagement mode.")
|
||||
return value
|
||||
|
||||
@field_validator("mobile", "alternate_mobile", mode="before")
|
||||
@classmethod
|
||||
def clean_mobile(cls, value):
|
||||
value = normalize_text(value)
|
||||
if value is None:
|
||||
return None
|
||||
value = value.replace(" ", "").replace("-", "")
|
||||
if value.startswith("+91"):
|
||||
value = value[3:]
|
||||
return value
|
||||
|
||||
@field_validator("pan")
|
||||
@classmethod
|
||||
def validate_pan(cls, value):
|
||||
if value and not PAN_RE.match(value):
|
||||
raise ValueError("Invalid PAN format.")
|
||||
return value
|
||||
|
||||
@field_validator("gstin")
|
||||
@classmethod
|
||||
def validate_gstin(cls, value):
|
||||
if value and not GSTIN_RE.match(value):
|
||||
raise ValueError("Invalid GSTIN format.")
|
||||
return value
|
||||
|
||||
@field_validator("tan")
|
||||
@classmethod
|
||||
def validate_tan(cls, value):
|
||||
if value and not TAN_RE.match(value):
|
||||
raise ValueError("Invalid TAN format.")
|
||||
return value
|
||||
|
||||
@field_validator("mobile", "alternate_mobile")
|
||||
@classmethod
|
||||
def validate_mobile(cls, value):
|
||||
if value and not MOBILE_RE.match(value):
|
||||
raise ValueError("Mobile number must be a valid 10-digit Indian mobile.")
|
||||
return value
|
||||
|
||||
@field_validator("pincode", mode="before")
|
||||
@classmethod
|
||||
def clean_pincode(cls, value):
|
||||
return normalize_text(value)
|
||||
|
||||
@field_validator("pincode")
|
||||
@classmethod
|
||||
def validate_pincode(cls, value):
|
||||
if value and not PIN_RE.match(value):
|
||||
raise ValueError("Pincode must be a valid 6-digit code.")
|
||||
return value
|
||||
|
||||
|
||||
class ClientOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
tenant_id: int
|
||||
branch_id: int
|
||||
partner_id: Optional[int] = None
|
||||
default_review_partner_user_id: Optional[int] = None
|
||||
engagement_mode: str
|
||||
client_code: str
|
||||
client_name: str
|
||||
trade_name: Optional[str] = None
|
||||
client_type: str
|
||||
pan: Optional[str] = None
|
||||
gstin: Optional[str] = None
|
||||
tan: Optional[str] = None
|
||||
cin_llpin: Optional[str] = None
|
||||
msme_no: Optional[str] = None
|
||||
iec_code: Optional[str] = None
|
||||
contact_person_name: Optional[str] = None
|
||||
contact_person_designation: Optional[str] = None
|
||||
mobile: Optional[str] = None
|
||||
alternate_mobile: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
alternate_email: Optional[str] = None
|
||||
address_line_1: Optional[str] = None
|
||||
address_line_2: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
pincode: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
status: str
|
||||
client_category: Optional[str] = None
|
||||
risk_category: Optional[str] = None
|
||||
onboarding_date: Optional[date] = None
|
||||
closing_date: Optional[date] = None
|
||||
notes: Optional[str] = None
|
||||
gst_applicable: bool
|
||||
income_tax_applicable: bool
|
||||
tds_applicable: bool
|
||||
roc_applicable: bool
|
||||
audit_applicable: bool
|
||||
pf_applicable: bool
|
||||
esi_applicable: bool
|
||||
professional_tax_applicable: bool
|
||||
payroll_applicable: bool
|
||||
msme_applicable: bool
|
||||
import_export_applicable: bool
|
||||
is_active: bool
|
||||
is_archived: bool
|
||||
created_at_utc: datetime
|
||||
updated_at_utc: datetime
|
||||
|
||||
|
||||
class ClientListRow(ClientOut):
|
||||
partner_name: Optional[str] = None
|
||||
branch_name: Optional[str] = None
|
||||
tenant_name: Optional[str] = None
|
||||
|
||||
|
||||
class ClientAuditLogOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
client_id: int
|
||||
tenant_id: int
|
||||
branch_id: int
|
||||
actor_user_id: Optional[int] = None
|
||||
action: str
|
||||
summary: str
|
||||
payload_json: Optional[dict] = None
|
||||
created_at_utc: datetime
|
||||
|
||||
|
||||
class PaginationMeta(BaseModel):
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
class ClientFilterOptions(BaseModel):
|
||||
client_types: list[str]
|
||||
client_statuses: list[str]
|
||||
client_categories: list[str]
|
||||
risk_categories: list[str]
|
||||
|
||||
|
||||
class ClientListStats(BaseModel):
|
||||
total: int = 0
|
||||
active: int = 0
|
||||
inactive: int = 0
|
||||
archived: int = 0
|
||||
|
||||
|
||||
class ClientListResponse(BaseModel):
|
||||
rows: list[ClientOut]
|
||||
meta: PaginationMeta
|
||||
stats: ClientListStats | None = None
|
||||
filter_options: ClientFilterOptions | None = None
|
||||
Reference in New Issue
Block a user