From a0f27fe36494be3b47f5bb3fc9527cb0d01fb19f Mon Sep 17 00:00:00 2001 From: tanshu Date: Sun, 17 May 2020 15:38:13 +0530 Subject: [PATCH] Attendance and Employee Attendance done!! Journal Done!! But there is issue of validation in purchase due to lack of proper journals when getting data from the frontend. The proposed solution is to create different schemas for various vouchers and then reexport and import them in a master schema for persisting Also, change "Supplier" to vendor in Purchase, etc. Also, convert all date field to date from datetime Rename "Salary Deduction" to "Employee Benefits" across the board --- brewman/main.py | 7 +- brewman/models/operations.py | 37 +--- brewman/routers/__init__.py | 17 +- brewman/routers/account.py | 2 +- brewman/routers/attendance_report.py | 71 +++++-- brewman/routers/deduction.py | 0 brewman/routers/incentive.py | 0 brewman/routers/issue.py | 0 brewman/routers/journal.py | 198 ++++++++++++++++++ brewman/routers/payment.py | 0 brewman/routers/purchase.py | 168 +++++++++++++++ brewman/routers/purchase_return.py | 117 +++++++++++ brewman/routers/receipt.py | 0 brewman/routers/voucher/__init__.py | 62 ++++-- brewman/routers/voucher/emptyvoucher.py | 4 - brewman/routers/voucher/incentive.py | 1 - brewman/routers/voucher/issue.py | 2 +- brewman/routers/voucher/journal.py | 103 --------- brewman/routers/voucher/purchase.py | 101 +-------- brewman/routers/voucher/salary_deduction.py | 5 +- brewman/routers/voucher/savevoucher.py | 123 ----------- brewman/routers/voucher/updatevoucher.py | 151 ------------- brewman/schemas/voucher.py | 139 +++++++++--- .../src/app/attendance/attendance.service.ts | 2 +- overlord/src/app/core/voucher.service.ts | 36 ++-- .../employee-benefits-resolver.service.ts | 3 +- .../employee-functions.component.ts | 2 +- .../incentive/incentive-resolver.service.ts | 2 +- .../src/app/issue/issue-resolver.service.ts | 2 +- .../app/journal/journal-resolver.service.ts | 2 +- .../app/payment/payment-resolver.service.ts | 2 +- .../purchase-return-resolver.service.ts | 2 +- .../app/purchase/purchase-resolver.service.ts | 2 +- .../app/receipt/receipt-resolver.service.ts | 2 +- 34 files changed, 734 insertions(+), 631 deletions(-) create mode 100644 brewman/routers/deduction.py create mode 100644 brewman/routers/incentive.py create mode 100644 brewman/routers/issue.py create mode 100644 brewman/routers/journal.py create mode 100644 brewman/routers/payment.py create mode 100644 brewman/routers/purchase.py create mode 100644 brewman/routers/purchase_return.py create mode 100644 brewman/routers/receipt.py delete mode 100644 brewman/routers/voucher/journal.py delete mode 100644 brewman/routers/voucher/savevoucher.py delete mode 100644 brewman/routers/voucher/updatevoucher.py 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 {