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

@ -1,8 +1,8 @@
import uuid import uuid
from datetime import datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from typing import List from typing import List, Optional
import brewman.schemas.input as schema_in import brewman.schemas.input as schema_in
import brewman.schemas.voucher as output import brewman.schemas.voucher as output
@ -20,6 +20,8 @@ from ..models.batch import Batch
from ..models.inventory import Inventory from ..models.inventory import Inventory
from ..models.journal import Journal from ..models.journal import Journal
from ..models.product import Product 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.validations import check_inventories_are_valid, check_journals_are_valid
from ..models.voucher import Voucher from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType from ..models.voucher_type import VoucherType
@ -44,7 +46,7 @@ def save_route(
try: try:
with SessionFuture() as db: with SessionFuture() as db:
item: Voucher = save(data, user, 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) check_inventories_are_valid(item)
save_journals(item, data.vendor, db) save_journals(item, data.vendor, db)
check_journals_are_valid(item) check_journals_are_valid(item)
@ -91,9 +93,18 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
return 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: for item in inventories:
product: Product = db.execute(select(Product).where(Product.id == item.product.id_)).scalar_one() 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( batch = Batch(
name=voucher.date, name=voucher.date,
product=product, product=product,
@ -156,7 +167,7 @@ def update_route(
try: try:
with SessionFuture() as db: with SessionFuture() as db:
item: Voucher = update_voucher(id_, data, user, 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) check_inventories_are_valid(item)
update_journals(item, data.vendor, db) update_journals(item, data.vendor, db)
check_journals_are_valid(item) check_journals_are_valid(item)
@ -203,45 +214,54 @@ def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken,
return voucher 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): for it in range(len(voucher.inventories), 0, -1):
item = voucher.inventories[it - 1] item = voucher.inventories[it - 1]
found = False index = next((idx for (idx, d) in enumerate(new_inventories) if d.id_ == item.id), None)
for j in range(len(new_inventories), 0, -1): if index is not None:
new_inventory = new_inventories[j - 1] new_inventory = new_inventories[index]
if new_inventory.id_ == item.id: 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)
found = True if rc_price is not None and rc_price != new_inventory.rate:
if item.product_id != new_inventory.product.id_: raise HTTPException(
raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Product price does not match the Rate Contract price",
detail="Product cannot be changed", )
) if rc_price is not None:
old_quantity = round(Decimal(item.quantity), 2) new_inventory.tax = 0
quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2) new_inventory.discount = 0
if new_inventory.quantity < (old_quantity - quantity_remaining): old_quantity = round(Decimal(item.quantity), 2)
raise HTTPException( quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2)
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, if new_inventory.quantity < (old_quantity - quantity_remaining):
detail=f"{old_quantity - quantity_remaining} is the minimum as it has been issued", raise HTTPException(
) status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
item.batch.quantity_remaining -= old_quantity - new_inventory.quantity detail=f"{old_quantity - quantity_remaining} is the minimum as it has been issued",
item.quantity = new_inventory.quantity )
if voucher.date != item.batch.name: item.batch.quantity_remaining -= old_quantity - new_inventory.quantity
item.batch.name = voucher.date item.quantity = new_inventory.quantity
if voucher.date < item.batch.name: if voucher.date != item.batch.name:
# TODO: check for issued products which might have been in a back date item.batch.name = voucher.date
pass if voucher.date < item.batch.name:
item.rate = new_inventory.rate # TODO: check for issued products which might have been in a back date
item.batch.rate = new_inventory.rate pass
item.discount = new_inventory.discount item.rate = new_inventory.rate
item.batch.discount = new_inventory.discount item.batch.rate = new_inventory.rate
item.tax = new_inventory.tax item.discount = new_inventory.discount
item.batch.tax = new_inventory.tax item.batch.discount = new_inventory.discount
product.price = new_inventory.rate item.tax = new_inventory.tax
new_inventories.remove(new_inventory) item.batch.tax = new_inventory.tax
# TODO: Update all references of the batch with the new rates product.price = new_inventory.rate
break new_inventories.remove(new_inventory)
if not found: # TODO: Update all references of the batch with the new rates
break
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)
).scalar() ).scalar()
@ -256,6 +276,15 @@ def update_inventory(voucher: Voucher, new_inventories: List[InventorySchema], d
voucher.inventories.remove(item) voucher.inventories.remove(item)
for new_inventory in new_inventories: for new_inventory in new_inventories:
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)
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( batch = Batch(
name=voucher.date, name=voucher.date,
product_id=product.id, product_id=product.id,
@ -344,3 +373,14 @@ def show_blank(
additional_info = {"date": get_date(request.session), "type": "Purchase"} additional_info = {"date": get_date(request.session), "type": "Purchase"}
with SessionFuture() as db: with SessionFuture() as db:
return blank_voucher(additional_info, 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; isPurchased: boolean;
isSold: boolean; isSold: boolean;
productGroup?: ProductGroup; productGroup?: ProductGroup;
isRateContracted?: boolean;
public constructor(init?: Partial<Product>) { public constructor(init?: Partial<Product>) {
this.code = 0; this.code = 0;

@ -51,8 +51,16 @@ export class ProductService {
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Product>; .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()) }; 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 return this.http
.get<Product[]>(`${url}/query`, options) .get<Product[]>(`${url}/query`, options)
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<Product[]>; .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)), map((x) => (x !== null && x.length >= 1 ? x : null)),
debounceTime(150), debounceTime(150),
distinctUntilChanged(), 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() { addRow() {
const formValue = (this.form.get('addRow') as FormControl).value; const formValue = (this.form.get('addRow') as FormControl).value;
const quantity = this.math.parseAmount(formValue.quantity, 2); const quantity = this.math.parseAmount(formValue.quantity, 2);
const price = this.math.parseAmount(formValue.price, 2); if (this.product === null || quantity <= 0) {
const tax = this.math.parseAmount(formValue.tax, 5); return;
const discount = this.math.parseAmount(formValue.discount, 5); }
if (this.product === null || quantity <= 0 || price <= 0) { 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; return;
} }
const oldFiltered = this.voucher.inventories.filter( const oldFiltered = this.voucher.inventories.filter(
@ -219,6 +235,9 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
discount: '', discount: '',
}); });
this.product = null; 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(() => { setTimeout(() => {
if (this.productElement) { if (this.productElement) {
this.productElement.nativeElement.focus(); this.productElement.nativeElement.focus();
@ -344,9 +363,22 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
} }
productSelected(event: MatAutocompleteSelectedEvent): void { productSelected(event: MatAutocompleteSelectedEvent): void {
const product = event.option.value; const product: Product = event.option.value;
this.product = product; this.product = product;
((this.form.get('addRow') as FormControl).get('price') as FormControl).setValue(product.price); ((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) { zoomImage(file: DbFile) {