brewman/brewman/brewman/routers/issue.py

352 lines
11 KiB
Python

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)