diff --git a/brewman/main.py b/brewman/main.py index aa63214c..978ea176 100644 --- a/brewman/main.py +++ b/brewman/main.py @@ -7,13 +7,18 @@ from .routers import ( account_types, attendance_types, attendance, + batch, employee_attendance, attendance_report, cost_centre, employee, fingerprint, + issue, + issue_grid, + payment, product, product_group, + receipt, recipe, login, journal, @@ -39,10 +44,6 @@ from .routers.reports import ( unposted, ) -from .routers.voucher import ( - issue_grid, - batch, -) from .db.base_class import Base from .config import Settings from .db.session import engine @@ -105,10 +106,11 @@ 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"]) +app.include_router(payment.router, prefix="/api/payment", tags=["vouchers"]) +app.include_router(receipt.router, prefix="/api/receipt", tags=["vouchers"]) app.include_router(purchase.router, prefix="/api/purchase", tags=["vouchers"]) app.include_router(purchase_return.router, prefix="/api/purchase-return", tags=["vouchers"]) +app.include_router(issue.router, prefix="/api/issue", tags=["vouchers"]) def init(): diff --git a/brewman/models/voucher.py b/brewman/models/voucher.py index a137d518..fd7b7020 100644 --- a/brewman/models/voucher.py +++ b/brewman/models/voucher.py @@ -22,13 +22,13 @@ from .meta import Base class VoucherType: - def __init__(self, id, name): - self.id = id + def __init__(self, id_, name): + self.id = id_ self.name = name @classmethod def list(cls): - list = [ + list_ = [ VoucherType(1, "Journal"), VoucherType(2, "Purchase"), VoucherType(3, "Issue"), @@ -43,20 +43,20 @@ class VoucherType: VoucherType(12, "Employee Benefit"), VoucherType(13, "Incentive"), ] - return list + return list_ @classmethod def by_name(cls, name): - list = cls.list() - for item in list: + list_ = cls.list() + for item in list_: if item.name == name: return item @classmethod - def by_id(cls, id): - list = cls.list() - for item in list: - if item.id == id: + def by_id(cls, id_): + list_ = cls.list() + for item in list_: + if item.id == id_: return item diff --git a/brewman/routers/__init__.py b/brewman/routers/__init__.py index 179b714f..ed365775 100644 --- a/brewman/routers/__init__.py +++ b/brewman/routers/__init__.py @@ -1,6 +1,6 @@ import re import uuid -from datetime import date, datetime, timedelta, time +from datetime import date, timedelta from decimal import Decimal from io import BytesIO from typing import Optional diff --git a/brewman/routers/attendance.py b/brewman/routers/attendance.py index 7a3efbfc..b6846926 100644 --- a/brewman/routers/attendance.py +++ b/brewman/routers/attendance.py @@ -1,5 +1,5 @@ import traceback -from datetime import datetime, date, timedelta, time +from datetime import datetime, date, timedelta from fastapi import APIRouter, HTTPException, status, Depends, Security, Request from sqlalchemy import or_ @@ -52,12 +52,7 @@ def attendance_date_report(date_: date, db: Session): employees = ( db.query(Employee) .filter(Employee.joining_date <= date_) - .filter( - or_( - Employee.is_active, - Employee.leaving_date >= date_, - ) - ) + .filter(or_(Employee.is_active, Employee.leaving_date >= date_,)) .order_by(Employee.cost_centre_id) .order_by(Employee.designation) .order_by(Employee.name) diff --git a/brewman/routers/attendance_report.py b/brewman/routers/attendance_report.py index f44c50c3..2e7db516 100644 --- a/brewman/routers/attendance_report.py +++ b/brewman/routers/attendance_report.py @@ -2,12 +2,10 @@ import csv from datetime import datetime, date import io -from fastapi import APIRouter, HTTPException, status, Depends, Security, Request +from fastapi import APIRouter, Depends 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 @@ -34,8 +32,15 @@ def get_report( ): 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'"} + 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: diff --git a/brewman/routers/voucher/batch.py b/brewman/routers/batch.py similarity index 90% rename from brewman/routers/voucher/batch.py rename to brewman/routers/batch.py index 8498cb43..2b87a5bb 100644 --- a/brewman/routers/voucher/batch.py +++ b/brewman/routers/batch.py @@ -3,9 +3,9 @@ import datetime from fastapi import APIRouter, Depends from sqlalchemy.orm import Session -from ...schemas.auth import UserToken -from ...core.security import get_current_active_user as get_user -from ...db.session import SessionLocal +from ..schemas.auth import UserToken +from ..core.security import get_current_active_user as get_user +from ..db.session import SessionLocal from brewman.models.voucher import Batch router = APIRouter() diff --git a/brewman/routers/cost_centre.py b/brewman/routers/cost_centre.py index d9673940..090b1789 100644 --- a/brewman/routers/cost_centre.py +++ b/brewman/routers/cost_centre.py @@ -11,6 +11,7 @@ import brewman.schemas.master as schemas from ..core.security import get_current_active_user as get_user from ..db.session import SessionLocal from ..models.master import CostCentre + router = APIRouter() @@ -37,8 +38,7 @@ def save( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -68,8 +68,7 @@ def update( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -90,8 +89,7 @@ def delete( if item is None: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Cost Centre not found", + status_code=status.HTTP_404_NOT_FOUND, detail="Cost Centre not found", ) elif item.is_fixture: raise HTTPException( diff --git a/brewman/routers/employee.py b/brewman/routers/employee.py index d197a902..7cd24178 100644 --- a/brewman/routers/employee.py +++ b/brewman/routers/employee.py @@ -119,7 +119,8 @@ def delete( @router.get("") def show_blank( - db: Session = Depends(get_db), user: UserToken = Security(get_user, scopes=["employees"]) + db: Session = Depends(get_db), + user: UserToken = Security(get_user, scopes=["employees"]), ): return employee_info(None, db) diff --git a/brewman/routers/employee_attendance.py b/brewman/routers/employee_attendance.py index b179d107..ac3e0993 100644 --- a/brewman/routers/employee_attendance.py +++ b/brewman/routers/employee_attendance.py @@ -30,8 +30,7 @@ def get_db() -> Session: @router.get("", response_model=schemas.EmployeeAttendance) def show_blank( - request: Request, - user: UserToken = Security(get_user, scopes=["attendance"]), + request: Request, user: UserToken = Security(get_user, scopes=["attendance"]), ): return { "startDate": get_start_date(request.session), @@ -53,8 +52,7 @@ def employee_attendance_report( employee: Employee = db.query(Employee).filter(Employee.id == id_).first() if employee is None: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Employee not found", + status_code=status.HTTP_404_NOT_FOUND, detail="Employee not found", ) start_date = s if s is not None else get_start_date(request.session) finish_date = f if f is not None else get_finish_date(request.session) @@ -73,13 +71,13 @@ def employee_attendance_report( if not employee.is_active and employee.leaving_date < finish_date else finish_date ) - info["body"] = employee_attendance( - employee, start_date, finish_date, db - ) + info["body"] = employee_attendance(employee, start_date, finish_date, db) return info -def employee_attendance(employee: Employee, start_date: date, finish_date: date, db: Session): +def employee_attendance( + employee: Employee, start_date: date, finish_date: date, db: Session +): list_ = [] for item in date_range(start_date, finish_date, inclusive=True): att = ( @@ -97,7 +95,7 @@ def employee_attendance(employee: Employee, start_date: date, finish_date: date, attendanceType={"id": att}, prints=prints, hoursWorked=hours_worked, - fullDay=full_day + fullDay=full_day, ) ) return list_ diff --git a/brewman/routers/employee_benefit.py b/brewman/routers/employee_benefit.py index e69de29b..9c502f2c 100644 --- a/brewman/routers/employee_benefit.py +++ b/brewman/routers/employee_benefit.py @@ -0,0 +1,103 @@ +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 incentive_create_voucher, employee_benefit_create_voucher, voucher_info, check_voucher_lock_info, check_voucher_edit_allowed +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 ["Employee Benefit"]: + voucher = employee_benefit_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 ["Employee Benefit"]: + voucher = employee_benefit_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/fingerprint.py b/brewman/routers/fingerprint.py index f9c4496d..c1b7c44e 100644 --- a/brewman/routers/fingerprint.py +++ b/brewman/routers/fingerprint.py @@ -5,7 +5,6 @@ from io import StringIO from sqlalchemy import bindparam, select, exists, and_ from sqlalchemy.dialects.postgresql import insert as pg_insert -# from zope.sqlalchemy import mark_changed from sqlalchemy.orm import Session from brewman.models.master import Employee diff --git a/brewman/routers/issue.py b/brewman/routers/issue.py index ad7c5efb..8407b5b6 100644 --- a/brewman/routers/issue.py +++ b/brewman/routers/issue.py @@ -1,19 +1,36 @@ import traceback import uuid -from typing import List +from datetime import datetime +from decimal import Decimal +from typing import List, Optional -from fastapi import APIRouter, HTTPException, status, Depends, Security, UploadFile, File, Request +from fastapi import ( + APIRouter, + HTTPException, + status, + Depends, + Security, + UploadFile, + File, + Request, +) from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session -from .voucher import issue_create_voucher, incentive_create_voucher, \ - employee_benefit_create_voucher, voucher_info, check_voucher_lock_info, check_voucher_edit_allowed -from ..core.session import set_date +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 CostCentre, 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 -import brewman.schemas.voucher as schemas +from ..models.voucher import Voucher, VoucherType, Batch, Inventory, Journal +import brewman.schemas.voucher as output +import brewman.schemas.input as schema_in router = APIRouter() @@ -27,25 +44,30 @@ def get_db() -> Session: db.close() -@router.post("", response_model=schemas.Voucher) +@router.post("", response_model=output.Voucher) def save_route( request: Request, - data: schemas.Voucher, + data: schema_in.IssueIn = Depends(schema_in.IssueIn.load_form), db: Session = Depends(get_db), - files: List[UploadFile] = File(...), - user: UserToken = Security(get_user, scopes=["journal"]), + i: List[UploadFile] = File(None), + t: List[UploadFile] = File(None), + user: UserToken = Security(get_user, scopes=["issue"]), ): try: - item: Voucher = save(data, files, user, db) + i = i or [] + t = t or [] + item, batch_consumed = save(data, user, db) + amount = save_inventories(item, data.inventories, batch_consumed, db) + save_journals(item, data.source, data.destination, amount, db) + save_files(i + t, 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) + 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), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -55,37 +77,129 @@ def save_route( ) -def save(data: schemas.Voucher, files: List[UploadFile], user: UserToken, db: Session) -> Voucher: +def save( + data: schema_in.IssueIn, user: UserToken, db: Session +) -> (Voucher, Optional[bool]): check_voucher_lock_info(None, data.date_, db) - if data.type_ in ["Issue"]: - voucher = issue_create_voucher(data, files, user, db) - elif data.type_ in ["Employee Benefit"]: - voucher = employee_benefit_create_voucher(data, files, user, db) - elif data.type_ in ["Incentive"]: - voucher = incentive_create_voucher(data, files, user, db) - return voucher + 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) + if data.source.id_ == data.destination.id_: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Source cannot be the same as destination", + ) + if data.source.id_ == CostCentre.cost_centre_purchase(): + batch_consumed = True + elif data.destination.id_ == CostCentre.cost_centre_purchase(): + batch_consumed = False + else: + batch_consumed = None + return voucher, batch_consumed -@router.get("/{id_}") +def save_inventories( + voucher: Voucher, + inventories: List[schema_in.Inventory], + batch_consumed: Optional[bool], + db: Session, +) -> Decimal: + amount: Decimal = Decimal(0) + for item in inventories: + batch = db.query(Batch).filter(Batch.id == item.batch.id_).first() + if batch_consumed and item.quantity > batch.quantity_remaining: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Quantity available is {batch.quantity_remaining} only", + ) + if batch.name > voucher.date: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Batch of {batch.product.name} was purchased after the issue date", + ) + if batch_consumed is None: + pass + elif batch_consumed: + batch.quantity_remaining -= item.quantity + else: + batch.quantity_remaining += item.quantity + + item = Inventory( + id_=item.id_, + product=batch.product, + quantity=item.quantity, + rate=batch.rate, + tax=batch.tax, + discount=batch.discount, + batch=batch, + ) + voucher.inventories.append(item) + db.add(item) + amount += round(item.amount, 2) + return amount + + +def save_journals( + voucher: Voucher, + source: schema_in.CostCentreLink, + destination: schema_in.CostCentreLink, + amount: Decimal, + db: Session, +): + s = Journal( + debit=-1, + account_id=AccountBase.all_purchases(), + amount=amount, + cost_centre_id=source.id_, + ) + d = Journal( + debit=1, + account_id=AccountBase.all_purchases(), + amount=amount, + cost_centre_id=destination.id_, + ) + voucher.journals.append(s) + db.add(s) + voucher.journals.append(d) + db.add(d) + + +def save_files(files: List[UploadFile], db: Session): + # for key, value in files.items(): + # db.add(DbImage(voucher.id, "voucher", value["f"], value["t"])) + pass + + +@router.put("/{id_}", response_model=output.Voucher) def update_route( id_: uuid.UUID, request: Request, - data: schemas.Voucher, + data: schema_in.IssueIn = Depends(schema_in.IssueIn.load_form), db: Session = Depends(get_db), - files: List[UploadFile] = File(...), - user: UserToken = Security(get_user, scopes=["journal"]), + i: List[UploadFile] = File(None), + t: List[UploadFile] = File(None), + user: UserToken = Security(get_user, scopes=["issue"]), ): try: - item: Voucher = update(id_, data, files, user, db) + i = i or [] + t = t or [] + item, batch_consumed = update(id_, data, user, db) + amount = update_inventories(item, data.inventories, batch_consumed, db) + update_journals(item, data.source, data.destination, amount) + update_files(data, i + t, db) db.commit() - set_date(request.session, data.date_) + set_date(data.date_.strftime("%d-%b-%Y"), 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), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -95,14 +209,177 @@ def update_route( ) -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 ["Issue"]: - voucher = issue_update_voucher(item, data, files, user, db) - elif data.type_ in ["Employee Benefit"]: - voucher = employee_benefit_update_voucher(item, data, files, user, db) - elif data.type_ in ["Incentive"]: - voucher = incentive_update_voucher(item, data, files, user, db) - return voucher +def update( + id_: uuid.UUID, data: schema_in.IssueIn, user: UserToken, db: Session +) -> (Voucher, Optional[bool]): + 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 item in voucher.journals: + if item.debit == 1: + destination = item.cost_centre_id + else: + source = item.cost_centre_id + + if source == CostCentre.cost_centre_purchase(): + old_batch_consumed = True + elif destination == CostCentre.cost_centre_purchase(): + old_batch_consumed = False + else: + old_batch_consumed = None + + if data.source.id_ == CostCentre.cost_centre_purchase(): + new_batch_consumed = True + elif data.destination.id_ == CostCentre.cost_centre_purchase(): + new_batch_consumed = False + else: + new_batch_consumed = None + + if new_batch_consumed != old_batch_consumed: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Purchase cost centre cannot be changed", + ) + return voucher, new_batch_consumed + + +def update_inventories( + voucher: Voucher, + inventories: List[schema_in.Inventory], + batch_consumed: Optional[bool], + db: Session, +): + amount: Decimal = Decimal(0) + for it in range(len(voucher.inventories), 0, -1): + item = voucher.inventories[it - 1] + found = False + for j in range(len(inventories), 0, -1): + i = inventories[j - 1] + if item.id == i.id_: + batch = db.query(Batch).filter(Batch.id == i.batch.id_).first() + found = True + if item.batch_id != batch.id: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Product / Batch cannot be changed", + ) + if ( + batch_consumed + and i.quantity - item.quantity > item.batch.quantity_remaining + ): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Maximum quantity available for {item.product.full_name} is {item.quantity + item.batch.quantity_remaining}", + ) + if item.batch.name > voucher.date: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Batch of {item.product.name} was purchased after the issue date", + ) + + if batch_consumed is None: + pass + elif batch_consumed: + item.batch.quantity_remaining -= i.quantity - item.quantity + else: + item.batch.quantity_remaining += i.quantity - item.quantity + + item.quantity = i.quantity + item.rate = batch.rate + item.tax = batch.tax + item.discount = batch.discount + amount += round(item.amount, 2) + + inventories.remove(i) + break + if not found: + if batch_consumed is None: + pass + elif batch_consumed: + item.batch.quantity_remaining += item.quantity + else: + if item.batch.quantity_remaining < item.quantity: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Product {item.product.name} cannot be removed, minimum quantity is {item.batch.quantity_remaining}", + ) + item.batch.quantity_remaining -= item.quantity + db.delete(item) + voucher.inventories.remove(item) + amount += save_inventories(voucher, inventories, batch_consumed, db) + return amount + + +def update_journals( + voucher: Voucher, + source: schema_in.CostCentreLink, + destination: schema_in.CostCentreLink, + amount: Decimal, +): + for i in range(len(voucher.journals), 0, -1): + item = voucher.journals[i - 1] + if item.debit == -1: + item.cost_centre_id = source.id_ + item.amount = amount + else: + item.cost_centre_id = destination.id_ + item.amount = amount + + +def update_files(data: schema_in.IssueIn, files: List[UploadFile], db: Session): + pass + # old_files = [ + # uuid.UUID(f["id"]) for f in json["files"] if "id" in f and f["id"] is not None + # ] + # images = db.query(DbImage).filter(DbImage.resource_id == voucher.id).all() + # for image in [i for i in images if i.id not in old_files]: + # db.delete(image) + # for key, value in files.items(): + # db.add(DbImage(voucher.id, "voucher", value["f"], value["t"])) + + +@router.get("/{id_}", response_model=output.Voucher) +def get_id( + id_: uuid.UUID, + db: Session = Depends(get_db), + user: UserToken = Security(get_user, scopes=["issue"]), +): + 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(), + ) + + +@router.get("") +def show_blank( + request: Request, + date: str = None, + source: str = None, + destination: str = None, + db: Session = Depends(get_db), + user: UserToken = Security(get_user, scopes=["issue"]), +): + date_ = date or get_date(request.session) + additional_info = {"date": date_, "type": "Issue"} + if source: + additional_info["source"] = source + if destination: + additional_info["destination"] = destination + return blank_voucher(additional_info, db) diff --git a/brewman/routers/voucher/issue_grid.py b/brewman/routers/issue_grid.py similarity index 91% rename from brewman/routers/voucher/issue_grid.py rename to brewman/routers/issue_grid.py index 4edd42db..26834bdc 100644 --- a/brewman/routers/voucher/issue_grid.py +++ b/brewman/routers/issue_grid.py @@ -5,9 +5,9 @@ from fastapi import APIRouter, Depends, Security, Request from sqlalchemy.orm import Session from sqlalchemy.orm.util import aliased -from ...schemas.auth import UserToken -from ...core.security import get_current_active_user as get_user -from ...db.session import SessionLocal +from ..schemas.auth import UserToken +from ..core.security import get_current_active_user as get_user +from ..db.session import SessionLocal from brewman.models.voucher import Voucher, Journal, VoucherType router = APIRouter() diff --git a/brewman/routers/journal.py b/brewman/routers/journal.py index a9cd8002..081cdbbc 100644 --- a/brewman/routers/journal.py +++ b/brewman/routers/journal.py @@ -2,11 +2,25 @@ 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 fastapi import ( + APIRouter, + HTTPException, + status, + Depends, + Security, + UploadFile, + File, + Request, +) 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 .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 @@ -49,8 +63,7 @@ def save_route( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -71,7 +84,9 @@ def save(data: schema_in.JournalIn, user: UserToken, db: Session) -> Voucher: ) db.add(voucher) for item in data.journals: - account: AccountBase = db.query(AccountBase).filter(AccountBase.id == item.account.id_).first() + account: AccountBase = db.query(AccountBase).filter( + AccountBase.id == item.account.id_ + ).first() journal = Journal( id=item.id_, amount=item.amount, @@ -111,8 +126,7 @@ def update_route( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -122,7 +136,9 @@ def update_route( ) -def update(id_: uuid.UUID, data: schema_in.JournalIn, user: UserToken, db: Session) -> Voucher: +def update( + id_: uuid.UUID, data: schema_in.JournalIn, 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) @@ -139,7 +155,11 @@ def update(id_: uuid.UUID, data: schema_in.JournalIn, user: UserToken, db: Sessi 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() + account = ( + db.query(AccountBase) + .filter(AccountBase.id == new_item.account.id_) + .first() + ) found = True item.debit = new_item.debit item.amount = new_item.amount @@ -150,7 +170,9 @@ def update(id_: uuid.UUID, data: schema_in.JournalIn, user: UserToken, db: Sessi 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() + account = ( + db.query(AccountBase).filter(AccountBase.id == new_item.account.id_).first() + ) journal = Journal( id=None, amount=new_item.amount, @@ -187,8 +209,7 @@ def get_id( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() diff --git a/brewman/routers/payment.py b/brewman/routers/payment.py index e69de29b..8d9707f3 100644 --- a/brewman/routers/payment.py +++ b/brewman/routers/payment.py @@ -0,0 +1,133 @@ +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 voucher_info, blank_voucher +from ..core.session import set_date, get_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 +from brewman.routers.journal import save, save_files, update, update_files +import brewman.schemas.voucher as output +import brewman.schemas.input as schema_in + +router = APIRouter() + + +# Dependency +def get_db() -> Session: + try: + db = SessionLocal() + yield db + finally: + db.close() + + +@router.post("", response_model=output.Voucher) +def save_route( + request: Request, + data: schema_in.JournalIn = Depends(schema_in.JournalIn.load_form), + db: Session = Depends(get_db), + i: List[UploadFile] = File(None), + t: List[UploadFile] = File(None), + user: UserToken = Security(get_user, scopes=["payment"]), +): + try: + i = i or [] + t = t or [] + item: Voucher = save(data, user, db) + save_files(i + t, 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(), + ) + + +@router.put("/{id_}", response_model=output.Voucher) +def update_route( + id_: uuid.UUID, + request: Request, + data: schema_in.JournalIn = Depends(schema_in.JournalIn.load_form), + db: Session = Depends(get_db), + i: List[UploadFile] = File(None), + t: List[UploadFile] = File(None), + user: UserToken = Security(get_user, scopes=["payment"]), +): + try: + i = i or [] + t = t or [] + item: Voucher = update(id_, data, user, db) + update_files(data, i + t, db) + db.commit() + set_date(data.date_.strftime("%d-%b-%Y"), request.session) + 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_}", response_model=output.Voucher) +def get_id( + id_: uuid.UUID, + db: Session = Depends(get_db), + user: UserToken = Security(get_user, scopes=["payment"]), +): + 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(), + ) + + +@router.get("") +def show_blank( + request: Request, + a: str = None, + db: Session = Depends(get_db), + user: UserToken = Security(get_user, scopes=["payment"]), +): + additional_info = {"date": get_date(request.session), "type": "Payment"} + if a: + additional_info["account"] = a + return blank_voucher(additional_info, db) diff --git a/brewman/routers/product_group.py b/brewman/routers/product_group.py index 6dc9b03b..fd1cbae0 100644 --- a/brewman/routers/product_group.py +++ b/brewman/routers/product_group.py @@ -11,6 +11,7 @@ from ..schemas.auth import UserToken from ..core.security import get_current_active_user as get_user from ..db.session import SessionLocal from brewman.models.master import ProductGroup + router = APIRouter() @@ -37,8 +38,7 @@ def save( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -68,8 +68,7 @@ def update( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -90,8 +89,7 @@ def delete( if item is None: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Product Group not found", + status_code=status.HTTP_404_NOT_FOUND, detail="Product Group not found", ) elif item.is_fixture: raise HTTPException( @@ -121,7 +119,7 @@ def show_blank( @router.get("/list", response_model=List[schemas.ProductGroup]) async def show_list(db: Session = Depends(get_db), user: UserToken = Depends(get_user)): - return[ + return [ {"id": item.id, "name": item.name, "isFixture": item.is_fixture} for item in db.query(ProductGroup).order_by(ProductGroup.name).all() ] diff --git a/brewman/routers/purchase.py b/brewman/routers/purchase.py index 026a33ff..283adc09 100644 --- a/brewman/routers/purchase.py +++ b/brewman/routers/purchase.py @@ -3,12 +3,26 @@ import uuid from decimal import Decimal from typing import List from datetime import datetime -from fastapi import APIRouter, HTTPException, status, Depends, Security, UploadFile, File, Request +from fastapi import ( + APIRouter, + HTTPException, + status, + Depends, + Security, + UploadFile, + File, + Request, +) from sqlalchemy import func 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 .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 Product, AccountBase from ..schemas.auth import UserToken @@ -53,8 +67,7 @@ def save_route( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -77,12 +90,31 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher: return voucher -def save_inventories(voucher: Voucher, inventories: List[schema_in.Inventory], db: Session): +def save_inventories( + voucher: Voucher, inventories: List[schema_in.Inventory], db: Session +): for item in inventories: - product: Product = db.query(Product).filter(Product.id == item.product.id_).first() - batch = Batch(name=voucher.date, product=product, quantity_remaining=item.quantity, rate=item.rate, tax=item.tax, discount=item.discount) + product: Product = db.query(Product).filter( + Product.id == item.product.id_ + ).first() + batch = Batch( + name=voucher.date, + product=product, + quantity_remaining=item.quantity, + rate=item.rate, + tax=item.tax, + discount=item.discount, + ) db.add(batch) - inventory = Inventory(id_=item.id_, product=product, batch=batch, quantity=item.quantity, rate=item.rate, tax=item.tax, discount=item.discount) + inventory = Inventory( + id_=item.id_, + product=product, + 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) @@ -145,8 +177,7 @@ def update_route( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -156,7 +187,9 @@ def update_route( ) -def update(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher: +def update( + id_: uuid.UUID, data: schema_in.PurchaseIn, 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) @@ -169,19 +202,26 @@ def update(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, db: Sess return voucher -def update_inventory(voucher: Voucher, new_inventories: List[schema_in.Inventory], db: Session): +def update_inventory( + voucher: Voucher, new_inventories: List[schema_in.Inventory], db: Session +): for it in range(len(voucher.inventories), 0, -1): item = voucher.inventories[it - 1] found = False for j in range(len(new_inventories), 0, -1): new_inventory = new_inventories[j - 1] if new_inventory.id_: - product = db.query(Product).filter(Product.id == new_inventory.product.id_).first() + product = ( + db.query(Product) + .filter(Product.id == new_inventory.product.id_) + .first() + ) found = True if item.product_id != new_inventory.product.id_: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Product cannot be changed") + detail="Product cannot be changed", + ) old_quantity = round(Decimal(item.quantity), 2) quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2) if new_inventory.quantity < (old_quantity - quantity_remaining): @@ -223,7 +263,9 @@ def update_inventory(voucher: Voucher, new_inventories: List[schema_in.Inventory db.delete(item) voucher.inventories.remove(item) for new_inventory in new_inventories: - product = db.query(Product).filter(Product.id == new_inventory.product.id_).first() + product = ( + db.query(Product).filter(Product.id == new_inventory.product.id_).first() + ) batch = Batch( name=voucher.date, product_id=product.id, @@ -312,8 +354,7 @@ def get_id( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() diff --git a/brewman/routers/purchase_return.py b/brewman/routers/purchase_return.py index 540a70fe..05268df0 100644 --- a/brewman/routers/purchase_return.py +++ b/brewman/routers/purchase_return.py @@ -3,14 +3,27 @@ import uuid from decimal import Decimal from typing import List from datetime import datetime -from fastapi import APIRouter, HTTPException, status, Depends, Security, UploadFile, File, Request -from sqlalchemy import func +from fastapi import ( + APIRouter, + HTTPException, + status, + Depends, + Security, + UploadFile, + File, + Request, +) 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 .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 Product, AccountBase +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 @@ -53,8 +66,7 @@ def save_route( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -77,14 +89,18 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher: return voucher -def save_inventories(voucher: Voucher, inventories: List[schema_in.Inventory], db: Session): +def save_inventories( + voucher: Voucher, inventories: List[schema_in.Inventory], db: Session +): for item in inventories: batch = db.query(Batch).filter(Batch.id == item.batch.id_).first() if item.quantity > batch.quantity_remaining: raise ValueError(f"Maximum quantity is {batch.quantity_remaining}.") if batch.name > voucher.date: - raise ValueError(f"Return date cannot be before {batch.product.name.strftime('%d-%b-%Y')}") + raise ValueError( + f"Return date cannot be before {batch.product.name.strftime('%d-%b-%Y')}" + ) batch.quantity_remaining -= item.quantity @@ -157,8 +173,7 @@ def update_route( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() @@ -168,7 +183,9 @@ def update_route( ) -def update(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher: +def update( + id_: uuid.UUID, data: schema_in.PurchaseIn, 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) @@ -181,7 +198,9 @@ def update(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, db: Sess return voucher -def update_inventory(voucher: Voucher, new_inventories: List[schema_in.Inventory], db: Session): +def update_inventory( + voucher: Voucher, new_inventories: List[schema_in.Inventory], db: Session +): for it in range(len(voucher.inventories), 0, -1): item = voucher.inventories[it - 1] found = False @@ -192,7 +211,8 @@ def update_inventory(voucher: Voucher, new_inventories: List[schema_in.Inventory if item.product_id != new_inventory.product.id_: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Product cannot be changed") + detail="Product cannot be changed", + ) old_quantity = round(Decimal(item.quantity), 2) quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2) if new_inventory.quantity - old_quantity > quantity_remaining: @@ -201,7 +221,9 @@ def update_inventory(voucher: Voucher, new_inventories: List[schema_in.Inventory detail=f"{old_quantity + quantity_remaining} is the maximum for {item.product.full_name}.", ) if item.batch.name > voucher.date: - raise ValueError(f"Voucher cannot be before {item.product.name.strftime('%d-%b-%Y')}") + raise ValueError( + f"Voucher cannot be before {item.product.name.strftime('%d-%b-%Y')}" + ) item.batch.quantity_remaining -= new_inventory.quantity - old_quantity item.quantity = new_inventory.quantity new_inventories.remove(new_inventory) @@ -275,8 +297,7 @@ def get_id( except SQLAlchemyError as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e), ) except Exception: db.rollback() diff --git a/brewman/routers/receipt.py b/brewman/routers/receipt.py index e69de29b..30a3cf5b 100644 --- a/brewman/routers/receipt.py +++ b/brewman/routers/receipt.py @@ -0,0 +1,133 @@ +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 voucher_info, blank_voucher +from ..core.session import set_date, get_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 +from brewman.routers.journal import save, save_files, update, update_files +import brewman.schemas.voucher as output +import brewman.schemas.input as schema_in + +router = APIRouter() + + +# Dependency +def get_db() -> Session: + try: + db = SessionLocal() + yield db + finally: + db.close() + + +@router.post("", response_model=output.Voucher) +def save_route( + request: Request, + data: schema_in.JournalIn = Depends(schema_in.JournalIn.load_form), + db: Session = Depends(get_db), + i: List[UploadFile] = File(None), + t: List[UploadFile] = File(None), + user: UserToken = Security(get_user, scopes=["receipt"]), +): + try: + i = i or [] + t = t or [] + item: Voucher = save(data, user, db) + save_files(i + t, 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(), + ) + + +@router.put("/{id_}", response_model=output.Voucher) +def update_route( + id_: uuid.UUID, + request: Request, + data: schema_in.JournalIn = Depends(schema_in.JournalIn.load_form), + db: Session = Depends(get_db), + i: List[UploadFile] = File(None), + t: List[UploadFile] = File(None), + user: UserToken = Security(get_user, scopes=["receipt"]), +): + try: + i = i or [] + t = t or [] + item: Voucher = update(id_, data, user, db) + update_files(data, i + t, db) + db.commit() + set_date(data.date_.strftime("%d-%b-%Y"), request.session) + 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_}", response_model=output.Voucher) +def get_id( + id_: uuid.UUID, + db: Session = Depends(get_db), + user: UserToken = Security(get_user, scopes=["receipt"]), +): + 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(), + ) + + +@router.get("") +def show_blank( + request: Request, + a: str = None, + db: Session = Depends(get_db), + user: UserToken = Security(get_user, scopes=["receipt"]), +): + additional_info = {"date": get_date(request.session), "type": "Receipt"} + if a: + additional_info["account"] = a + return blank_voucher(additional_info, db) diff --git a/brewman/routers/voucher/__init__.py b/brewman/routers/voucher/__init__.py index d37c56bf..b136db68 100644 --- a/brewman/routers/voucher/__init__.py +++ b/brewman/routers/voucher/__init__.py @@ -22,7 +22,6 @@ from brewman.models.voucher import ( Journal, ) from brewman.routers import get_lock_info -from .issue import issue_create_voucher, issue_update_voucher from .incentive import incentive_create_voucher, incentive_update_voucher from .employee_benefit import employee_benefit_create_voucher, employee_benefit_update_voucher from ...core.session import get_first_day @@ -195,6 +194,11 @@ def voucher_info(voucher, db): if voucher.type == 2: # "Purchase" item = [j for j in voucher.journals if j.debit == -1][0] json_voucher["vendor"] = {"id": item.account.id, "name": item.account.name} + elif voucher.type == 3: # "Issue" + item = [j for j in voucher.journals if j.debit == -1][0] + json_voucher["source"] = {"id": item.cost_centre_id, "name": ""} + item = [j for j in voucher.journals if j.debit == 1][0] + json_voucher["destination"] = {"id": item.cost_centre_id, "name": ""} else: for item in voucher.journals: json_voucher["journals"].append( @@ -340,52 +344,14 @@ def blank_voucher(info, db): {"account": AccountBase.local_purchase(), "amount": 0, "debit": 1} ) elif type_ == "Issue": - if "dource" in info and "destination" in info: - json_voucher["journals"].append( - { - "account": {"id": AccountBase.all_purchases()}, - "amount": 0, - "debit": -1, - "costCentre": {"id": info["source"]}, - } - ) - json_voucher["journals"].append( - { - "account": {"id": AccountBase.all_purchases()}, - "amount": 0, - "debit": 1, - "costCentre": {"id": info["destination"]}, - } - ) - elif "journals" not in info: - json_voucher["journals"].append( - { - "account": {"id": AccountBase.all_purchases()}, - "amount": 0, - "debit": -1, - "costCentre": {"id": CostCentre.cost_centre_purchase()}, - } - ) - json_voucher["journals"].append( - { - "account": {"id": AccountBase.all_purchases()}, - "amount": 0, - "debit": 1, - "costCentre": {"id": CostCentre.cost_centre_kitchen()}, - } - ) + if "source" in info: + json_voucher["source"] = {"id": info["source"]} else: - json_voucher["date"] = info["date"] - for item in info["journals"]: - json_voucher["journals"].append( - { - "account": {"id": item["account"]["id"]}, - "amount": 0, - "debit": item["debit"], - "costCentre": {"id": item["costCentre"]["id"]}, - } - ) - + json_voucher["source"] = {"id": CostCentre.cost_centre_purchase()} + if "destination" in info: + json_voucher["destination"] = {"id": info["destination"]} + else: + json_voucher["destination"] = {"id": CostCentre.cost_centre_kitchen()} elif type_ == "Employee Benefit": json_voucher["employeeBenefits"] = [] elif type_ == "Incentive": diff --git a/brewman/routers/voucher/emptyvoucher.py b/brewman/routers/voucher/emptyvoucher.py index cbfe17cf..10fc2563 100644 --- a/brewman/routers/voucher/emptyvoucher.py +++ b/brewman/routers/voucher/emptyvoucher.py @@ -6,43 +6,10 @@ class EmptyVoucher(object): def __init__(self, request): self.request = request - @router.get("/api/voucher", request_param="t=Payment") # "Payment" - def payment(self): - account = self.request.GET.get("a", None) - return self.get_blank({"account": account}) - - @router.get("/api/voucher", request_param="t=Receipt") # "Receipt" - def receipt(self): - account = self.request.GET.get("a", None) - return self.get_blank({"account": account}) - - @router.get("/api/voucher", request_param="t=Purchase Return") # "Purchase Return" - def purchase_return(self): - return self.get_blank() - @router.get("/api/voucher", request_param="t=Employee Benefit") # "Purchase Return" def purchase_return(self): return self.get_blank() - @router.get("/api/voucher", request_param="t=Issue") # "Issue" - def issue(self): - voucher_type = self.request.GET.get("t", None) - date = self.request.GET.get("date", None) - source = self.request.GET.get("source", None) - destination = self.request.GET.get("destination", None) - if date is not None and source is not None and destination is not None: - return blank_voucher( - { - "type": voucher_type, - "date": date, - "source": source, - "destination": destination, - }, - self.request.dbsession, - ) - else: - return self.get_blank() - @router.get("/api/voucher", request_param="t=Incentive") # "Incentive" def incentive(self): voucher_type = self.request.GET.get("t", None) diff --git a/brewman/routers/voucher/issue.py b/brewman/routers/voucher/issue.py deleted file mode 100644 index 46135abe..00000000 --- a/brewman/routers/voucher/issue.py +++ /dev/null @@ -1,246 +0,0 @@ -import datetime -from decimal import Decimal -import uuid - -from fastapi import HTTPException, status - -from brewman.models.master import CostCentre, AccountBase -from brewman.models.voucher import Voucher, VoucherType, Batch, Inventory, Journal - - -def issue_create_voucher(json, user, dbsession): - dt = datetime.datetime.strptime(json["date"], "%d-%b-%Y") - voucher = Voucher( - date=dt, - narration=json["narration"].strip(), - user_id=user.id, - type=VoucherType.by_name("Issue"), - ) - dbsession.add(voucher) - - for item in json["journals"]: - if int(item["debit"]) == 1: - destination = uuid.UUID(item["costCentre"]["id"]) - else: - source = uuid.UUID(item["costCentre"]["id"]) - if source == destination: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Source cannot be the same as destination", - ) - if source == CostCentre.cost_centre_purchase(): - batch_consumed = True - elif destination == CostCentre.cost_centre_purchase(): - batch_consumed = False - else: - batch_consumed = None - for item in json["inventories"]: - issue_create_inventory(voucher, item, batch_consumed, dbsession) - for item in issue_create_journals(voucher.inventories, source, destination): - voucher.journals.append(item) - dbsession.add(item) - journals_valid(voucher) - inventory_valid(voucher) - return voucher - - -def issue_create_inventory(voucher, item, batch_consumed, dbsession): - batch = ( - dbsession.query(Batch) - .filter(Batch.id == uuid.UUID(item["batch"]["id"])) - .first() - ) - inventory_id = ( - uuid.UUID(item["id"]) if "id" in item and item["id"] is not None else None - ) - quantity = round(Decimal(item["quantity"]), 2) - - if quantity <= 0: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Quantity of {item.product.name} cannot be zero", - ) - if batch_consumed == True and quantity > batch.quantity_remaining: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Quantity available is {batch.quantity_remaining} only", - ) - if batch.name > voucher.date: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Batch of {batch.product.name} was purchased after the issue date", - ) - if batch_consumed is None: - pass - elif batch_consumed: - batch.quantity_remaining -= quantity - else: - batch.quantity_remaining += quantity - - item = Inventory( - id=inventory_id, - product_id=batch.product.id, - quantity=quantity, - rate=batch.rate, - tax=batch.tax, - discount=batch.discount, - batch=batch, - ) - voucher.inventories.append(item) - dbsession.add(item) - - -def issue_create_journals(inventories, source, destination): - amount = 0 - for item in inventories: - amount += item.amount - return [ - Journal( - debit=-1, - account_id=AccountBase.all_purchases(), - amount=round(amount, 2), - cost_centre_id=source, - ), - Journal( - debit=1, - account_id=AccountBase.all_purchases(), - amount=round(amount, 2), - cost_centre_id=destination, - ), - ] - - -def issue_update_voucher(voucher, json, user, dbsession): - voucher.date = datetime.datetime.strptime(json["date"], "%d-%b-%Y") - voucher.narration = json["narration"].strip() - voucher.user_id = user.id - voucher.last_edit_date = datetime.datetime.utcnow() - - for item in voucher.journals: - if item.debit == 1: - destination = item.cost_centre_id - else: - source = item.cost_centre_id - if source == CostCentre.cost_centre_purchase(): - old_batch_consumed = True - elif destination == CostCentre.cost_centre_purchase(): - old_batch_consumed = False - else: - old_batch_consumed = None - - for item in json["journals"]: - if int(item["debit"]) == 1: - destination = uuid.UUID(item["costCentre"]["id"]) - else: - source = uuid.UUID(item["costCentre"]["id"]) - if source == destination: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Source cannot be the same as destination", - ) - - if source == CostCentre.cost_centre_purchase(): - new_batch_consumed = True - elif destination == CostCentre.cost_centre_purchase(): - new_batch_consumed = False - else: - new_batch_consumed = None - - if new_batch_consumed != old_batch_consumed: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Purchase cost centre cannot be changed", - ) - - issue_update_inventory(voucher, json["inventories"], old_batch_consumed, dbsession) - issue_update_journals(voucher, source, destination) - journals_valid(voucher) - inventory_valid(voucher) - return voucher - - -def issue_update_inventory(voucher, new_inventories, batch_consumed, dbsession): - for it in range(len(voucher.inventories), 0, -1): - item = voucher.inventories[it - 1] - found = False - for j in range(len(new_inventories), 0, -1): - i = new_inventories[j - 1] - if "id" in i and i["id"] is not None and item.id == uuid.UUID(i["id"]): - batch = ( - dbsession.query(Batch) - .filter(Batch.id == uuid.UUID(i["batch"]["id"])) - .first() - ) - found = True - if item.batch_id != batch.id: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Product / Batch cannot be changed", - ) - new_quantity = round(Decimal(i["quantity"]), 2) - old_quantity = round(Decimal(item.quantity), 2) - quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2) - if new_quantity <= 0: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Quantity of {item.product.name} cannot be zero", - ) - if ( - batch_consumed is True - and new_quantity - old_quantity > quantity_remaining - ): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Maximum quantity available for {item.product.full_name} is {old_quantity + quantity_remaining}", - ) - if item.batch.name > voucher.date: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Batch of {item.product.name} was purchased after the issue date", - ) - - if batch_consumed is None: - pass - elif batch_consumed: - item.batch.quantity_remaining -= new_quantity - old_quantity - else: - item.batch.quantity_remaining += new_quantity - old_quantity - - item.quantity = new_quantity - item.rate = batch.rate - item.tax = batch.tax - item.discount = batch.discount - - new_inventories.remove(i) - break - if not found: - if batch_consumed is None: - pass - elif batch_consumed: - item.batch.quantity_remaining += item.quantity - else: - if item.batch.quantity_remaining < item.quantity: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Product {item.product.name} cannot be removed, minimum quantity is {item.batch.quantity_remaining}", - ) - item.batch.quantity_remaining -= item.quantity - dbsession.delete(item) - voucher.inventories.remove(item) - for i in new_inventories: - issue_create_inventory(voucher, i, batch_consumed, dbsession) - - -def issue_update_journals(voucher, source, destination): - amount = 0 - for item in voucher.inventories: - amount += item.amount - - for i in range(len(voucher.journals), 0, -1): - item = voucher.journals[i - 1] - if item.debit == -1: - item.cost_centre_id = source - item.amount = round(amount, 2) - else: - item.cost_centre_id = destination - item.amount = round(amount, 2) diff --git a/brewman/schemas/input.py b/brewman/schemas/input.py index f2397309..fe064a2c 100644 --- a/brewman/schemas/input.py +++ b/brewman/schemas/input.py @@ -9,7 +9,7 @@ from pydantic import validator from sqlalchemy.orm import Session from brewman.schemas import to_camel -from brewman.schemas.master import AccountLink, CostCentreLink, ProductLink +from brewman.schemas.master import AccountLink, CostCentreLink from brewman.schemas.voucher import VoucherIn, Journal, Inventory @@ -84,12 +84,45 @@ class PurchaseIn(VoucherIn): json_data = json.loads(data) return cls.parse_obj(json_data) - def validate_inventory(self, date_: date, old_inv: List[Inventory], db: Session): - for it in range(len(old_inv), 0, -1): - item = old_inv[it - 1] - found = False - for j in range(len(self.inventories), 0, -1): - i = self.inventories[j - 1] +class IssueIn(VoucherIn): + source: CostCentreLink + destination: CostCentreLink + inventories: List[Inventory] - pass \ No newline at end of file + 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("inventories") # For Purchase, Issue and Return Vouchers + def validate_enough_inventories(cls, value: List[Inventory]): + if len(value) < 1: + raise ValueError("Not enough inventories") + return value + + @validator("inventories") # For Purchase, Issue and Return Vouchers + def validate_inventories_unique(cls, value: List[Inventory]): + if len(set(x.product.id_ for x in value)) != len(value): + raise ValueError("Duplicate products") + return value + + @validator("destination") # For Purchase, Issue and Return Vouchers + def source_destination_unique(cls, value: CostCentreLink, values): + if value.id_ == values['source'].id_: + raise ValueError("Source and destination cannot be the same") + return value + + @classmethod + def load_form(cls, data: str = Form(...)): + json_data = json.loads(data) + return cls.parse_obj(json_data) diff --git a/brewman/schemas/voucher.py b/brewman/schemas/voucher.py index a009c983..e8557ca5 100644 --- a/brewman/schemas/voucher.py +++ b/brewman/schemas/voucher.py @@ -102,6 +102,8 @@ class Voucher(VoucherIn): journals: List[Journal] inventories: List[Inventory] vendor: Optional[AccountLink] + source: Optional[CostCentreLink] + destination: Optional[CostCentreLink] files: List[Any] class Config: diff --git a/overlord/src/app/core/voucher.ts b/overlord/src/app/core/voucher.ts index 6f2d0180..e610345e 100644 --- a/overlord/src/app/core/voucher.ts +++ b/overlord/src/app/core/voucher.ts @@ -11,6 +11,8 @@ export class Voucher { narration: string; incentive?: number; vendor?: Account; + source?: CostCentre; + destination?: CostCentre; journals: Journal[]; inventories: Inventory[]; employeeBenefits: EmployeeBenefit[]; diff --git a/overlord/src/app/issue/issue.component.html b/overlord/src/app/issue/issue.component.html index 4d01f7c7..1f09a2fc 100644 --- a/overlord/src/app/issue/issue.component.html +++ b/overlord/src/app/issue/issue.component.html @@ -13,14 +13,14 @@ - + {{ costCentre.name }} - + {{ costCentre.name }} diff --git a/overlord/src/app/issue/issue.component.ts b/overlord/src/app/issue/issue.component.ts index 543d4ac9..e3b2c9ff 100644 --- a/overlord/src/app/issue/issue.component.ts +++ b/overlord/src/app/issue/issue.component.ts @@ -7,7 +7,7 @@ import {BehaviorSubject, Observable, of as observableOf} from 'rxjs'; import {debounceTime, distinctUntilChanged, map, startWith, switchMap} from 'rxjs/operators'; import {IssueDataSource} from './issue-datasource'; import {VoucherService} from '../core/voucher.service'; -import {Batch, Inventory, Journal, Voucher} from '../core/voucher'; +import {Batch, Inventory, Voucher} from '../core/voucher'; import * as moment from 'moment'; import {AuthService} from '../auth/auth.service'; import {ConfirmDialogComponent} from '../shared/confirm-dialog/confirm-dialog.component'; @@ -17,7 +17,7 @@ import {BatchService} from '../core/batch.service'; import {IssueGridService} from './issue-grid.service'; import {CostCentre} from '../core/cost-centre'; import {IssueGridDataSource} from './issue-grid-datasource'; -import {Hotkey, HotkeysService} from "angular2-hotkeys"; +import {Hotkey, HotkeysService} from 'angular2-hotkeys'; @Component({ selector: 'app-issue', @@ -32,8 +32,6 @@ export class IssueComponent implements OnInit, AfterViewInit, OnDestroy { dataSource: IssueDataSource; gridDataSource: IssueGridDataSource; form: FormGroup; - sourceJournal: Journal; - destinationJournal: Journal; voucher: Voucher; costCentres: CostCentre[]; batch: Batch; @@ -74,8 +72,9 @@ export class IssueComponent implements OnInit, AfterViewInit, OnDestroy { return false; // Prevent bubbling }, ['INPUT', 'SELECT', 'TEXTAREA'])); this.hotkeys.add(new Hotkey('ctrl+s', (event: KeyboardEvent): boolean => { - if (this.canSave()) + if (this.canSave()) { this.save(); + } return false; // Prevent bubbling }, ['INPUT', 'SELECT', 'TEXTAREA'])); } @@ -90,13 +89,11 @@ export class IssueComponent implements OnInit, AfterViewInit, OnDestroy { loadVoucher(voucher: Voucher) { this.voucher = voucher; - this.sourceJournal = this.voucher.journals.filter(x => x.debit === -1)[0]; - this.destinationJournal = this.voucher.journals.filter(x => x.debit === 1)[0]; this.form.setValue({ date: moment(this.voucher.date, 'DD-MMM-YYYY').toDate(), - sourceCostCentre: this.sourceJournal.costCentre.id, - destinationCostCentre: this.destinationJournal.costCentre.id, - amount: this.sourceJournal.amount, + source: this.voucher.source.id, + destination: this.voucher.destination.id, + amount: Math.abs(this.voucher.inventories.map((x) => x.amount).reduce((p, c) => p + c, 0)), addRow: { batch: '', quantity: '' @@ -116,10 +113,7 @@ export class IssueComponent implements OnInit, AfterViewInit, OnDestroy { addRow() { const formValue = this.form.get('addRow').value; const quantity = +(formValue.quantity); - const isConsumption = this.voucher.journals - .filter( - x => x.debit === -1 - )[0].costCentre.id === '7b845f95-dfef-fa4a-897c-f0baf15284a3'; + const isConsumption = this.form.value.source === '7b845f95-dfef-fa4a-897c-f0baf15284a3'; if (this.batch === null || quantity <= 0) { return; } @@ -169,8 +163,6 @@ export class IssueComponent implements OnInit, AfterViewInit, OnDestroy { updateView() { this.inventoryObservable.next(this.voucher.inventories); const amount = Math.abs(this.voucher.inventories.map((x) => x.amount).reduce((p, c) => p + c, 0)); - this.sourceJournal.amount = amount; - this.destinationJournal.amount = amount; this.form.get('amount').setValue(amount); } @@ -201,8 +193,8 @@ export class IssueComponent implements OnInit, AfterViewInit, OnDestroy { createForm() { this.form = this.fb.group({ date: '', - sourceCostCentre: '', - destinationCostCentre: '', + source: '', + destination: '', amount: {value: '', disabled: true}, addRow: this.fb.group({ batch: '', @@ -243,8 +235,8 @@ export class IssueComponent implements OnInit, AfterViewInit, OnDestroy { getVoucher(): Voucher { const formModel = this.form.value; this.voucher.date = moment(formModel.date).format('DD-MMM-YYYY'); - this.sourceJournal.costCentre.id = formModel.sourceCostCentre; - this.destinationJournal.costCentre.id = formModel.destinationCostCentre; + this.voucher.source.id = formModel.source; + this.voucher.destination.id = formModel.destination; this.voucher.narration = formModel.narration; return this.voucher; }