fix:updated assets and payslip body
Browse files- alembic/versions/c5bdab52a98c_modified_assets_id.py +39 -0
- requirements.txt +5 -0
- src/core/models.py +13 -7
- src/data_add/seed_from_excel.py +214 -0
- src/data_add/users.xlsx +0 -0
- src/payslip/service.py +9 -4
- src/profile/schemas.py +1 -1
alembic/versions/c5bdab52a98c_modified_assets_id.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""modified assets id
|
| 2 |
+
|
| 3 |
+
Revision ID: c5bdab52a98c
|
| 4 |
+
Revises: a34f13019598
|
| 5 |
+
Create Date: 2025-12-08 14:05:03.769843
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
import sqlmodel.sql.sqltypes
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# revision identifiers, used by Alembic.
|
| 16 |
+
revision: str = 'c5bdab52a98c'
|
| 17 |
+
down_revision: Union[str, Sequence[str], None] = 'a34f13019598'
|
| 18 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 19 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def upgrade() -> None:
|
| 23 |
+
"""Upgrade schema."""
|
| 24 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 25 |
+
op.alter_column('assets', 'id',
|
| 26 |
+
existing_type=sa.UUID(),
|
| 27 |
+
type_=sqlmodel.sql.sqltypes.AutoString(),
|
| 28 |
+
existing_nullable=False)
|
| 29 |
+
# ### end Alembic commands ###
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def downgrade() -> None:
|
| 33 |
+
"""Downgrade schema."""
|
| 34 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 35 |
+
op.alter_column('assets', 'id',
|
| 36 |
+
existing_type=sqlmodel.sql.sqltypes.AutoString(),
|
| 37 |
+
type_=sa.UUID(),
|
| 38 |
+
existing_nullable=False)
|
| 39 |
+
# ### end Alembic commands ###
|
requirements.txt
CHANGED
|
@@ -15,6 +15,7 @@ cryptography==46.0.3
|
|
| 15 |
dnspython==2.8.0
|
| 16 |
ecdsa==0.19.1
|
| 17 |
email-validator==2.3.0
|
|
|
|
| 18 |
fastapi==0.121.0
|
| 19 |
filelock==3.20.0
|
| 20 |
flatbuffers==25.9.23
|
|
@@ -40,7 +41,9 @@ mpmath==1.3.0
|
|
| 40 |
numpy==2.2.6
|
| 41 |
oauthlib==3.3.1
|
| 42 |
onnxruntime==1.23.2
|
|
|
|
| 43 |
packaging==25.0
|
|
|
|
| 44 |
passlib==1.7.4
|
| 45 |
pgvector==0.4.1
|
| 46 |
proto-plus==1.26.1
|
|
@@ -58,6 +61,7 @@ python-dateutil==2.9.0.post0
|
|
| 58 |
python-dotenv==1.2.1
|
| 59 |
python-jose==3.5.0
|
| 60 |
python-multipart==0.0.20
|
|
|
|
| 61 |
PyYAML==6.0.3
|
| 62 |
regex==2025.11.3
|
| 63 |
requests==2.32.5
|
|
@@ -75,6 +79,7 @@ tqdm==4.67.1
|
|
| 75 |
transformers==4.57.1
|
| 76 |
typing-inspection==0.4.2
|
| 77 |
typing_extensions==4.15.0
|
|
|
|
| 78 |
uritemplate==4.2.0
|
| 79 |
urllib3==2.5.0
|
| 80 |
uvicorn==0.38.0
|
|
|
|
| 15 |
dnspython==2.8.0
|
| 16 |
ecdsa==0.19.1
|
| 17 |
email-validator==2.3.0
|
| 18 |
+
et_xmlfile==2.0.0
|
| 19 |
fastapi==0.121.0
|
| 20 |
filelock==3.20.0
|
| 21 |
flatbuffers==25.9.23
|
|
|
|
| 41 |
numpy==2.2.6
|
| 42 |
oauthlib==3.3.1
|
| 43 |
onnxruntime==1.23.2
|
| 44 |
+
openpyxl==3.1.5
|
| 45 |
packaging==25.0
|
| 46 |
+
pandas==2.3.3
|
| 47 |
passlib==1.7.4
|
| 48 |
pgvector==0.4.1
|
| 49 |
proto-plus==1.26.1
|
|
|
|
| 61 |
python-dotenv==1.2.1
|
| 62 |
python-jose==3.5.0
|
| 63 |
python-multipart==0.0.20
|
| 64 |
+
pytz==2025.2
|
| 65 |
PyYAML==6.0.3
|
| 66 |
regex==2025.11.3
|
| 67 |
requests==2.32.5
|
|
|
|
| 79 |
transformers==4.57.1
|
| 80 |
typing-inspection==0.4.2
|
| 81 |
typing_extensions==4.15.0
|
| 82 |
+
tzdata==2025.2
|
| 83 |
uritemplate==4.2.0
|
| 84 |
urllib3==2.5.0
|
| 85 |
uvicorn==0.38.0
|
src/core/models.py
CHANGED
|
@@ -18,6 +18,7 @@ class AssetStatus(str, Enum):
|
|
| 18 |
ON_REQUEST = "On Request"
|
| 19 |
IN_SERVICE = "In Service"
|
| 20 |
|
|
|
|
| 21 |
class Emotion(str, Enum):
|
| 22 |
JOYFUL = "joyful"
|
| 23 |
HAPPY = "happy"
|
|
@@ -26,12 +27,14 @@ class Emotion(str, Enum):
|
|
| 26 |
ANXIOUS = "anxious"
|
| 27 |
SAD = "sad"
|
| 28 |
FRUSTRATED = "frustrated"
|
| 29 |
-
|
|
|
|
| 30 |
class AppVersion(SQLModel, table=True):
|
| 31 |
__tablename__ = "app_version"
|
| 32 |
version: str = Field(primary_key=True)
|
| 33 |
apk_download_link: str
|
| 34 |
|
|
|
|
| 35 |
class Users(SQLModel, table=True):
|
| 36 |
__tablename__ = "users"
|
| 37 |
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
|
@@ -91,7 +94,7 @@ class UserTeamsRole(SQLModel, table=True):
|
|
| 91 |
|
| 92 |
class Assets(SQLModel, table=True):
|
| 93 |
__tablename__ = "assets"
|
| 94 |
-
id:
|
| 95 |
user_id: uuid.UUID = Field(
|
| 96 |
sa_column=Column(
|
| 97 |
UUID(as_uuid=True),
|
|
@@ -120,11 +123,14 @@ class EmotionLogs(SQLModel, table=True):
|
|
| 120 |
)
|
| 121 |
|
| 122 |
morning_emotion: Optional[Emotion] = Field(
|
| 123 |
-
|
| 124 |
-
)
|
|
|
|
|
|
|
| 125 |
evening_emotion: Optional[Emotion] = Field(
|
| 126 |
-
|
| 127 |
-
)
|
|
|
|
|
|
|
| 128 |
|
| 129 |
log_date: date = Field(default_factory=date.today)
|
| 130 |
-
|
|
|
|
| 18 |
ON_REQUEST = "On Request"
|
| 19 |
IN_SERVICE = "In Service"
|
| 20 |
|
| 21 |
+
|
| 22 |
class Emotion(str, Enum):
|
| 23 |
JOYFUL = "joyful"
|
| 24 |
HAPPY = "happy"
|
|
|
|
| 27 |
ANXIOUS = "anxious"
|
| 28 |
SAD = "sad"
|
| 29 |
FRUSTRATED = "frustrated"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
class AppVersion(SQLModel, table=True):
|
| 33 |
__tablename__ = "app_version"
|
| 34 |
version: str = Field(primary_key=True)
|
| 35 |
apk_download_link: str
|
| 36 |
|
| 37 |
+
|
| 38 |
class Users(SQLModel, table=True):
|
| 39 |
__tablename__ = "users"
|
| 40 |
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
|
|
|
| 94 |
|
| 95 |
class Assets(SQLModel, table=True):
|
| 96 |
__tablename__ = "assets"
|
| 97 |
+
id: str = Field(primary_key=True)
|
| 98 |
user_id: uuid.UUID = Field(
|
| 99 |
sa_column=Column(
|
| 100 |
UUID(as_uuid=True),
|
|
|
|
| 123 |
)
|
| 124 |
|
| 125 |
morning_emotion: Optional[Emotion] = Field(
|
| 126 |
+
sa_column=Column(
|
| 127 |
+
SQLEnum(Emotion, name="emotion_enum", native_enum=True), nullable=True
|
| 128 |
+
)
|
| 129 |
+
)
|
| 130 |
evening_emotion: Optional[Emotion] = Field(
|
| 131 |
+
sa_column=Column(
|
| 132 |
+
SQLEnum(Emotion, name="emotion_enum", native_enum=True), nullable=True
|
| 133 |
+
)
|
| 134 |
+
)
|
| 135 |
|
| 136 |
log_date: date = Field(default_factory=date.today)
|
|
|
src/data_add/seed_from_excel.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import asyncio
|
| 3 |
+
import math
|
| 4 |
+
import json
|
| 5 |
+
from datetime import datetime, date
|
| 6 |
+
from sqlmodel import select
|
| 7 |
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
| 8 |
+
|
| 9 |
+
from passlib.context import CryptContext
|
| 10 |
+
|
| 11 |
+
from src.core.database import async_session
|
| 12 |
+
from src.core.models import Users, Teams, Roles, UserTeamsRole, Assets, AssetStatus
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# ---------------------------------------------
|
| 16 |
+
# PASSWORD HASHER
|
| 17 |
+
# ---------------------------------------------
|
| 18 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def hash_password(password: str) -> str:
|
| 22 |
+
return pwd_context.hash(password)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ---------------------------------------------
|
| 26 |
+
# CLEAN NaN / EMPTY VALUES
|
| 27 |
+
# ---------------------------------------------
|
| 28 |
+
def clean(value):
|
| 29 |
+
if value is None:
|
| 30 |
+
return None
|
| 31 |
+
if isinstance(value, float) and math.isnan(value):
|
| 32 |
+
return None
|
| 33 |
+
if isinstance(value, str) and value.strip() == "":
|
| 34 |
+
return None
|
| 35 |
+
return value
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ---------------------------------------------
|
| 39 |
+
# NORMALIZE ASSET ID (NEW)
|
| 40 |
+
# ---------------------------------------------
|
| 41 |
+
def normalize_asset_id(value: str) -> str:
|
| 42 |
+
"""Convert messy input to clean format YB-73-M or YB-77-MS"""
|
| 43 |
+
if not value:
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
value = value.strip().replace(" ", "")
|
| 47 |
+
value = value.upper()
|
| 48 |
+
|
| 49 |
+
# Fix repeated hyphens
|
| 50 |
+
while "--" in value:
|
| 51 |
+
value = value.replace("--", "-")
|
| 52 |
+
|
| 53 |
+
# If looks like YB73M (missing hyphens)
|
| 54 |
+
if "-" not in value:
|
| 55 |
+
prefix = value[:2] # YB
|
| 56 |
+
number = "".join(filter(str.isdigit, value))
|
| 57 |
+
suffix = value[len(prefix) + len(number) :]
|
| 58 |
+
return f"{prefix}-{number}-{suffix}"
|
| 59 |
+
|
| 60 |
+
return value
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# ---------------------------------------------
|
| 64 |
+
# ASSET TYPE MAP (fallback)
|
| 65 |
+
# ---------------------------------------------
|
| 66 |
+
ASSET_TYPE_MAP = {
|
| 67 |
+
"M": "Monitor",
|
| 68 |
+
"L": "Laptop",
|
| 69 |
+
"H": "Headphone",
|
| 70 |
+
"MS": "Mouse",
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ---------------------------------------------
|
| 75 |
+
# get_or_create
|
| 76 |
+
# ---------------------------------------------
|
| 77 |
+
async def get_or_create(session: AsyncSession, model, name: str):
|
| 78 |
+
result = await session.exec(select(model).where(model.name == name))
|
| 79 |
+
row = result.first()
|
| 80 |
+
if row:
|
| 81 |
+
return row
|
| 82 |
+
|
| 83 |
+
row = model(name=name)
|
| 84 |
+
session.add(row)
|
| 85 |
+
await session.commit()
|
| 86 |
+
await session.refresh(row)
|
| 87 |
+
return row
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# ---------------------------------------------
|
| 91 |
+
# PARSE ASSETS JSON
|
| 92 |
+
# ---------------------------------------------
|
| 93 |
+
def parse_assets_from_excel(raw_value):
|
| 94 |
+
if raw_value is None:
|
| 95 |
+
return []
|
| 96 |
+
|
| 97 |
+
raw = str(raw_value).strip()
|
| 98 |
+
raw = raw.replace('""', '"')
|
| 99 |
+
|
| 100 |
+
if raw.startswith('"') and raw.endswith('"'):
|
| 101 |
+
raw = raw[1:-1]
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
parsed = json.loads(raw)
|
| 105 |
+
if isinstance(parsed, list):
|
| 106 |
+
return parsed
|
| 107 |
+
return []
|
| 108 |
+
except:
|
| 109 |
+
return []
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# ---------------------------------------------
|
| 113 |
+
# MAIN IMPORT
|
| 114 |
+
# ---------------------------------------------
|
| 115 |
+
async def seed_from_excel(session: AsyncSession, excel_path="src/data_add/users.xlsx"):
|
| 116 |
+
df = pd.read_excel(excel_path)
|
| 117 |
+
print("\n📌 Importing users from Excel...\n")
|
| 118 |
+
|
| 119 |
+
for _, row in df.iterrows():
|
| 120 |
+
|
| 121 |
+
# --- DOB ---
|
| 122 |
+
raw_dob = clean(row["dob"])
|
| 123 |
+
dob = None
|
| 124 |
+
if isinstance(raw_dob, str):
|
| 125 |
+
try:
|
| 126 |
+
dob = datetime.strptime(raw_dob, "%d.%m.%Y").date()
|
| 127 |
+
except:
|
| 128 |
+
dob = None
|
| 129 |
+
elif isinstance(raw_dob, datetime):
|
| 130 |
+
dob = raw_dob.date()
|
| 131 |
+
else:
|
| 132 |
+
dob = raw_dob
|
| 133 |
+
|
| 134 |
+
# --- CREATE USER (is_verified = True) ---
|
| 135 |
+
user = Users(
|
| 136 |
+
email_id=row["email"],
|
| 137 |
+
password=hash_password(row["password"]),
|
| 138 |
+
user_name=row["User_name"],
|
| 139 |
+
dob=dob,
|
| 140 |
+
address=clean(row["address"]),
|
| 141 |
+
join_date=str(clean(row["join_date"])),
|
| 142 |
+
is_verified=True, # <--- Added here
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
session.add(user)
|
| 146 |
+
await session.commit()
|
| 147 |
+
await session.refresh(user)
|
| 148 |
+
|
| 149 |
+
# --- TEAM / ROLE ---
|
| 150 |
+
team = await get_or_create(session, Teams, row["team"])
|
| 151 |
+
role = await get_or_create(session, Roles, row["role"])
|
| 152 |
+
|
| 153 |
+
mapping = UserTeamsRole(
|
| 154 |
+
user_id=user.id,
|
| 155 |
+
team_id=team.id,
|
| 156 |
+
role_id=role.id,
|
| 157 |
+
)
|
| 158 |
+
session.add(mapping)
|
| 159 |
+
|
| 160 |
+
# --- ASSETS JSON ---
|
| 161 |
+
assets_list = parse_assets_from_excel(clean(row["assets"]))
|
| 162 |
+
|
| 163 |
+
for asset in assets_list:
|
| 164 |
+
asset_id = normalize_asset_id(asset.get("id"))
|
| 165 |
+
asset_type = asset.get("type", "Unknown")
|
| 166 |
+
|
| 167 |
+
if not asset_id:
|
| 168 |
+
continue
|
| 169 |
+
|
| 170 |
+
asset_obj = Assets(
|
| 171 |
+
id=asset_id, # cleaned ID like YB-73-M
|
| 172 |
+
user_id=user.id,
|
| 173 |
+
name=asset_type, # Monitor
|
| 174 |
+
type=asset_type, # Monitor
|
| 175 |
+
status=AssetStatus.ACTIVE,
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
session.add(asset_obj)
|
| 179 |
+
|
| 180 |
+
await session.commit()
|
| 181 |
+
|
| 182 |
+
print("\n🎉 Excel Import Completed Successfully!\n")
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# ---------------------------------------------
|
| 186 |
+
# RUN SEEDS
|
| 187 |
+
# ---------------------------------------------
|
| 188 |
+
async def run_all_seeds():
|
| 189 |
+
|
| 190 |
+
async with async_session() as session:
|
| 191 |
+
|
| 192 |
+
print("\n🟦 Seeding TEAMS...")
|
| 193 |
+
for t in [
|
| 194 |
+
"AI/Tech",
|
| 195 |
+
"Shared services",
|
| 196 |
+
"Digital Marketing",
|
| 197 |
+
"Bevolve",
|
| 198 |
+
"Bridge",
|
| 199 |
+
"HR",
|
| 200 |
+
]:
|
| 201 |
+
await get_or_create(session, Teams, t)
|
| 202 |
+
|
| 203 |
+
print("\n🟦 Seeding ROLES...")
|
| 204 |
+
for r in ["Mentor", "Team Lead", "HR", "Member"]:
|
| 205 |
+
await get_or_create(session, Roles, r)
|
| 206 |
+
|
| 207 |
+
print("\n🟦 Importing USERS from Excel...")
|
| 208 |
+
await seed_from_excel(session, "src/data_add/users.xlsx")
|
| 209 |
+
|
| 210 |
+
print("\n✔ All seeding complete!\n")
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
if __name__ == "__main__":
|
| 214 |
+
asyncio.run(run_all_seeds())
|
src/data_add/users.xlsx
ADDED
|
Binary file (6.97 kB). View file
|
|
|
src/payslip/service.py
CHANGED
|
@@ -51,7 +51,7 @@ async def one_request_per_day(session: AsyncSession, user_id):
|
|
| 51 |
|
| 52 |
|
| 53 |
async def get_hr_email(session: AsyncSession):
|
| 54 |
-
q = select(Roles).where(Roles.name == "HR
|
| 55 |
role = (await session.execute(q)).scalar_one_or_none()
|
| 56 |
if not role:
|
| 57 |
raise HTTPException(500, "HR role missing")
|
|
@@ -118,9 +118,14 @@ async def process_payslip_request(
|
|
| 118 |
# 8. Build email body
|
| 119 |
subject = "Payslip Request"
|
| 120 |
body = (
|
| 121 |
-
f"
|
| 122 |
-
f"
|
| 123 |
-
f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
)
|
| 125 |
|
| 126 |
raw = build_email(user.email_id, hr_email, subject, body)
|
|
|
|
| 51 |
|
| 52 |
|
| 53 |
async def get_hr_email(session: AsyncSession):
|
| 54 |
+
q = select(Roles).where(Roles.name == "HR")
|
| 55 |
role = (await session.execute(q)).scalar_one_or_none()
|
| 56 |
if not role:
|
| 57 |
raise HTTPException(500, "HR role missing")
|
|
|
|
| 118 |
# 8. Build email body
|
| 119 |
subject = "Payslip Request"
|
| 120 |
body = (
|
| 121 |
+
f"Dear Team,\n\n"
|
| 122 |
+
f"I would like to request the payslip for the following period:\n\n"
|
| 123 |
+
f"Employee Name : {user.user_name}\n"
|
| 124 |
+
f"Email : {user.email_id}\n"
|
| 125 |
+
f"Team : {team}\n"
|
| 126 |
+
f"Period : {period_start} → {period_end}\n\n"
|
| 127 |
+
f"Kindly process this request at the earliest.\n"
|
| 128 |
+
f"Thank you.\n"
|
| 129 |
)
|
| 130 |
|
| 131 |
raw = build_email(user.email_id, hr_email, subject, body)
|
src/profile/schemas.py
CHANGED
|
@@ -93,7 +93,7 @@ class AssetUpdateRequest(BaseModel):
|
|
| 93 |
|
| 94 |
|
| 95 |
class AssetResponse(BaseModel):
|
| 96 |
-
id:
|
| 97 |
user_id: uuid.UUID
|
| 98 |
name: str
|
| 99 |
type: str
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
class AssetResponse(BaseModel):
|
| 96 |
+
id: str
|
| 97 |
user_id: uuid.UUID
|
| 98 |
name: str
|
| 99 |
type: str
|