Split bill done - TODO: If the products have different price due to paid modifiers
This commit is contained in:
Amritanshu Agrawal 2020-09-28 08:09:38 +05:30
parent cf34c2b855
commit f8778fee74
9 changed files with 159 additions and 123 deletions

View File

@ -18,6 +18,7 @@ ALGORITHM=HS256
JWT_TOKEN_EXPIRE_MINUTES=30
NEW_DAY_OFFSET_MINUTES =420
TIMEZONE_OFFSET_MINUTES=330
ALEMBIC_LOG_LEVEL=INFO
ALEMBIC_SQLALCHEMY_LOG_LEVEL=WARN

View File

@ -26,7 +26,7 @@ from .routers.reports import (
sale_report,
tax_report
)
from .routers.voucher import show, save, update, receive_payment, void, merge_move
from .routers.voucher import show, save, update, receive_payment, void, merge_move, split
from .db.base_class import Base
from .core.config import settings
@ -75,6 +75,7 @@ app.include_router(update.router, prefix="/api/voucher", tags=["voucher"])
app.include_router(receive_payment.router, prefix="/api/voucher", tags=["voucher"])
app.include_router(void.router, prefix="/api/voucher", tags=["voucher"])
app.include_router(merge_move.router, prefix="/api", tags=["voucher"])
app.include_router(split.router, prefix="/api", tags=["voucher"])
# app.include_router(issue_grid.router, prefix="/api/issue-grid", tags=["vouchers"])
# app.include_router(batch.router, prefix="/api/batch", tags=["vouchers"])

View File

@ -68,7 +68,7 @@ def check_permissions(item: Optional[Voucher], voucher_type: VoucherType, permis
if item is None:
return
if item.voucher_type != VoucherType.KOT and "Edit Printed Bill" not in permissions:
if item.voucher_type != VoucherType.KOT and "edit-printed-bill" not in permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="You are not allowed to edit a printed bill",
)

View File

