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:
Amritanshu Agrawal 2021-09-25 10:58:21 +05:30
parent 6d0d95070b
commit 119c5b71f1
8 changed files with 169 additions and 161 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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)

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):
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()

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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()