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