From aae48faf913c9a665143e3810b53d50bee390ab3 Mon Sep 17 00:00:00 2001 From: tanshu Date: Sun, 10 May 2020 13:32:08 +0530 Subject: [PATCH] Renamed service points to points consistently Renamed Schemas to shorten them Added the name validators for Account Base Added joining_date/leaving_date validators for employees Employees should be working now --- brewman/models/master.py | 6 +- brewman/routers/account.py | 39 +-- brewman/routers/attendance.py | 25 +- brewman/routers/attendance_report.py | 4 +- brewman/routers/cost_centre.py | 4 +- brewman/routers/employee.py | 344 ++++++++----------- brewman/routers/services/voucher/__init__.py | 2 +- brewman/schemas/master.py | 91 ++++- 8 files changed, 268 insertions(+), 247 deletions(-) diff --git a/brewman/models/master.py b/brewman/models/master.py index 861067d5..f0f0ed16 100644 --- a/brewman/models/master.py +++ b/brewman/models/master.py @@ -396,7 +396,7 @@ class Employee(AccountBase): id = Column("id", GUID(), ForeignKey(AccountBase.id), primary_key=True) designation = Column("Designation", Unicode(255)) salary = Column("Salary", Integer) - service_points = Column("ServicePoints", Numeric(precision=5, scale=2)) + points = Column("ServicePoints", Numeric(precision=5, scale=2)) joining_date = Column("JoiningDate", DateTime) leaving_date = Column("LeavingDate", DateTime) @@ -416,13 +416,13 @@ class Employee(AccountBase): cost_centre_id=None, designation=None, salary=None, - service_points=None, + points=None, joining_date=None, leaving_date=None, ): self.designation = designation self.salary = salary - self.service_points = service_points + self.points = points self.joining_date = joining_date self.leaving_date = leaving_date super().__init__( diff --git a/brewman/routers/account.py b/brewman/routers/account.py index 86e6e65c..4c539f0d 100644 --- a/brewman/routers/account.py +++ b/brewman/routers/account.py @@ -28,7 +28,7 @@ def get_db(): @router.post("/", response_model=schemas.Account) def save( - data: schemas.AccountSaveUpdate, + data: schemas.AccountIn, db: Session = Depends(get_db), user: User = Security(get_user, scopes=["accounts"]), ): @@ -60,7 +60,7 @@ def save( @router.put("/{id_}", response_model=schemas.Account) def update( id_: uuid.UUID, - data: schemas.AccountSaveUpdate, + data: schemas.AccountIn, db: Session = Depends(get_db), user: User = Security(get_user, scopes=["accounts"]), ): @@ -123,28 +123,19 @@ def show_blank( @router.get("/list") async def show_list(db: Session = Depends(get_db), user: User = Depends(get_user)): - list_ = ( - db.query(Account) - .order_by(Account.type) - .order_by(Account.name) - .order_by(Account.code) - .all() - ) - accounts = [] - for item in list_: - accounts.append( - { - "id": item.id, - "name": item.name, - "type": item.type_object.name, - "isActive": item.is_active, - "isReconcilable": item.is_reconcilable, - "isStarred": item.is_starred, - "costCentre": item.cost_centre.name, - "isFixture": item.is_fixture, - } - ) - return accounts + return [ + { + "id": item.id, + "name": item.name, + "type": item.type_object.name, + "isActive": item.is_active, + "isReconcilable": item.is_reconcilable, + "isStarred": item.is_starred, + "costCentre": item.cost_centre.name, + "isFixture": item.is_fixture, + } + for item in db.query(Account).order_by(Account.type).order_by(Account.name).order_by(Account.code).all() + ] @router.get("/query") diff --git a/brewman/routers/attendance.py b/brewman/routers/attendance.py index 43a811ca..d2e54c4d 100644 --- a/brewman/routers/attendance.py +++ b/brewman/routers/attendance.py @@ -2,19 +2,32 @@ import datetime import uuid from sqlalchemy import or_ +from sqlalchemy.orm import Session +from fastapi import Depends, Security -from brewman.models.master import Employee -from brewman.models.voucher import Attendance -from brewman.routers.fingerprint import get_prints -from brewman.routers.services.session import session_current_date +from ..models.master import Employee +from ..models.voucher import Attendance +from ..routers.fingerprint import get_prints +from ..routers.services.session import session_current_date +from ..db.session import SessionLocal +from ..core.security import User, get_current_active_user as get_user from fastapi import APIRouter router = APIRouter() -@router.get("/") # "Attendance" -def attendance_blank(request): +# Dependency +def get_db(): + try: + db = SessionLocal() + yield db + finally: + db.close() + + +@router.get("/") +def attendance_blank(db: Session = Depends(get_db), user: User = Security(get_user, scopes=["attendance"])): return {"date": session_current_date(request), "body": []} diff --git a/brewman/routers/attendance_report.py b/brewman/routers/attendance_report.py index b3cd4516..e6d67801 100644 --- a/brewman/routers/attendance_report.py +++ b/brewman/routers/attendance_report.py @@ -42,9 +42,9 @@ def attendance_record(start_date, finish_date, dbsession): employee.designation, employee.cost_centre.name, employee.salary, - employee.service_points, + employee.points, ] - row_value = ["", "", "", "", employee.salary, employee.service_points] + row_value = ["", "", "", "", employee.salary, employee.points] for date in daterange(start_date, finish_date, inclusive=True): att = ( dbsession.query(Attendance) diff --git a/brewman/routers/cost_centre.py b/brewman/routers/cost_centre.py index 18e7e0ee..eba76223 100644 --- a/brewman/routers/cost_centre.py +++ b/brewman/routers/cost_centre.py @@ -23,7 +23,7 @@ def get_db(): @router.post("/", response_model=schemas.CostCentre) def save( - data: schemas.CostCentreSaveUpdate, + data: schemas.CostCentreIn, db: Session = Depends(get_db), user: User = Security(get_user, scopes=["cost-centres"]), ): @@ -43,7 +43,7 @@ def save( @router.put("/{id_}", response_model=schemas.CostCentre) def update( id_: uuid.UUID, - data: schemas.CostCentreSaveUpdate, + data: schemas.CostCentreIn, db: Session = Depends(get_db), user: User = Security(get_user, scopes=["cost-centres"]), ): diff --git a/brewman/routers/employee.py b/brewman/routers/employee.py index a48d0cc0..c093deb4 100644 --- a/brewman/routers/employee.py +++ b/brewman/routers/employee.py @@ -1,213 +1,165 @@ -import datetime +import traceback import uuid -from decimal import Decimal from sqlalchemy import desc -from sqlalchemy.orm import joinedload_all +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import joinedload_all, Session +from fastapi import APIRouter, HTTPException, status, Depends, Security +from ..core.security import User, get_current_active_user as get_user +from ..db.session import SessionLocal from brewman.models.master import CostCentre, Employee, AccountBase, Account from brewman.models.validation_exception import ValidationError from brewman.models.voucher import Voucher, Journal, VoucherType -from brewman.routers import to_uuid +import brewman.schemas.master as schemas -from fastapi import APIRouter router = APIRouter() -@router.post("/new") # "Employees" -def save(request): - name = request.json_body.get("name", "").strip() - if name == "": - raise ValidationError("Name cannot be blank") - - cost_centre_id = uuid.UUID(request.json_body["costCentre"]["id"]) - designation = request.json_body.get("designation", "").strip() - +# Dependency +def get_db(): try: - salary = int(request.json_body["salary"]) - if salary < 0: - raise ValidationError("Salary must be an integer >= 0") - except (ValueError, KeyError): - raise ValidationError("Salary must be an integer >= 0") + db = SessionLocal() + yield db + finally: + db.close() - try: - service_points = round(Decimal(request.json_body["points"]), 2) - if service_points < 0: - raise ValidationError("Points must be a decimal >= 0 and < 1000") - except (ValueError, KeyError): - raise ValidationError("Points must be a decimal >= 0 and < 1000") +@router.post("/", response_model=schemas.Employee) +def save( + data: schemas.EmployeeIn, + db: Session = Depends(get_db), + user: User = Security(get_user, scopes=["employees"]), +): try: - joining_date = datetime.datetime.strptime( - request.json_body["joiningDate"], "%d-%b-%Y" + item = Employee( + name=data.name, + is_starred=data.is_starred, + is_active=data.is_active, + cost_centre_id=data.cost_centre.id_, + designation=data.designation, + salary=data.salary, + points=data.points, + joining_date=data.joining_date, + leaving_date=None if data.is_active else data.leaving_date, + ).create(db) + db.commit() + return employee_info(item.id, db) + except SQLAlchemyError as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), + ) + except Exception: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=traceback.format_exc(), ) - except (ValueError, KeyError, TypeError): - raise ValidationError("Joining Date is not a valid date") - is_starred = request.json_body["isStarred"] - is_active = request.json_body["isActive"] + +@router.put("/{id_}", response_model=schemas.Employee) +def update( + id_: uuid.UUID, + data: schemas.EmployeeIn, + db: Session = Depends(get_db), + user: User = Security(get_user, scopes=["employees"]), +): try: - if is_active: - leaving_date = None - else: - leaving_date = datetime.datetime.strptime( - request.json_body["leavingDate"], "%d-%b-%Y" + item: Employee = db.query(Employee).filter(Employee.id == id_).first() + if item.is_fixture: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"{item.name} is a fixture and cannot be edited or deleted.", ) - if leaving_date < joining_date: - raise ValidationError("Leaving Date cannot be less than Joining Date") - except (ValueError, KeyError, TypeError): - raise ValidationError("Leaving Date is not a valid date") - - item = Employee( - 0, - name, - is_starred, - is_active, - cost_centre_id, - designation, - salary, - service_points, - joining_date, - leaving_date, - ).create(request.dbsession) - transaction.commit() - return employee_info(item.id, request.dbsession) - - -@router.put("/{id}") # "Employees" -def update(request): - item = ( - request.dbsession.query(Employee) - .filter(Employee.id == uuid.UUID(request.matchdict["id"])) - .first() - ) - if item.is_fixture: - raise ValidationError( - "{0} is a fixture and cannot be edited or deleted.".format(item.name) + item.name = data.name + item.cost_centre_id = data.cost_centre.id_ + item.designation = data.designation + item.salary = data.salary + item.points = data.points + item.joining_date = data.joining_date + item.is_starred = data.is_starred + item.is_active = data.is_active + item.leaving_date = data.leaving_date + db.commit() + return employee_info(item.id, db) + except SQLAlchemyError as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), + ) + except Exception: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=traceback.format_exc(), ) - item.name = request.json_body.get("name", "").strip() - if item.name == "": - raise ValidationError("Name cannot be blank") - item.cost_centre_id = uuid.UUID(request.json_body["costCentre"]["id"]) - item.designation = request.json_body.get("designation", "").strip() - - try: - item.salary = int(request.json_body["salary"]) - if item.salary < 0: - raise ValidationError("Salary must be an integer >= 0") - except (ValueError, KeyError): - raise ValidationError("Salary must be an integer >= 0") - - try: - item.service_points = round(Decimal(request.json_body["points"]), 2) - if item.service_points < 0: - raise ValidationError("Points must be a decimal >= 0") - except (ValueError, KeyError): - raise ValidationError("Points must be a decimal >= 0") - - try: - item.joining_date = datetime.datetime.strptime( - request.json_body["joiningDate"], "%d-%b-%Y" - ) - except (ValueError, KeyError, TypeError): - raise ValidationError("Joining Date is not a valid date") - - item.is_starred = request.json_body["isStarred"] - item.is_active = request.json_body["isActive"] - try: - if item.is_active: - item.leaving_date = None - else: - item.leaving_date = datetime.datetime.strptime( - request.json_body["leavingDate"], "%d-%b-%Y" - ) - if item.leaving_date < item.joining_date: - raise ValidationError("Leaving Date cannot be less than Joining Date") - except (ValueError, KeyError, TypeError): - raise ValidationError("Leaving Date is not a valid date") - - transaction.commit() - return employee_info(item.id, request.dbsession) - - -@router.delete("/{id}") # "Employees" -def delete(request): - employee = ( - request.dbsession.query(Employee) - .filter(Employee.id == uuid.UUID(request.matchdict["id"])) - .first() - ) - can_delete, reason = employee.can_delete(request.has_permission("Advanced Delete")) +@router.delete("/{id_}") +def delete( + id_: uuid.UUID, + db: Session = Depends(get_db), + user: User = Security(get_user, scopes=["employees"]), +): + employee: Employee = db.query(Employee).filter(Employee.id == id_).first() + can_delete, reason = employee.can_delete("Advanced Delete" in user.permissions) if can_delete: - delete_with_data(employee, request.dbsession) - transaction.commit() - return employee_info(None, request.dbsession) + delete_with_data(employee, db) + db.commit() + return employee_info(None, db) else: - transaction.abort() - response = Response("Cannot delete account because {0}".format(reason)) - response.status_int = 500 - return response + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Cannot delete account because {reason}", + ) -@router.get("/{id}") # "Employees" -def show_id(request): - id_ = to_uuid(request.matchdict["id"]) - if id_ is None: - raise ValidationError("Invalid Employee") - return employee_info(id_, request.dbsession) +@router.get("/") +def show_blank( + db: Session = Depends(get_db), user: User = Security(get_user, scopes=["employees"]) +): + return employee_info(None, db) -@router.get("/new") # "Employees" -def show_blank(request): - return employee_info(None, request.dbsession) - - -@router.get("/") # "Authenticated" -async def show_list(l: bool): - list_ = ( - request.dbsession.query(Employee) +@router.get("/list") +async def show_list(db: Session = Depends(get_db), user: User = Depends(get_user)): + return [ + { + "id": item.id, + "code": item.code, + "name": item.name, + "designation": item.designation, + "salary": item.salary, + "points": item.points, + "isActive": item.is_active, + "costCentre": item.cost_centre.name, + # "url": request.route_url("employees_id", id=item.id), + "joiningDate": item.joining_date.strftime("%d-%b-%Y"), + "leavingDate": "" + if item.is_active + else item.leaving_date.strftime("%d-%b-%Y"), + } + for item in db.query(Employee) .order_by(desc(Employee.is_active)) .order_by(Account.cost_centre_id) .order_by(Employee.designation) .order_by(Employee.name) .all() - ) - accounts = [] - for item in list_: - accounts.append( - { - "id": item.id, - "code": item.code, - "name": item.name, - "designation": item.designation, - "salary": item.salary, - "points": item.service_points, - "isActive": item.is_active, - "costCentre": item.cost_centre.name, - "url": request.route_url("employees_id", id=item.id), - "joiningDate": item.joining_date.strftime("%d-%b-%Y"), - "leavingDate": "" - if item.is_active - else item.leaving_date.strftime("%d-%b-%Y"), - } - ) - return accounts + ] -@router.get("/", ) # "Authenticated" -async def show_term(q: str): - filter_ = request.GET.get("q", None) - filter_ = None if filter_ == "" else filter_ - count = request.GET.get("c", None) - count = None if count is None or count == "" else int(count) - +@router.get("/query",) # "Authenticated" +async def show_term( + q: str, + c: int = None, + db: Session = Depends(get_db), + current_user: User = Depends(get_user), +): list_ = [] - for index, item in enumerate( - AccountBase.list(10, filter_, dbsession=request.dbsession) - ): + for index, item in enumerate(AccountBase.list(10, q, dbsession=db)): list_.append( { "id": item.id, @@ -219,12 +171,21 @@ async def show_term(q: str): }, } ) - if count is not None and index == count - 1: + if c is not None and index == c - 1: break return list_ -def employee_info(id_, dbsession): +@router.get("/{id_}") +def show_id( + id_: uuid.UUID, + db: Session = Depends(get_db), + user: User = Security(get_user, scopes=["employees"]), +): + return employee_info(id_, db) + + +def employee_info(id_, db): if id_ is None: employee = { "code": "(Auto)", @@ -233,7 +194,7 @@ def employee_info(id_, dbsession): "costCentre": CostCentre.overall(), } else: - employee = dbsession.query(Employee).filter(Employee.id == id_).first() + employee = db.query(Employee).filter(Employee.id == id_).first() if employee is None: raise ValidationError("Invalid Employee") employee = { @@ -244,7 +205,7 @@ def employee_info(id_, dbsession): "isStarred": employee.is_starred, "designation": employee.designation, "salary": employee.salary, - "points": employee.service_points, + "points": employee.points, "joiningDate": employee.joining_date.strftime("%d-%b-%Y"), "leavingDate": None if employee.is_active @@ -253,16 +214,17 @@ def employee_info(id_, dbsession): "id": employee.cost_centre_id, "name": employee.cost_centre.name, }, + "isFixture": employee.is_fixture, } return employee -def delete_with_data(employee, dbsession): +def delete_with_data(employee, db): suspense_account = ( - dbsession.query(Account).filter(Account.id == Account.suspense()).first() + db.query(Account).filter(Account.id == Account.suspense()).first() ) query = ( - dbsession.query(Voucher) + db.query(Voucher) .options(joinedload_all(Voucher.journals, Journal.account, innerjoin=True)) .filter(Voucher.journals.any(Journal.account_id == employee.id)) .all() @@ -278,35 +240,33 @@ def delete_with_data(employee, dbsession): else: others = True if not others: - dbsession.delete(voucher) + db.delete(voucher) else: if sus_jnl is None: acc_jnl.account = suspense_account - voucher.narration += "\nSuspense \u20B9 {0:,.2f} is {1}".format( - acc_jnl.amount, employee.name + voucher.narration += ( + f"\nSuspense \u20B9 {acc_jnl.amount:,.2f} is {employee.name}" ) else: amount = (sus_jnl.debit * sus_jnl.amount) + ( acc_jnl.debit * acc_jnl.amount ) if acc_jnl.salary_deduction is not None: - dbsession.delete(acc_jnl.salary_deduction) - dbsession.delete(acc_jnl) + db.delete(acc_jnl.salary_deduction) + db.delete(acc_jnl) if amount == 0: - dbsession.delete(sus_jnl) + db.delete(sus_jnl) else: sus_jnl.amount = abs(amount) sus_jnl.debit = -1 if amount < 0 else 1 - voucher.narration += "\nDeleted \u20B9 {0:,.2f} of {1}".format( - acc_jnl.amount * acc_jnl.debit, employee.name - ) + voucher.narration += f"\nDeleted \u20B9 {acc_jnl.amount * acc_jnl.debit:,.2f} of {employee.name}" if voucher.type in ( VoucherType.by_name("Payment").id, VoucherType.by_name("Receipt").id, ): voucher.type = VoucherType.by_name("Journal") for fingerprint in employee.fingerprints: - dbsession.delete(fingerprint) + db.delete(fingerprint) for attendance in employee.attendances: - dbsession.delete(attendance) - dbsession.delete(employee) + db.delete(attendance) + db.delete(employee) diff --git a/brewman/routers/services/voucher/__init__.py b/brewman/routers/services/voucher/__init__.py index 6b9ce8f5..b9976493 100644 --- a/brewman/routers/services/voucher/__init__.py +++ b/brewman/routers/services/voucher/__init__.py @@ -442,7 +442,7 @@ def service_charge_employees(date, dbsession): "designation": employee.designation, "department": employee.cost_centre.name, "daysWorked": att, - "points": employee.service_points, + "points": employee.points, } ) diff --git a/brewman/schemas/master.py b/brewman/schemas/master.py index 146a3377..e86c52aa 100644 --- a/brewman/schemas/master.py +++ b/brewman/schemas/master.py @@ -1,8 +1,14 @@ import uuid -from datetime import date +from typing import Optional +from datetime import date, datetime from decimal import Decimal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, validator + + +def to_camel(string: str) -> str: + first, *others = string.split('_') + return ''.join([first] + [word.capitalize() for word in others]) class Product(BaseModel): @@ -53,36 +59,87 @@ class ProductGroup(BaseModel): is_fixture: bool -class CostCentreSaveUpdate(BaseModel): - name: str +class CostCentreLink(BaseModel): + id_: uuid.UUID + + class Config: + fields = {'id_': 'id'} -class CostCentre(CostCentreSaveUpdate): - id_: uuid.UUID = Field(alias="id") - is_fixture: bool = Field(alias="isFixture") +class CostCentreIn(BaseModel): + name: str = Field(..., min_length=1) -class AccountSaveUpdate(BaseModel): - name: str - type: int +class CostCentre(CostCentreIn): + id_: uuid.UUID + is_fixture: bool + + class Config: + fields = {'id_': 'id'} + anystr_strip_whitespace = True + alias_generator = to_camel + + +class AccountBase(BaseModel): + name: str = Field(..., min_length=1) is_starred: bool is_active: bool + cost_centre: CostCentreLink + + class Config: + fields = {'id_': 'id'} + anystr_strip_whitespace = True + alias_generator = to_camel + + +class AccountIn(AccountBase): + type: int is_reconcilable: bool - cost_centre: CostCentre -class Account(AccountSaveUpdate): - id_: uuid.UUID = Field(alias="id") +class Account(AccountIn): + id_: uuid.UUID code: int is_fixture: bool -class Employee(Account): +class EmployeeIn(AccountBase): designation: str - salary: int - service_points: Decimal + salary: int = Field(ge=0) + points: Decimal = Field(ge=0, lt=1000, multiple_of=0.01) joining_date: date - leaving_date: date + leaving_date: Optional[date] + + @validator("joining_date", pre=True) + def parse_joining_date(cls, value): + return datetime.strptime( + value, + "%d-%b-%Y" + ).date() + + @validator("leaving_date", pre=True) + def parse_leaving_date(cls, value): + if value is None or value == "": + return None + else: + return datetime.strptime( + value, + "%d-%b-%Y" + ).date() + + @validator('leaving_date') + def leaving_date_more_than_joining_date(cls, v, values, **kwargs): + if values['is_active']: + return None + if v < values['joining_date']: + raise ValueError('Leaving Date cannot be less than Joining Date') + return v + + +class Employee(EmployeeIn): + id_: uuid.UUID + code: int + is_fixture: bool class DbSetting(BaseModel):