6 Commits

Author SHA1 Message Date
e685941804 Version Bump v11.5.0 2023-04-09 15:43:00 +05:30
8bc7d66123 Fix: Prevent creation of customer with blank name when adding a guest book item.
Fix: Also prevent creation of customer with blank phone number when adding a guest book.
Feature: Show the old bill of a customer in guest book
Fix: In reprint, allow changing of customer
Chore: Updated dependencies
2023-04-09 15:42:32 +05:30
302ed4a18f Version Bump v11.4.2 2023-03-24 12:46:54 +05:30
2fb2e01ca1 Feature: Allow edit of time in guest book.
Feature: Guest book row color is the same as running table colors
2023-03-24 12:46:42 +05:30
94cc8ccd47 Version Bump v11.4.1 2023-03-24 10:17:16 +05:30
c13047e812 Fix: Check if moved/merged kots could lead to negative products of a situation where happy hour products were more than regular products.
Case:
  Kot 1:
    Beer Mug => 2 regular, 2 happy hour
  Kot 2:
    Beer Mug => 2 Regular
  Kot 3:
    Beer Mug => -2 Regular

Then move Kot 2 to new table would leave the voucher with 2 hh and 0 regular
Also, just move did not check if products were going negative.
2023-03-24 10:17:06 +05:30
24 changed files with 208 additions and 77 deletions

View File

@ -11,5 +11,6 @@
"**/.mypy_cache": true,
".idea": true,
"**/node_modules": true,
"**/package-lock.json": true,
}
}

View File

@ -1 +1 @@
__version__ = "11.4.0"
__version__ = "11.5.0"

View File

@ -30,11 +30,11 @@ class GuestBook:
customer: Mapped["Customer"] = relationship("Customer")
status: Mapped[Optional["Overview"]] = relationship(back_populates="guest", uselist=False)
def __init__(self, pax=None, id_=None, customer_id=None, customer=None):
def __init__(self, pax=None, id_=None, customer_id=None, customer=None, date_=None):
self.customer_id = customer_id
self.pax = pax
self.id = id_
self.date = datetime.utcnow()
self.date = datetime.utcnow() if date_ is None else date_
if customer is None:
self.customer_id = customer_id
else:

View File

@ -2,12 +2,12 @@ import uuid
from typing import TYPE_CHECKING, List
from barker.models.role_permission import RolePermission
from sqlalchemy import Unicode, text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
from .role_permission import RolePermission
if TYPE_CHECKING:

View File

@ -122,6 +122,6 @@ class ProductVersion:
return False, f"{self.name} is a fixture and cannot be edited or deleted."
if self.is_active:
return False, "Product is active"
if len(self.inventories) > 0 and not advanced_delete:
return False, "Product has entries"
# if len(self.inventories) > 0 and not advanced_delete:
# return False, "Product has entries"
return True, ""

View File

