diff --git a/brewman/main.py b/brewman/main.py index 83dddd00..9915fc0c 100644 --- a/brewman/main.py +++ b/brewman/main.py @@ -16,6 +16,7 @@ from .routers import ( product_group, recipe, login, + journal ) from .routers.auth import client, user, role from .routers.reports import ( @@ -65,8 +66,9 @@ app.include_router( employee_attendance.router, prefix="/api/employee-attendance", tags=["attendance"] ) app.include_router( - attendance_report.router, prefix="/api/attendance-report", tags=["attendance"] + attendance_report.router, prefix="/attendance-report", tags=["attendance"] ) + app.include_router( cost_centre.router, prefix="/api/cost-centres", tags=["cost-centres"] ) @@ -100,6 +102,9 @@ app.include_router(unposted.router, prefix="/api/unposted", tags=["reports"]) app.include_router(issue_grid.router, prefix="/api/issue-grid", tags=["vouchers"]) app.include_router(batch.router, prefix="/api/batch", tags=["vouchers"]) +app.include_router(journal.router, prefix="/api/journal", tags=["vouchers"]) +app.include_router(journal.router, prefix="/api/payment", tags=["vouchers"]) +app.include_router(journal.router, prefix="/api/receipt", tags=["vouchers"]) def init(): diff --git a/brewman/models/operations.py b/brewman/models/operations.py index f4636ba6..821f98b3 100644 --- a/brewman/models/operations.py +++ b/brewman/models/operations.py @@ -154,39 +154,4 @@ def batch_valid(voucher): def is_date_allowed(voucher): - pass - - -def journals_valid(voucher): - if len(voucher.journals) < 2: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Not enough journals", - ) - signed_amount = 0 - for item in voucher.journals: - if item.amount < 0: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Amounts cannot be negative", - ) - signed_amount += item.signed_amount - if not signed_amount == 0: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Journal amounts do no match", - ) - is_distinct_journal(voucher.journals) - - -def is_distinct_journal(journals): - found = set() - for item in journals: - item_hash = hash(item.account_id) ^ hash(item.cost_centre_id) - if item_hash in found: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Duplicate journals", - ) - else: - found.add(item_hash) + pass \ No newline at end of file diff --git a/brewman/routers/__init__.py b/brewman/routers/__init__.py index c415992d..179b714f 100644 --- a/brewman/routers/__init__.py +++ b/brewman/routers/__init__.py @@ -3,6 +3,7 @@ import uuid from datetime import date, datetime, timedelta, time from decimal import Decimal from io import BytesIO +from typing import Optional from brewman.models.master import DbSetting from brewman.models.voucher import DbImage @@ -28,7 +29,9 @@ def db_image(request): return response -def get_lock_info(dbsession): +def get_lock_info(dbsession) -> (Optional[date], Optional[date]): + start: Optional[date] + finish: Optional[date] data = dbsession.query(DbSetting).filter(DbSetting.name == "Lock Info").first() if data is None: return None, None @@ -37,20 +40,16 @@ def get_lock_info(dbsession): if not data["Start"]["Locked"]: start = None elif data["Start"]["Rolling"]: - start = datetime.combine(date.today(), time()) - timedelta( - days=data["Start"]["Days"] - ) + start: date = date.today() - timedelta(days=data["Start"]["Days"]) else: - start = data["Start"]["Date"] + start = data["Start"]["Date"].date() if not data["Finish"]["Locked"]: finish = None elif data["Finish"]["Rolling"]: - finish = datetime.combine(date.today(), time()) + timedelta( - days=data["Finish"]["Days"] - ) + finish = date.today() + timedelta(days=data["Finish"]["Days"]) else: - finish = data["Finish"]["Date"] + finish = data["Finish"]["Date"].date() return start, finish diff --git a/brewman/routers/account.py b/brewman/routers/account.py index c79a5e17..0d23f4cd 100644 --- a/brewman/routers/account.py +++ b/brewman/routers/account.py @@ -166,7 +166,7 @@ async def show_balance( db: Session = Depends(get_db), user: UserToken = Depends(get_user), ): - date = None if d is None or d == "" else datetime.datetime.strptime(d, "%d-%b-%Y") + date = None if d is None or d == "" else datetime.strptime(d, "%d-%b-%Y") return {"date": balance(id_, date, db), "total": balance(id_, None, db)} diff --git a/brewman/routers/attendance_report.py b/brewman/routers/attendance_report.py index fd05e335..f44c50c3 100644 --- a/brewman/routers/attendance_report.py +++ b/brewman/routers/attendance_report.py @@ -1,33 +1,56 @@ -import datetime +import csv +from datetime import datetime, date +import io + +from fastapi import APIRouter, HTTPException, status, Depends, Security, Request +from fastapi.responses import StreamingResponse from sqlalchemy import or_ +from sqlalchemy.orm import Session +from ..schemas.auth import UserToken +from ..core.security import get_current_active_user as get_user from ..models.master import AttendanceType, Employee from ..models.voucher import Attendance from .attendance import date_range -from ..core.session import get_start_date, get_finish_date - -from fastapi import APIRouter +from ..db.session import SessionLocal router = APIRouter() -@router.get("/") # "Attendance", renderer="csv" -def get_report(request): - start_date = request.GET.get("StartDate", get_start_date(request)) - finish_date = request.GET.get("FinishDate", get_finish_date(request)) - start_date = datetime.datetime.strptime(start_date, "%d-%b-%Y") - finish_date = datetime.datetime.strptime(finish_date, "%d-%b-%Y") - header, report = attendance_record(start_date, finish_date, request.dbsession) - return {"header": header, "rows": report, "filename": "Attendance Record.csv"} + +# Dependency +def get_db() -> Session: + try: + db = SessionLocal() + yield db + finally: + db.close() -def attendance_record(start_date, finish_date, dbsession): - report = [] - header = ["Code", "Name", "Designation", "Department", "Salary", "SC Points"] - for date in date_range(start_date, finish_date, inclusive=True): - header.append(date.strftime("%d-%b")) +@router.get("") +def get_report( + s: str = None, + f: str = None, + db: Session = Depends(get_db), + # user: UserToken = Security(get_user, scopes=["attendance"]) ## removed as jwt headers are a pain in the ass +): + try: + output = io.StringIO() + attendance_record(datetime.strptime(s, "%d-%b-%Y"), datetime.strptime(f, "%d-%b-%Y"), output, db) + headers = {"Content-Disposition": "attachment; filename = 'attendance-record.csv'"} + output.seek(0) + return StreamingResponse(output, media_type="text/csv", headers=headers) + finally: + pass + # output.close() + + +def attendance_record(start_date: date, finish_date: date, output, db: Session): + header = ["Code", "Name", "Designation", "Department", "Salary", "Points"] + for date_ in date_range(start_date, finish_date, inclusive=True): + header.append(date_.strftime("%d-%b")) not_set = AttendanceType.by_id(0) employees = ( - dbsession.query(Employee) + 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) @@ -35,6 +58,8 @@ def attendance_record(start_date, finish_date, dbsession): .order_by(Employee.name) .all() ) + writer = csv.writer(output) + writer.writerow(header) for employee in employees: row_display = [ employee.code, @@ -45,16 +70,16 @@ def attendance_record(start_date, finish_date, dbsession): employee.points, ] row_value = ["", "", "", "", employee.salary, employee.points] - for date in date_range(start_date, finish_date, inclusive=True): + for date_ in date_range(start_date, finish_date, inclusive=True): att = ( - dbsession.query(Attendance) + db.query(Attendance) .filter(Attendance.employee_id == employee.id) - .filter(Attendance.date == date) + .filter(Attendance.date == date_) .filter(Attendance.is_valid == True) .first() ) att = not_set if att is None else AttendanceType.by_id(att.attendance_type) row_display.append(att.name) row_value.append(att.value) - report.extend([tuple(row_display), tuple(row_value)]) - return header, report + writer.writerow(row_display) + writer.writerow(row_value) diff --git a/brewman/routers/deduction.py b/brewman/routers/deduction.py new file mode 100644 index 00000000..e69de29b diff --git a/brewman/routers/incentive.py b/brewman/routers/incentive.py new file mode 100644 index 00000000..e69de29b diff --git a/brewman/routers/issue.py b/brewman/routers/issue.py new file mode 100644 index 00000000..e69de29b diff --git a/brewman/routers/journal.py b/brewman/routers/journal.py new file mode 100644 index 00000000..5f6cf8fb --- /dev/null +++ b/brewman/routers/journal.py @@ -0,0 +1,198 @@ +import traceback +import uuid +from typing import List +from datetime import datetime +from fastapi import APIRouter, HTTPException, status, Depends, Security, UploadFile, File, Request, Form +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from .voucher import voucher_info, check_voucher_lock_info, check_voucher_edit_allowed, blank_voucher +from ..core.session import set_date, get_date +from ..models import AccountBase +from ..schemas.auth import UserToken +from ..core.security import get_current_active_user as get_user +from ..db.session import SessionLocal +from ..models.voucher import Voucher, VoucherType, Journal +import brewman.schemas.voucher as schemas + +router = APIRouter() + + +# Dependency +def get_db() -> Session: + try: + db = SessionLocal() + yield db + finally: + db.close() + + +@router.post("", response_model=schemas.Voucher) +def save_route( + request: Request, + data: schemas.Voucher = Depends(schemas.Voucher.load_form), + db: Session = Depends(get_db), + i: List[UploadFile] = File(None), + t: List[UploadFile] = File(None), + user: UserToken = Security(get_user, scopes=["journal"]), +): + try: + i = i or [] + t = t or [] + item: Voucher = save(data, i + t, user, db) + db.commit() + set_date(data.date_.strftime("%d-%b-%Y"), request.session) + info = voucher_info(item, db) + return info + 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(), + ) + + +def save(data: schemas.Voucher, files: List[UploadFile], user: UserToken, db: Session) -> Voucher: + check_voucher_lock_info(None, data.date_, db) + voucher = Voucher( + date=data.date_, + narration=data.narration, + is_starred=data.is_starred, + user_id=user.id_, + type=VoucherType.by_name(data.type_), + ) + db.add(voucher) + for item in data.journals: + account: AccountBase = db.query(AccountBase).filter(AccountBase.id == item.account.id_).first() + journal = Journal( + id=item.id_, + amount=item.amount, + debit=item.debit, + account_id=account.id, + cost_centre_id=account.cost_centre_id, + ) + voucher.journals.append(journal) + db.add(journal) + # for key, value in files.items(): + # db.add(DbImage(voucher.id, "voucher", value["f"], value["t"])) + return voucher + + +@router.put("/{id_}") +def update_route( + id_: uuid.UUID, + request: Request, + data: schemas.Voucher, + db: Session = Depends(get_db), + i: List[UploadFile] = File(None), + t: List[UploadFile] = File(None), + user: UserToken = Security(get_user, scopes=["journal"]), +): + try: + i = i or [] + t = t or [] + item: Voucher = update(id_, data, i + t, user, db) + db.commit() + set_date(data.date_, request.session) + # item: Voucher = db.query(Voucher).filter(Voucher.id == item.id).first() + return voucher_info(item, 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(), + ) + + +@router.get("/{id_}") +def get_id( + id_: uuid.UUID, + db: Session = Depends(get_db), + user: UserToken = Security(get_user, scopes=["journal"]), +): + try: + item: Voucher = db.query(Voucher).filter(Voucher.id == id_).first() + return voucher_info(item, 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(), + ) + + +def update(id_: uuid.UUID, data: schemas.Voucher, files: List[UploadFile], user: UserToken, db: Session) -> Voucher: + voucher: Voucher = db.query(Voucher).filter(Voucher.id == id_).first() + check_voucher_lock_info(voucher.date, data.date_, db) + check_voucher_edit_allowed(voucher, user) + voucher.date = data.date_ + voucher.is_starred = data.is_starred + voucher.narration = data.narration + voucher.user_id = user.id_ + voucher.posted = False + voucher.last_edit_date = datetime.utcnow() + + for i in range(len(voucher.journals), 0, -1): + item = voucher.journals[i - 1] + found = False + for j in range(len(data.journals), 0, -1): + new_item = data.journals[j - 1] + if new_item.id_ is not None and item.id == new_item.id_: + account = db.query(AccountBase).filter(AccountBase.id == new_item.account.id_).first() + found = True + item.debit = new_item.debit + item.amount = new_item.amount + item.account_id = account.id + item.cost_centre_id = account.cost_centre_id + data.journals.remove(new_item) + break + if not found: + voucher.journals.remove(item) + for new_item in data.journals: + account = db.query(AccountBase).filter(AccountBase.id == new_item.account.id_).first() + journal = Journal( + id=None, + amount=new_item.amount, + debit=new_item.debit, + account_id=account.id, + cost_centre_id=account.cost_centre_id, + ) + db.add(journal) + voucher.journals.append(journal) + + # old_files = [ + # uuid.UUID(f["id"]) for f in json["files"] if "id" in f and f["id"] is not None + # ] + # images = dbsession.query(DbImage).filter(DbImage.resource_id == voucher.id).all() + # for image in [i for i in images if i.id not in old_files]: + # dbsession.delete(image) + # for key, value in files.items(): + # dbsession.add(DbImage(voucher.id, "voucher", value["f"], value["t"])) return voucher + + +@router.get("") +def show_blank( + request: Request, + db: Session = Depends(get_db), + user: UserToken = Security(get_user, scopes=["journal"]), +): + additional_info = {"date": get_date(request.session), "type": "Journal"} + return blank_voucher(additional_info, db) diff --git a/brewman/routers/payment.py b/brewman/routers/payment.py new file mode 100644 index 00000000..e69de29b diff --git a/brewman/routers/purchase.py b/brewman/routers/purchase.py new file mode 100644 index 00000000..8d0bdf0c --- /dev/null +++ b/brewman/routers/purchase.py @@ -0,0 +1,168 @@ +import traceback +import uuid +from typing import List + +from fastapi import APIRouter, HTTPException, status, Depends, Security, UploadFile, File, Request +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from .voucher import journal_create_voucher, purchase_create_voucher, issue_create_voucher, incentive_create_voucher, \ + salary_deduction_create_voucher, voucher_info, check_voucher_lock_info, check_voucher_edit_allowed +from .voucher.purchase_return import purchase_return_create_voucher +from ..core.session import set_date +from ..models import Product, AccountBase +from ..schemas.auth import UserToken +from ..core.security import get_current_active_user as get_user +from ..db.session import SessionLocal +from ..models.voucher import Voucher, VoucherType, Batch, Inventory, Journal +import brewman.schemas.voucher as schemas + +router = APIRouter() + + +# Dependency +def get_db() -> Session: + try: + db = SessionLocal() + yield db + finally: + db.close() + + +@router.post("", response_model=schemas.Voucher) +def save_route( + request: Request, + data: schemas.Voucher = Depends(schemas.Voucher.load_form), + db: Session = Depends(get_db), + i: List[UploadFile] = File(None), + t: List[UploadFile] = File(None), + user: UserToken = Security(get_user, scopes=["purchase"]), +): + try: + i = i or [] + t = t or [] + item: Voucher = save(data, i + t, user, db) + db.commit() + set_date(data.date_.strftime("%d-%b-%Y"), request.session) + info = voucher_info(item, db) + return info + 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(), + ) + + +def save(data: schemas.Voucher, files: List[UploadFile], user: UserToken, db: Session) -> Voucher: + check_voucher_lock_info(None, data.date_, db) + voucher = Voucher( + date=data.date_, + narration=data.narration, + is_starred=data.is_starred, + user_id=user.id_, + type=VoucherType.by_name(data.type_), + ) + db.add(voucher) + journals = {} + for item in data.inventories: + product: Product = db.query(Product).filter(Product.id == item.product.id_).first() + batch = Batch(name=data.date_, product_id=product.id, quantity_remaining=item.quantity, rate=item.rate, tax=item.tax,discount=item.discount) + db.add(batch) + inventory = Inventory(id=item.id_, product_id=product.id, batch=batch, quantity=item.quantity, rate=item.rate,tax=item.tax, discount=item.discount) + product.price = item.rate + voucher.inventories.append(inventory) + db.add(inventory) + if product.account_id not in journals: + journals[product.account_id] = {"account_id":product.account_id, "cost_centre_id": product.account.cost_centre_id, "debit": 1, "amount": inventory.amount} + else: + journals[product.account_id]["amount"] += inventory.amount + supplier = db.query(AccountBase).filter(AccountBase.id == data.journals[0].account.id_).first() + + journals[supplier.id] = {"account_id":supplier.id, "cost_centre_id": supplier.cost_centre_id, "debit":-1, "amount": sum(round(x["amount"], 2) for x in journals)} + for item in journals: + journal = Journal( + debit=item["debit"], + account_id=item["account_id"], + cost_centre_id=item["cost_centre_id"], + amount=round(item["amount"], 2), + ) + + voucher.journals.append(item) + db.add(item) + + journals_valid(voucher) + inventory_valid(voucher) + for key, value in files.items(): + dbsession.add(DbImage(voucher.id, "voucher", value["f"], value["t"])) + return voucher + + +def purchase_create_journals(account_id: uuid.UUID, db: Session): + amount = 0 + for item in inventories: + product = db.query(Product).filter(Product.id == item.product_id).first() + account = product.account + item_amount = round(item.amount, 2) + amount += item_amount + if account.id in journals: + journals[account.id].amount += item_amount + else: + journals[other_account.id] = Journal( + debit=-1, + cost_centre_id=other_account.cost_centre_id, + account_id=other_account.id, + amount=amount, + ) + return list(journals.values()) + +@router.get("/{id_}") +def update_route( + id_: uuid.UUID, + request: Request, + data: schemas.Voucher, + db: Session = Depends(get_db), + files: List[UploadFile] = File(...), + user: UserToken = Security(get_user, scopes=["journal"]), +): + try: + item: Voucher = update(id_, data, files, user, db) + db.commit() + set_date(request.session, data.date_) + # item: Voucher = db.query(Voucher).filter(Voucher.id == item.id).first() + return voucher_info(item, 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(), + ) + + +def update(id_: uuid.UUID, data: schemas.Voucher, files: List[UploadFile], user: UserToken, db: Session) -> Voucher: + item: Voucher = db.query(Voucher).filter(Voucher.id == id_).first() + check_voucher_lock_info(item.date, data.date_, db) + check_voucher_edit_allowed(item, user) + if data.type_ in ["Purchase"]: + voucher = purchase_update_voucher(item, data, files, user, db) + elif data.type_ in ["Purchase Return"]: + voucher = purchase_return_update_voucher(item, data, files, user, db) + elif data.type_ in ["Issue"]: + voucher = issue_update_voucher(item, data, files, user, db) + elif data.type_ in ["Salary Deduction"]: + voucher = salary_deduction_update_voucher(item, data, files, user, db) + elif data.type_ in ["Incentive"]: + voucher = incentive_update_voucher(item, data, files, user, db) + return voucher diff --git a/brewman/routers/purchase_return.py b/brewman/routers/purchase_return.py new file mode 100644 index 00000000..af0aaaf9 --- /dev/null +++ b/brewman/routers/purchase_return.py @@ -0,0 +1,117 @@ +import traceback +import uuid +from typing import List + +from fastapi import APIRouter, HTTPException, status, Depends, Security, UploadFile, File, Request +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from .voucher import journal_create_voucher, purchase_create_voucher, issue_create_voucher, incentive_create_voucher, \ + salary_deduction_create_voucher, voucher_info, check_voucher_lock_info, check_voucher_edit_allowed +from .voucher.purchase_return import purchase_return_create_voucher +from ..core.session import set_date +from ..schemas.auth import UserToken +from ..core.security import get_current_active_user as get_user +from ..db.session import SessionLocal +from ..models.voucher import Voucher +import brewman.schemas.voucher as schemas + +router = APIRouter() + + +# Dependency +def get_db() -> Session: + try: + db = SessionLocal() + yield db + finally: + db.close() + + +@router.post("", response_model=schemas.Voucher) +def save_route( + request: Request, + data: schemas.Voucher, + db: Session = Depends(get_db), + files: List[UploadFile] = File(...), + user: UserToken = Security(get_user, scopes=["journal"]), +): + try: + item: Voucher = save(data, files, user, db) + db.commit() + set_date(request.session, data.date_) + # item: Voucher = db.query(Voucher).filter(Voucher.id == item.id).first() + return voucher_info(item, 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(), + ) + + +def save(data: schemas.Voucher, files: List[UploadFile], user: UserToken, db: Session) -> Voucher: + check_voucher_lock_info(None, data.date_, db) + if data.type_ in ["Purchase"]: + voucher = purchase_create_voucher(data, files, user, db) + elif data.type_ in ["Purchase Return"]: + voucher = purchase_return_create_voucher(data, files, user, db) + elif data.type_ in ["Issue"]: + voucher = issue_create_voucher(data, files, user, db) + elif data.type_ in ["Salary Deduction"]: + voucher = salary_deduction_create_voucher(data, files, user, db) + elif data.type_ in ["Incentive"]: + voucher = incentive_create_voucher(data, files, user, db) + return voucher + + +@router.get("/{id_}") +def update_route( + id_: uuid.UUID, + request: Request, + data: schemas.Voucher, + db: Session = Depends(get_db), + files: List[UploadFile] = File(...), + user: UserToken = Security(get_user, scopes=["journal"]), +): + try: + item: Voucher = update(id_, data, files, user, db) + db.commit() + set_date(request.session, data.date_) + # item: Voucher = db.query(Voucher).filter(Voucher.id == item.id).first() + return voucher_info(item, 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(), + ) + + +def update(id_: uuid.UUID, data: schemas.Voucher, files: List[UploadFile], user: UserToken, db: Session) -> Voucher: + item: Voucher = db.query(Voucher).filter(Voucher.id == id_).first() + check_voucher_lock_info(item.date, data.date_, db) + check_voucher_edit_allowed(item, user) + if data.type_ in ["Purchase"]: + voucher = purchase_update_voucher(item, data, files, user, db) + elif data.type_ in ["Purchase Return"]: + voucher = purchase_return_update_voucher(item, data, files, user, db) + elif data.type_ in ["Issue"]: + voucher = issue_update_voucher(item, data, files, user, db) + elif data.type_ in ["Salary Deduction"]: + voucher = salary_deduction_update_voucher(item, data, files, user, db) + elif data.type_ in ["Incentive"]: + voucher = incentive_update_voucher(item, data, files, user, db) + return voucher diff --git a/brewman/routers/receipt.py b/brewman/routers/receipt.py new file mode 100644 index 00000000..e69de29b diff --git a/brewman/routers/voucher/__init__.py b/brewman/routers/voucher/__init__.py index 3b2b4733..0a6690e1 100644 --- a/brewman/routers/voucher/__init__.py +++ b/brewman/routers/voucher/__init__.py @@ -1,7 +1,9 @@ -import datetime +from typing import Optional +from datetime import datetime, date import uuid from decimal import Decimal from sqlalchemy import func, or_ +from sqlalchemy.orm import Session from brewman.models.auth import User from brewman.models.master import ( @@ -21,13 +23,16 @@ from brewman.models.voucher import ( ) from brewman.routers import get_lock_info from .issue import issue_create_voucher, issue_update_voucher -from .journal import journal_update_voucher, journal_create_voucher from .purchase import purchase_create_voucher, purchase_update_voucher from .incentive import incentive_create_voucher, incentive_update_voucher +from .salary_deduction import salary_deduction_create_voucher, salary_deduction_update_voucher from ...core.session import get_first_day -from fastapi import APIRouter, Depends, Security, Request +from fastapi import APIRouter, Depends, Security, Request, HTTPException, status + +from ...schemas.auth import UserToken + router = APIRouter() @@ -168,7 +173,7 @@ def get_old(request): return voucher_info(voucher, request) -def voucher_info(voucher, request): +def voucher_info(voucher, db): json_voucher = { "id": voucher.id, "date": voucher.date.strftime("%d-%b-%Y"), @@ -188,7 +193,7 @@ def voucher_info(voucher, request): } if voucher.reconcile_date is not None: json_voucher["reconcileDate"] = voucher.reconcile_date.strftime( - "%d-%b-%Y %H:%M" + "%d-%b-%Y" ) for item in voucher.journals: json_voucher["journals"].append( @@ -225,7 +230,7 @@ def voucher_info(voucher, request): ) for item in voucher.incentives: employee = ( - request.dbsession.query(Employee) + db.query(Employee) .filter(Employee.id == item.journal.account_id) .first() ) @@ -282,14 +287,10 @@ def voucher_info(voucher, request): } ) images = ( - request.dbsession.query(DbImage).filter(DbImage.resource_id == voucher.id).all() + db.query(DbImage).filter(DbImage.resource_id == voucher.id).all() ) for image in images: - resized = request.route_url("db_image", id=image.id, type="resized") - thumbnail = request.route_url("db_image", id=image.id, type="thumbnail") - json_voucher["files"].append( - {"id": image.id, "resized": resized, "thumbnail": thumbnail} - ) + json_voucher["files"].append({"id": image.id, "resized": "", "thumbnail": ""}) return json_voucher @@ -448,3 +449,40 @@ def incentive_employees(date, dbsession): ) 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", + ) diff --git a/brewman/routers/voucher/emptyvoucher.py b/brewman/routers/voucher/emptyvoucher.py index 08181012..1523b947 100644 --- a/brewman/routers/voucher/emptyvoucher.py +++ b/brewman/routers/voucher/emptyvoucher.py @@ -6,10 +6,6 @@ class EmptyVoucher(object): def __init__(self, request): self.request = request - @router.get("/api/voucher", request_param="t=Journal") # "Journal" - def journal(self): - return self.get_blank() - @router.get("/api/voucher", request_param="t=Payment") # "Payment" def payment(self): account = self.request.GET.get("a", None) diff --git a/brewman/routers/voucher/incentive.py b/brewman/routers/voucher/incentive.py index 068bac05..63881d41 100644 --- a/brewman/routers/voucher/incentive.py +++ b/brewman/routers/voucher/incentive.py @@ -5,7 +5,6 @@ from decimal import Decimal from sqlalchemy import or_, func from brewman.models.master import Employee, AttendanceType, Account -from brewman.models.operations import journals_valid from brewman.models.voucher import ( Journal, Voucher, diff --git a/brewman/routers/voucher/issue.py b/brewman/routers/voucher/issue.py index 0d32a8a6..b39af475 100644 --- a/brewman/routers/voucher/issue.py +++ b/brewman/routers/voucher/issue.py @@ -5,7 +5,7 @@ import uuid from fastapi import HTTPException, status from brewman.models.master import CostCentre, AccountBase -from brewman.models.operations import journals_valid, inventory_valid +from brewman.models.operations import inventory_valid from brewman.models.voucher import Voucher, VoucherType, Batch, Inventory, Journal diff --git a/brewman/routers/voucher/journal.py b/brewman/routers/voucher/journal.py deleted file mode 100644 index 9b0f1765..00000000 --- a/brewman/routers/voucher/journal.py +++ /dev/null @@ -1,103 +0,0 @@ -import datetime -import uuid -from decimal import Decimal - -from brewman.models.master import AccountBase -from brewman.models.operations import journals_valid -from brewman.models.voucher import Journal, Voucher, VoucherType, DbImage - - -def journal_create_voucher(json, files, user, dbsession): - dt = datetime.datetime.strptime(json["date"], "%d-%b-%Y") - voucher = Voucher( - date=dt, - narration=json["narration"].strip(), - is_starred=json["isStarred"], - user_id=user.id, - type=VoucherType.by_name(json["type"]), - ) - dbsession.add(voucher) - for item in json["journals"]: - account = ( - dbsession.query(AccountBase) - .filter(AccountBase.id == uuid.UUID(item["account"]["id"])) - .first() - ) - journal_id = ( - uuid.UUID(item["id"]) if "id" in item and item["id"] is not None else None - ) - amount = round(Decimal(item["amount"]), 2) - journal = Journal( - id=journal_id, - amount=amount, - debit=int(item["debit"]), - account_id=account.id, - cost_centre_id=account.cost_centre_id, - ) - voucher.journals.append(journal) - dbsession.add(journal) - journals_valid(voucher) - for key, value in files.items(): - dbsession.add(DbImage(voucher.id, "voucher", value["f"], value["t"])) - return voucher - - -def journal_update_voucher(voucher, json, files, user, dbsession): - voucher.date = datetime.datetime.strptime(json["date"], "%d-%b-%Y") - voucher.is_starred = json["isStarred"] - voucher.narration = json["narration"].strip() - voucher.user_id = user.id - voucher.posted = False - voucher.last_edit_date = datetime.datetime.utcnow() - - new_journals = json["journals"] - for i in range(len(voucher.journals), 0, -1): - item = voucher.journals[i - 1] - found = False - for j in range(len(new_journals), 0, -1): - new_item = new_journals[j - 1] - if ( - "id" in new_item - and new_item["id"] is not None - and item.id == uuid.UUID(new_item["id"]) - ): - account = ( - dbsession.query(AccountBase) - .filter(AccountBase.id == uuid.UUID(new_item["account"]["id"])) - .first() - ) - found = True - item.debit = int(new_item["debit"]) - item.amount = round(Decimal(new_item["amount"]), 2) - item.account_id = account.id - item.cost_centre_id = account.cost_centre_id - new_journals.remove(new_item) - break - if not found: - voucher.journals.remove(item) - for new_item in new_journals: - account = ( - dbsession.query(AccountBase) - .filter(AccountBase.id == uuid.UUID(new_item["account"]["id"])) - .first() - ) - journal = Journal( - id=None, - amount=round(Decimal(new_item["amount"]), 2), - debit=int(new_item["debit"]), - account_id=account.id, - cost_centre_id=account.cost_centre_id, - ) - dbsession.add(journal) - voucher.journals.append(journal) - journals_valid(voucher) - - old_files = [ - uuid.UUID(f["id"]) for f in json["files"] if "id" in f and f["id"] is not None - ] - images = dbsession.query(DbImage).filter(DbImage.resource_id == voucher.id).all() - for image in [i for i in images if i.id not in old_files]: - dbsession.delete(image) - for key, value in files.items(): - dbsession.add(DbImage(voucher.id, "voucher", value["f"], value["t"])) - return voucher diff --git a/brewman/routers/voucher/purchase.py b/brewman/routers/voucher/purchase.py index 645d3708..7e3ffa16 100644 --- a/brewman/routers/voucher/purchase.py +++ b/brewman/routers/voucher/purchase.py @@ -6,7 +6,7 @@ from fastapi import HTTPException, status from sqlalchemy import func from brewman.models.master import Product, AccountBase -from brewman.models.operations import journals_valid, inventory_valid +from brewman.models.operations import inventory_valid from brewman.models.voucher import ( Voucher, VoucherType, @@ -17,106 +17,7 @@ from brewman.models.voucher import ( ) -def purchase_create_voucher(json, files, user, dbsession): - dt = datetime.datetime.strptime(json["date"], "%d-%b-%Y") - voucher = Voucher( - date=dt, - narration=json["narration"].strip(), - is_starred=json["isStarred"], - user_id=user.id, - type=VoucherType.by_name(json["type"]), - ) - dbsession.add(voucher) - for item in json["inventories"]: - purchase_create_inventory(voucher, item, dt, dbsession) - for item in purchase_create_journals( - voucher.inventories, json["journals"][0]["account"]["id"], dbsession - ): - voucher.journals.append(item) - dbsession.add(item) - journals_valid(voucher) - inventory_valid(voucher) - for key, value in files.items(): - dbsession.add(DbImage(voucher.id, "voucher", value["f"], value["t"])) - return voucher - - -def purchase_create_inventory(voucher, item, date, dbsession): - if "product" not in item or "id" not in item["product"]: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="No Product in item", - ) - product = ( - dbsession.query(Product) - .filter(Product.id == uuid.UUID(item["product"]["id"])) - .first() - ) - if product is None: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="No Product in item", - ) - inventory_id = ( - uuid.UUID(item["id"]) if "id" in item and item["id"] is not None else None - ) - quantity = round(Decimal(item["quantity"]), 2) - rate = round(Decimal(item["rate"]), 2) - tax = round(Decimal(item["tax"]), 5) - discount = round(Decimal(item["discount"]), 5) - batch = Batch( - name=date, - product_id=product.id, - quantity_remaining=quantity, - rate=rate, - tax=tax, - discount=discount, - ) - dbsession.add(batch) - inventory = Inventory( - id=inventory_id, - product_id=product.id, - batch=batch, - quantity=quantity, - rate=rate, - tax=tax, - discount=discount, - ) - product.price = rate - voucher.inventories.append(inventory) - dbsession.add(inventory) - - -def purchase_create_journals(inventories, account_id, dbsession): - other_account = ( - dbsession.query(AccountBase) - .filter(AccountBase.id == uuid.UUID(account_id)) - .first() - ) - journals = dict() - amount = 0 - for item in inventories: - product = dbsession.query(Product).filter(Product.id == item.product_id).first() - account = product.account - item_amount = round(item.amount, 2) - amount += item_amount - if account.id in journals: - journals[account.id].amount += item_amount - else: - journals[account.id] = Journal( - debit=1, - cost_centre_id=account.cost_centre_id, - account_id=account.id, - amount=item_amount, - ) - journals[other_account.id] = Journal( - debit=-1, - cost_centre_id=other_account.cost_centre_id, - account_id=other_account.id, - amount=amount, - ) - return list(journals.values()) def purchase_update_voucher(voucher, json, files, user, dbsession): diff --git a/brewman/routers/voucher/salary_deduction.py b/brewman/routers/voucher/salary_deduction.py index 403da5d6..4e89bdf4 100644 --- a/brewman/routers/voucher/salary_deduction.py +++ b/brewman/routers/voucher/salary_deduction.py @@ -1,11 +1,10 @@ import datetime from math import ceil import uuid + +from brewman.core.session import get_last_day from brewman.models.master import AccountBase, Employee -from brewman.models.operations import journals_valid -from brewman.models.validation_exception import ValidationError from brewman.models.voucher import Journal, Voucher, VoucherType, SalaryDeduction -from brewman.routers.services.session import get_last_day def salary_deduction_create_voucher(json, user, dbsession): diff --git a/brewman/routers/voucher/savevoucher.py b/brewman/routers/voucher/savevoucher.py deleted file mode 100644 index d2412b27..00000000 --- a/brewman/routers/voucher/savevoucher.py +++ /dev/null @@ -1,123 +0,0 @@ -import uuid -from json import loads -import datetime -from brewman.models.auth import User -from brewman.models.validation_exception import ValidationError -from brewman.models.voucher import Voucher -from brewman.routers import get_lock_info -from ....core.session import set_date -from . import ( - voucher_info, - journal_create_voucher, - purchase_create_voucher, - issue_create_voucher, -) -from . import incentive_create_voucher -from .purchase_return import purchase_return_create_voucher -from .salary_deduction import salary_deduction_create_voucher - - -@router.post("/api/voucher") -class SaveVoucher(object): - def __init__(self, request): - def parse_post(req): - keys = req.POST.keys() - files = {} - model = {} - for key in keys: - if key == "model": - model = loads(req.POST[key], encoding=req.charset) - else: - index = key[1:] - if index in files: - files[index][key[:1]] = bytearray(req.POST[key].file.read()) - else: - files[index] = {key[:1]: bytearray(req.POST[key].file.read())} - return model, files - - self.request = request - self.user = ( - request.dbsession.query(User) - .filter(User.id == uuid.UUID(request.authenticated_userid)) - .one() - ) - self.json, self.files = parse_post(request) - self.start, self.finish = get_lock_info(request.dbsession) - self.voucher_date = datetime.datetime.strptime(self.json["date"], "%d-%b-%Y") - - @router.get("/api/voucher", request_param="t=Journal") # "Journal" - def journal(self): - return self.save() - - @router.get("/api/voucher", request_param="t=Payment") # "Payment" - def payment(self): - return self.save() - - @router.get("/api/voucher", request_param="t=Receipt") # "Receipt" - def receipt(self): - return self.save() - - @router.get("/api/voucher", request_param="t=Purchase") # "Purchase" - def purchase(self): - return self.save() - - @router.get("/api/voucher", request_param="t=Purchase Return") # "Purchase Return" - def purchase_return(self): - return self.save() - - @router.get("/api/voucher", request_param="t=Issue") # "Issue" - def issue(self): - return self.save() - - @router.get("/api/voucher", request_param="t=Salary Deduction") # "Salary Deduction" - def salary_deduction(self): - return self.save() - - @router.get("/api/voucher", request_param="t=Incentive") # "Incentive" - def incentive(self): - return self.save() - - def save(self): - if self.start is not None and self.start > self.voucher_date: - raise ValidationError( - "Vouchers before {0} have been locked.".format( - self.start.strftime("%d-%b-%Y") - ) - ) - elif self.finish is not None and self.finish < self.voucher_date: - raise ValidationError( - "Vouchers after {0} have been locked.".format( - self.finish.strftime("%d-%b-%Y") - ) - ) - - if self.json["type"] in ["Journal", "Payment", "Receipt"]: - voucher = journal_create_voucher( - self.json, self.files, self.user, self.request.dbsession - ) - elif self.json["type"] in ["Purchase"]: - voucher = purchase_create_voucher( - self.json, self.files, self.user, self.request.dbsession - ) - elif self.json["type"] in ["Purchase Return"]: - voucher = purchase_return_create_voucher( - self.json, self.files, self.user, self.request.dbsession - ) - elif self.json["type"] in ["Issue"]: - voucher = issue_create_voucher(self.json, self.user, self.request.dbsession) - elif self.json["type"] in ["Salary Deduction"]: - voucher = salary_deduction_create_voucher( - self.json, self.user, self.request.dbsession - ) - elif self.json["type"] in ["Incentive"]: - voucher = incentive_create_voucher( - self.json, self.user, self.request.dbsession - ) - transaction.commit() - set_date(self.request, self.json["date"]) - new_voucher = ( - self.request.dbsession.query(Voucher) - .filter(Voucher.id == voucher.id) - .first() - ) - return voucher_info(new_voucher, self.request) diff --git a/brewman/routers/voucher/updatevoucher.py b/brewman/routers/voucher/updatevoucher.py deleted file mode 100644 index f0c4cf01..00000000 --- a/brewman/routers/voucher/updatevoucher.py +++ /dev/null @@ -1,151 +0,0 @@ -import datetime -import uuid - -from json import loads -from pyramid.response import Response - -from brewman.models.auth import User -from brewman.models.voucher import Voucher -from brewman.routers import get_lock_info -from ...core.session import set_date -from . import ( - voucher_info, - issue_update_voucher, - purchase_update_voucher, - journal_update_voucher, -) -from . import incentive_update_voucher -from brewman.routers.voucher.purchase_return import ( - purchase_return_update_voucher, -) -from brewman.routers.voucher.salary_deduction import ( - salary_deduction_update_voucher, -) - - -class UpdateVoucher(object): - def __init__(self, request): - def parse_post(request): - keys = request.POST.keys() - files = {} - model = {} - for key in keys: - if key == "model": - model = loads(request.POST[key], encoding=request.charset) - else: - index = key[1:] - if index in files: - files[index][key[:1]] = bytearray(request.POST[key].file.read()) - else: - files[index] = { - key[:1]: bytearray(request.POST[key].file.read()) - } - return model, files - - self.request = request - self.user = ( - request.dbsession.query(User) - .filter(User.id == uuid.UUID(request.authenticated_userid)) - .one() - ) - self.voucher = ( - self.request.dbsession.query(Voucher) - .filter(Voucher.id == uuid.UUID(request.matchdict["id"])) - .first() - ) - self.json, self.files = parse_post(request) - self.start, self.finish = get_lock_info(request.dbsession) - self.voucher_date = datetime.datetime.strptime(self.json["date"], "%d-%b-%Y") - - if self.voucher.posted and not request.has_permission("Edit Posted Vouchers"): - response = Response("You are not allowed to edit posted vouchers") - response.status_int = 403 - self.error = response - elif self.voucher.user_id != self.user.id and not request.has_permission( - "Edit Other User's Vouchers" - ): - response = Response("You are not allowed to edit other user's vouchers") - response.status_int = 403 - self.error = response - elif self.start is not None and ( - self.start > self.voucher_date or self.start > self.voucher.date - ): - response = Response(f"Vouchers before {self.start.strftime('%d-%b-%Y')} have been locked.") - response.status_int = 403 - self.error = response - elif self.finish is not None and ( - self.finish < self.voucher_date or self.finish < self.voucher.date - ): - response = Response(f"Vouchers after {self.finish.strftime('%d-%b-%Y')} have been locked.") - response.status_int = 403 - self.error = response - else: - self.error = None - - @router.get("/api/voucher/{id}", request_param="t=Journal") # "Journal" - def journal(self): - return self.update() - - @router.get("/api/voucher/{id}", request_param="t=Payment") # "Payment" - def payment(self): - return self.update() - - @router.get("/api/voucher/{id}", request_param="t=Receipt") # "Receipt" - def receipt(self): - return self.update() - - @router.get("/api/voucher/{id}", request_param="t=Purchase") # "Purchase" - def purchase(self): - return self.update() - - @router.get("/api/voucher/{id}", request_param="t=Purchase Return") # "Purchase Return" - def purchase_return(self): - return self.update() - - @router.get("/api/voucher/{id}", request_param="t=Issue") # "Issue" - def issue(self): - return self.update() - - @router.get("/api/voucher/{id}", request_param="t=Salary Deduction") # "Salary Deduction" - def salary_deduction(self): - return self.update() - - @router.get("/api/voucher/{id}", request_param="t=Incentive") # "Incentive" - def incentive(self): - return self.update() - - def update(self): - if self.error is not None: - return self.error - if self.json["type"] in ["Journal", "Payment", "Receipt"]: - voucher = journal_update_voucher( - self.voucher, self.json, self.files, self.user, self.request.dbsession - ) - elif self.json["type"] in ["Purchase"]: - voucher = purchase_update_voucher( - self.voucher, self.json, self.files, self.user, self.request.dbsession - ) - elif self.json["type"] in ["Purchase Return"]: - voucher = purchase_return_update_voucher( - self.voucher, self.json, self.files, self.user, self.request.dbsession - ) - elif self.json["type"] in ["Issue"]: - voucher = issue_update_voucher( - self.voucher, self.json, self.user, self.request.dbsession - ) - elif self.json["type"] in ["Salary Deduction"]: - voucher = salary_deduction_update_voucher( - self.voucher, self.json, self.user, self.request.dbsession - ) - elif self.json["type"] in ["Incentive"]: - voucher = incentive_update_voucher( - self.voucher, self.json, self.user, self.request.dbsession - ) - transaction.commit() - set_date(self.request, self.json["date"]) - new_voucher = ( - self.request.dbsession.query(Voucher) - .filter(Voucher.id == voucher.id) - .first() - ) - return voucher_info(new_voucher, self.request) diff --git a/brewman/schemas/voucher.py b/brewman/schemas/voucher.py index 18d6bc4c..eeaf21da 100644 --- a/brewman/schemas/voucher.py +++ b/brewman/schemas/voucher.py @@ -1,35 +1,39 @@ import uuid +import json from datetime import datetime, date from decimal import Decimal from typing import List, Optional -from pydantic import BaseModel, validator + +from fastapi import Form +from pydantic import BaseModel, validator, Field from brewman.schemas import to_camel -from brewman.schemas.master import AccountLink - - -class Voucher(BaseModel): - id: uuid.UUID - date: date - narration: str - is_reconciled: bool - reconcile_date: datetime - is_starred: bool - creation_date: datetime - last_edit_date: datetime - _type: int - user_id: uuid.UUID - posted: bool - poster_id: uuid.UUID +from brewman.schemas.master import AccountLink, CostCentreLink, ProductLink class Journal(BaseModel): - id: uuid.UUID - debit: int - amount: Decimal - voucher_id: uuid.UUID - account_id: uuid.UUID - cost_centre_id: uuid.UUID + id_: Optional[uuid.UUID] + debit: int = Field(ge=-1, le=1, multiple_of=1) + amount: Decimal = Field(ge=0, multiple_of=0.01) + account: AccountLink + cost_centre: Optional[CostCentreLink] + + class Config: + anystr_strip_whitespace = True + alias_generator = to_camel + + +class Inventory(BaseModel): + id_: uuid.UUID + product: ProductLink + batch_id: uuid.UUID + quantity: Decimal = Field(ge=0, multiple_of=0.01) + rate: Decimal = Field(ge=0, multiple_of=0.01) + tax: Decimal = Field(ge=0, multiple_of=0.00001) + discount: Decimal = Field(ge=0, multiple_of=0.00001) + + class Config: + alias_generator = to_camel class SalaryDeduction(BaseModel): @@ -52,17 +56,6 @@ class Incentive(BaseModel): points: Decimal -class Inventory(BaseModel): - id: uuid.UUID - voucher_id: uuid.UUID - product_id: uuid.UUID - batch_id: uuid.UUID - quantity: Decimal - rate: Decimal - tax: Decimal - discount: Decimal - - class Batch(BaseModel): id: uuid.UUID name: datetime @@ -73,6 +66,84 @@ class Batch(BaseModel): discount: Decimal +class Voucher(BaseModel): + id_: Optional[uuid.UUID] + date_: date + narration: str + is_reconciled: Optional[bool] + reconcile_date: Optional[date] + is_starred: bool + creation_date: Optional[datetime] + last_edit_date: Optional[datetime] + type_: str + user_id: Optional[uuid.UUID] + posted: Optional[bool] + poster_id: Optional[uuid.UUID] + journals: List[Journal] + inventories: List[Inventory] + + class Config: + anystr_strip_whitespace = True + alias_generator = to_camel + json_encoders = { + date: lambda v: v.strftime("%d-%b-%Y"), + datetime: lambda v: v.strftime("%d-%b-%Y %H:%I") + } + + @validator("date_", pre=True) + def parse_date(cls, value): + if isinstance(value, date): + return value + return datetime.strptime(value, "%d-%b-%Y").date() + + @validator("creation_date", pre=True) + def parse_creation_date(cls, value): + if isinstance(value, datetime): + return value + return datetime.strptime(value, "%d-%b-%Y %H:%M") + + @validator("last_edit_date", pre=True) + def parse_last_edit_date(cls, value): + if isinstance(value, datetime): + return value + return datetime.strptime(value, "%d-%b-%Y %H:%M") + + @validator("reconcile_date", pre=True) + def parse_reconcile_date(cls, value): + if value is None or value == "": + return None + elif isinstance(value, date): + return value + return datetime.strptime( + value, + "%d-%b-%Y" + ).date() + + @validator("journals") + def validate_enough_journals(cls, value: List[Journal]): + if len(value) < 2: + raise ValueError("Not enough journals") + return value + + @validator("journals") + def validate_signed_amount(cls, value: List[Journal]): + if sum(x.debit * x.amount for x in value) != 0: + raise ValueError("Journal amounts do no match") + return value + + @validator("journals") + def is_distinct(cls, value: List[Journal]): + journal_set = set(hash(x.account.id_) ^ hash(None if x.cost_centre is None else x.cost_centre.id_) for x in value) + if len(value) != len(journal_set): + raise ValueError("Duplicate journals") + return value + + @classmethod + def load_form(cls, data: str = Form(...)): + json_data = json.loads(data) + return cls.parse_obj(json_data) + + class AttendanceType(BaseModel): id_: int name: Optional[str] diff --git a/overlord/src/app/attendance/attendance.service.ts b/overlord/src/app/attendance/attendance.service.ts index 63e91729..1e2a9f04 100644 --- a/overlord/src/app/attendance/attendance.service.ts +++ b/overlord/src/app/attendance/attendance.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; import {Observable} from 'rxjs/internal/Observable'; import {catchError} from 'rxjs/operators'; -import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; import {Attendance} from './attendance'; import {ErrorLoggerService} from '../core/error-logger.service'; diff --git a/overlord/src/app/core/voucher.service.ts b/overlord/src/app/core/voucher.service.ts index cf04eebf..403413db 100644 --- a/overlord/src/app/core/voucher.service.ts +++ b/overlord/src/app/core/voucher.service.ts @@ -9,7 +9,7 @@ const httpOptions = { headers: new HttpHeaders({'Content-Type': 'application/json'}) }; -const url = '/api/voucher'; +const url = '/api'; const serviceName = 'VoucherService'; @Injectable({ @@ -20,22 +20,23 @@ export class VoucherService { constructor(private http: HttpClient, private log: ErrorLoggerService) { } - get(id: string): Observable { - const options = {params: new HttpParams()}; - return >this.http.get(`${url}/${id}`, options) + get(id: string, type: string): Observable { + const endpoint = type.replace(/ /g, '-').toLowerCase(); + return >this.http.get(`${url}/${endpoint}/${id}`) .pipe( - catchError(this.log.handleError(serviceName, 'list')) + catchError(this.log.handleError(serviceName, 'get')) ); } getOfType(type: string, account?: string): Observable { - const options = {params: new HttpParams().set('t', type)}; + const endpoint = type.replace(/ /g, '-').toLowerCase(); + const options = {}; if (account !== undefined && account !== null) { - options.params = options.params.set('a', account); + options['params'] = new HttpParams().set('a', account); } - return >this.http.get(url, options) + return >this.http.get(`${url}/${endpoint}`, options) .pipe( - catchError(this.log.handleError(serviceName, 'list')) + catchError(this.log.handleError(serviceName, 'getOfType')) ); } @@ -63,18 +64,17 @@ export class VoucherService { } save(voucher: Voucher): Observable { - const options = { - params: new HttpParams().set('t', voucher.type) - }; + const endpoint = voucher.type.replace(/ /g, '-').toLowerCase(); + const fd = new FormData(); - voucher.files.filter(x => !x.id).forEach((file, index) => { - fd.append('f' + index, this.dataURLtoBlob(file.resized)); - fd.append('t' + index, this.dataURLtoBlob(file.thumbnail)); + voucher.files.filter(x => !x.id).forEach((file) => { + fd.append('i' , this.dataURLtoBlob(file.resized)); + fd.append('t' , this.dataURLtoBlob(file.thumbnail)); }); voucher.files = voucher.files.filter(x => x.id); - fd.append('model', JSON.stringify(voucher)); - const saveUrl: string = (voucher.id === undefined || voucher.id === null) ? url : `${url}/${voucher.id}`; - return >this.http.post(saveUrl, fd, options) + fd.append('data', JSON.stringify(voucher)); + const saveUrl: string = (voucher.id === undefined || voucher.id === null) ? `${url}/${endpoint}` : `${url}/${endpoint}/${voucher.id}`; + return >this.http.post(saveUrl, fd) .pipe( catchError(this.log.handleError(serviceName, 'list')) ); diff --git a/overlord/src/app/employee-benefits/employee-benefits-resolver.service.ts b/overlord/src/app/employee-benefits/employee-benefits-resolver.service.ts index ff9b243b..bc486020 100644 --- a/overlord/src/app/employee-benefits/employee-benefits-resolver.service.ts +++ b/overlord/src/app/employee-benefits/employee-benefits-resolver.service.ts @@ -3,7 +3,6 @@ import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/rou import {Observable} from 'rxjs/internal/Observable'; import {Voucher} from '../core/voucher'; import {VoucherService} from '../core/voucher.service'; -import {tap} from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -18,7 +17,7 @@ export class EmployeeBenefitsResolver implements Resolve { if (id === null) { return this.ser.getOfType('Salary Deduction'); } else { - return this.ser.get(id); + return this.ser.get(id, 'Employee-Benefits'); } } } diff --git a/overlord/src/app/employee-functions/employee-functions.component.ts b/overlord/src/app/employee-functions/employee-functions.component.ts index 21da20b2..215ab2a8 100644 --- a/overlord/src/app/employee-functions/employee-functions.component.ts +++ b/overlord/src/app/employee-functions/employee-functions.component.ts @@ -91,7 +91,7 @@ export class EmployeeFunctionsComponent implements OnInit { // this.toaster.show('Danger', 'Please choose a start and finish date.'); return; } - return '/attendance-report?StartDate=' + startDate + '&FinishDate=' + finishDate; + return '/attendance-report?s=' + startDate + '&f=' + finishDate; } detectFiles(event) { diff --git a/overlord/src/app/incentive/incentive-resolver.service.ts b/overlord/src/app/incentive/incentive-resolver.service.ts index 50c97191..5fef9177 100644 --- a/overlord/src/app/incentive/incentive-resolver.service.ts +++ b/overlord/src/app/incentive/incentive-resolver.service.ts @@ -18,7 +18,7 @@ export class IncentiveResolver implements Resolve { if (id === null) { return this.ser.getOfType('Incentive'); } else { - return this.ser.get(id); + return this.ser.get(id, 'Incentive'); } } } diff --git a/overlord/src/app/issue/issue-resolver.service.ts b/overlord/src/app/issue/issue-resolver.service.ts index 65a3937a..02725dc2 100644 --- a/overlord/src/app/issue/issue-resolver.service.ts +++ b/overlord/src/app/issue/issue-resolver.service.ts @@ -17,7 +17,7 @@ export class IssueResolver implements Resolve { if (id === null) { return this.ser.getOfType('Issue'); } else { - return this.ser.get(id); + return this.ser.get(id, 'Issue'); } } } diff --git a/overlord/src/app/journal/journal-resolver.service.ts b/overlord/src/app/journal/journal-resolver.service.ts index 735050a1..af7dc2ac 100644 --- a/overlord/src/app/journal/journal-resolver.service.ts +++ b/overlord/src/app/journal/journal-resolver.service.ts @@ -17,7 +17,7 @@ export class JournalResolver implements Resolve { if (id === null) { return this.ser.getOfType('Journal'); } else { - return this.ser.get(id); + return this.ser.get(id, 'Journal'); } } } diff --git a/overlord/src/app/payment/payment-resolver.service.ts b/overlord/src/app/payment/payment-resolver.service.ts index a1ff50bd..a5f65f15 100644 --- a/overlord/src/app/payment/payment-resolver.service.ts +++ b/overlord/src/app/payment/payment-resolver.service.ts @@ -16,7 +16,7 @@ export class PaymentResolver implements Resolve { const id = route.paramMap.get('id'); const account = route.queryParamMap.get('a') || null; if (id !== null) { - return this.ser.get(id); + return this.ser.get(id, 'Payment'); } else if (account !== null) { return this.ser.getOfType('Payment', account); } else { diff --git a/overlord/src/app/purchase-return/purchase-return-resolver.service.ts b/overlord/src/app/purchase-return/purchase-return-resolver.service.ts index 6ca71401..42cc2fa2 100644 --- a/overlord/src/app/purchase-return/purchase-return-resolver.service.ts +++ b/overlord/src/app/purchase-return/purchase-return-resolver.service.ts @@ -17,7 +17,7 @@ export class PurchaseReturnResolver implements Resolve { if (id === null) { return this.ser.getOfType('Purchase Return'); } else { - return this.ser.get(id); + return this.ser.get(id, 'Purchase Return'); } } } diff --git a/overlord/src/app/purchase/purchase-resolver.service.ts b/overlord/src/app/purchase/purchase-resolver.service.ts index 9ff057d0..a8caa7f4 100644 --- a/overlord/src/app/purchase/purchase-resolver.service.ts +++ b/overlord/src/app/purchase/purchase-resolver.service.ts @@ -17,7 +17,7 @@ export class PurchaseResolver implements Resolve { if (id === null) { return this.ser.getOfType('Purchase'); } else { - return this.ser.get(id); + return this.ser.get(id, 'Purchase'); } } } diff --git a/overlord/src/app/receipt/receipt-resolver.service.ts b/overlord/src/app/receipt/receipt-resolver.service.ts index de1403d2..6e5b3965 100644 --- a/overlord/src/app/receipt/receipt-resolver.service.ts +++ b/overlord/src/app/receipt/receipt-resolver.service.ts @@ -16,7 +16,7 @@ export class ReceiptResolver implements Resolve { const id = route.paramMap.get('id'); const account = route.queryParamMap.get('a') || null; if (id !== null) { - return this.ser.get(id); + return this.ser.get(id, 'Receipt'); } else if (account !== null) { return this.ser.getOfType('Receipt', account); } else {