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

This commit is contained in:
2021-09-13 13:01:34 +05:30
parent ceaf93d1cd
commit d34c8ea0a4
5 changed files with 155 additions and 52 deletions

View File

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

View File

@ -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,20 +214,29 @@ 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):
for it in range(len(voucher.inventories), 0, -1): old_set = set([(i.id, i.product_id) for i in voucher.inventories])
item = voucher.inventories[it - 1] new_set = set([(i.id_, i.product.id_) for i in new_inventories if i.id_ is not None])
found = False if len(new_set - old_set):
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( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Product cannot be changed", detail="Product cannot be changed",
) )
for it in range(len(voucher.inventories), 0, -1):
item = voucher.inventories[it - 1]
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) old_quantity = round(Decimal(item.quantity), 2)
quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2) quantity_remaining = round(Decimal(item.batch.quantity_remaining), 2)
if new_inventory.quantity < (old_quantity - quantity_remaining): if new_inventory.quantity < (old_quantity - quantity_remaining):
@ -241,7 +261,7 @@ def update_inventory(voucher: Voucher, new_inventories: List[InventorySchema], d
new_inventories.remove(new_inventory) new_inventories.remove(new_inventory)
# TODO: Update all references of the batch with the new rates # TODO: Update all references of the batch with the new rates
break break
if not found: 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()

View File

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

View File

@ -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[]>;

View File

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