@ -5,14 +5,16 @@ from datetime import date, datetime, timedelta
import barker.schemas.guest_book as schemas
from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy import select
from sqlalchemy import desc, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import joinedload
from ..core.config import settings
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..models.customer import Customer
from ..models.guest_book import GuestBook
from ..models.voucher import Voucher
from ..schemas.user_token import UserToken
@ -28,6 +30,13 @@ def save(
with SessionFuture() as db:
customer = db.execute(select(Customer).where(Customer.phone == data.phone)).scalars().one_or_none()
if customer is None:
if len(data.name) == 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="New customer name cannot be blank",
)
if len(data.phone) == 0:
data.phone = data.name
customer = Customer(
name=data.name,
phone=data.phone,
@ -38,7 +47,9 @@ def save(
else:
customer.name = data.name or customer.name
customer.address = data.address or customer.address
item = GuestBook(pax=data.pax, customer=customer)
item = GuestBook(
pax=data.pax, customer=customer, date_=data.date_ - timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
)
db.add(item)
db.commit()
return guest_book_info(item)
@ -62,6 +73,7 @@ def update_route(
item.customer.phone = data.phone
item.customer.address = data.address
item.pax = data.pax
item.date = data.date_ - timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
db.commit()
return guest_book_info(item)
except SQLAlchemyError as e:
@ -124,21 +136,34 @@ def show_list(
guest_book: list[schemas.GuestBookListItem] = []
with SessionFuture() as db:
for i, item in enumerate(db.execute(list_).scalars().all()):
guest_book.insert(
0,
schemas.GuestBookListItem(
id=item.id,
serial=i + 1,
name=item.customer.name,
phone=item.customer.phone,
pax=item.pax,
date=item.date,
status=None if item.status is None else item.status.status,
tableId=None if item.status is None else item.status.food_table.id,
voucherId=None if item.status is None else item.status.voucher_id,
tableName=None if item.status is None else item.status.food_table.name,
),
gbli = schemas.GuestBookListItem(
id=item.id,
serial=i + 1,
name=item.customer.name,
phone=item.customer.phone,
pax=item.pax,
date=item.date,
status=None if item.status is None else item.status.status,
tableId=None if item.status is None else item.status.food_table.id,
voucherId=None if item.status is None else item.status.voucher_id,
tableName=None if item.status is None else item.status.food_table.name,
)
if item.status is None:
last = (
db.execute(
select(Voucher)
.where(Voucher.customer_id == item.customer_id)
.order_by(desc(Voucher.date))
.options(joinedload(Voucher.food_table, innerjoin=True))
)
.scalars()
.first()
)
if last is not None:
gbli.status = "old"
gbli.voucher_id = last.id
gbli.table_name = last.food_table.name
guest_book.insert(0, gbli)
return schemas.GuestBookList(date=d, list=guest_book)
@ -159,9 +184,15 @@ def guest_book_info(item: GuestBook) -> schemas.GuestBook:
phone=item.customer.phone,
pax=item.pax,
address=item.customer.address,
date=item.date,
date=item.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES),
)
def blank_guest_book_info() -> schemas.GuestBookIn:
return schemas.GuestBookIn(name="", phone="", pax=0, address="")
return schemas.GuestBookIn(
name="",
phone="",
pax=0,
address="",
date=datetime.utcnow() + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES),
)

View File

@ -1,17 +1,16 @@
import uuid
from datetime import datetime
from decimal import Decimal
from typing import List, Set
from typing import Set
import barker.schemas.voucher as schemas
from barker.models.bill import Bill
from fastapi import HTTPException, status
from sqlalchemy import func
from sqlalchemy.orm import Session
from sqlalchemy.sql import expression
from ...models.bill import Bill
from ...models.guest_book import GuestBook
from ...models.overview import Overview
from ...models.regime import Regime

View File

@ -56,6 +56,10 @@ def change(
if bill_changed:
id_ = void_and_issue_new_bill(data, u, g, old, db, user)
else:
if data.customer is not None:
old.customer_id = data.customer.id_
else:
old.customer_id = None
reprint_bill(id_, user.id_, db)
db.commit()
with SessionFuture() as db:

View File

@ -50,7 +50,20 @@ def merge_kot(
.where(Kot.id == data.kot_id, Kot.voucher_id == data.voucher_id)
.values(voucher_id=data.new_voucher_id)
)
update_settlements([data.voucher_id, data.new_voucher_id], db)
donor: Voucher = db.execute(select(Voucher).where(Voucher.id == data.voucher_id)).scalar_one()
recipent: Voucher = db.execute(select(Voucher).where(Voucher.id == data.new_voucher_id)).scalar_one()
if has_negatives(donor.kots) or happy_hour_exceeds_regular(donor.kots):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Donor voucher will have negative products or too many happy hours",
)
if has_negatives(recipent.kots) or happy_hour_exceeds_regular(recipent.kots):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Recipient voucher will have negative products or too many happy hours",
)
do_update_settlements(donor, [], db)
do_update_settlements(recipent, [], db)
db.commit()
except SQLAlchemyError as e:
raise HTTPException(
@ -91,7 +104,19 @@ def move_kot(
db.flush()
do_update_bill_numbers(item, db)
do_update_table(item, None, db)
update_settlements([data.voucher_id, item.id], db)
donor: Voucher = db.execute(select(Voucher).where(Voucher.id == data.voucher_id)).scalar_one()
if has_negatives(donor.kots) or happy_hour_exceeds_regular(donor.kots):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Donor voucher will have negative products or too many happy hours",
)
if has_negatives(item.kots) or happy_hour_exceeds_regular(item.kots):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Recipient voucher will have negative products or too many happy hours",
)
do_update_settlements(donor, [], db)
do_update_settlements(item, [], db)
db.commit()
except SQLAlchemyError as e:
raise HTTPException(
@ -133,7 +158,8 @@ def merge_table(
db.execute(delete(Overview).where(Overview.voucher_id == data.voucher_id))
db.execute(delete(Settlement).where(Settlement.voucher_id == data.voucher_id))
db.execute(delete(Voucher).where(Voucher.id == data.voucher_id))
update_settlements([data.new_voucher_id], db)
recipent: Voucher = db.execute(select(Voucher).where(Voucher.id == data.new_voucher_id)).scalar_one()
do_update_settlements(recipent, [], db)
db.commit()
except SQLAlchemyError as e:
traceback.print_exc()
@ -143,12 +169,6 @@ def merge_table(
)
def update_settlements(vouchers: list[uuid.UUID], db: Session):
for v in vouchers:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == v)).scalar_one()
do_update_settlements(voucher, [], db)
def check_if_voucher_is_unprinted(voucher_id: uuid.UUID, db: Session) -> None:
voucher_type: VoucherType = db.execute(select(Voucher.voucher_type).where(Voucher.id == voucher_id)).scalar_one()
if voucher_type is None:
@ -161,3 +181,26 @@ def check_if_voucher_is_unprinted(voucher_id: uuid.UUID, db: Session) -> None:
status_code=status.HTTP_403_FORBIDDEN,
detail="Bill is printed or void.",
)
def has_negatives(kots: list[Kot]) -> bool:
inv = [i for k in kots for i in k.inventories]
products = set((i.product_id, i.is_happy_hour) for i in inv)
for id_, hh in products:
quantity = sum(i.quantity for i in inv if i.product_id == id_ and i.is_happy_hour == hh)
if quantity < 0:
return True
return False
# This is for the whole bill. eg. Kot 1 => Reg 2 + HH 2; Kot 2 => Reg 4; Kot 3 => Reg - 4
# This is pass okay in happy hours items balanced, but overall this is wrong. Hence this check
def happy_hour_exceeds_regular(kots: list[Kot]) -> bool:
inv = [i for k in kots for i in k.inventories]
products = set(i.product_id for i in inv if i.is_happy_hour)
for p in products:
r = sum(i.quantity for i in inv if i.product_id == p and not i.is_happy_hour)
h = sum(i.quantity for i in inv if i.product_id == p and i.is_happy_hour)
if r < h:
return True
return False

