Fix: Purchase edit will make sure that there are no backdated issues.

Fix: Purchase edit will also update issue prices.
Chore: Simplify loop-in-loops
This commit is contained in:
2021-09-25 10:58:21 +05:30
parent 6d0d95070b
commit 119c5b71f1
8 changed files with 169 additions and 161 deletions
+24 -8
View File
@@ -30,13 +30,13 @@ def post_check_batch_integrity(
) -> List[schemas.BatchIntegrity]: ) -> List[schemas.BatchIntegrity]:
with SessionFuture() as db: with SessionFuture() as db:
info = batches(db) + batch_dates(db) info = negative_batches(db) + batch_dates(db)
fix_batch_prices(db) fix_batch_prices(db)
db.commit() db.commit()
return info 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") inv_sum = func.sum(Inventory.quantity * Journal.debit).label("quantity")
list_ = db.execute( list_ = db.execute(
select(Batch, Product.full_name, inv_sum) 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: def fix_batch_prices(db: Session) -> None:
quantities = ( list_ = (
db.execute( db.execute(
select(Batch) select(Batch)
.join(Batch.product)
.join(Batch.inventories) .join(Batch.inventories)
.join(Inventory.voucher)
.where(Batch.rate != Inventory.rate) .where(Batch.rate != Inventory.rate)
.options(contains_eager(Batch.product), contains_eager(Batch.inventories).contains_eager(Inventory.voucher)) .options(contains_eager(Batch.inventories))
) )
.unique() .unique()
.scalars() .scalars()
.all() .all()
) )
for batch in quantities: for batch in list_:
for inv in batch.inventories: 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)
+6 -9
View File
@@ -215,15 +215,12 @@ def update_employee_benefits(
exp, total = 0, 0 exp, total = 0, 0
for i in range(len(voucher.employee_benefits), 0, -1): for i in range(len(voucher.employee_benefits), 0, -1):
item = voucher.employee_benefits[i - 1] item = voucher.employee_benefits[i - 1]
found = False index = next((idx for (idx, d) in enumerate(employee_benefits) if d.id_ == item.id), None)
for j in range(len(employee_benefits), 0, -1): if index is not None:
new_item = employee_benefits[j - 1] employee_benefits.pop(index)
if new_item.id_ == item.id: exp += item.esi_er + item.pf_er
exp += item.esi_er + item.pf_er total += item.esi_ee + item.pf_ee + item.esi_er + item.pf_er
total += item.esi_ee + item.pf_ee + item.esi_er + item.pf_er else:
employee_benefits.remove(new_item)
break
if not found:
voucher.employee_benefits.remove(item) voucher.employee_benefits.remove(item)
voucher.journals.remove(item.journal) voucher.journals.remove(item.journal)
new_exp, new_total = save_employee_benefits(voucher, employee_benefits, days_in_month, db) new_exp, new_total = save_employee_benefits(voucher, employee_benefits, days_in_month, db)
+45 -43
View File
@@ -28,7 +28,12 @@ from ..schemas.inventory import Inventory as InventorySchema
from ..schemas.user import UserToken from ..schemas.user import UserToken
from . import get_lock_info from . import get_lock_info
from .db_image import save_files, update_files 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() router = APIRouter()
@@ -251,60 +256,57 @@ def update_inventories(
batch_consumed: Optional[bool], batch_consumed: Optional[bool],
db: Session, 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) amount: Decimal = Decimal(0)
for it in range(len(voucher.inventories), 0, -1): for it in range(len(voucher.inventories), 0, -1):
item = voucher.inventories[it - 1] item = voucher.inventories[it - 1]
found = False batch = db.execute(select(Batch).where(Batch.id == item.batch_id)).scalar_one()
for j in range(len(inventories), 0, -1): batch_quantity = get_batch_quantity(item.batch_id, voucher.id, db)
i = inventories[j - 1] index = next((idx for (idx, d) in enumerate(inventories) if d.id_ == item.id), None)
if item.id == i.id_: if index is not None:
batch = db.execute(select(Batch).where(Batch.id == i.batch.id_)).scalar_one() new_inventory = inventories.pop(index)
found = True if batch_consumed and new_inventory.quantity > batch_quantity:
if item.batch_id != batch.id: raise HTTPException(
raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Maximum quantity available for {item.product.full_name} is {batch_quantity}",
detail="Product / Batch cannot be changed", )
) if item.batch.name > voucher.date:
if batch_consumed and i.quantity - item.quantity > item.batch.quantity_remaining: raise HTTPException(
raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Batch of {item.product.name} was purchased after the issue date",
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",
)
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: if batch_consumed is None:
pass pass
elif batch_consumed: elif batch_consumed:
item.batch.quantity_remaining += item.quantity item.batch.quantity_remaining = batch_quantity - new_inventory.quantity
else: 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( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Product {item.product.name} cannot be removed," 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) db.delete(item)
voucher.inventories.remove(item) voucher.inventories.remove(item)
amount += save_inventories(voucher, inventories, batch_consumed, db) amount += save_inventories(voucher, inventories, batch_consumed, db)
+9 -13
View File
@@ -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): for i in range(len(voucher.journals), 0, -1):
item = voucher.journals[i - 1] item = voucher.journals[i - 1]
found = False index = next((idx for (idx, d) in enumerate(data.journals) if d.id_ == item.id), None)
for j in range(len(data.journals), 0, -1): if index is not None:
new_item = data.journals[j - 1] new_item = data.journals.pop(index)
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()
account = db.execute(select(AccountBase).where(AccountBase.id == new_item.account.id_)).scalar_one() item.debit = new_item.debit
found = True item.amount = round(new_item.amount, 2)
item.debit = new_item.debit item.account_id = account.id
item.amount = round(new_item.amount, 2) item.cost_centre_id = account.cost_centre_id
item.account_id = account.id else:
item.cost_centre_id = account.cost_centre_id
data.journals.remove(new_item)
break
if not found:
voucher.journals.remove(item) voucher.journals.remove(item)
for new_item in data.journals: for new_item in data.journals:
account = db.execute(select(AccountBase).where(AccountBase.id == new_item.account.id_)).scalar_one() account = db.execute(select(AccountBase).where(AccountBase.id == new_item.account.id_)).scalar_one()
+31 -47
View File
@@ -28,8 +28,14 @@ from ..models.voucher_type import VoucherType
from ..schemas.inventory import Inventory as InventorySchema from ..schemas.inventory import Inventory as InventorySchema
from ..schemas.user import UserToken from ..schemas.user import UserToken
from . import get_lock_info from . import get_lock_info
from .batch_integrity import fix_single_batch_prices
from .db_image import save_files, update_files 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() 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): for it in range(len(voucher.inventories), 0, -1):
item = voucher.inventories[it - 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) index = next((idx for (idx, d) in enumerate(new_inventories) if d.id_ == item.id), None)
if index is not None: if index is not None:
new_inventory = new_inventories.pop(index) new_inventory = new_inventories.pop(index)
product = db.execute(select(Product).where(Product.id == new_inventory.product.id_)).scalar_one() 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) 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: if rc_price is not None and rc_price != new_inventory.rate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 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: if rc_price is not None:
new_inventory.tax = 0 new_inventory.tax = 0
new_inventory.discount = 0 new_inventory.discount = 0
old_quantity = round(Decimal(item.quantity), 2) if new_inventory.quantity < quantity_consumed:
quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2)
if new_inventory.quantity < (old_quantity - quantity_remaining):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 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 item.quantity = new_inventory.quantity
if voucher.date != item.batch.name: item.batch.name = voucher.date
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.rate = new_inventory.rate
item.batch.rate = new_inventory.rate item.batch.rate = new_inventory.rate
item.discount = new_inventory.discount 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.tax = new_inventory.tax
item.batch.tax = new_inventory.tax item.batch.tax = new_inventory.tax
product.price = new_inventory.rate 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: else:
has_been_issued = db.execute( has_been_issued = db.execute(
select(func.count(Inventory.id)).where(Inventory.batch_id == item.batch.id, Inventory.id != item.id) 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.batch)
db.delete(item) db.delete(item)
voucher.inventories.remove(item) voucher.inventories.remove(item)
for new_inventory in new_inventories: save_inventories(voucher, vendor_id, new_inventories, db)
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)
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() vendor = db.execute(select(AccountBase).where(AccountBase.id == ven.id_)).scalar_one()
journals = {} journals = {}
amount = 0 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) RateContractItem.product_id == product_id, RateContractItem.rate_contract_id.in_(contracts)
) )
).scalar_one_or_none() ).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
+35 -30
View File
@@ -1,7 +1,6 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from decimal import Decimal
from typing import List from typing import List
import brewman.schemas.input as schema_in import brewman.schemas.input as schema_in
@@ -27,7 +26,12 @@ from ..schemas.inventory import Inventory as InventorySchema
from ..schemas.user import UserToken from ..schemas.user import UserToken
from . import get_lock_info from . import get_lock_info
from .db_image import save_files, update_files 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() 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): 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): for it in range(len(voucher.inventories), 0, -1):
item = voucher.inventories[it - 1] item = voucher.inventories[it - 1]
found = False batch = db.execute(select(Batch).where(Batch.id == item.batch_id)).scalar_one()
for j in range(len(new_inventories), 0, -1): batch_quantity = get_batch_quantity(item.batch_id, voucher.id, db)
new_inventory = new_inventories[j - 1] index = next((idx for (idx, d) in enumerate(new_inventories) if d.id_ == item.id), None)
if new_inventory.id_ == item.id: if index is not None:
found = True new_inventory = new_inventories.pop(index)
if item.product_id != new_inventory.product.id_: if new_inventory.quantity > batch_quantity:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Product cannot be changed", detail=f"{batch_quantity} is the maximum for {item.product.full_name}.",
) )
old_quantity = round(Decimal(item.quantity), 2) if batch.name > voucher.date:
quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2) raise HTTPException(
if new_inventory.quantity - old_quantity > quantity_remaining: status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
raise HTTPException( detail=f"Voucher cannot be before {item.product.name.strftime('%d-%b-%Y')}",
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, )
detail=f"{old_quantity + quantity_remaining} is the maximum for {item.product.full_name}.", item.batch.quantity_remaining = batch_quantity - new_inventory.quantity
) item.quantity = new_inventory.quantity
if item.batch.name > voucher.date: item.rate = batch.rate
raise HTTPException( item.tax = batch.tax
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, item.discount = batch.discount
detail=f"Voucher cannot be before {item.product.name.strftime('%d-%b-%Y')}", else:
) item.batch.quantity_remaining = batch_quantity
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
voucher.inventories.remove(item) voucher.inventories.remove(item)
db.delete(item) db.delete(item)
save_inventories(voucher, new_inventories, db) save_inventories(voucher, new_inventories, db)
+6 -11
View File
@@ -97,20 +97,15 @@ async def update_route(
def update_items(rate_contract: RateContract, items: List[RateContractItemSchema], db: Session): def update_items(rate_contract: RateContract, items: List[RateContractItemSchema], db: Session):
for it in range(len(rate_contract.items), 0, -1): for it in range(len(rate_contract.items), 0, -1):
item = rate_contract.items[it - 1] item = rate_contract.items[it - 1]
for j in range(len(items), 0, -1): index = next((idx for (idx, d) in enumerate(items) if d.id_ == item.id), None)
new_item = items[j - 1] if index is not None:
if new_item.id_ == item.id: new_item = items.pop(index)
item.product_id = new_item.product.id_ item.product_id = new_item.product.id_
item.price = new_item.price item.price = new_item.price
items.remove(new_item)
break
else: else:
db.delete(item) db.delete(item)
rate_contract.items.remove(item) rate_contract.items.remove(item)
for item in items: add_items(rate_contract, items, db)
rci = RateContractItem(rate_contract_id=rate_contract.id, product_id=item.product.id_, price=item.price)
rate_contract.items.append(rci)
db.add(rci)
@router.delete("/{id_}", response_model=RateContractInSchema) @router.delete("/{id_}", response_model=RateContractInSchema)
+13
View File
@@ -2,6 +2,7 @@ import uuid
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
from typing import Optional
import brewman.schemas.voucher as output 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, status_code=status.HTTP_403_FORBIDDEN,
detail="You are not allowed to edit other user's vouchers", 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()