@ -1,101 +1,114 @@
import uuid
import transaction
from datetime import datetime
from pyramid.view import view_config
from typing import Optional, List
from fastapi import APIRouter, HTTPException, status, Depends, Security
from sqlalchemy import func
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from barker.models import Voucher, Overview, Kot, Inventory, InventoryModifier
from barker.models.validation_exception import ValidationError
from barker.views.voucher import get_bill_id, do_update_settlements, do_update_table
from barker.views.voucher.update import check_permissions
from barker.views.voucher.void import do_void_settlements
@view_config(
request_method="POST",
route_name="v1_vouchers_id",
renderer="json",
request_param="split-bill",
permission="Split Bill",
trans=True,
from ...schemas.auth import UserToken
import barker.schemas.split as schemas
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionLocal
from ...models import Voucher, VoucherType, Kot, Inventory, InventoryModifier, SettleOption, Overview
from ...routers.voucher import (
do_update_settlements,
get_bill_id,
do_update_table,
check_permissions,
)
def split_voucher(request):
json = request.json_body
now = datetime.now()
id_ = uuid.UUID(request.matchdict["id"])
update_table = request.GET["u"] == "true"
item = request.dbsession.query(Voucher).filter(Voucher.id == id_).first()
item.void = True
item.reason = "Bill Split"
do_void_settlements(item, request.dbsession)
if update_table:
request.dbsession.query(Overview).filter(Overview.voucher_id == item.id).delete()
from barker.schemas.receive_payment import ReceivePaymentItem as SettleSchema
inventories = [uuid.UUID(i) for i in json["inventories"]]
router = APIRouter()
one_inventories = [i for k in item.kots for i in k.inventories if i.id in inventories]
two_inventories = [i for k in item.kots for i in k.inventories if i.id not in inventories]
save(
one_inventories,
now,
item.voucher_type,
0,
uuid.UUID(json["table"]["id"]),
item.customer_id,
update_table,
uuid.UUID(request.authenticated_userid),
request.effective_principals,
request.dbsession,
)
save(
two_inventories,
now,
item.voucher_type,
item.pax,
item.food_table_id,
item.customer_id,
update_table,
uuid.UUID(request.authenticated_userid),
request.effective_principals,
request.dbsession,
)
# Dependency
def get_db():
try:
db = SessionLocal()
yield db
finally:
db.close()
transaction.commit()
return True
@router.post("/split-bill/{id_}")
def split(
id_: uuid.UUID,
data: schemas.Split,
u: bool, # Update table?
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["split-bill"]),
):
try:
now = datetime.now()
update_table = u
item: Voucher = db.query(Voucher).filter(Voucher.id == id_).first()
item.void = True
item.reason = "Bill Split"
do_update_settlements(item, [SettleSchema(id=SettleOption.VOID(), amount=round(item.amount))], db)
if update_table:
db.query(Overview).filter(Overview.voucher_id == item.id).delete()
check_permissions(None, item.voucher_type, user.permissions)
one_inventories = [i for k in item.kots for i in k.inventories if i.id in data.inventories]
two_inventories = [i for k in item.kots for i in k.inventories if i.id not in data.inventories]
one = save(one_inventories, now, item.voucher_type, 0, data.table_id, item.customer_id, user.id_, db,)
if update_table:
do_update_table(one, None, db)
two = save(
two_inventories, now, item.voucher_type, item.pax, item.food_table_id, item.customer_id, user.id_, db,
)
if update_table:
do_update_table(two, None, db)
db.commit()
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e),
)
except Exception:
db.rollback()
raise
def save(
inventories, now, voucher_type, pax, table_id, customer_id, update_table, user_id, permissions, dbsession,
inventories: List[Inventory],
now: datetime,
voucher_type: VoucherType,
pax: int,
table_id: uuid.UUID,
customer_id: Optional[uuid.UUID],
user_id: uuid.UUID,
db: Session,
):
product_quantities = {}
skip_products = set()
for i in inventories:
if i.product_id in product_quantities:
product_quantities[i.product_id] += i.quantity
if (i.product_id, i.is_happy_hour) in product_quantities:
product_quantities[(i.product_id, i.is_happy_hour)][1] += i.quantity
else:
product_quantities[i.product_id] = i.quantity
for product, quantity in product_quantities.items():
product_quantities[(i.product_id, i.is_happy_hour)] = (i, i.quantity)
for (product, happy_hour), (inventory, quantity) in product_quantities.items():
if quantity < 0:
raise ValidationError("Quantity is negative")
elif quantity == 0:
skip_products.add(product)
check_permissions(None, voucher_type, permissions)
bill_id = get_bill_id(voucher_type, dbsession)
kot_id = dbsession.query(func.coalesce(func.max(Voucher.kot_id), 0) + 1).scalar()
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Quantity of a product is negative",
)
bill_id = get_bill_id(voucher_type, db)
kot_id = db.query(func.coalesce(func.max(Voucher.kot_id), 0) + 1).scalar()
item = Voucher(now, pax, bill_id, kot_id, table_id, customer_id, voucher_type, user_id)
dbsession.add(item)
code = dbsession.query(func.coalesce(func.max(Kot.code), 0) + 1).scalar()
item: Voucher = Voucher(now, pax, bill_id, kot_id, table_id, customer_id, voucher_type, user_id)
db.add(item)
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)
dbsession.add(kot)
for index, old_inventory in enumerate([i for i in inventories if i.product_id not in skip_products]):
db.add(kot) # Multiple inventories of the same product give a key error, but combining messes with modifiers, this
# will get important when modifiers have a price.
for index, (old_inventory, q) in enumerate([i for i in product_quantities.values() if i[1] != 0]):
inv = Inventory(
kot.id,
old_inventory.product_id,
old_inventory.quantity,
q,
old_inventory.price,
old_inventory.discount,
old_inventory.is_happy_hour,
@ -104,15 +117,16 @@ def save(
index,
)
kot.inventories.append(inv)
dbsession.add(inv)
db.add(inv)
for m in old_inventory.modifiers:
mod = InventoryModifier(None, m.modifier_id, m.price)
inv.modifiers.append(mod)
dbsession.add(mod)
do_update_settlements(item, dbsession)
db.add(mod)
do_update_settlements(item, [], db)
if len(kot.inventories) == 0:
raise ValidationError("Please add some products!")
if update_table:
do_update_table(item, None, dbsession)
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="No inventories selected",
)
db.flush()
return item

View File

@ -58,6 +58,11 @@ def update(
item.food_table_id = data.table.id_
if data.customer is not None:
item.customer_id = data.customer.id_
if item.voucher_type != VoucherType.KOT:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal error, reprints should not reach here",
)
if item.voucher_type == VoucherType.KOT and voucher_type != VoucherType.KOT:
item.date = now
item.bill_id = get_bill_id(voucher_type, db)

15
barker/schemas/split.py Normal file
View File

@ -0,0 +1,15 @@
import uuid
from typing import List
from pydantic import BaseModel
from barker.schemas import to_camel
class Split(BaseModel):
inventories: List[uuid.UUID]
table_id: uuid.UUID
class Config:
alias_generator = to_camel
fields = {"id_": "id"}

View File

