diff --git a/barker/.env b/barker/.env index dedd4e9..f3c5180 100644 --- a/barker/.env +++ b/barker/.env @@ -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 diff --git a/barker/main.py b/barker/main.py index 7836486..e8a2757 100644 --- a/barker/main.py +++ b/barker/main.py @@ -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"]) diff --git a/barker/routers/voucher/__init__.py b/barker/routers/voucher/__init__.py index bc645d3..4c2b3fd 100644 --- a/barker/routers/voucher/__init__.py +++ b/barker/routers/voucher/__init__.py @@ -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", ) diff --git a/barker/routers/voucher/split.py b/barker/routers/voucher/split.py index 6adf7e7..66d1948 100644 --- a/barker/routers/voucher/split.py +++ b/barker/routers/voucher/split.py @@ -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 diff --git a/barker/routers/voucher/update.py b/barker/routers/voucher/update.py index e3c9326..5ef7873 100644 --- a/barker/routers/voucher/update.py +++ b/barker/routers/voucher/update.py @@ -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) diff --git a/barker/schemas/split.py b/barker/schemas/split.py new file mode 100644 index 0000000..752d9f4 --- /dev/null +++ b/barker/schemas/split.py @@ -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"} diff --git a/bookie/src/app/home/home.component.html b/bookie/src/app/home/home.component.html index 1e081f6..aed58cd 100644 --- a/bookie/src/app/home/home.component.html +++ b/bookie/src/app/home/home.component.html @@ -1,68 +1,68 @@
- +

Guest Book

- +

Sales

- +

Cashier Report

- +

Sale Report

- +

Tax Report

- +

Product Sale Report

- +

Bill Settlement Report

- +

Beer Consumption Report

- +

Discount Report

- +

Tables

- +

Sections

- +

Menu Categories

- +

Sale Categories

- +

Products

- +

Modifier Categories

- +

Modifiers

- +

Taxes

- +

Devices

- +

Section Printers

- +

Printers

- +

Roles

- +

Users

diff --git a/bookie/src/app/sales/bills/voucher.service.ts b/bookie/src/app/sales/bills/voucher.service.ts index 324f0c0..7a0f3e0 100644 --- a/bookie/src/app/sales/bills/voucher.service.ts +++ b/bookie/src/app/sales/bills/voucher.service.ts @@ -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 >this.http.post(`${url}/${id}`, { - voucher: {id: id}, + const options = {params: new HttpParams().set('u', 'true')}; + return >this.http.post(`${urlSplitBill}/${id}`, { inventories: inventoriesToMove, - table: {id: table.id} + tableId: table.id }, options).pipe( catchError(this.log.handleError(serviceName, 'splitBill')) ); diff --git a/bookie/src/app/sales/home/sales-home.component.ts b/bookie/src/app/sales/home/sales-home.component.ts index 79e5d7c..8524657 100644 --- a/bookie/src/app/sales/home/sales-home.component.ts +++ b/bookie/src/app/sales/home/sales-home.component.ts @@ -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 { @@ -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 = (this.discountAllowed()) ? this.showDiscount() : observableOf(''); + const discountObservable: Observable = (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 { + receivePaymentWithReason(type: string, amount: number): Observable { 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) {