Chore: In overlord / sale / bill.service now the BillViewItem is just a view item

The data is kept as the original bill object and this view generated on every change. It has no sanctity.

To deal with the challenges of Selection of items in the bill.component.html created a bill selection item.
This is converted to string while checking else the selection model fails.

Feature: It now checks if Happy Hour items have equivalent regular items in each kot.
Feature: Discount won't apply to happy hour items.
Checks for both are both in front end and back end.
This commit is contained in:
Amritanshu Agrawal 2020-12-16 22:34:41 +05:30
parent 28952402aa
commit 6c83c74424
9 changed files with 304 additions and 195 deletions

View File

@ -2,6 +2,8 @@ import uuid
from typing import List, Optional
import barker.schemas.voucher as schemas
from fastapi import HTTPException, status
from sqlalchemy import func
from sqlalchemy.orm import Session
@ -116,3 +118,17 @@ def do_update_settlements(voucher: Voucher, others: List[SettleSchema], db: Sess
for i in (i for i in voucher.settlements if i.settled not in [x.id_ for x in settlements]):
voucher.settlements.remove(i)
db.delete(i)
def happy_hour_items_balanced(inventories: [schemas.Inventory]):
happy = set((i.product.id_, i.quantity) for i in inventories if i.is_happy_hour)
other = set(
(i.product.id_, i.quantity) for i in inventories if not i.is_happy_hour and (i.product.id_, i.quantity) in happy
)
return happy == other
def happy_hour_has_discount(inventories: [schemas.Inventory]):
happy = set(i.product.id_ for i in inventories if i.is_happy_hour)
offenders = [i for i in inventories if i.product.id_ in happy and i.discount != 0]
return len(offenders) > 0

View File

@ -24,6 +24,8 @@ from ...routers.voucher import (
get_bill_id,
get_guest_book,
get_tax,
happy_hour_has_discount,
happy_hour_items_balanced,
)
from ...schemas.auth import UserToken
@ -99,6 +101,16 @@ def do_save(
for k in data.kots:
if not len(k.inventories):
continue
if not happy_hour_items_balanced(k.inventories):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Happy hour products are not balanced.",
)
if happy_hour_has_discount(k.inventories):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Discount is not allowed on happy hour products.",
)
code = db.query(func.coalesce(func.max(Kot.code), 0) + 1).scalar()
kot = Kot(item.id, code, item.food_table_id, item.date, item.user_id)
item.kots.append(kot)

View File

@ -24,6 +24,8 @@ from ...routers.voucher import (
get_bill_id,
get_guest_book,
get_tax,
happy_hour_has_discount,
happy_hour_items_balanced,
)
from ...schemas.auth import UserToken
@ -87,6 +89,17 @@ def update(
i.discount = next(
round(inv.discount, 5) for ko in data.kots for inv in ko.inventories if inv.id_ == i.id
)
for k in data.kots:
if happy_hour_has_discount(k.inventories):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Discount is not allowed on happy hour products.",
)
if not happy_hour_items_balanced(k.inventories):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Happy hour products are not balanced.",
)
for k in (k for k in data.kots if k.id_ is None and len(k.inventories) > 0):
need_to_print_kot = True
code = db.query(func.coalesce(func.max(Kot.code), 0) + 1).scalar()

View File

