import uuid from datetime import datetime from decimal import Decimal from typing import List 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 import func 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, Product 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.PurchaseIn = Depends(schema_in.PurchaseIn.load_form), db: Session = Depends(get_db), i: List[bytes] = File(None), t: List[bytes] = File(None), user: UserToken = Security(get_user, scopes=["purchase"]), ): try: item: Voucher = save(data, user, db) save_inventories(item, data.inventories, db) save_journals(item, data.vendor, 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.PurchaseIn, 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) return voucher 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, ) db.add(batch) 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) def save_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session): vendor = db.query(AccountBase).filter(AccountBase.id == ven.id_).first() journals = {} amount = 0 for item in voucher.inventories: account = item.product.account amount += round(item.amount, 2) if account.id in journals: journals[account.id].amount += round(item.amount, 2) else: journals[account.id] = Journal( debit=1, cost_centre_id=account.cost_centre_id, account_id=account.id, amount=round(item.amount, 2), ) journals[vendor.id] = Journal( debit=-1, cost_centre_id=vendor.cost_centre_id, account_id=vendor.id, amount=amount, ) for item in journals.values(): voucher.journals.append(item) db.add(item) @router.put("/{id_}", response_model=output.Voucher) def update_route( id_: uuid.UUID, request: Request, data: schema_in.PurchaseIn = Depends(schema_in.PurchaseIn.load_form), db: Session = Depends(get_db), i: List[bytes] = File(None), t: List[bytes] = File(None), user: UserToken = Security(get_user, scopes=["purchase"]), ): try: item: Voucher = update(id_, data, user, db) update_inventory(item, data.inventories, db) update_journals(item, data.vendor, db) # journals_valid(voucher) update_files(item.id, data.files, 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 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) 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() return voucher 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_ == item.id: 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", ) old_quantity = round(Decimal(item.quantity), 2) quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2) if new_inventory.quantity < (old_quantity - quantity_remaining): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"{old_quantity - quantity_remaining} is the minimum as it has been issued", ) item.batch.quantity_remaining -= old_quantity - new_inventory.quantity item.quantity = new_inventory.quantity if voucher.date != item.batch.name: item.batch.name = voucher.date if voucher.date < item.batch.name: # TODO: check for issued products which might have been in a back date pass item.rate = new_inventory.rate item.batch.rate = new_inventory.rate item.discount = new_inventory.discount item.batch.discount = new_inventory.discount item.tax = new_inventory.tax item.batch.tax = new_inventory.tax product.price = new_inventory.rate new_inventories.remove(new_inventory) # TODO: Update all references of the batch with the new rates break if not found: has_been_issued = ( db.query(func.count(Inventory.id)) .filter(Inventory.batch_id == item.batch.id) .filter(Inventory.id != item.id) .scalar() ) if has_been_issued > 0: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"{item.product.name} has been issued, it cannot be deleted", ) else: db.delete(item.batch) 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() ) batch = Batch( name=voucher.date, product_id=product.id, quantity_remaining=new_inventory.quantity, rate=new_inventory.rate, tax=new_inventory.tax, discount=new_inventory.discount, ) inventory = Inventory( id_=None, product_id=product.id, batch=batch, quantity=new_inventory.quantity, rate=new_inventory.rate, tax=new_inventory.tax, discount=new_inventory.discount, ) inventory.voucher_id = voucher.id db.add(batch) inventory.batch_id = batch.id product.price = new_inventory.rate voucher.inventories.append(inventory) db.add(inventory) def update_journals(voucher: Voucher, ven: schema_in.AccountLink, db): vendor = db.query(AccountBase).filter(AccountBase.id == ven.id_).first() journals = {} amount = 0 for item in voucher.inventories: product = db.query(Product).filter(Product.id == item.product_id).first() account = product.account 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[vendor.id] = Journal( debit=-1, cost_centre_id=vendor.cost_centre_id, account_id=vendor.id, amount=amount, ) for i in range(len(voucher.journals), 0, -1): item = voucher.journals[i - 1] if item.account_id in journals: item.debit = journals[item.account_id].debit item.amount = round(journals[item.account_id].amount, 2) item.cost_centre_id = journals[item.account_id].cost_centre_id del journals[item.account_id] else: db.delete(item) voucher.journals.remove(item) for item in journals.values(): item.amount = round(item.amount, 2) voucher.journals.append(item) db.add(item) @router.get("/{id_}", response_model=output.Voucher) def get_id( id_: uuid.UUID, db: Session = Depends(get_db), user: UserToken = Security(get_user, scopes=["purchase"]), ): 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, db: Session = Depends(get_db), user: UserToken = Security(get_user, scopes=["purchase"]), ): additional_info = {"date": get_date(request.session), "type": "Purchase"} return blank_voucher(additional_info, db)