From d34c8ea0a4eebd33e38c5fe876c886ed35cabf37 Mon Sep 17 00:00:00 2001 From: tanshu <git@tanshu.com> Date: Mon, 13 Sep 2021 13:01:34 +0530 Subject: [PATCH] Rate Contract is checked during save and update of Purchase at the backend --- brewman/brewman/routers/product.py | 28 +++- brewman/brewman/routers/purchase.py | 124 ++++++++++++------ overlord/src/app/core/product.ts | 1 + overlord/src/app/product/product.service.ts | 10 +- .../src/app/purchase/purchase.component.ts | 44 ++++++- 5 files changed, 155 insertions(+), 52 deletions(-) diff --git a/brewman/brewman/routers/product.py b/brewman/brewman/routers/product.py index 3efe473a..ed849369 100644 --- a/brewman/brewman/routers/product.py +++ b/brewman/brewman/routers/product.py @@ -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 diff --git a/brewman/brewman/routers/purchase.py b/brewman/brewman/routers/purchase.py index c43fcff2..f49998b0 100644 --- a/brewman/brewman/routers/purchase.py +++ b/brewman/brewman/routers/purchase.py @@ -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() diff --git a/overlord/src/app/core/product.ts b/overlord/src/app/core/product.ts index 20597861..32c06d40 100644 --- a/overlord/src/app/core/product.ts +++ b/overlord/src/app/core/product.ts @@ -15,6 +15,7 @@ export class Product { isPurchased: boolean; isSold: boolean; productGroup?: ProductGroup; + isRateContracted?: boolean; public constructor(init?: Partial<Product>) { this.code = 0; diff --git a/overlord/src/app/product/product.service.ts b/overlord/src/app/product/product.service.ts index c0d4da11..32b761d3 100644 --- a/overlord/src/app/product/product.service.ts +++ b/overlord/src/app/product/product.service.ts @@ -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[]>; diff --git a/overlord/src/app/purchase/purchase.component.ts b/overlord/src/app/purchase/purchase.component.ts index 76a266cb..69130058 100644 --- a/overlord/src/app/purchase/purchase.component.ts +++ b/overlord/src/app/purchase/purchase.component.ts @@ -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) {