@ -4,42 +4,32 @@ import { Tax } from './tax';
export class BillViewItem {
id: string | undefined;
kotId: string | undefined;
isKot: boolean;
info: string;
kotId: string;
product: Product;
productId: string | undefined;
productId: string;
isHappyHour: boolean;
isPrinted: boolean;
price: number;
quantity: number;
discount: number;
taxRate: number;
tax: Tax;
modifiers: Modifier[];
public get isOldKot(): boolean {
return this.isKot && this.id !== undefined;
return this.isKot && this.kotId !== undefined;
}
public get isNewKot(): boolean {
return this.isKot && this.id === undefined;
return this.isKot && this.kotId === undefined;
}
public constructor(init?: Partial<BillViewItem>) {
this.isKot = true;
this.info = '';
this.kotId = '';
this.product = new Product();
this.productId = this.product.id;
this.productId = '';
this.isHappyHour = false;
this.isPrinted = false;
this.price = 0;
this.quantity = 0;
this.discount = 0;
this.taxRate = 0;
this.tax = new Tax();
this.modifiers = [];
Object.assign(this, init);
}

View File

@ -2,8 +2,9 @@ import { SelectionModel } from '@angular/cdk/collections';
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { round } from 'mathjs';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, throwError } from 'rxjs';
import { Observable } from 'rxjs/internal/Observable';
import { tap } from 'rxjs/operators';
import { BillViewItem } from '../core/bill-view-item';
import { ModifierCategory } from '../core/modifier-category';
@ -15,6 +16,7 @@ import { ToasterService } from '../core/toaster.service';
import { ModifierCategoryService } from '../modifier-categories/modifier-category.service';
import { Bill } from './bills/bill';
import { BillSelectionItem } from './bills/bill-selection-item';
import { Inventory } from './bills/inventory';
import { Kot } from './bills/kot';
import { VoucherType } from './bills/voucher-type';
@ -24,13 +26,13 @@ import { ModifiersComponent } from './modifiers/modifiers.component';
@Injectable()
export class BillService {
public dataObs: BehaviorSubject<BillViewItem[]>;
public data: BillViewItem[];
public bill: Bill = new Bill();
public netAmount: BehaviorSubject<number>;
public discountAmount: BehaviorSubject<number>;
public taxAmount: BehaviorSubject<number>;
public amount: BehaviorSubject<number>;
public selection = new SelectionModel<BillViewItem>(true, []);
public amountVal: number;
public selection = new SelectionModel<string>(true, []);
constructor(
private dialog: MatDialog,
@ -38,59 +40,70 @@ export class BillService {
private ser: VoucherService,
private modifierCategoryService: ModifierCategoryService,
) {
this.data = [];
this.dataObs = new BehaviorSubject<BillViewItem[]>(this.data);
this.dataObs = new BehaviorSubject<BillViewItem[]>([]);
this.netAmount = new BehaviorSubject(0);
this.discountAmount = new BehaviorSubject(0);
this.taxAmount = new BehaviorSubject(0);
this.amount = new BehaviorSubject(0);
this.amountVal = 0;
this.amount.pipe(tap((x) => (this.amountVal = x)));
}
loadData(bill: Bill): void {
this.bill = bill;
const view: BillViewItem[][] = this.bill.kots.map((k: Kot) => [
new BillViewItem({
id: k.id,
isKot: true,
info: `Kot: ${k.code} / ${k.date} (${k.user.name}) `,
}),
...k.inventories.map(
(i) =>
new BillViewItem({
id: i.id,
kotId: k.id,
isKot: false,
product: i.product,
productId: i.product.id,
isHappyHour: i.isHappyHour,
isPrinted: true,
info: `${i.product.name} @ ${i.price} - ${round(i.discount * 100, 2)}%`,
price: i.price,
quantity: i.quantity,
discount: i.discount,
taxRate: i.taxRate,
tax: i.tax,
modifiers: i.modifiers,
}),
),
]);
this.data = view.reduce((a, c) => a.concat(c), []);
this.data.push(new BillViewItem({ isKot: true, info: '== New Kot ==' }));
this.dataObs.next(this.data);
displayBill(): void {
const data = this.transformBillToView(this.bill);
this.dataObs.next(data);
this.updateAmounts();
}
transformBillToView(bill: Bill): BillViewItem[] {
const view: BillViewItem[] = bill.kots
.map((k: Kot) => [
new BillViewItem({
kotId: k.id,
isKot: true,
info: k.id ? `Kot: ${k.code} / ${k.date} (${k.user.name})` : '== New Kot ==',
}),
...k.inventories.map(
(i) =>
new BillViewItem({
id: i.id,
kotId: k.id,
isKot: false,
productId: i.product.id,
isHappyHour: i.isHappyHour,
isPrinted: !!k.id,
info: `${i.product.name} @ ${i.price} - ${round(i.discount * 100, 2)}%`,
quantity: i.quantity,
modifiers: i.modifiers,
}),
),
])
.reduce((a, c) => a.concat(c), []);
return view;
}
loadData(bill: Bill): void {
bill.kots.push(new Kot());
this.bill = bill;
this.displayBill();
}
minimum(productId: string, happyHour: boolean): number {
return this.data.reduce(
(a, c) => (c.productId === productId && c.isHappyHour === happyHour ? a + c.quantity : a),
return this.bill.kots.reduce(
(t, k) =>
k.inventories.reduce(
(a, c) =>
c.product.id === productId && c.isHappyHour === happyHour ? a + c.quantity : a,
0,
) + t,
0,
);
}
addProduct(product: Product, quantity: number, discount: number): void {
const old = this.data.find(
(x) =>
!x.isKot && !x.id && x.productId === product.id && x.isHappyHour === product.hasHappyHour,
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
const old = newKot.inventories.find(
(x) => x.product.id === product.id && x.isHappyHour === product.hasHappyHour,
);
if (quantity < 0) {
const minimum = this.minimum(product.id as string, product.hasHappyHour) + quantity;
@ -102,38 +115,34 @@ export class BillService {
if (old !== undefined) {
old.quantity += quantity;
} else {
const item = new BillViewItem({
isKot: false,
const item = new Inventory({
product,
productId: product.id,
isHappyHour: product.hasHappyHour,
info: `${product.name} @ ${product.price} - ${0}%`,
price: product.price,
quantity,
discount,
price: product.price,
isHappyHour: product.hasHappyHour,
taxRate: product.tax.rate,
tax: product.tax,
discount,
modifiers: [],
});
this.data.push(item);
newKot.inventories.push(item);
this.modifierCategoryService.listForProduct(product.id as string).subscribe((result) => {
if (result.reduce((a: number, c: ModifierCategory) => a + c.minimum, 0)) {
this.showModifier(item);
}
});
}
this.dataObs.next(this.data);
this.updateAmounts();
this.displayBill();
}
showModifier(item: BillViewItem): void {
showModifier(item: Inventory): void {
// [routerLink]="['/sales', 'modifiers', item.id]"
const dialogRef = this.dialog.open(ModifiersComponent, {
position: {
top: '10vh',
},
data: {
list: this.modifierCategoryService.listForProduct(item.productId as string),
list: this.modifierCategoryService.listForProduct(item.product.id as string),
selected: item.modifiers,
},
});
@ -143,75 +152,99 @@ export class BillService {
item.modifiers = result;
}
});
this.displayBill();
}
addOne(item: BillViewItem): void {
item.quantity += 1;
this.dataObs.next(this.data);
this.updateAmounts();
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
const old = newKot.inventories.find(
(x) => x.product.id === item.productId && x.isHappyHour === item.isHappyHour,
) as Inventory;
old.quantity += 1;
this.displayBill();
}
quantity(item: BillViewItem, quantity: number): void {
item.quantity = quantity;
this.dataObs.next(this.data);
this.updateAmounts();
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
const old = newKot.inventories.find(
(x) => x.product.id === item.productId && x.isHappyHour === item.isHappyHour,
) as Inventory;
old.quantity = quantity;
this.displayBill();
}
subtractOne(item: BillViewItem, canEdit: boolean): void {
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
const old = newKot.inventories.find(
(x) => x.product.id === item.productId && x.isHappyHour === item.isHappyHour,
) as Inventory;
if (
item.quantity > 1 ||
(canEdit && this.minimum(item.productId as string, item.isHappyHour) >= 1)
) {
item.quantity -= 1;
this.dataObs.next(this.data);
this.updateAmounts();
old.quantity -= 1;
} else if (item.quantity === 0) {
this.removeItem(item);
newKot.inventories.splice(newKot.inventories.indexOf(old), 1);
}
this.displayBill();
}
removeItem(item: BillViewItem): void {
this.data.splice(this.data.indexOf(item), 1);
this.dataObs.next(this.data);
this.updateAmounts();
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
const old = newKot.inventories.find(
(x) => x.product.id === item.productId && x.isHappyHour === item.isHappyHour,
) as Inventory;
newKot.inventories.splice(newKot.inventories.indexOf(old), 1);
this.displayBill();
}
modifier(item: BillViewItem): void {
this.showModifier(item);
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
const old = newKot.inventories.find(
(x) => x.product.id === item.productId && x.isHappyHour === item.isHappyHour,
) as Inventory;
this.showModifier(old);
}
discount(discounts: { id: string; name: string; discount: number }[]): void {
this.data.forEach((x) => {
if (!x.isKot) {
const e = discounts.find((d) => d.id === (x.product.saleCategory as SaleCategory).id);
if (e !== undefined) {
x.discount = e.discount;
x.info = `${x.product.name} @ ${x.price} - ${round(x.discount * 100, 2)}%`;
for (const kot of this.bill.kots) {
const noDiscount = kot.inventories
.filter((x) => x.isHappyHour)
.map((x) => x.product.id as string);
for (const inventory of kot.inventories) {
const e = discounts.find(
(d) => d.id === (inventory.product.saleCategory as SaleCategory).id,
);
if (e === undefined || noDiscount.indexOf(inventory.product.id as string) !== -1) {
continue;
}
inventory.discount = e.discount;
}
});
this.dataObs.next(this.data);
this.updateAmounts();
}
this.displayBill();
}
printKot(guestBookId: string | null): Observable<boolean> {
const item = JSON.parse(JSON.stringify(this.bill));
const newKot = this.getKot();
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
if (newKot.inventories.length === 0) {
this.toaster.show('Error', 'Cannot print a blank KOT\nPlease add some products!');
return throwError('Cannot print a blank KOT\nPlease add some products!');
}
if (!this.happyHourItemsBalanced()) {
return throwError('Happy hour products are not balanced.');
}
item.kots.push(newKot);
return this.ser.saveOrUpdate(item, VoucherType.Kot, guestBookId, true);
}
printBill(guest_book_id: string | null, voucherType: VoucherType): Observable<boolean> {
const item = JSON.parse(JSON.stringify(this.bill));
item.kots.forEach((k: Kot) => {
k.inventories.forEach((i: Inventory) => {
i.discount = (this.data.find((x) => !x.isKot && x.id === i.id) as BillViewItem).discount;
});
});
item.kots.push(this.getKot());
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
if (item.kots.length === 1 && newKot.inventories.length === 0) {
return throwError('Cannot print a blank Bill\nPlease add some products!');
}
if (!this.happyHourItemsBalanced()) {
return throwError('Happy hour products are not balanced.');
}
return this.ser.saveOrUpdate(item, voucherType, guest_book_id, true);
}
@ -246,88 +279,73 @@ export class BillService {
updateAmounts() {
this.netAmount.next(
round(
this.data
.filter((x) => !x.isKot)
.reduce(
(ca: number, c: BillViewItem) => ca + (c.isHappyHour ? 0 : c.price) * c.quantity,
0,
),
this.bill.kots.reduce(
(t, k) =>
k.inventories.reduce((a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity, 0) + t,
0,
),
),
);
this.discountAmount.next(
round(
this.data
.filter((x) => !x.isKot)
.reduce(
(ca: number, c: BillViewItem) =>
ca + (c.isHappyHour ? 0 : c.price) * c.quantity * c.discount,
0,
),
this.bill.kots.reduce(
(t, k) =>
k.inventories.reduce(
(a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity * c.discount,
0,
) + t,
0,
),
),
);
this.taxAmount.next(
round(
this.data
.filter((x) => !x.isKot)
.reduce(
(ca: number, c: BillViewItem) =>
ca + (c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * c.taxRate,
0,
),
this.bill.kots.reduce(
(t, k) =>
k.inventories.reduce(
(a, c) =>
a + (c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * c.taxRate,
0,
) + t,
0,
),
),
);
this.amount.next(
round(
this.data
.filter((x) => !x.isKot)
.reduce(
(ca: number, c: BillViewItem) =>
ca + (c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * (1 + c.taxRate),
0,
),
),
);
}
amountVal(): number {
return round(
this.bill.kots.reduce(
(ka: number, k: Kot) =>
ka +
k.inventories.reduce(
(ca: number, c: Inventory) =>
ca + (c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * (1 + c.taxRate),
0,
),
0,
this.bill.kots.reduce(
(t, k) =>
k.inventories.reduce(
(a, c) =>
a + (c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * (1 + c.taxRate),
0,
) + t,
0,
),
),
);
}
splitBill(table: Table): Observable<boolean> {
const inventoriesToMove: string[] = this.selection.selected.map(
(x: BillViewItem) => x.id as string,
(x: string) => (JSON.parse(x) as BillSelectionItem).inventoryId as string,
);
return this.ser.splitBill(this.bill.id as string, inventoriesToMove, table);
}
private getKot(): Kot {
return new Kot({
inventories: this.data
.filter((x) => !x.isKot && !x.isPrinted)
.map(
(y) =>
new Inventory({
product: y.product,
quantity: y.quantity,
price: y.price,
isHappyHour: y.isHappyHour,
discount: y.discount,
modifiers: y.modifiers,
taxRate: y.taxRate,
tax: y.tax,
}),
),
});
private happyHourItemsBalanced(): boolean {
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
const happyHourItems = newKot.inventories
.filter((x) => x.isHappyHour)
.map((x) => ({ id: x.product.id as string, quantity: x.quantity }));
for (const item of happyHourItems) {
const q = newKot.inventories.find(
(x) => !x.isHappyHour && x.product.id === item.id && x.quantity === item.quantity,
);
if (q === undefined) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,14 @@
export class BillSelectionItem {
kotId?: string;
inventoryId?: string;
productId: string;
isHappyHour: boolean;
public constructor(init?: Partial<BillSelectionItem>) {
this.kotId = undefined;
this.inventoryId = undefined;
this.productId = '';
this.isHappyHour = false;
Object.assign(this, init);
}
}

View File

@ -62,8 +62,8 @@
*ngIf="!row.isKot"
[disabled]="!row.id"
(click)="$event.stopPropagation()"
(change)="$event ? bs.selection.toggle(row) : null"
[checked]="bs.selection.isSelected(row)"
(change)="$event ? toggle(row) : null"
[checked]="isSelected(row)"
>
</mat-checkbox>
</mat-cell>

View File

@ -16,7 +16,9 @@ import { QuantityComponent } from '../quantity/quantity.component';
import { TablesDialogComponent } from '../tables-dialog/tables-dialog.component';
import { Bill } from './bill';
import { BillSelectionItem } from './bill-selection-item';
import { BillsDataSource } from './bills-datasource';
import { Inventory } from './inventory';
import { Kot } from './kot';
import { VoucherType } from './voucher-type';
@ -66,33 +68,88 @@ export class BillsComponent implements OnInit {
});
}
isAllSelected(kot: Kot) {
return this.bs.data
.filter((x) => x.kotId === kot.id)
.reduce((p: boolean, c: BillViewItem) => p && this.bs.selection.isSelected(c), true);
isAllSelected(kotView: BillViewItem): boolean {
const kot = this.bs.bill.kots.find((k) => k.id === kotView.kotId) as Kot;
return kot.inventories.reduce(
(p: boolean, c: Inventory) =>
p &&
this.bs.selection.isSelected(
JSON.stringify(
new BillSelectionItem({
kotId: kot.id,
inventoryId: c.id,
productId: c.product.id,
isHappyHour: c.isHappyHour,
}),
),
),
true,
);
}
isAnySelected(kot: Kot) {
toggle(invView: BillViewItem) {
const key = JSON.stringify(
new BillSelectionItem({
kotId: invView.kotId,
inventoryId: invView.id,
productId: invView.productId,
isHappyHour: invView.isHappyHour,
}),
);
this.bs.selection.toggle(key);
}
isSelected(invView: BillViewItem): boolean {
const key = JSON.stringify(
new BillSelectionItem({
kotId: invView.kotId,
inventoryId: invView.id,
productId: invView.productId,
isHappyHour: invView.isHappyHour,
}),
);
return this.bs.selection.isSelected(key);
}
isAnySelected(kotView: BillViewItem) {
const kot = this.bs.bill.kots.find((k) => k.id === kotView.kotId) as Kot;
let total = 0;
let found = 0;
this.bs.data
.filter((x) => x.kotId === kot.id)
.forEach((c: BillViewItem) => {
total += 1;
if (this.bs.selection.isSelected(c)) {
found += 1;
}
});
for (const item of kot.inventories) {
const key = JSON.stringify(
new BillSelectionItem({
kotId: kot.id,
inventoryId: item.id,
productId: item.product.id,
isHappyHour: item.isHappyHour,
}),
);
total += 1;
if (this.bs.selection.isSelected(key)) {
found += 1;
}
}
return found > 0 && found < total;
}
masterToggle(kot: Kot) {
const isAllSelected = this.isAllSelected(kot);
this.bs.data
.filter((x) => x.kotId === kot.id)
.forEach((row) =>
isAllSelected ? this.bs.selection.deselect(row) : this.bs.selection.select(row),
masterToggle(kotView: BillViewItem) {
const isAllSelected = this.isAllSelected(kotView);
const kot = this.bs.bill.kots.find((k) => k.id === kotView.kotId) as Kot;
for (const item of kot.inventories) {
const key = JSON.stringify(
new BillSelectionItem({
kotId: kot.id,
inventoryId: item.id,
productId: item.product.id,
isHappyHour: item.isHappyHour,
}),
);
if (isAllSelected) {
this.bs.selection.deselect(key);
} else {
this.bs.selection.select(key);
}
}
}
addOne(item: BillViewItem): void {
@ -109,18 +166,7 @@ export class BillsComponent implements OnInit {
if (!result) {
return;
}
if (!item.isPrinted) {
this.bs.quantity(item, result as number);
} else {
const quantity = result as number;
const product = {
...item.product,
hasHappyHour: item.isHappyHour,
tax: item.tax,
price: item.price,
};
this.bs.addProduct(product, quantity, item.discount);
}
this.bs.quantity(item, result as number);
});
}

View File

@ -191,7 +191,7 @@ export class SalesHomeComponent {
if (!this.receivePaymentAllowed()) {
return;
}
const amount = this.bs.amountVal();
const amount = this.bs.amountVal;
const type = this.bs.type();
this.dialog
.open(ReceivePaymentComponent, {