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
|
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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user