450 lines
16 KiB
Python
450 lines
16 KiB
Python
import uuid
|
|
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from typing import Optional
|
|
|
|
import brewman.schemas.voucher as output
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Security, status
|
|
from sqlalchemy import func, or_
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ..core.security import get_current_active_user as get_user
|
|
from ..core.session import get_first_day
|
|
from ..db.session import SessionLocal
|
|
from ..models.master import Account, AccountBase, AttendanceType, CostCentre, Employee
|
|
from ..models.voucher import (
|
|
Attendance,
|
|
DbImage,
|
|
Inventory,
|
|
Journal,
|
|
Voucher,
|
|
VoucherType,
|
|
)
|
|
from ..routers import get_lock_info
|
|
from ..schemas.auth import UserToken
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# Dependency
|
|
def get_db() -> Session:
|
|
try:
|
|
db = SessionLocal()
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/post-voucher/{id_}", response_model=output.Voucher)
|
|
def post_voucher(
|
|
id_: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
user: UserToken = Security(get_user, scopes=["post-vouchers"]),
|
|
):
|
|
try:
|
|
voucher: Voucher = db.query(Voucher).filter(Voucher.id == id_).first()
|
|
check_voucher_lock_info(voucher.date, voucher.date, db)
|
|
voucher.posted = True
|
|
voucher.poster_id = user.id_
|
|
db.commit()
|
|
return voucher_info(voucher, 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
|
|
|
|
|
|
def check_delete_permissions(voucher: Voucher, user: UserToken):
|
|
voucher_type = VoucherType.by_id(voucher.type).name.replace(" ", "-").lower()
|
|
if voucher_type in ["payment", "receipt"]:
|
|
voucher_type = "journal"
|
|
if voucher.posted and "edit-posted-vouchers" not in user.permissions:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You are not allowed to edit posted vouchers",
|
|
)
|
|
elif (
|
|
voucher.user_id != user.id_
|
|
and "edit-other-user's-vouchers" not in user.permissions
|
|
):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You are not allowed to edit other user's vouchers",
|
|
)
|
|
elif voucher_type not in user.permissions:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"You are not allowed ({VoucherType.by_id(voucher.type).name}) vouchers",
|
|
)
|
|
|
|
|
|
@router.delete("/delete/{id_}")
|
|
def delete_voucher(
|
|
id_: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
user: UserToken = Security(get_user),
|
|
):
|
|
voucher: Voucher = db.query(Voucher).filter(Voucher.id == id_).first()
|
|
images = db.query(DbImage).filter(DbImage.resource_id == voucher.id).all()
|
|
check_delete_permissions(voucher, user)
|
|
check_voucher_lock_info(None, voucher.date, db)
|
|
json_voucher = voucher_info(voucher, db)
|
|
batches_to_delete = []
|
|
if voucher.type == VoucherType.by_name("Issue").id:
|
|
for item in voucher.journals:
|
|
if item.debit == 1:
|
|
destination = item.cost_centre_id
|
|
else:
|
|
source = item.cost_centre_id
|
|
if source == CostCentre.cost_centre_purchase():
|
|
batch_consumed = True
|
|
elif destination == CostCentre.cost_centre_purchase():
|
|
batch_consumed = False
|
|
else:
|
|
batch_consumed = None
|
|
|
|
if batch_consumed is None:
|
|
pass
|
|
elif batch_consumed:
|
|
for item in voucher.inventories:
|
|
item.batch.quantity_remaining += item.quantity
|
|
else:
|
|
for item in voucher.inventories:
|
|
if item.batch.quantity_remaining < item.quantity:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_423_LOCKED,
|
|
detail=f"Only {item.batch.quantity_remaining} of {item.product.name} remaining.\n So it cannot be deleted",
|
|
)
|
|
item.batch.quantity_remaining -= item.quantity
|
|
elif voucher.type == VoucherType.by_name("Purchase").id:
|
|
for item in voucher.inventories:
|
|
uses = (
|
|
db.query(func.count(Inventory.id))
|
|
.filter(Inventory.batch_id == item.batch.id)
|
|
.filter(Inventory.id != item.id)
|
|
.scalar()
|
|
)
|
|
if uses > 0:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_423_LOCKED,
|
|
detail=f"{item.product.name} has been issued and cannot be deleted",
|
|
)
|
|
batches_to_delete.append(item.batch)
|
|
elif voucher.type == VoucherType.by_name("Purchase Return").id:
|
|
for item in voucher.inventories:
|
|
item.batch.quantity_remaining += item.quantity
|
|
for b in batches_to_delete:
|
|
db.delete(b)
|
|
db.delete(voucher)
|
|
for image in images:
|
|
db.delete(image)
|
|
db.commit()
|
|
return blank_voucher(info=json_voucher, db=db)
|
|
|
|
|
|
def voucher_info(voucher, db):
|
|
json_voucher = {
|
|
"id": voucher.id,
|
|
"date": voucher.date.strftime("%d-%b-%Y"),
|
|
"isStarred": voucher.is_starred,
|
|
"type": VoucherType.by_id(voucher.type).name,
|
|
"posted": voucher.posted,
|
|
"narration": voucher.narration,
|
|
"journals": [],
|
|
"inventories": [],
|
|
"employeeBenefits": [],
|
|
"incentives": [],
|
|
"incentive": 0,
|
|
"files": [],
|
|
"creationDate": voucher.creation_date.strftime("%d-%b-%Y %H:%M"),
|
|
"lastEditDate": voucher.last_edit_date.strftime("%d-%b-%Y %H:%M"),
|
|
"user": {"id": voucher.user.id, "name": voucher.user.name},
|
|
"poster": voucher.poster.name if voucher.posted else "",
|
|
}
|
|
if voucher.reconcile_date is not None:
|
|
json_voucher["reconcileDate"] = voucher.reconcile_date.strftime("%d-%b-%Y")
|
|
if voucher.type == 2: # "Purchase"
|
|
item = [j for j in voucher.journals if j.debit == -1][0]
|
|
json_voucher["vendor"] = {"id": item.account.id, "name": item.account.name}
|
|
elif voucher.type == 3: # "Issue"
|
|
item = [j for j in voucher.journals if j.debit == -1][0]
|
|
json_voucher["source"] = {"id": item.cost_centre_id, "name": ""}
|
|
item = [j for j in voucher.journals if j.debit == 1][0]
|
|
json_voucher["destination"] = {"id": item.cost_centre_id, "name": ""}
|
|
else:
|
|
for item in voucher.journals:
|
|
json_voucher["journals"].append(
|
|
{
|
|
"id": item.id,
|
|
"debit": item.debit,
|
|
"amount": item.amount,
|
|
"account": {"id": item.account.id, "name": item.account.name},
|
|
"costCentre": {"id": item.cost_centre_id},
|
|
}
|
|
)
|
|
for item in voucher.employee_benefits:
|
|
json_voucher["employeeBenefits"].append(
|
|
{
|
|
"grossSalary": item.gross_salary,
|
|
"daysWorked": item.days_worked,
|
|
"esiEmployee": item.esi_ee,
|
|
"pfEmployee": item.pf_ee,
|
|
"esiEmployer": item.esi_er,
|
|
"pfEmployer": item.pf_er,
|
|
"employee": {
|
|
"id": item.journal.account.id,
|
|
"name": item.journal.account.name,
|
|
"designation": item.journal.account.designation,
|
|
"costCentre": {
|
|
"id": item.journal.account.cost_centre.id,
|
|
"name": item.journal.account.cost_centre.name,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
for item in voucher.incentives:
|
|
employee = (
|
|
db.query(Employee).filter(Employee.id == item.journal.account_id).first()
|
|
)
|
|
json_voucher["incentives"].append(
|
|
{
|
|
"employeeId": item.journal.account_id,
|
|
"name": item.journal.account.name,
|
|
"designation": employee.designation,
|
|
"department": item.journal.account.cost_centre.name,
|
|
"daysWorked": item.days_worked,
|
|
"points": item.points,
|
|
}
|
|
)
|
|
if len(json_voucher["incentives"]) > 0:
|
|
json_voucher["incentive"] = next(
|
|
x.amount for x in voucher.journals if x.account_id == Account.incentive_id()
|
|
)
|
|
for item in voucher.inventories:
|
|
text = f"{item.product.name} ({item.product.units}) {item.batch.quantity_remaining:.2f}@{item.batch.rate:.2f} from {item.batch.name.strftime('%d-%b-%Y')}"
|
|
json_voucher["inventories"].append(
|
|
{
|
|
"id": item.id,
|
|
"quantity": item.quantity,
|
|
"rate": item.rate,
|
|
"tax": item.tax,
|
|
"discount": item.discount,
|
|
"amount": item.amount,
|
|
"product": {
|
|
"id": item.product.id,
|
|
"name": item.product.full_name,
|
|
"units": item.product.units,
|
|
"price": item.rate,
|
|
},
|
|
"batch": {
|
|
"id": item.batch.id,
|
|
"name": text,
|
|
"quantityRemaining": item.batch.quantity_remaining,
|
|
"tax": item.batch.tax,
|
|
"discount": item.batch.discount,
|
|
"rate": item.batch.rate,
|
|
"product": {
|
|
"id": item.batch.product.id,
|
|
"name": item.batch.product.full_name,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
images = db.query(DbImage).filter(DbImage.resource_id == voucher.id).all()
|
|
for image in images:
|
|
json_voucher["files"].append(
|
|
{
|
|
"id": image.id,
|
|
"resized": f"/db-image/{image.id}/resized",
|
|
"thumbnail": f"/db-image/{image.id}/thumbnail",
|
|
}
|
|
)
|
|
return json_voucher
|
|
|
|
|
|
def blank_voucher(info, db):
|
|
if "type" not in info:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Voucher Type is null",
|
|
)
|
|
type_ = info["type"]
|
|
if "date" not in info:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Date cannot be null",
|
|
)
|
|
json_voucher = {
|
|
"type": type_,
|
|
"date": info["date"],
|
|
"isStarred": False,
|
|
"posted": False,
|
|
"narration": "",
|
|
"journals": [],
|
|
"inventories": [],
|
|
"employeeBenefits": [],
|
|
}
|
|
if type_ == "Journal":
|
|
pass
|
|
elif type_ == "Payment":
|
|
account = None
|
|
if info and "account" in info and info["account"]:
|
|
account = (
|
|
db.query(AccountBase)
|
|
.filter(AccountBase.id == uuid.UUID(info["account"]))
|
|
.first()
|
|
)
|
|
if account is not None:
|
|
account = {"id": account.id, "name": account.name}
|
|
else:
|
|
account = AccountBase.cash_in_hand()
|
|
json_voucher["journals"].append({"account": account, "amount": 0, "debit": -1})
|
|
elif type_ == "Receipt":
|
|
account = None
|
|
if info and "account" in info and info["account"]:
|
|
account = (
|
|
db.query(AccountBase)
|
|
.filter(AccountBase.id == uuid.UUID(info["account"]))
|
|
.first()
|
|
)
|
|
if account is not None:
|
|
account = {"id": account.id, "name": account.name}
|
|
else:
|
|
account = AccountBase.cash_in_hand()
|
|
json_voucher["journals"].append({"account": account, "amount": 0, "debit": 1})
|
|
elif type_ == "Purchase":
|
|
json_voucher["vendor"] = AccountBase.local_purchase()
|
|
|
|
elif type_ == "Purchase Return":
|
|
json_voucher["journals"].append(
|
|
{"account": AccountBase.local_purchase(), "amount": 0, "debit": 1}
|
|
)
|
|
elif type_ == "Issue":
|
|
if "source" in info:
|
|
json_voucher["source"] = {"id": info["source"]}
|
|
else:
|
|
json_voucher["source"] = {"id": CostCentre.cost_centre_purchase()}
|
|
if "destination" in info:
|
|
json_voucher["destination"] = {"id": info["destination"]}
|
|
else:
|
|
json_voucher["destination"] = {"id": CostCentre.cost_centre_kitchen()}
|
|
elif type_ == "Employee Benefit":
|
|
json_voucher["employeeBenefits"] = []
|
|
elif type_ == "Incentive":
|
|
json_voucher["incentives"], json_voucher["incentive"] = incentive_employees(
|
|
info["date"], db
|
|
)
|
|
else:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f'Voucher of type "{type_}" does not exist.',
|
|
)
|
|
json_voucher["files"] = []
|
|
return json_voucher
|
|
|
|
|
|
def incentive_employees(date_, db: Session):
|
|
date_ = datetime.strptime(date_, "%d-%b-%Y")
|
|
start_date = get_first_day(date_)
|
|
finish_date = date_
|
|
details = []
|
|
employees = (
|
|
db.query(Employee)
|
|
.filter(Employee.joining_date <= finish_date)
|
|
.filter(or_(Employee.is_active, Employee.leaving_date >= start_date))
|
|
.order_by(Employee.cost_centre_id)
|
|
.order_by(Employee.designation)
|
|
.order_by(Employee.name)
|
|
.all()
|
|
)
|
|
for employee in employees:
|
|
att = (
|
|
db.query(Attendance)
|
|
.filter(Attendance.employee_id == employee.id)
|
|
.filter(Attendance.date >= start_date)
|
|
.filter(Attendance.date <= finish_date)
|
|
.filter(Attendance.is_valid == True)
|
|
.all()
|
|
)
|
|
att = sum(map(lambda x: AttendanceType.by_id(x.attendance_type).value, att))
|
|
details.append(
|
|
{
|
|
"employeeId": employee.id,
|
|
"name": employee.name,
|
|
"designation": employee.designation,
|
|
"department": employee.cost_centre.name,
|
|
"daysWorked": att,
|
|
"points": employee.points,
|
|
}
|
|
)
|
|
|
|
amount = (
|
|
db.query(func.sum(Journal.amount * Journal.debit))
|
|
.join(Journal.voucher)
|
|
.filter(Voucher.date < finish_date)
|
|
.filter(Voucher.type != VoucherType.by_name("Issue").id)
|
|
.filter(Journal.account_id == Account.incentive_id())
|
|
.scalar()
|
|
)
|
|
amount = 0 if amount is None else amount * Decimal(0.9) * -1
|
|
return details, amount
|
|
|
|
|
|
def check_voucher_lock_info(voucher_date: Optional[date], data_date: date, db: Session):
|
|
start, finish = get_lock_info(db)
|
|
if (
|
|
start is not None
|
|
and start > data_date
|
|
or voucher_date is not None
|
|
and start > voucher_date
|
|
):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_423_LOCKED,
|
|
detail=f"Vouchers before {start.strftime('%d-%b-%Y')} have been locked.",
|
|
)
|
|
elif (
|
|
finish is not None
|
|
and finish < data_date
|
|
or voucher_date is not None
|
|
and finish < voucher_date
|
|
):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_423_LOCKED,
|
|
detail=f"Vouchers after {finish.strftime('%d-%b-%Y')} have been locked.",
|
|
)
|
|
# elif start is not None and (start > data.date_ or start > item.date):
|
|
# raise HTTPException(
|
|
# status_code=status.HTTP_403_FORBIDDEN,
|
|
# detail=f"Vouchers before {start.strftime('%d-%b-%Y')} have been locked.",
|
|
# )
|
|
# elif finish is not None and (finish < data.date_ or finish < item.date):
|
|
# raise HTTPException(
|
|
# status_code=status.HTTP_403_FORBIDDEN,
|
|
# detail=f"Vouchers after {finish.strftime('%d-%b-%Y')} have been locked.",
|
|
# )
|
|
|
|
|
|
def check_voucher_edit_allowed(voucher: Voucher, user: UserToken):
|
|
if voucher.posted and "edit-posted-vouchers" not in user.permissions:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You are not allowed to edit posted vouchers",
|
|
)
|
|
elif (
|
|
voucher.user_id != user.id_
|
|
and "edit-other-user's-vouchers" not in user.permissions
|
|
):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="You are not allowed to edit other user's vouchers",
|
|
)
|