diff --git a/brewman/brewman/routers/batch_integrity.py b/brewman/brewman/routers/batch_integrity.py index 1716e2fc..d93c62c1 100644 --- a/brewman/brewman/routers/batch_integrity.py +++ b/brewman/brewman/routers/batch_integrity.py @@ -30,13 +30,13 @@ def post_check_batch_integrity( ) -> List[schemas.BatchIntegrity]: with SessionFuture() as db: - info = batches(db) + batch_dates(db) + info = negative_batches(db) + batch_dates(db) fix_batch_prices(db) db.commit() return info -def batches(db: Session) -> List[schemas.BatchIntegrity]: +def negative_batches(db: Session) -> List[schemas.BatchIntegrity]: inv_sum = func.sum(Inventory.quantity * Journal.debit).label("quantity") list_ = db.execute( select(Batch, Product.full_name, inv_sum) @@ -153,20 +153,36 @@ def batch_dates(db: Session) -> List[schemas.BatchIntegrity]: def fix_batch_prices(db: Session) -> None: - quantities = ( + list_ = ( db.execute( select(Batch) - .join(Batch.product) .join(Batch.inventories) - .join(Inventory.voucher) .where(Batch.rate != Inventory.rate) - .options(contains_eager(Batch.product), contains_eager(Batch.inventories).contains_eager(Inventory.voucher)) + .options(contains_eager(Batch.inventories)) ) .unique() .scalars() .all() ) - for batch in quantities: + for batch in list_: for inv in batch.inventories: - refresh_voucher(inv.voucher.id, batch.product_id, db) + refresh_voucher(inv.voucher_id, inv.product_id, db) + + +def fix_single_batch_prices(batch_id: uuid.UUID, db: Session) -> None: + list_ = ( + db.execute( + select(Batch) + .join(Batch.inventories) + .where(Batch.id == batch_id, Batch.rate != Inventory.rate) + .options(contains_eager(Batch.inventories)) + ) + .unique() + .scalars() + .all() + ) + + for batch in list_: + for inv in batch.inventories: + refresh_voucher(inv.voucher_id, inv.product_id, db) diff --git a/brewman/brewman/routers/employee_benefit.py b/brewman/brewman/routers/employee_benefit.py index 44438b5f..f3345694 100644 --- a/brewman/brewman/routers/employee_benefit.py +++ b/brewman/brewman/routers/employee_benefit.py @@ -215,15 +215,12 @@ def update_employee_benefits( exp, total = 0, 0 for i in range(len(voucher.employee_benefits), 0, -1): item = voucher.employee_benefits[i - 1] - found = False - for j in range(len(employee_benefits), 0, -1): - new_item = employee_benefits[j - 1] - if new_item.id_ == item.id: - exp += item.esi_er + item.pf_er - total += item.esi_ee + item.pf_ee + item.esi_er + item.pf_er - employee_benefits.remove(new_item) - break - if not found: + index = next((idx for (idx, d) in enumerate(employee_benefits) if d.id_ == item.id), None) + if index is not None: + employee_benefits.pop(index) + exp += item.esi_er + item.pf_er + total += item.esi_ee + item.pf_ee + item.esi_er + item.pf_er + else: voucher.employee_benefits.remove(item) voucher.journals.remove(item.journal) new_exp, new_total = save_employee_benefits(voucher, employee_benefits, days_in_month, db) diff --git a/brewman/brewman/routers/issue.py b/brewman/brewman/routers/issue.py index f5a50e76..dd2f9d0f 100644 --- a/brewman/brewman/routers/issue.py +++ b/brewman/brewman/routers/issue.py @@ -28,7 +28,12 @@ from ..schemas.inventory import Inventory as InventorySchema from ..schemas.user import UserToken from . import get_lock_info from .db_image import save_files, update_files -from .voucher import blank_voucher, check_voucher_edit_allowed, voucher_info +from .voucher import ( + blank_voucher, + check_voucher_edit_allowed, + get_batch_quantity, + voucher_info, +) router = APIRouter() @@ -251,60 +256,57 @@ def update_inventories( batch_consumed: Optional[bool], db: Session, ): + old_set = set([(i.id, i.batch_id) for i in voucher.inventories]) + new_set = set([(i.id_, i.batch.id_) for i in inventories if i.id_ is not None]) + if len(new_set - old_set): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Product / Batch cannot be changed", + ) 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.execute(select(Batch).where(Batch.id == i.batch.id_)).scalar_one() - 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} " - f"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", - ) + batch = db.execute(select(Batch).where(Batch.id == item.batch_id)).scalar_one() + batch_quantity = get_batch_quantity(item.batch_id, voucher.id, db) + index = next((idx for (idx, d) in enumerate(inventories) if d.id_ == item.id), None) + if index is not None: + new_inventory = inventories.pop(index) + if batch_consumed and new_inventory.quantity > batch_quantity: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Maximum quantity available for {item.product.full_name} is {batch_quantity}", + ) + 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 + item.batch.quantity_remaining = batch_quantity - new_inventory.quantity else: - if item.batch.quantity_remaining < item.quantity: + item.batch.quantity_remaining = batch_quantity + new_inventory.quantity + + item.quantity = new_inventory.quantity + item.rate = batch.rate + item.tax = batch.tax + item.discount = batch.discount + amount += round(item.amount, 2) + else: + if batch_consumed is None: + pass + elif batch_consumed: + item.batch.quantity_remaining = batch_quantity + else: + if batch_quantity < item.quantity: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Product {item.product.name} cannot be removed," - f" minimum quantity is {item.batch.quantity_remaining}", + f" minimum quantity is {batch_quantity}", ) - item.batch.quantity_remaining -= item.quantity + item.batch.quantity_remaining = batch_quantity db.delete(item) voucher.inventories.remove(item) amount += save_inventories(voucher, inventories, batch_consumed, db) diff --git a/brewman/brewman/routers/journal.py b/brewman/brewman/routers/journal.py index ba67894d..63914421 100644 --- a/brewman/brewman/routers/journal.py +++ b/brewman/brewman/routers/journal.py @@ -146,19 +146,15 @@ def update_voucher(id_: uuid.UUID, data: schema_in.JournalIn, user: UserToken, d for i in range(len(voucher.journals), 0, -1): item = voucher.journals[i - 1] - found = False - for j in range(len(data.journals), 0, -1): - new_item = data.journals[j - 1] - if new_item.id_ is not None and item.id == new_item.id_: - account = db.execute(select(AccountBase).where(AccountBase.id == new_item.account.id_)).scalar_one() - found = True - item.debit = new_item.debit - item.amount = round(new_item.amount, 2) - item.account_id = account.id - item.cost_centre_id = account.cost_centre_id - data.journals.remove(new_item) - break - if not found: + index = next((idx for (idx, d) in enumerate(data.journals) if d.id_ == item.id), None) + if index is not None: + new_item = data.journals.pop(index) + account = db.execute(select(AccountBase).where(AccountBase.id == new_item.account.id_)).scalar_one() + item.debit = new_item.debit + item.amount = round(new_item.amount, 2) + item.account_id = account.id + item.cost_centre_id = account.cost_centre_id + else: voucher.journals.remove(item) for new_item in data.journals: account = db.execute(select(AccountBase).where(AccountBase.id == new_item.account.id_)).scalar_one() diff --git a/brewman/brewman/routers/purchase.py b/brewman/brewman/routers/purchase.py index ea4dd275..77aeb724 100644 --- a/brewman/brewman/routers/purchase.py +++ b/brewman/brewman/routers/purchase.py @@ -28,8 +28,14 @@ from ..models.voucher_type import VoucherType from ..schemas.inventory import Inventory as InventorySchema from ..schemas.user import UserToken from . import get_lock_info +from .batch_integrity import fix_single_batch_prices from .db_image import save_files, update_files -from .voucher import blank_voucher, check_voucher_edit_allowed, voucher_info +from .voucher import ( + blank_voucher, + check_voucher_edit_allowed, + get_batch_quantity, + voucher_info, +) router = APIRouter() @@ -224,11 +230,17 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, new_inventories: Li ) for it in range(len(voucher.inventories), 0, -1): item = voucher.inventories[it - 1] + quantity_consumed = -1 * get_batch_quantity(item.batch_id, voucher.id, db) index = next((idx for (idx, d) in enumerate(new_inventories) if d.id_ == item.id), None) if index is not None: new_inventory = new_inventories.pop(index) product = db.execute(select(Product).where(Product.id == new_inventory.product.id_)).scalar_one() rc_price = rate_contract_price(product.id, vendor_id, voucher.date, db) + if batch_has_older_vouchers(item.batch_id, voucher.date, voucher.id, db): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"{item.product.name} has older vouchers", + ) if rc_price is not None and rc_price != new_inventory.rate: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, @@ -237,20 +249,14 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, new_inventories: Li if rc_price is not None: new_inventory.tax = 0 new_inventory.discount = 0 - old_quantity = round(Decimal(item.quantity), 2) - quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2) - if new_inventory.quantity < (old_quantity - quantity_remaining): + if new_inventory.quantity < quantity_consumed: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"{old_quantity - quantity_remaining} is the minimum as it has been issued", + detail=f"{quantity_consumed} is the minimum as it has been issued", ) - item.batch.quantity_remaining -= old_quantity - new_inventory.quantity + item.batch.quantity_remaining = new_inventory.quantity - quantity_consumed 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.batch.name = voucher.date item.rate = new_inventory.rate item.batch.rate = new_inventory.rate item.discount = new_inventory.discount @@ -258,7 +264,8 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, new_inventories: Li item.tax = new_inventory.tax item.batch.tax = new_inventory.tax product.price = new_inventory.rate - # TODO: Update all references of the batch with the new rates + db.flush() + fix_single_batch_prices(item.batch_id, db) else: has_been_issued = db.execute( select(func.count(Inventory.id)).where(Inventory.batch_id == item.batch.id, Inventory.id != item.id) @@ -272,43 +279,10 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, new_inventories: Li db.delete(item.batch) db.delete(item) voucher.inventories.remove(item) - for new_inventory in new_inventories: - product = db.execute(select(Product).where(Product.id == new_inventory.product.id_)).scalar_one() - rc_price = rate_contract_price(product.id, vendor_id, voucher.date, db) - if rc_price is not None and rc_price != new_inventory.rate: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Product price does not match the Rate Contract price", - ) - if rc_price is not None: - new_inventory.tax = 0 - new_inventory.discount = 0 - 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) + save_inventories(voucher, vendor_id, new_inventories, db) -def update_journals(voucher: Voucher, ven: schema_in.AccountLink, db): +def update_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session): vendor = db.execute(select(AccountBase).where(AccountBase.id == ven.id_)).scalar_one() journals = {} amount = 0 @@ -382,3 +356,13 @@ def rate_contract_price(product_id: uuid.UUID, vendor_id: uuid.UUID, date_: date RateContractItem.product_id == product_id, RateContractItem.rate_contract_id.in_(contracts) ) ).scalar_one_or_none() + + +def batch_has_older_vouchers(id_: uuid.UUID, date_: date, voucher_id: uuid.UUID, db: Session) -> bool: + count_ = db.execute( + select(func.count()) + .join(Batch.inventories) + .join(Inventory.voucher) + .where(Batch.id == id_, Voucher.date < date_, Voucher.id != voucher_id) + ).scalar_one() + return count_ > 0 diff --git a/brewman/brewman/routers/purchase_return.py b/brewman/brewman/routers/purchase_return.py index 5dc22627..a5a3e86e 100644 --- a/brewman/brewman/routers/purchase_return.py +++ b/brewman/brewman/routers/purchase_return.py @@ -1,7 +1,6 @@ import uuid from datetime import datetime -from decimal import Decimal from typing import List import brewman.schemas.input as schema_in @@ -27,7 +26,12 @@ from ..schemas.inventory import Inventory as InventorySchema from ..schemas.user import UserToken from . import get_lock_info from .db_image import save_files, update_files -from .voucher import blank_voucher, check_voucher_edit_allowed, voucher_info +from .voucher import ( + blank_voucher, + check_voucher_edit_allowed, + get_batch_quantity, + voucher_info, +) router = APIRouter() @@ -210,36 +214,37 @@ def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, def update_inventory(voucher: Voucher, new_inventories: List[InventorySchema], db: Session): + old_set = set([(i.id, i.product_id) for i in voucher.inventories]) + new_set = set([(i.id_, i.product.id_) for i in new_inventories if i.id_ is not None]) + if len(new_set - old_set): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Product cannot be changed", + ) 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: - 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 maximum for {item.product.full_name}.", - ) - if item.batch.name > voucher.date: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=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) - break - if not found: - item.batch.quantity_remaining += item.quantity + batch = db.execute(select(Batch).where(Batch.id == item.batch_id)).scalar_one() + batch_quantity = get_batch_quantity(item.batch_id, voucher.id, db) + index = next((idx for (idx, d) in enumerate(new_inventories) if d.id_ == item.id), None) + if index is not None: + new_inventory = new_inventories.pop(index) + if new_inventory.quantity > batch_quantity: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"{batch_quantity} is the maximum for {item.product.full_name}.", + ) + if batch.name > voucher.date: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Voucher cannot be before {item.product.name.strftime('%d-%b-%Y')}", + ) + item.batch.quantity_remaining = batch_quantity - new_inventory.quantity + item.quantity = new_inventory.quantity + item.rate = batch.rate + item.tax = batch.tax + item.discount = batch.discount + else: + item.batch.quantity_remaining = batch_quantity voucher.inventories.remove(item) db.delete(item) save_inventories(voucher, new_inventories, db) diff --git a/brewman/brewman/routers/rate_contract.py b/brewman/brewman/routers/rate_contract.py index 5f16dc67..8c73ff4e 100644 --- a/brewman/brewman/routers/rate_contract.py +++ b/brewman/brewman/routers/rate_contract.py @@ -97,20 +97,15 @@ async def update_route( def update_items(rate_contract: RateContract, items: List[RateContractItemSchema], db: Session): for it in range(len(rate_contract.items), 0, -1): item = rate_contract.items[it - 1] - for j in range(len(items), 0, -1): - new_item = items[j - 1] - if new_item.id_ == item.id: - item.product_id = new_item.product.id_ - item.price = new_item.price - items.remove(new_item) - break + index = next((idx for (idx, d) in enumerate(items) if d.id_ == item.id), None) + if index is not None: + new_item = items.pop(index) + item.product_id = new_item.product.id_ + item.price = new_item.price else: db.delete(item) rate_contract.items.remove(item) - for item in items: - rci = RateContractItem(rate_contract_id=rate_contract.id, product_id=item.product.id_, price=item.price) - rate_contract.items.append(rci) - db.add(rci) + add_items(rate_contract, items, db) @router.delete("/{id_}", response_model=RateContractInSchema) diff --git a/brewman/brewman/routers/voucher.py b/brewman/brewman/routers/voucher.py index cc412b3f..82b59bed 100644 --- a/brewman/brewman/routers/voucher.py +++ b/brewman/brewman/routers/voucher.py @@ -2,6 +2,7 @@ import uuid from datetime import datetime from decimal import Decimal +from typing import Optional import brewman.schemas.voucher as output @@ -428,3 +429,15 @@ def check_voucher_edit_allowed(voucher: Voucher, user: UserToken): status_code=status.HTTP_403_FORBIDDEN, detail="You are not allowed to edit other user's vouchers", ) + + +def get_batch_quantity(id_: uuid.UUID, voucher_id: Optional[uuid.UUID], db: Session) -> Decimal: + query = ( + select(func.sum(Inventory.quantity * Journal.debit)) + .join(Inventory.voucher) + .join(Voucher.journals) + .where(Inventory.batch_id == id_, Journal.cost_centre_id == CostCentre.cost_centre_purchase()) + ) + if voucher_id is not None: + query = query.where(Voucher.id != voucher_id) + return db.execute(query).scalar_one()