Rate Contract is checked during save and update of Purchase at the backend
This commit is contained in:
parent
ceaf93d1cd
commit
d34c8ea0a4
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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user