import uuid from datetime import datetime from decimal import Decimal from typing import List, Optional import brewman.schemas.input as schema_in import brewman.schemas.voucher as output from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session from ..core.security import get_current_active_user as get_user from ..core.session import get_date, set_date from ..db.session import SessionLocal from ..models import AccountBase, CostCentre from ..models.voucher import Batch, Inventory, Journal, Voucher, VoucherType from ..schemas.auth import UserToken from .db_image import save_files, update_files from .voucher import ( blank_voucher, check_voucher_edit_allowed, check_voucher_lock_info, voucher_info, ) 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.IssueIn = Depends(schema_in.IssueIn.load_form), db: Session = Depends(get_db), i: List[bytes] = File(None), t: List[bytes] = File(None), user: UserToken = Security(get_user, scopes=["issue"]), ): try: 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(item.id, 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 def save( data: schema_in.IssueIn, user: UserToken, db: Session ) -> (Voucher, Optional[bool]): 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) 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 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) @router.put("/{id_}", response_model=output.Voucher) def update_route( id_: uuid.UUID, request: Request, data: schema_in.IssueIn = Depends(schema_in.IssueIn.load_form), db: Session = Depends(get_db), i: List[bytes] = File(None), t: List[bytes] = File(None), user: UserToken = Security(get_user, scopes=["issue"]), ): try: 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(item.id, data.files, i, t, db) db.commit() 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), ) except Exception: db.rollback() raise 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 @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 @router.get("", response_model=output.Voucher) 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)