Rate Contract is checked during save and update of Purchase at the backend

This commit is contained in:
Amritanshu Agrawal 2021-09-13 13:01:34 +05:30
parent ceaf93d1cd
commit d34c8ea0a4
5 changed files with 155 additions and 52 deletions
brewman/brewman/routers
overlord/src/app

@ -1,6 +1,7 @@
import uuid
from typing import List
from datetime import datetime
from typing import List, Optional
import brewman.schemas.product as schemas
@ -15,6 +16,8 @@ from ..models.account import Account
from ..models.batch import Batch
from ..models.inventory import Inventory
from ..models.product import Product
from ..models.rate_contract import RateContract
from ..models.rate_contract_item import RateContractItem
from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType
from ..schemas.user import UserToken
@ -138,6 +141,8 @@ async def show_term(
c: int = None,
p: bool = None,
e: bool = False,
v: Optional[uuid.UUID] = None,
d: Optional[str] = None,
current_user: UserToken = Depends(get_user),
):
count = c
@ -145,20 +150,37 @@ async def show_term(
list_ = []
with SessionFuture() as db:
for index, item in enumerate(Product.query(q, p, a, db)):
rc_price = None
if v is not None and d is not None:
date_ = datetime.strptime(d, "%d-%b-%Y")
contracts = select(RateContract.id).where(
RateContract.vendor_id == v, RateContract.valid_from <= date_, RateContract.valid_till >= date_
)
rc_price = db.execute(
select(RateContractItem.price).where(
RateContractItem.product_id == item.id, RateContractItem.rate_contract_id.in_(contracts)
)
).scalar_one_or_none()
list_.append(
{
"id": item.id,
"name": item.name,
"price": item.price,
"price": item.price if rc_price is None else rc_price,
"units": item.units,
"fraction": item.fraction,
"fractionUnits": item.fraction_units,
"productYield": item.product_yield,
"isSold": item.is_sold,
"salePrice": item.sale_price,
"isRateContracted": False if rc_price is None else True,
}
if extended
else {"id": item.id, "name": item.full_name, "price": item.price}
else {
"id": item.id,
"name": item.full_name,
"price": item.price if rc_price is None else rc_price,
"isRateContracted": False if rc_price is None else True,
}
)
if count is not None and index == count - 1:
break

@ -1,8 +1,8 @@
import uuid
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from typing import List
from typing import List, Optional
import brewman.schemas.input as schema_in
import brewman.schemas.voucher as output
@ -20,6 +20,8 @@ from ..models.batch import Batch
from ..models.inventory import Inventory
from ..models.journal import Journal
from ..models.product import Product
from ..models.rate_contract import RateContract
from ..models.rate_contract_item import RateContractItem
from ..models.validations import check_inventories_are_valid, check_journals_are_valid
from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType
@ -44,7 +46,7 @@ def save_route(
try:
with SessionFuture() as db:
item: Voucher = save(data, user, db)
save_inventories(item, data.inventories, db)
save_inventories(item, data.vendor.id_, data.inventories, db)
check_inventories_are_valid(item)
save_journals(item, data.vendor, db)
check_journals_are_valid(item)
@ -91,9 +93,18 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
return voucher
def save_inventories(voucher: Voucher, inventories: List[InventorySchema], db: Session):
def save_inventories(voucher: Voucher, vendor_id: uuid.UUID, inventories: List[InventorySchema], db: Session):
for item in inventories:
product: Product = db.execute(select(Product).where(Product.id == item.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 != item.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:
item.tax = 0
item.discount = 0
batch = Batch(
name=voucher.date,
product=product,
@ -156,7 +167,7 @@ def update_route(
try:
with SessionFuture() as db:
item: Voucher = update_voucher(id_, data, user, db)
update_inventory(item, data.inventories, db)
update_inventory(item, data.vendor.id_, data.inventories, db)
check_inventories_are_valid(item)
update_journals(item, data.vendor, db)
check_journals_are_valid(item)
@ -203,45 +214,54 @@ def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken,
return voucher
def update_inventory(voucher: Voucher, new_inventories: List[InventorySchema], db: Session):
def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, 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:
product = db.execute(select(Product).where(Product.id == new_inventory.product.id_)).scalar_one()
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:
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[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 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
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
else:
has_been_issued = db.execute(
select(func.count(Inventory.id)).where(Inventory.batch_id == item.batch.id, Inventory.id != item.id)
).scalar()
@ -256,6 +276,15 @@ def update_inventory(voucher: Voucher, new_inventories: List[InventorySchema], d
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,
@ -344,3 +373,14 @@ def show_blank(
additional_info = {"date": get_date(request.session), "type": "Purchase"}
with SessionFuture() as db:
return blank_voucher(additional_info, db)
def rate_contract_price(product_id: uuid.UUID, vendor_id: uuid.UUID, date_: date, db: Session) -> Optional[Decimal]:
contracts = select(RateContract.id).where(
RateContract.vendor_id == vendor_id, RateContract.valid_from <= date_, RateContract.valid_till >= date_
)
return db.execute(
select(RateContractItem.price).where(
RateContractItem.product_id == product_id, RateContractItem.rate_contract_id.in_(contracts)
)
).scalar_one_or_none()

@ -15,6 +15,7 @@ export class Product {
isPurchased: boolean;
isSold: boolean;
productGroup?: ProductGroup;
isRateContracted?: boolean;
public constructor(init?: Partial<Product>) {
this.code = 0;

@ -51,8 +51,16 @@ export class ProductService {
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Product>;
}
autocomplete(query: string, extended: boolean = false): Observable<Product[]> {
autocomplete(
query: string,
extended: boolean = false,
date?: string,
vendorId?: string,
): Observable<Product[]> {
const options = { params: new HttpParams().set('q', query).set('e', extended.toString()) };
if (!!vendorId && !!date) {
options.params = options.params.set('v', vendorId as string).set('d', date as string);
}
return this.http
.get<Product[]>(`${url}/query`, options)
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<Product[]>;

@ -94,7 +94,16 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
map((x) => (x !== null && x.length >= 1 ? x : null)),
debounceTime(150),
distinctUntilChanged(),
switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocomplete(x))),
switchMap((x) =>
x === null
? observableOf([])
: this.productSer.autocomplete(
x,
false,
moment(this.form.value.date).format('DD-MMM-YYYY'),
this.form.value.account.id,
),
),
);
}
@ -182,10 +191,17 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
addRow() {
const formValue = (this.form.get('addRow') as FormControl).value;
const quantity = this.math.parseAmount(formValue.quantity, 2);
const price = this.math.parseAmount(formValue.price, 2);
const tax = this.math.parseAmount(formValue.tax, 5);
const discount = this.math.parseAmount(formValue.discount, 5);
if (this.product === null || quantity <= 0 || price <= 0) {
if (this.product === null || quantity <= 0) {
return;
}
const price = this.product.isRateContracted
? this.product.price
: this.math.parseAmount(formValue.price, 2);
const tax = this.product.isRateContracted ? 0 : this.math.parseAmount(formValue.tax, 5);
const discount = this.product.isRateContracted
? 0
: this.math.parseAmount(formValue.discount, 5);
if (price <= 0 || tax < 0 || discount < 0) {
return;
}
const oldFiltered = this.voucher.inventories.filter(
@ -219,6 +235,9 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
discount: '',
});
this.product = null;
((this.form.get('addRow') as FormControl).get('price') as FormControl).enable();
((this.form.get('addRow') as FormControl).get('tax') as FormControl).enable();
((this.form.get('addRow') as FormControl).get('discount') as FormControl).enable();
setTimeout(() => {
if (this.productElement) {
this.productElement.nativeElement.focus();
@ -344,9 +363,22 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
}
productSelected(event: MatAutocompleteSelectedEvent): void {
const product = event.option.value;
const product: Product = event.option.value;
this.product = product;
((this.form.get('addRow') as FormControl).get('price') as FormControl).setValue(product.price);
if (product.isRateContracted) {
((this.form.get('addRow') as FormControl).get('price') as FormControl).disable();
((this.form.get('addRow') as FormControl).get('tax') as FormControl).disable();
((this.form.get('addRow') as FormControl).get('discount') as FormControl).disable();
((this.form.get('addRow') as FormControl).get('tax') as FormControl).setValue('RC');
((this.form.get('addRow') as FormControl).get('discount') as FormControl).setValue('RC');
} else {
((this.form.get('addRow') as FormControl).get('price') as FormControl).enable();
((this.form.get('addRow') as FormControl).get('tax') as FormControl).enable();
((this.form.get('addRow') as FormControl).get('discount') as FormControl).enable();
((this.form.get('addRow') as FormControl).get('tax') as FormControl).setValue('');
((this.form.get('addRow') as FormControl).get('discount') as FormControl).setValue('');
}
}
zoomImage(file: DbFile) {