Hp137 commited on
Commit
7d3bfe5
·
1 Parent(s): d7da01b

fix:updated assets and payslip body

Browse files
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: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
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
- sa_column=Column(SQLEnum(Emotion, name="emotion_enum", native_enum=True), nullable=True)
124
- )
 
 
125
  evening_emotion: Optional[Emotion] = Field(
126
- sa_column=Column(SQLEnum(Emotion, name="emotion_enum", native_enum=True), nullable=True)
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 Manager")
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"Payslip request from {user.user_name} ({user.email_id})\n"
122
- f"Team: {team}\n"
123
- f"Period: {period_start} {period_end}\n"
 
 
 
 
 
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: uuid.UUID
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