@ -1,68 +1,68 @@
<div fxLayout="row wrap" fxLayoutGap="grid 20px">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('guest-book') !== -1" [routerLink]="['/', 'guest-book']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('guest-book') !== -1" [routerLink]="['/', 'guest-book']">
<h3 class="item-name">Guest Book</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('sales') !== -1" [routerLink]="['/', 'sales']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('sales') !== -1" [routerLink]="['/', 'sales']">
<h3 class="item-name">Sales</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('cashier-report') !== -1" [routerLink]="['/', 'cashier-report']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('cashier-report') !== -1" [routerLink]="['/', 'cashier-report']">
<h3 class="item-name">Cashier Report</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('sale-report') !== -1" [routerLink]="['/', 'sale-report']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('sale-report') !== -1" [routerLink]="['/', 'sale-report']">
<h3 class="item-name">Sale Report</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('tax-report') !== -1" [routerLink]="['/', 'tax-report']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('tax-report') !== -1" [routerLink]="['/', 'tax-report']">
<h3 class="item-name">Tax Report</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('product-sale-report') !== -1" [routerLink]="['/', 'product-sale-report']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('product-sale-report') !== -1" [routerLink]="['/', 'product-sale-report']">
<h3 class="item-name">Product Sale Report</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('bill-settlement-report') !== -1" [routerLink]="['/', 'bill-settlement-report']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('bill-settlement-report') !== -1" [routerLink]="['/', 'bill-settlement-report']">
<h3 class="item-name">Bill Settlement Report</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('beer-consumption-report') !== -1" [routerLink]="['/', 'beer-consumption-report']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('beer-consumption-report') !== -1" [routerLink]="['/', 'beer-consumption-report']">
<h3 class="item-name">Beer Consumption Report</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('discount-report') !== -1" [routerLink]="['/', 'discount-report']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('discount-report') !== -1" [routerLink]="['/', 'discount-report']">
<h3 class="item-name">Discount Report</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('tables') !== -1" [routerLink]="['/', 'tables']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('tables') !== -1" [routerLink]="['/', 'tables']">
<h3 class="item-name">Tables</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('sections') !== -1" [routerLink]="['/', 'sections']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('sections') !== -1" [routerLink]="['/', 'sections']">
<h3 class="item-name">Sections</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('products') !== -1" [routerLink]="['/', 'menu-categories']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('products') !== -1" [routerLink]="['/', 'menu-categories']">
<h3 class="item-name">Menu Categories</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('products') !== -1" [routerLink]="['/', 'sale-categories']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('products') !== -1" [routerLink]="['/', 'sale-categories']">
<h3 class="item-name">Sale Categories</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('products') !== -1" [routerLink]="['/', 'products']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('products') !== -1" [routerLink]="['/', 'products']">
<h3 class="item-name">Products</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('modifiers') !== -1" [routerLink]="['/', 'modifier-categories']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('modifiers') !== -1" [routerLink]="['/', 'modifier-categories']">
<h3 class="item-name">Modifier Categories</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('modifiers') !== -1" [routerLink]="['/', 'modifiers']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('modifiers') !== -1" [routerLink]="['/', 'modifiers']">
<h3 class="item-name">Modifiers</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('taxes') !== -1" [routerLink]="['/', 'taxes']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('taxes') !== -1" [routerLink]="['/', 'taxes']">
<h3 class="item-name">Taxes</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('devices') !== -1" [routerLink]="['/', 'devices']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('devices') !== -1" [routerLink]="['/', 'devices']">
<h3 class="item-name">Devices</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('section-printers') !== -1" [routerLink]="['/', 'section-printers']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('section-printers') !== -1" [routerLink]="['/', 'section-printers']">
<h3 class="item-name">Section Printers</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('printers') !== -1" [routerLink]="['/', 'printers']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('printers') !== -1" [routerLink]="['/', 'printers']">
<h3 class="item-name">Printers</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('users') !== -1" [routerLink]="['/', 'roles']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('users') !== -1" [routerLink]="['/', 'roles']">
<h3 class="item-name">Roles</h3>
</mat-card>
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user.perms.indexOf('users') !== -1" [routerLink]="['/', 'users']">
<mat-card fxLayout="column" class="square-button" matRipple *ngIf="auth.user && auth.user.perms.indexOf('users') !== -1" [routerLink]="['/', 'users']">
<h3 class="item-name">Users</h3>
</mat-card>
</div>

View File