View File

@ -147,7 +147,7 @@ def voucher_info(item: Voucher, db: Session):
"kotId": item.kot_id,
"billId": ", ".join(f"{x.regime.prefix}-{x.bill_number}" for x in item.bills),
"table": {"id": item.food_table_id, "name": item.food_table.name},
"customer": {"id": item.customer_id, "name": item.customer.name} if item.customer is not None else None,
"customer": {"id": item.customer.id, "name": item.customer.name} if item.customer is not None else None,
"narration": item.narration,
"reason": item.reason,
"voucherType": item.voucher_type,

View File

@ -12,16 +12,23 @@ class GuestBookIn(BaseModel):
phone: str
address: str | None
pax: int = Field(ge=0)
date_: datetime
@validator("date_", pre=True)
def parse_date(cls, value):
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
class Config:
fields = {"id_": "id"}
anystr_strip_whitespace = True
alias_generator = to_camel
json_encoders = {datetime: lambda v: v.strftime("%d-%b-%Y %H:%M")}
class GuestBook(GuestBookIn):
id_: uuid.UUID
date_: datetime
@validator("date_", pre=True)
def parse_date(cls, value):

View File

@ -1,36 +1,36 @@
[tool.poetry]
name = "barker"
version = "11.4.0"
version = "11.5.0"
description = "Point of Sale for a restaurant"
authors = ["tanshu <git@tanshu.com>"]
[tool.poetry.dependencies]
python = "^3.11"
uvicorn = {extras = ["standard"], version = "^0.20.0"}
fastapi = "^0.92.0"
uvicorn = {extras = ["standard"], version = "^0.21.1"}
fastapi = "^0.95.0"
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
psycopg2-binary = "^2.9.5"
SQLAlchemy = "^2.0.4"
python-multipart = "^0.0.5"
SQLAlchemy = "^2.0.7"
python-multipart = "^0.0.6"
PyJWT = "^2.6.0"
alembic = "^1.9.4"
alembic = "^1.10.2"
itsdangerous = "^2.1.2"
python-dotenv = "^0.21.1"
pydantic = {extras = ["dotenv"], version = "^1.10.5"}
starlette = "^0.25.0"
python-dotenv = "^1.0.0"
pydantic = {extras = ["dotenv"], version = "^1.10.7"}
starlette = "^0.26.1"
arq = "^0.25.0"
aiohttp = "^3.8.4"
cryptography = "^39.0.1"
cryptography = "^40.0.1"
gunicorn = "^20.1.0"
[tool.poetry.dev-dependencies]
flake8 = "^6.0.0"
black = "^23.1.0"
isort = {extras = ["toml"], version = "^5.12.0"}
pre-commit = "^3.0.4"
mypy = "^1.0.1"
types-python-jose = "^3.3.4.4"
pre-commit = "^3.2.0"
mypy = "^1.1.1"
types-python-jose = "^3.3.4.5"
[tool.poetry.group.dev.dependencies]
tomli = "^2.0.1"

