Compare commits

...
11 Commits
Author SHA1 Message Date
tanshu dcf1ffad98 Version Bump v14.3.0 2026-02-17 09:45:32 +05:30
tanshu 338d9d63d5 Validation: Making Product name unique ignoring the fraction_units.
Making the sku_version.units unique for a product across stock_keeping_units.

Both respecting the validity
2026-02-17 04:13:11 +00:00
tanshu 9b0da9cb65 Fix: Product sale report was aggregating on the product_version_id 2026-02-14 12:20:09 +00:00
tanshu 4cc2ff2229 Version Bump v14.2.1 2026-02-14 11:30:40 +05:30
tanshu c9fa83e8ac Fix: Accidentally used menu category instead of sale category 2026-02-14 06:00:09 +00:00
tanshu 061dd1310d Version Bump v14.2.0 2026-02-14 05:30:05 +00:00
tanshu a1d6919d9d Beer consumption report can choose menu category for beer 2026-02-14 05:29:11 +00:00
tanshu ecc92795e6 Fix: In product list, if menu category is reset, all products get filtered out as it becomes undefined. 2026-02-12 09:01:12 +00:00
tanshu 535a145742 Fix: Cannot move kot as row.kotId was always undefined. 2026-02-12 08:20:54 +00:00
tanshu 1271e26dd8 Changed Docker CMD to remove redundant run.sh 2026-02-12 04:01:35 +00:00
tanshu 2ae3f3ba7e Dockerfile will kill the process if stuck so that restart policy can restart the container
Gunicorn will kill worker every 10000 requests which can help reclaim memory lost to leaks
2026-02-12 03:52:53 +00:00
29 changed files with 331 additions and 77 deletions
+4 -3
View File
@@ -54,7 +54,8 @@ RUN chmod 777 /app/docker-entrypoint.sh \
ENTRYPOINT ["docker-entrypoint.sh"]
# at the end of your Dockerfile, before CMD or after EXPOSE
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost/health || exit 1
# Kill the main process if the healthcheck fails. This will kill the container and restart policy can restart it.
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsS http://localhost/health || kill -s 15 1
CMD ["poetry", "run", "/app/run.sh"]
CMD ["gunicorn", "barker.main:app", "--worker-class", "uvicorn.workers.UvicornWorker", "--config", "./gunicorn.conf.py", "--log-config", "./logging.conf"]
@@ -34,7 +34,7 @@ def upgrade():
)
op.create_exclude_constraint(
"uq_product_versions_product_id",
op.f("uq_product_versions_product_id"),
"product_versions",
(prod.c.product_id, "="),
(func.daterange(prod.c.valid_from, prod.c.valid_till, text("'[]'")), "&&"),
@@ -0,0 +1,156 @@
"""index
Revision ID: 8260414066d6
Revises: 5cb65066be86
Create Date: 2026-02-13 06:22:39.926120
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "8260414066d6"
down_revision = "5cb65066be86"
branch_labels = None
depends_on = None
def upgrade():
# 1) Add the column (nullable for backfill)
op.add_column("sku_versions", sa.Column("product_id", sa.UUID(), nullable=True))
# 2) Define lightweight table objects (no autoload; explicit columns only)
sku_versions = sa.Table(
"sku_versions",
sa.MetaData(),
sa.Column("id", sa.UUID(), primary_key=True),
sa.Column("sku_id", sa.UUID(), nullable=False),
sa.Column("product_id", sa.UUID(), nullable=True),
sa.Column("valid_from", sa.Date(), nullable=True),
sa.Column("valid_till", sa.Date(), nullable=True),
sa.Column("units", sa.Unicode(), nullable=False),
)
stock_keeping_units = sa.Table(
"stock_keeping_units",
sa.MetaData(),
sa.Column("id", sa.UUID(), primary_key=True),
sa.Column("product_id", sa.UUID(), nullable=False),
)
# 3) Backfill via SQLAlchemy Core UPDATE..SET..(SELECT ...)
product_id_subq = (
sa.select(stock_keeping_units.c.product_id)
.where(stock_keeping_units.c.id == sku_versions.c.sku_id)
.scalar_subquery()
)
backfill_stmt = (
sa.update(sku_versions).values(product_id=product_id_subq).where(sku_versions.c.product_id.is_(None))
)
op.execute(backfill_stmt)
# 5) Enforce NOT NULL at schema level
op.alter_column("sku_versions", "product_id", nullable=False)
# 6) Trigger keeps product_id in sync when sku_id changes
op.execute(
sa.text(
"""
CREATE OR REPLACE FUNCTION sku_versions_set_product_id()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
SELECT s.product_id INTO NEW.product_id
FROM stock_keeping_units s
WHERE s.id = NEW.sku_id;
IF NEW.product_id IS NULL THEN
RAISE EXCEPTION 'Invalid sku_id %, no product found', NEW.sku_id;
END IF;
RETURN NEW;
END;
$$;
"""
)
)
op.execute(sa.text("DROP TRIGGER IF EXISTS trg_sku_versions_set_product_id ON sku_versions;"))
op.execute(
sa.text(
"""
CREATE TRIGGER trg_sku_versions_set_product_id
BEFORE INSERT OR UPDATE OF sku_id
ON sku_versions
FOR EACH ROW
EXECUTE FUNCTION sku_versions_set_product_id();
"""
)
)
# 7) Exclusion constraint: product_id + units must not overlap in time
# daterange(valid_from, valid_till, '[]') overlap operator &&.
sv = sa.table(
"sku_versions",
sa.column("product_id", sa.UUID()),
sa.column("units", sa.UUID()),
sa.column("valid_from", sa.Date()),
sa.column("valid_till", sa.Date()),
)
op.create_exclude_constraint(
op.f("uq_sku_versions_product_id_units"),
"sku_versions",
(sv.c.product_id, "="),
(sv.c.units, "="),
(sa.func.daterange(sv.c.valid_from, sv.c.valid_till, sa.text("'[]'")), "&&"),
)
prod = sa.table(
"product_versions",
sa.column("id", sa.UUID()),
sa.column("name", sa.Unicode(length=255)),
sa.column("fraction_units", sa.Unicode(length=255)),
sa.column("valid_from", sa.Date()),
sa.column("valid_till", sa.Date()),
)
# Update the exclude constraint on product_versions to only be on the name and drop fraction_units from it
op.drop_constraint("uq_product_versions_name", "product_versions", type_="unique")
op.create_exclude_constraint(
op.f("uq_product_versions_name"),
"product_versions",
(prod.c.name, "="),
(sa.func.daterange(prod.c.valid_from, prod.c.valid_till, sa.text("'[]'")), "&&"),
)
# ### end Alembic commands ###
def downgrade():
op.drop_constraint("uq_sku_versions_product_id_units", "sku_versions", type_="exclude")
# Drop trigger + function
op.execute(sa.text("DROP TRIGGER IF EXISTS trg_sku_versions_set_product_id ON sku_versions;"))
op.execute(sa.text("DROP FUNCTION IF EXISTS sku_versions_set_product_id();"))
# Drop column
op.drop_column("sku_versions", "product_id")
prod = sa.table(
"product_versions",
sa.column("id", sa.UUID()),
sa.column("name", sa.Unicode(length=255)),
sa.column("fraction_units", sa.Unicode(length=255)),
sa.column("valid_from", sa.Date()),
sa.column("valid_till", sa.Date()),
)
op.drop_constraint("uq_product_versions_name", "product_versions", type_="unique")
op.create_exclude_constraint(
"uq_product_versions_name",
"product_versions",
(prod.c.name, "="),
(prod.c.units, "="),
(sa.func.daterange(prod.c.valid_from, prod.c.valid_till, sa.text("'[]'")), "&&"),
)
+1 -1
View File
@@ -1 +1 @@
__version__ = "14.1.0"
__version__ = "14.3.0"
+1 -1
View File
@@ -53,7 +53,7 @@ class Inventory:
type_: Mapped[InventoryType] = mapped_column(
"type", Enum(InventoryType), server_default=text("regular"), nullable=False
)
parent_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, ForeignKey("inventories.id"), nullable=True)
parent_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, ForeignKey("inventories.id"), nullable=True, index=True)
kot: Mapped[Kot] = relationship(back_populates="inventories")
tax: Mapped[Tax] = relationship(back_populates="inventories")
+1 -1
View File
@@ -22,7 +22,7 @@ class InventoryModifier:
__table_args__ = (UniqueConstraint("inventory_id", "modifier_id"),)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
inventory_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("inventories.id"), nullable=False)
inventory_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("inventories.id"), nullable=False, index=True)
modifier_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("modifiers.id"), nullable=False)
price: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
+2 -2
View File
@@ -24,5 +24,5 @@ class RoleInclude:
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
role_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("roles.id"), nullable=False)
included_role_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("roles.id"), nullable=False)
role_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("roles.id"), nullable=False, index=True)
included_role_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("roles.id"), nullable=False, index=True)
+25 -1
View File
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
from sqlalchemy import Boolean, Date, ForeignKey, Numeric, Unicode, Uuid, func, text
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
from ..db.base_class import reg
@@ -26,6 +26,14 @@ class SkuVersion:
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()")
)
sku_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
# DB column is "product_id", but ORM attribute is private: "_product_id"
_product_id: Mapped[uuid.UUID] = mapped_column(
"product_id",
Uuid,
nullable=False,
repr=False, # hides in dataclass repr
init=False, # not part of __init__ signature (dataclass)
)
units: Mapped[str] = mapped_column(
Unicode, nullable=False
) # Need to have logic in the application to handle unit uniqueness since we don't have product_id here
@@ -56,8 +64,24 @@ class SkuVersion:
(sku_id, "="),
(func.daterange(valid_from, valid_till, text("'[]'")), "&&"),
),
# product-level uniqueness per time range
postgresql.ExcludeConstraint(
(_product_id, "="),
(units, "="),
(func.daterange(valid_from, valid_till, text("'[]'")), "&&"),
name="uq_sku_versions_product_units_time",
using="gist",
),
)
@validates("_product_id")
def _prevent_writes_to_product_id(self, key, value):
"""
This column is integrity-only. We never accept app-side writes.
DB trigger sets it.
"""
raise ValueError("product_id is managed by the database and must not be set in application code.")
def __init__(
self,
units: str = "",
+3 -16
View File
@@ -32,7 +32,7 @@ from ..schemas.sale_category import SaleCategoryLink
from ..schemas.stock_keeping_unit import StockKeepingUnit as StockKeepingUnitSchema
from ..schemas.tax import TaxLink
from ..schemas.user_token import UserToken
from . import _pv_onclause, _sv_onclause, effective_date
from . import _pv_active, _pv_onclause, _sv_onclause, effective_date
router = APIRouter()
@@ -241,7 +241,6 @@ def update_route(
or new_data_sku.has_happy_hour != sku.has_happy_hour
or new_data_sku.menu_category.id_ != sku.menu_category_id
)
print(f"SKU Changed: {sku_changed} for {sku.id}")
sku.sku.sort_order = new_data_sku.sort_order
sku.sku.is_not_available = new_data_sku.is_not_available
if sku_changed:
@@ -471,7 +470,6 @@ def show_term(
) -> list[ProductQuery]:
product_version_onclause = _pv_onclause(date_)
sku_version_onclause = _sv_onclause(date_)
print(f"Fetching products for MenuCategory: {mc}, SaleCategory: {sc}, Date: {date_}")
list_: list[ProductQuery] = []
query = (
select(SkuVersion)
@@ -525,17 +523,6 @@ def show_id(
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
) -> schemas.Product:
pv_active = and_(
ProductVersion.product_id == id_,
or_(ProductVersion.valid_from == None, ProductVersion.valid_from <= date_), # noqa: E711
or_(ProductVersion.valid_till == None, ProductVersion.valid_till >= date_), # noqa: E711
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= date_), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= date_), # noqa: E711
)
with SessionFuture() as db:
version: ProductVersion = (
db.execute(
@@ -543,9 +530,9 @@ def show_id(
.join(ProductVersion.sale_category)
.join(ProductVersion.product)
.join(Product.skus)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(date_))
.join(SkuVersion.menu_category)
.where(pv_active)
.where(ProductVersion.product_id == id_, _pv_active(date_))
.order_by(
StockKeepingUnit.sort_order,
SkuVersion.units,
@@ -1,3 +1,5 @@
import uuid
from datetime import date, timedelta
from fastapi import APIRouter, Depends, Security
@@ -16,6 +18,7 @@ from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas.beer_consumption_report import BeerConsumptionReport, BeerConsumptionReportItem
from ...schemas.sale_category import SaleCategoryLink
from ...schemas.user_token import UserToken
from .. import _pv_onclause, _sv_onclause
from . import check_audit_permission, report_finish_date, report_start_date
@@ -28,6 +31,7 @@ router = APIRouter()
def beer_consumption(
start_date: date = Depends(report_start_date),
finish_date: date = Depends(report_finish_date),
sc: uuid.UUID | None = None, # Sale Category
r: bool | None = True,
h: bool | None = True,
st: bool | None = True,
@@ -55,6 +59,8 @@ def beer_consumption(
day <= finish_date,
)
)
if sc:
query = query.where(ProductVersion.sale_category_id == sc)
if h is False and r is not False:
query = query.where(Inventory.is_happy_hour == h)
if r is False and h is not False:
@@ -89,6 +95,7 @@ def beer_consumption(
return BeerConsumptionReport(
start_date=start_date,
finish_date=finish_date,
sale_category=SaleCategoryLink(id_=sc, name="") if sc else None,
regular=r,
happy=h,
staff=st,
@@ -53,13 +53,11 @@ def product_sale_report(
start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session
) -> list[ProductSaleReportItem]:
day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
Kot.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
product_version_onclause = _pv_onclause(day)
sku_version_onclause = _sv_onclause(day)
query = (
select(
ProductVersion.id,
StockKeepingUnit.id,
ProductVersion.name,
SkuVersion.units,
Voucher.voucher_type,
@@ -69,10 +67,10 @@ def product_sale_report(
.join(Inventory.kot)
.join(Kot.voucher)
.join(Inventory.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(day))
.join(SkuVersion.menu_category)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(day))
.join(ProductVersion.sale_category)
.join(Voucher.food_table)
.where(
@@ -85,7 +83,7 @@ def product_sale_report(
query = query.group_by(
SaleCategory.name,
MenuCategory.name,
ProductVersion.id,
StockKeepingUnit.id,
ProductVersion.name,
SkuVersion.units,
Voucher.voucher_type,
@@ -93,14 +91,14 @@ def product_sale_report(
).order_by(SaleCategory.name, MenuCategory.name, ProductVersion.name, SkuVersion.units)
list_ = db.execute(query).all()
info: list[ProductSaleReportItem] = []
for product_version_id, name, units, v_type, hh, quantity in list_:
for sku_id, name, units, v_type, hh, quantity in list_:
type_ = VoucherType(v_type).name
old = next((i for i in info if i.product_version_id == product_version_id and i.is_happy_hour == hh), None)
old = next((i for i in info if i.sku_id == sku_id and i.is_happy_hour == hh), None)
if old:
old[type_] = old[type_] + quantity
else:
item = ProductSaleReportItem(
product_version_id=product_version_id, name=f"{'H H ' if hh else ''}{name} ({units})", is_happy_hour=hh
sku_id=sku_id, name=f"{'H H ' if hh else ''}{name} ({units})", is_happy_hour=hh
)
item[type_] = quantity
info.append(item)
+1 -1
View File
@@ -60,7 +60,7 @@ def get_sale_report(
def get_sale(start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session) -> list[SaleReportItem]:
day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
Kot.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
product_version_onclause = _pv_onclause(day)
query = (
@@ -13,6 +13,7 @@ from pydantic import (
)
from . import Daf, to_camel
from .sale_category import SaleCategoryLink
class BeerConsumptionReportItem(BaseModel):
@@ -56,6 +57,7 @@ class BeerConsumptionReportItem(BaseModel):
class BeerConsumptionReport(BaseModel):
start_date: date
finish_date: date
sale_category: SaleCategoryLink | None
regular: bool | None
happy: bool | None
staff: bool | None
+2 -2
View File
@@ -19,7 +19,7 @@ from .user import UserLink
class ProductSaleReportItem(BaseModel):
product_version_id: uuid.UUID
sku_id: uuid.UUID
name: str
is_happy_hour: bool
@@ -40,7 +40,7 @@ class ProductSaleReportItem(BaseModel):
@model_serializer(mode="plain")
def custom_dump(self) -> dict: # type: ignore
base = {
"productVersionId": str(self.product_version_id),
"skuId": str(self.sku_id),
"name": self.name,
"isHappyHour": self.is_happy_hour,
}
+71 -22
View File
@@ -3,6 +3,9 @@ import multiprocessing
import os
# --- Worker sizing -----------------------------------------------------------
# Prefer explicit WEB_CONCURRENCY in containers. Otherwise fall back to
# WORKERS_PER_CORE * cpu_count(), with a minimum of 2 and optional MAX_WORKERS.
workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
max_workers_str = os.getenv("MAX_WORKERS")
use_max_workers = None
@@ -10,12 +13,6 @@ if max_workers_str:
use_max_workers = int(max_workers_str)
web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)
host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "9995")
bind_env = os.getenv("BIND", None)
use_loglevel = os.getenv("LOG_LEVEL", "info")
use_bind = bind_env if bind_env else f"{host}:{port}"
cores = multiprocessing.cpu_count()
workers_per_core = float(workers_per_core_str)
default_web_concurrency = workers_per_core * cores
@@ -26,27 +23,47 @@ else:
web_concurrency = max(int(default_web_concurrency), 2)
if use_max_workers:
web_concurrency = min(web_concurrency, use_max_workers)
accesslog_var = os.getenv("ACCESS_LOG", "-")
use_accesslog = accesslog_var or None
errorlog_var = os.getenv("ERROR_LOG", "-")
use_errorlog = errorlog_var or None
graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
timeout_str = os.getenv("TIMEOUT", "120")
keepalive_str = os.getenv("KEEP_ALIVE", "5")
# Gunicorn config variables
loglevel = use_loglevel
# --- Bind / logging ----------------------------------------------------------
host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "9995")
bind_env = os.getenv("BIND", None)
use_bind = bind_env if bind_env else f"{host}:{port}"
loglevel = os.getenv("LOG_LEVEL", "info")
accesslog_var = os.getenv("ACCESS_LOG", "-")
accesslog = accesslog_var or None
errorlog_var = os.getenv("ERROR_LOG", "-")
errorlog = errorlog_var or None
worker_tmp_dir = "/dev/shm"
# --- Timeouts / keepalive ----------------------------------------------------
graceful_timeout = int(os.getenv("GRACEFUL_TIMEOUT", "120"))
timeout = int(os.getenv("TIMEOUT", "120"))
keepalive = int(os.getenv("KEEP_ALIVE", "5"))
# --- Robustness knobs (recommended) -----------------------------------------
# Recycle workers gradually to mitigate memory leaks / fragmentation without
# full container restarts. Tune via env if needed.
max_requests = int(os.getenv("MAX_REQUESTS", "10000"))
max_requests_jitter = int(os.getenv("MAX_REQUESTS_JITTER", "1000"))
# Prevent slow clients from holding connections forever (defaults are fine).
# You can tune these via env if you ever need to.
# worker_connections matters only for async worker types; kept here for clarity.
worker_connections = int(os.getenv("WORKER_CONNECTIONS", "1000"))
# Helpful in containerized environments: ensure workers are responsive.
# (Defaults are okay; leaving commented unless you want strict behavior.)
# heartbeat_interval = int(os.getenv("HEARTBEAT_INTERVAL", "30"))
# --- Gunicorn config vars ----------------------------------------------------
workers = web_concurrency
bind = use_bind
errorlog = use_errorlog
worker_tmp_dir = "/dev/shm"
accesslog = use_accesslog
graceful_timeout = int(graceful_timeout_str)
timeout = int(timeout_str)
keepalive = int(keepalive_str)
# For debugging and testing
# --- Debug print (keep if you like) ------------------------------------------
log_data = {
"loglevel": loglevel,
"workers": workers,
@@ -56,10 +73,42 @@ log_data = {
"keepalive": keepalive,
"errorlog": errorlog,
"accesslog": accesslog,
"worker_tmp_dir": worker_tmp_dir,
"max_requests": max_requests,
"max_requests_jitter": max_requests_jitter,
"worker_connections": worker_connections,
# Additional, non-gunicorn variables
"workers_per_core": workers_per_core,
"use_max_workers": use_max_workers,
"host": host,
"port": port,
"cores": cores,
}
print(json.dumps(log_data))
# Variables Gunicorn reads
# (these must be module-level names)
# fmt: off
# Core
loglevel = loglevel
workers = workers
bind = bind
# Logging
accesslog = accesslog
errorlog = errorlog
# Runtime
worker_tmp_dir = worker_tmp_dir
graceful_timeout = graceful_timeout
timeout = timeout
keepalive = keepalive
# Recycling
max_requests = max_requests
max_requests_jitter = max_requests_jitter
# Concurrency (relevant for some worker types; harmless otherwise)
worker_connections = worker_connections
# fmt: on
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "barker"
version = "14.1.0"
version = "14.3.0"
description = "Point of Sale for a restaurant"
authors = ["tanshu <git@tanshu.com>"]
-3
View File
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
gunicorn barker.main:app --worker-class uvicorn.workers.UvicornWorker --config ./gunicorn.conf.py --log-config ./logging.conf
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bookie",
"version": "14.1.0",
"version": "14.3.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
+1 -1
View File
@@ -1,7 +1,7 @@
export const environment = {
production: true,
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
version: '14.1.0',
version: '14.3.0',
};
export const dateFormat = {
@@ -31,6 +31,16 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Sale Category</mat-label>
<mat-select formControlName="saleCategory" (selectionChange)="filterOn($event.value)">
@for (sc of saleCategories; track sc) {
<mat-option [value]="sc.id">
{{ sc.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
</div>
<div class="row-container sm:max-lg:flex-col">
@@ -51,7 +61,7 @@
@for (col of info.headers; track col) {
<ng-container matColumnDef="{{ col }}">
<mat-header-cell *matHeaderCellDef class="right">{{ col }}</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row[col] }}</mat-cell>
<mat-cell *matCellDef="let row" class="right">{{ row[col] | number: '1.2-2' }}</mat-cell>
</ng-container>
}
@@ -1,3 +1,4 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@@ -6,10 +7,12 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import moment from 'moment';
import { SaleCategory } from '../core/sale-category';
import { ToCsvService } from '../shared/to-csv.service';
import { BeerSaleExportHeaderInterface } from './beer-sale-export-header-interface';
import { BeerSaleReport } from './beer-sale-report';
@@ -20,13 +23,14 @@ import { BeerSaleReportDataSource } from './beer-sale-report-datasource';
templateUrl: './beer-sale-report.component.html',
styleUrls: ['./beer-sale-report.component.css'],
imports: [
CommonModule,
MatButtonModule,
MatCheckboxModule,
MatDatepickerModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatTableModule,
ReactiveFormsModule,
],
@@ -36,11 +40,13 @@ export class BeerSaleReportComponent implements OnInit {
private router = inject(Router);
private toCsv = inject(ToCsvService);
saleCategories: SaleCategory[] = [];
info: BeerSaleReport = new BeerSaleReport();
dataSource: BeerSaleReportDataSource = new BeerSaleReportDataSource(this.info.data);
form: FormGroup<{
startDate: FormControl<Date>;
finishDate: FormControl<Date>;
saleCategory: FormControl<string>;
regular: FormControl<boolean>;
happy: FormControl<boolean>;
staff: FormControl<boolean>;
@@ -55,6 +61,7 @@ export class BeerSaleReportComponent implements OnInit {
this.form = new FormGroup({
startDate: new FormControl(new Date(), { nonNullable: true }),
finishDate: new FormControl(new Date(), { nonNullable: true }),
saleCategory: new FormControl('', { nonNullable: true }),
regular: new FormControl<boolean>(true, { nonNullable: true }),
happy: new FormControl<boolean>(true, { nonNullable: true }),
staff: new FormControl<boolean>(true, { nonNullable: true }),
@@ -64,12 +71,14 @@ export class BeerSaleReportComponent implements OnInit {
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: BeerSaleReport };
const data = value as { info: BeerSaleReport; saleCategories: SaleCategory[] };
this.info = data.info;
this.saleCategories = data.saleCategories;
this.displayedColumns = ['date'].concat(this.info.headers);
this.form.setValue({
startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
saleCategory: this.info.saleCategory?.id ?? '',
regular: this.info.regular,
happy: this.info.happy,
staff: this.info.staff,
@@ -85,6 +94,7 @@ export class BeerSaleReportComponent implements OnInit {
queryParams: {
startDate: info.startDate,
finishDate: info.finishDate,
saleCategory: info.saleCategory?.id ?? '',
regular: info.regular,
happy: info.happy,
staff: info.staff,
@@ -93,12 +103,18 @@ export class BeerSaleReportComponent implements OnInit {
});
}
filterOn(id: string) {
const sc = this.saleCategories.find((x) => x.id === id);
this.info.saleCategory = sc ? sc : new SaleCategory({ id });
}
getInfo(): BeerSaleReport {
const formModel = this.form.value;
return new BeerSaleReport({
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
saleCategory: this.info.saleCategory,
regular: formModel.regular,
happy: formModel.happy,
staff: formModel.staff,
@@ -7,9 +7,10 @@ import { BeerSaleReportService } from './beer-sale-report.service';
export const beerSaleReportResolver: ResolveFn<BeerSaleReport> = (route) => {
const startDate = route.queryParamMap.get('startDate') ?? null;
const finishDate = route.queryParamMap.get('finishDate') ?? null;
const saleCategory = route.queryParamMap.get('saleCategory') ?? null;
const regular = route.queryParamMap.get('regular') !== 'false';
const happy = route.queryParamMap.get('happy') !== 'false';
const staff = route.queryParamMap.get('staff') !== 'false';
const nc = route.queryParamMap.get('nc') !== 'false';
return inject(BeerSaleReportService).get(startDate, finishDate, regular, happy, staff, nc);
return inject(BeerSaleReportService).get(startDate, finishDate, saleCategory, regular, happy, staff, nc);
};
@@ -1,6 +1,7 @@
import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service';
import { saleCategoryListResolver } from '../sale-category/sale-category-list.resolver';
import { BeerSaleReportComponent } from './beer-sale-report.component';
import { beerSaleReportResolver } from './beer-sale-report.resolver';
@@ -14,6 +15,7 @@ export const routes: Routes = [
},
resolve: {
info: beerSaleReportResolver,
saleCategories: saleCategoryListResolver,
},
runGuardsAndResolvers: 'always',
},
@@ -19,6 +19,7 @@ export class BeerSaleReportService {
get(
startDate: string | null,
finishDate: string | null,
saleCategory: string | null,
regular: boolean,
happy: boolean,
staff: boolean,
@@ -31,6 +32,9 @@ export class BeerSaleReportService {
if (finishDate !== null) {
options.params = options.params.set('f', finishDate);
}
if (saleCategory != null) {
options.params = options.params.set('sc', saleCategory);
}
options.params = options.params.set('r', regular);
options.params = options.params.set('h', happy);
options.params = options.params.set('st', staff);
@@ -1,8 +1,10 @@
import { SaleCategory } from '../core/sale-category';
import { BeerSaleReportItem } from './beer-sale-report-item';
export class BeerSaleReport {
startDate: string;
finishDate: string;
saleCategory: SaleCategory;
regular: boolean;
happy: boolean;
staff: boolean;
@@ -13,6 +15,7 @@ export class BeerSaleReport {
public constructor(init?: Partial<BeerSaleReport>) {
this.startDate = '';
this.finishDate = '';
this.saleCategory = new SaleCategory();
this.regular = true;
this.happy = true;
this.staff = true;
@@ -71,7 +71,7 @@ export class ProductListDataSource extends DataSource<Product> {
}),
);
const matchesMenuCategory = menuCategory === '' || skus.some((k) => (k.menuCategory?.id ?? '') === menuCategory);
const matchesMenuCategory = !menuCategory || skus.some((k) => (k.menuCategory?.id ?? '') === menuCategory);
return matchesSearch && matchesMenuCategory;
});
}
@@ -75,9 +75,6 @@ export class ProductListComponent implements OnInit {
this.menuCategoryFilter.subscribe((val) => {
console.log('Menu category filter changed to ', val);
});
this.searchFilter.subscribe((val) => {
console.log('Search filter changed to ', val);
});
}
filterOn(val: string) {
@@ -90,7 +90,7 @@
<ng-container matColumnDef="kotActions">
<mat-header-cell *matHeaderCellDef>Quantity</mat-header-cell>
<mat-cell *matCellDef="let row" class="right-align">
<button mat-icon-button (click)="moveKot(row)" [disabled]="!row.kotId">
<button mat-icon-button (click)="moveKot(row)" [disabled]="!row.kot.id">
<mat-icon class="del">open_in_new</mat-icon>
</button>
</mat-cell>
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "frank"
version = "14.1.0"
version = "14.3.0"
description = "Point of Sale for a restaurant"
authors = ["tanshu <git@tanshu.com>"]