@ -13,6 +13,7 @@ const httpOptions = {
const url = '/api/voucher';
const urlMoveTable = '/api/move-table';
const urlMoveKot = '/api/move-kot';
const urlSplitBill = '/api/split-bill';
const serviceName = 'VoucherService';
@Injectable({providedIn: 'root'})
@ -140,11 +141,10 @@ export class VoucherService {
}
splitBill(id: string, inventoriesToMove: string[], table: Table) {
const options = {params: new HttpParams().set('split-bill', '').set('u', 'true')};
return <Observable<boolean>>this.http.post<boolean>(`${url}/${id}`, {
voucher: {id: id},
const options = {params: new HttpParams().set('u', 'true')};
return <Observable<boolean>>this.http.post<boolean>(`${urlSplitBill}/${id}`, {
inventories: inventoriesToMove,
table: {id: table.id}
tableId: table.id
}, options).pipe(
catchError(this.log.handleError(serviceName, 'splitBill'))
);

View File

@ -39,7 +39,7 @@ export class SalesHomeComponent implements OnInit {
}
printKotAllowed(): boolean {
if (this.auth.user.perms.indexOf('print-kot') === -1) {
if (this.auth.user && this.auth.user.perms.indexOf('print-kot') === -1) {
return false;
}
if (!this.bs.bill.id) {
@ -69,7 +69,7 @@ export class SalesHomeComponent implements OnInit {
}
discountAllowed(): boolean {
return this.auth.user.perms.indexOf('discount') !== -1;
return this.auth.user && this.auth.user.perms.indexOf('discount') !== -1;
}
showDiscount(): Observable<boolean | { id: string, name: string, discount: number }[]> {
@ -120,7 +120,7 @@ export class SalesHomeComponent implements OnInit {
}
printBillAllowed(): boolean {
if (this.auth.user.perms.indexOf('print-bill') === -1) {
if (this.auth.user && this.auth.user.perms.indexOf('print-bill') === -1) {
return false;
}
if (!this.bs.bill.id) {
@ -143,7 +143,7 @@ export class SalesHomeComponent implements OnInit {
if (this.route.snapshot.queryParamMap.has('guest')) {
guestBookId = this.route.snapshot.queryParamMap.get('guest');
}
let discountObservable: Observable<any> = (this.discountAllowed()) ? this.showDiscount() : observableOf('');
const discountObservable: Observable<any> = (this.discountAllowed()) ? this.showDiscount() : observableOf('');
discountObservable.pipe(
tap((result: boolean | { id: string, name: string, discount: number }[]) => {
@ -169,7 +169,7 @@ export class SalesHomeComponent implements OnInit {
}
receivePaymentAllowed(): boolean {
if (this.auth.user.perms.indexOf('settle-bill') === -1) {
if (this.auth.user && this.auth.user.perms.indexOf('settle-bill') === -1) {
return false;
}
if (!this.bs.bill.id) {
@ -184,7 +184,7 @@ export class SalesHomeComponent implements OnInit {
return true;
}
receivePaymentWithReason(type:string, amount: number): Observable<boolean> {
receivePaymentWithReason(type: string, amount: number): Observable<boolean> {
const types = {
NO_CHARGE: [
{
@ -203,13 +203,13 @@ export class SalesHomeComponent implements OnInit {
};
return this.dialog.open(ReasonComponent, {
// width: '750px'
data: {title: (type === "NO_CHARGE") ? "NC for whom" : "Staff name"}
data: {title: (type === 'NO_CHARGE') ? 'NC for whom' : 'Staff name'}
}).afterClosed().pipe(
switchMap((value: boolean | string) => {
if (!!value) {
return this.bs.receivePayment(types[type], value as string)
return this.bs.receivePayment(types[type], value as string);
} else {
return throwError("Cancelled");
return throwError('Cancelled');
}
})
);
@ -227,7 +227,7 @@ export class SalesHomeComponent implements OnInit {
if (!!value) {
return this.bs.receivePayment(value as { id: number, name: string, amount: number }[], '')
} else {
return throwError("Cancelled");
return throwError('Cancelled');
}
})
);
@ -252,7 +252,7 @@ export class SalesHomeComponent implements OnInit {
moveTableAllowed(): boolean {
// TODO: Check if this condition should be "and" or "or"
if (this.auth.user.perms.indexOf('move-table') === -1 && this.auth.user.perms.indexOf('merge-tables') === -1) {
if (this.auth.user && this.auth.user.perms.indexOf('move-table') === -1 && this.auth.user.perms.indexOf('merge-tables') === -1) {
return false;
}
if (!this.bs.bill.id) {
@ -300,7 +300,7 @@ export class SalesHomeComponent implements OnInit {
}
voidBillAllowed(): boolean {
if (this.auth.user.perms.indexOf('void-bill') === -1) {
if (this.auth.user && this.auth.user.perms.indexOf('void-bill') === -1) {
return false;
}
if (!this.bs.bill.id) {
@ -352,7 +352,7 @@ export class SalesHomeComponent implements OnInit {
}
splitBillAllowed(): boolean {
if (this.auth.user.perms.indexOf('split-bill') === -1) {
if (this.auth.user && this.auth.user.perms.indexOf('split-bill') === -1) {
return false;
}
if (!this.bs.bill.id) {