View File

@ -1,6 +1,6 @@
{
"name": "bookie",
"version": "11.4.0",
"version": "11.5.0",
"scripts": {
"ng": "ng",
"start": "ng serve",

View File

@ -39,6 +39,16 @@
<textarea matInput formControlName="address"></textarea>
</mat-form-field>
</div>
<div class="flex flex-row justify-around content-start items-start">
<mat-form-field class="flex-auto">
<mat-label>Hours</mat-label>
<input matInput formControlName="hours" />
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Minutes</mat-label>
<input matInput formControlName="minutes" />
</mat-form-field>
</div>
</form>
</mat-card-content>
<mat-card-actions>

View File

@ -23,6 +23,8 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
phone: FormControl<string | Customer>;
pax: FormControl<number>;
address: FormControl<string | null>;
hours: FormControl<number>;
minutes: FormControl<number>;
}>;
item: GuestBook = new GuestBook();
@ -44,6 +46,8 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
}),
pax: new FormControl(0, { validators: Validators.required, nonNullable: true }),
address: new FormControl<string | null>(null),
hours: new FormControl(0, { validators: [Validators.min(0), Validators.max(23)], nonNullable: true }),
minutes: new FormControl(0, { validators: [Validators.min(0), Validators.max(59)], nonNullable: true }),
});
// Setup Account Autocomplete
this.customers = this.form.controls.phone.valueChanges.pipe(
@ -77,6 +81,8 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
phone: item.phone,
pax: item.pax,
address: item.address,
hours: +item.date.substring(12, 14),
minutes: +item.date.substring(15, 17),
});
}
@ -115,6 +121,9 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
}
this.item.pax = formModel.pax ?? 0;
this.item.address = formModel.address ?? null;
const hours = ('' + (formModel.hours as number)).padStart(2, '0');
const minutes = ('' + (formModel.minutes as number)).padStart(2, '0');
this.item.date = this.item.date.substring(0, 12) + hours + ':' + minutes;
return this.item;
}
}

View File

@ -1,17 +0,0 @@
.full-width-table {
width: 100%;
}
.running {
background-color: #f53d24;
color: #000000;
}
.printed {
background-color: #00f518;
color: #000000;
}
.center {
display: flex;
justify-content: center;
}

View File

@ -23,7 +23,7 @@
<!-- SNo Column -->
<ng-container matColumnDef="sno">
<mat-header-cell *matHeaderCellDef>S. No</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.serial }} {{ row.status }}</mat-cell>
<mat-cell *matCellDef="let row">{{ row.serial }}</mat-cell>
</ng-container>
<!-- Name Column -->
@ -54,7 +54,12 @@
<ng-container matColumnDef="action">
<mat-header-cell *matHeaderCellDef class="center">Action</mat-header-cell>
<mat-cell *matCellDef="let row" class="center">
<button mat-icon-button [routerLink]="['/sales']" [queryParams]="{ guest: row.id }" *ngIf="!row.tableId">
<button
mat-icon-button
[routerLink]="['/sales']"
[queryParams]="{ guest: row.id }"
*ngIf="!row.tableId && !row.voucherId"
>
<mat-icon>chair</mat-icon>
</button>
<button
@ -66,6 +71,15 @@
>
{{ row.tableName }}
</button>
<button
mat-stroked-button
color="primary"
[routerLink]="['/sales', 'bill']"
[queryParams]="{ voucher: row.voucherId }"
*ngIf="!row.tableId && row.voucherId"
>
{{ row.tableName }}
</button>
<button mat-icon-button [routerLink]="['/guest-book/', row.id]">
<mat-icon>edit</mat-icon>
</button>
@ -78,8 +92,8 @@
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
[class.running]="row.status === 'running'"
[class.printed]="row.status === 'printed'"
[class.accent]="row.status === 'running'"
[class.strong-accent]="row.status === 'printed'"
></mat-row>
</mat-table>
</mat-card-content>

View File

@ -0,0 +1,21 @@
@use '@angular/material' as mat
$my-primary: mat.define-palette(mat.$indigo-palette, 500)
$my-accent: mat.define-palette(mat.$amber-palette, A200, A100, A400)
table
width: 100%
.center
display: flex
justify-content: center
.accent
/* Read the 200 hue from the primary color palete.*/
color: mat.get-color-from-palette($my-accent, '200-contrast')
background: mat.get-color-from-palette($my-accent, 200)
.strong-accent
/* Read the 700 hue from the primary color palete.*/
color: mat.get-color-from-palette($my-accent, '700-contrast')
background: mat.get-color-from-palette($my-accent, 700)

View File

@ -17,7 +17,7 @@ import { GuestBookListDataSource } from './guest-book-list-datasource';
@Component({
selector: 'app-guest-book-list',
templateUrl: './guest-book-list.component.html',
styleUrls: ['./guest-book-list.component.css'],
styleUrls: ['./guest-book-list.component.sass'],
})
export class GuestBookListComponent implements OnInit {
data: BehaviorSubject<GuestBook[]> = new BehaviorSubject<GuestBook[]>([]);

View File

@ -22,6 +22,9 @@ export class BillResolver implements Resolve<Bill> {
if (tableId !== null) {
return this.ser.getFromTable(tableId as string, voucherId, guestId);
}
if (voucherId !== null) {
return this.ser.getFromId(voucherId);
}
throw new Error('Unable to get bill');
}
}

View File

@ -52,6 +52,12 @@ export class VoucherService {
.pipe(catchError(this.log.handleError(serviceName, `getFromBill billId=${billId}`))) as Observable<Bill>;
}
getFromId(voucherId: string): Observable<Bill> {
return this.http
.get<Bill>(`${url}/from-id/${voucherId}`)
.pipe(catchError(this.log.handleError(serviceName, `getFromId voucherId=${voucherId}`))) as Observable<Bill>;
}
save(voucher: Bill, voucherType: VoucherType, guestBookId: string | null, updateTable: boolean): Observable<boolean> {
const options = {
params: new HttpParams().set('p', voucherType.toString()).set('u', updateTable.toString()),

View File

@ -1,5 +1,5 @@
export const environment = {
production: true,
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
version: '11.4.0',
version: '11.5.0',
};

View File

@ -5,7 +5,7 @@
export const environment = {
production: false,
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
version: '11.4.0',
version: '11.5.0',
};
/*

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "frank"
version = "11.4.0"
version = "11.5.0"
description = "Point of Sale for a restaurant"
authors = ["tanshu <git@tanshu.com>"]