Compare commits

...
7 Commits
Author SHA1 Message Date
tanshu 325bcae998 Version Bump v14.3.1 2026-02-20 21:58:40 +05:30
tanshu ab8fed17d0 Fix: Product update failed as sku_version_id was not sent on the schema and hence update tried to create a new sku.
Fix: If a product sku was updated and then deleted on the same day, it failed as the valid_till for the sku_version was set to yesterday while the valid_from was set to today.
Fix: Product update also failed as the db.commit was on the loop adding new skus
2026-02-20 16:27:14 +00:00
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
25 changed files with 264 additions and 118 deletions
+9
View File
@@ -0,0 +1,9 @@
{
// For more information, visit: https://angular.dev/ai/mcp
"servers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
@@ -34,7 +34,7 @@ def upgrade():
) )
op.create_exclude_constraint( op.create_exclude_constraint(
"uq_product_versions_product_id", op.f("uq_product_versions_product_id"),
"product_versions", "product_versions",
(prod.c.product_id, "="), (prod.c.product_id, "="),
(func.daterange(prod.c.valid_from, prod.c.valid_till, text("'[]'")), "&&"), (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.2.0" __version__ = "14.3.1"
+1 -1
View File
@@ -38,7 +38,7 @@ from ..models.stock_keeping_unit import StockKeepingUnit
from ..models.tax import Tax from ..models.tax import Tax
from ..models.user import User from ..models.user import User
from ..models.user_role import UserRole from ..models.user_role import UserRole
from ..models.voucher import Voucher # noqa from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType from ..models.voucher_type import VoucherType
from .base_class import reg from .base_class import reg
+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 import Boolean, Date, ForeignKey, Numeric, Unicode, Uuid, func, text
from sqlalchemy.dialects import postgresql 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 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()") 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) 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( units: Mapped[str] = mapped_column(
Unicode, nullable=False Unicode, nullable=False
) # Need to have logic in the application to handle unit uniqueness since we don't have product_id here ) # 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, "="), (sku_id, "="),
(func.daterange(valid_from, valid_till, text("'[]'")), "&&"), (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__( def __init__(
self, self,
units: str = "", units: str = "",
+14 -54
View File
@@ -32,7 +32,7 @@ from ..schemas.sale_category import SaleCategoryLink
from ..schemas.stock_keeping_unit import StockKeepingUnit as StockKeepingUnitSchema from ..schemas.stock_keeping_unit import StockKeepingUnit as StockKeepingUnitSchema
from ..schemas.tax import TaxLink from ..schemas.tax import TaxLink
from ..schemas.user_token import UserToken from ..schemas.user_token import UserToken
from . import _pv_active, _pv_onclause, _sv_onclause, effective_date from . import _pv_active, _pv_onclause, _sv_active, _sv_onclause, effective_date
router = APIRouter() router = APIRouter()
@@ -117,21 +117,6 @@ def save(
def add_modifiers( def add_modifiers(
sku_id: uuid.UUID, product_id: uuid.UUID, menu_category_id: uuid.UUID, date_: date, db: Session sku_id: uuid.UUID, product_id: uuid.UUID, menu_category_id: uuid.UUID, date_: date, db: Session
) -> None: ) -> None:
sv_active = and_(
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= date_), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= date_), # noqa: E711
)
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
# how many DISTINCT products (excluding current product via sku_id) are in this menu category today? # how many DISTINCT products (excluding current product via sku_id) are in this menu category today?
products_in_category = db.execute( products_in_category = db.execute(
select(func.count(func.distinct(StockKeepingUnit.product_id))) select(func.count(func.distinct(StockKeepingUnit.product_id)))
@@ -139,7 +124,7 @@ def add_modifiers(
.join(SkuVersion.sku) # -> StockKeepingUnit .join(SkuVersion.sku) # -> StockKeepingUnit
.where( .where(
and_( and_(
sv_active, _sv_active(date_),
SkuVersion.menu_category_id == menu_category_id, SkuVersion.menu_category_id == menu_category_id,
SkuVersion.sku_id != sku_id, SkuVersion.sku_id != sku_id,
) )
@@ -152,7 +137,7 @@ def add_modifiers(
ModifierCategory.id, ModifierCategory.id,
) )
.select_from(ProductVersion) .select_from(ProductVersion)
.join(Product, onclause=product_version_onclause) .join(Product, onclause=_pv_onclause(date_))
.join(Product.modifier_categories) .join(Product.modifier_categories)
.group_by(ModifierCategory.id) .group_by(ModifierCategory.id)
).all() ).all()
@@ -224,11 +209,15 @@ def update_route(
.scalars() .scalars()
.all() .all()
) )
for i in range(len(old_svers), 0, -1): for i in range(len(old_svers), 0, -1):
sku: SkuVersion = old_svers[i - 1] sku: SkuVersion = old_svers[i - 1]
index = next((idx for (idx, d) in enumerate(data.skus) if d.id_ == sku.id), None) index = next((idx for (idx, d) in enumerate(data.skus) if d.version_id == sku.id), None)
if index is None: if index is None:
if sku.valid_from == date_:
# Created/changed effective today, and now removed in the same request.
# Delete instead of creating an invalid interval.
db.delete(sku)
else:
sku.valid_till = date_ - timedelta(days=1) sku.valid_till = date_ - timedelta(days=1)
continue continue
new_data_sku = data.skus.pop(index) new_data_sku = data.skus.pop(index)
@@ -301,11 +290,6 @@ def delete_route(
) -> None: ) -> None:
with SessionFuture() as db: with SessionFuture() as db:
# Active SkuVersion filter # Active SkuVersion filter
sv_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
)
day = func.cast( day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
@@ -323,19 +307,7 @@ def delete_route(
) )
pv_active: ProductVersion = db.execute( pv_active: ProductVersion = db.execute(
select(ProductVersion).where( select(ProductVersion).where(ProductVersion.product_id == id_, _pv_active(date_))
and_(
ProductVersion.product_id == id_,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
)
).scalar_one() ).scalar_one()
if pv_active.valid_from == date_: if pv_active.valid_from == date_:
db.delete(pv_active) db.delete(pv_active)
@@ -345,7 +317,7 @@ def delete_route(
sv_active = ( sv_active = (
db.execute( db.execute(
select(SkuVersion) select(SkuVersion)
.join(StockKeepingUnit, onclause=sv_onclause) # -> StockKeepingUnit .join(StockKeepingUnit, onclause=_sv_onclause(date_)) # -> StockKeepingUnit
.where(StockKeepingUnit.product_id == id_) .where(StockKeepingUnit.product_id == id_)
) )
.scalars() .scalars()
@@ -374,28 +346,15 @@ def show_list(date_: date = Depends(effective_date), user: UserToken = Depends(g
def product_list(date_: date, db: Session) -> list[schemas.Product]: def product_list(date_: date, db: Session) -> list[schemas.Product]:
# Active ProductVersion filter
pv_active = and_(
or_(ProductVersion.valid_from == None, ProductVersion.valid_from <= date_), # noqa: E711
or_(ProductVersion.valid_till == None, ProductVersion.valid_till >= date_), # noqa: E711
)
# Active SkuVersion filter
sv_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
)
rows = ( rows = (
db.execute( db.execute(
select(ProductVersion) select(ProductVersion)
.join(ProductVersion.sale_category) # ProductVersion has sale_category .join(ProductVersion.sale_category) # ProductVersion has sale_category
.join(ProductVersion.product) .join(ProductVersion.product)
.join(Product.skus) .join(Product.skus)
.join(SkuVersion, sv_onclause) .join(SkuVersion, onclause=_sv_onclause(date_))
.join(SkuVersion.menu_category) # Menu category lives here .join(SkuVersion.menu_category) # Menu category lives here
.where(pv_active) .where(_pv_active(date_))
.order_by( .order_by(
MenuCategory.sort_order, MenuCategory.sort_order,
MenuCategory.name, MenuCategory.name,
@@ -605,6 +564,7 @@ def product_info(version: ProductVersion) -> schemas.Product:
skus=[ skus=[
StockKeepingUnitSchema( StockKeepingUnitSchema(
id_=sku.id, id_=sku.id,
version_id=sku_version.id,
units=sku_version.units, units=sku_version.units,
fraction=sku_version.fraction, fraction=sku_version.fraction,
product_yield=sku_version.product_yield, product_yield=sku_version.product_yield,
@@ -18,7 +18,7 @@ from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType from ...models.voucher_type import VoucherType
from ...schemas.beer_consumption_report import BeerConsumptionReport, BeerConsumptionReportItem from ...schemas.beer_consumption_report import BeerConsumptionReport, BeerConsumptionReportItem
from ...schemas.menu_category import MenuCategoryLink from ...schemas.sale_category import SaleCategoryLink
from ...schemas.user_token import UserToken from ...schemas.user_token import UserToken
from .. import _pv_onclause, _sv_onclause from .. import _pv_onclause, _sv_onclause
from . import check_audit_permission, report_finish_date, report_start_date from . import check_audit_permission, report_finish_date, report_start_date
@@ -31,7 +31,7 @@ router = APIRouter()
def beer_consumption( def beer_consumption(
start_date: date = Depends(report_start_date), start_date: date = Depends(report_start_date),
finish_date: date = Depends(report_finish_date), finish_date: date = Depends(report_finish_date),
m: uuid.UUID | None = None, # Menu Category sc: uuid.UUID | None = None, # Sale Category
r: bool | None = True, r: bool | None = True,
h: bool | None = True, h: bool | None = True,
st: bool | None = True, st: bool | None = True,
@@ -51,7 +51,6 @@ def beer_consumption(
.join(Kot.inventories) .join(Kot.inventories)
.join(Inventory.sku) .join(Inventory.sku)
.join(SkuVersion, onclause=sku_version_onclause) .join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion.menu_category)
.join(StockKeepingUnit.product) .join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause) .join(ProductVersion, onclause=product_version_onclause)
.where( .where(
@@ -60,8 +59,8 @@ def beer_consumption(
day <= finish_date, day <= finish_date,
) )
) )
if m: if sc:
query = query.where(SkuVersion.menu_category_id == m) query = query.where(ProductVersion.sale_category_id == sc)
if h is False and r is not False: if h is False and r is not False:
query = query.where(Inventory.is_happy_hour == h) query = query.where(Inventory.is_happy_hour == h)
if r is False and h is not False: if r is False and h is not False:
@@ -96,7 +95,7 @@ def beer_consumption(
return BeerConsumptionReport( return BeerConsumptionReport(
start_date=start_date, start_date=start_date,
finish_date=finish_date, finish_date=finish_date,
menu_category=MenuCategoryLink(id_=m, name="", skus=[]) if m else None, sale_category=SaleCategoryLink(id_=sc, name="") if sc else None,
regular=r, regular=r,
happy=h, happy=h,
staff=st, staff=st,
@@ -53,13 +53,11 @@ def product_sale_report(
start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session
) -> list[ProductSaleReportItem]: ) -> list[ProductSaleReportItem]:
day = func.cast( 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") ).label("day")
product_version_onclause = _pv_onclause(day)
sku_version_onclause = _sv_onclause(day)
query = ( query = (
select( select(
ProductVersion.id, StockKeepingUnit.id,
ProductVersion.name, ProductVersion.name,
SkuVersion.units, SkuVersion.units,
Voucher.voucher_type, Voucher.voucher_type,
@@ -69,10 +67,10 @@ def product_sale_report(
.join(Inventory.kot) .join(Inventory.kot)
.join(Kot.voucher) .join(Kot.voucher)
.join(Inventory.sku) .join(Inventory.sku)
.join(SkuVersion, onclause=sku_version_onclause) .join(SkuVersion, onclause=_sv_onclause(day))
.join(SkuVersion.menu_category) .join(SkuVersion.menu_category)
.join(StockKeepingUnit.product) .join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause) .join(ProductVersion, onclause=_pv_onclause(day))
.join(ProductVersion.sale_category) .join(ProductVersion.sale_category)
.join(Voucher.food_table) .join(Voucher.food_table)
.where( .where(
@@ -85,7 +83,7 @@ def product_sale_report(
query = query.group_by( query = query.group_by(
SaleCategory.name, SaleCategory.name,
MenuCategory.name, MenuCategory.name,
ProductVersion.id, StockKeepingUnit.id,
ProductVersion.name, ProductVersion.name,
SkuVersion.units, SkuVersion.units,
Voucher.voucher_type, Voucher.voucher_type,
@@ -93,14 +91,14 @@ def product_sale_report(
).order_by(SaleCategory.name, MenuCategory.name, ProductVersion.name, SkuVersion.units) ).order_by(SaleCategory.name, MenuCategory.name, ProductVersion.name, SkuVersion.units)
list_ = db.execute(query).all() list_ = db.execute(query).all()
info: list[ProductSaleReportItem] = [] 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 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: if old:
old[type_] = old[type_] + quantity old[type_] = old[type_] + quantity
else: else:
item = ProductSaleReportItem( 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 item[type_] = quantity
info.append(item) 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]: def get_sale(start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session) -> list[SaleReportItem]:
day = func.cast( 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") ).label("day")
product_version_onclause = _pv_onclause(day) product_version_onclause = _pv_onclause(day)
query = ( query = (
@@ -13,7 +13,7 @@ from pydantic import (
) )
from . import Daf, to_camel from . import Daf, to_camel
from .menu_category import MenuCategoryLink from .sale_category import SaleCategoryLink
class BeerConsumptionReportItem(BaseModel): class BeerConsumptionReportItem(BaseModel):
@@ -57,7 +57,7 @@ class BeerConsumptionReportItem(BaseModel):
class BeerConsumptionReport(BaseModel): class BeerConsumptionReport(BaseModel):
start_date: date start_date: date
finish_date: date finish_date: date
menu_category: MenuCategoryLink | None sale_category: SaleCategoryLink | None
regular: bool | None regular: bool | None
happy: bool | None happy: bool | None
staff: bool | None staff: bool | None
+1 -1
View File
@@ -56,7 +56,7 @@ class GuestBookIn(BaseModel):
return None if value is None else value.strftime("%d-%b-%Y %H:%M") return None if value is None else value.strftime("%d-%b-%Y %H:%M")
@model_validator(mode="after") @model_validator(mode="after")
def nulls(self) -> "GuestBookIn": def nulls(self) -> GuestBookIn:
if self.arrival_date is None and self.booking_date is None: if self.arrival_date is None and self.booking_date is None:
raise ValueError("Both arrival and booking date cannot be null") raise ValueError("Both arrival and booking date cannot be null")
return self return self
+2 -2
View File
@@ -19,7 +19,7 @@ from .user import UserLink
class ProductSaleReportItem(BaseModel): class ProductSaleReportItem(BaseModel):
product_version_id: uuid.UUID sku_id: uuid.UUID
name: str name: str
is_happy_hour: bool is_happy_hour: bool
@@ -40,7 +40,7 @@ class ProductSaleReportItem(BaseModel):
@model_serializer(mode="plain") @model_serializer(mode="plain")
def custom_dump(self) -> dict: # type: ignore def custom_dump(self) -> dict: # type: ignore
base = { base = {
"productVersionId": str(self.product_version_id), "skuId": str(self.sku_id),
"name": self.name, "name": self.name,
"isHappyHour": self.is_happy_hour, "isHappyHour": self.is_happy_hour,
} }
+2 -2
View File
@@ -26,7 +26,7 @@ class Inventory(BaseModel):
tax_rate: Daf | None = None tax_rate: Daf | None = None
discount: Annotated[Daf, Field(ge=0, le=1)] discount: Annotated[Daf, Field(ge=0, le=1)]
modifiers: list[ModifierLink] modifiers: list[ModifierLink]
children: Annotated[list["Inventory"], Field(default_factory=list)] children: Annotated[list[Inventory], Field(default_factory=list)]
amount: Daf | None = None amount: Daf | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@@ -59,7 +59,7 @@ class Inventory(BaseModel):
return round(value, 5) return round(value, 5)
@model_validator(mode="after") @model_validator(mode="after")
def calculate_amount(self) -> "Inventory": def calculate_amount(self) -> Inventory:
price = Decimal(0) if self.is_happy_hour else (self.price or Decimal(0)) price = Decimal(0) if self.is_happy_hour else (self.price or Decimal(0))
self.amount = round( self.amount = round(
Decimal(price * self.quantity * (1 - self.discount) * (1 + (self.tax_rate or Decimal(0)))), Decimal(price * self.quantity * (1 - self.discount) * (1 + (self.tax_rate or Decimal(0)))),
+2 -2
View File
@@ -50,7 +50,7 @@ class Inventory(BaseModel):
is_happy_hour: bool is_happy_hour: bool
type_: InventoryType type_: InventoryType
parent_id: uuid.UUID | None = None parent_id: uuid.UUID | None = None
children: Annotated[list["Inventory"], Field(default_factory=list)] children: Annotated[list[Inventory], Field(default_factory=list)]
modifiers: list[ModifierLink] modifiers: list[ModifierLink]
amount: Daf | None = None amount: Daf | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@@ -84,7 +84,7 @@ class Inventory(BaseModel):
return round(value, 5) return round(value, 5)
@model_validator(mode="after") @model_validator(mode="after")
def calculate_amount(self) -> "Inventory": def calculate_amount(self) -> Inventory:
price = Decimal(0) if self.is_happy_hour else (self.price or Decimal(0)) price = Decimal(0) if self.is_happy_hour else (self.price or Decimal(0))
self.amount = round( self.amount = round(
( (
+3 -3
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "barker" name = "barker"
version = "14.2.0" version = "14.3.1"
description = "Point of Sale for a restaurant" description = "Point of Sale for a restaurant"
authors = ["tanshu <git@tanshu.com>"] authors = ["tanshu <git@tanshu.com>"]
@@ -36,8 +36,8 @@ build-backend = "poetry.core.masonry.api"
[tool.ruff] [tool.ruff]
line-length = 120 line-length = 120
# Assume Python 3.13. # Assume Python 3.14.
target-version = "py313" target-version = "py314"
exclude = [ exclude = [
".eggs", ".eggs",
".git", ".git",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "bookie", "name": "bookie",
"version": "14.2.0", "version": "14.3.1",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
+1 -1
View File
@@ -1,7 +1,7 @@
export const environment = { export const environment = {
production: true, production: true,
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
version: '14.2.0', version: '14.3.1',
}; };
export const dateFormat = { export const dateFormat = {
@@ -32,11 +32,11 @@
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Menu Category</mat-label> <mat-label>Sale Category</mat-label>
<mat-select formControlName="menuCategory" (selectionChange)="filterOn($event.value)"> <mat-select formControlName="saleCategory" (selectionChange)="filterOn($event.value)">
@for (mc of menuCategories; track mc) { @for (sc of saleCategories; track sc) {
<mat-option [value]="mc.id"> <mat-option [value]="sc.id">
{{ mc.name }} {{ sc.name }}
</mat-option> </mat-option>
} }
</mat-select> </mat-select>
@@ -12,7 +12,7 @@ import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import moment from 'moment'; import moment from 'moment';
import { MenuCategory } from '../core/menu-category'; import { SaleCategory } from '../core/sale-category';
import { ToCsvService } from '../shared/to-csv.service'; import { ToCsvService } from '../shared/to-csv.service';
import { BeerSaleExportHeaderInterface } from './beer-sale-export-header-interface'; import { BeerSaleExportHeaderInterface } from './beer-sale-export-header-interface';
import { BeerSaleReport } from './beer-sale-report'; import { BeerSaleReport } from './beer-sale-report';
@@ -40,13 +40,13 @@ export class BeerSaleReportComponent implements OnInit {
private router = inject(Router); private router = inject(Router);
private toCsv = inject(ToCsvService); private toCsv = inject(ToCsvService);
menuCategories: MenuCategory[] = []; saleCategories: SaleCategory[] = [];
info: BeerSaleReport = new BeerSaleReport(); info: BeerSaleReport = new BeerSaleReport();
dataSource: BeerSaleReportDataSource = new BeerSaleReportDataSource(this.info.data); dataSource: BeerSaleReportDataSource = new BeerSaleReportDataSource(this.info.data);
form: FormGroup<{ form: FormGroup<{
startDate: FormControl<Date>; startDate: FormControl<Date>;
finishDate: FormControl<Date>; finishDate: FormControl<Date>;
menuCategory: FormControl<string>; saleCategory: FormControl<string>;
regular: FormControl<boolean>; regular: FormControl<boolean>;
happy: FormControl<boolean>; happy: FormControl<boolean>;
staff: FormControl<boolean>; staff: FormControl<boolean>;
@@ -61,7 +61,7 @@ export class BeerSaleReportComponent implements OnInit {
this.form = new FormGroup({ this.form = new FormGroup({
startDate: new FormControl(new Date(), { nonNullable: true }), startDate: new FormControl(new Date(), { nonNullable: true }),
finishDate: new FormControl(new Date(), { nonNullable: true }), finishDate: new FormControl(new Date(), { nonNullable: true }),
menuCategory: new FormControl('', { nonNullable: true }), saleCategory: new FormControl('', { nonNullable: true }),
regular: new FormControl<boolean>(true, { nonNullable: true }), regular: new FormControl<boolean>(true, { nonNullable: true }),
happy: new FormControl<boolean>(true, { nonNullable: true }), happy: new FormControl<boolean>(true, { nonNullable: true }),
staff: new FormControl<boolean>(true, { nonNullable: true }), staff: new FormControl<boolean>(true, { nonNullable: true }),
@@ -71,14 +71,14 @@ export class BeerSaleReportComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.route.data.subscribe((value) => { this.route.data.subscribe((value) => {
const data = value as { info: BeerSaleReport; menuCategories: MenuCategory[] }; const data = value as { info: BeerSaleReport; saleCategories: SaleCategory[] };
this.info = data.info; this.info = data.info;
this.menuCategories = data.menuCategories; this.saleCategories = data.saleCategories;
this.displayedColumns = ['date'].concat(this.info.headers); this.displayedColumns = ['date'].concat(this.info.headers);
this.form.setValue({ this.form.setValue({
startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(), startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(), finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
menuCategory: this.info.menuCategory?.id ?? '', saleCategory: this.info.saleCategory?.id ?? '',
regular: this.info.regular, regular: this.info.regular,
happy: this.info.happy, happy: this.info.happy,
staff: this.info.staff, staff: this.info.staff,
@@ -94,7 +94,7 @@ export class BeerSaleReportComponent implements OnInit {
queryParams: { queryParams: {
startDate: info.startDate, startDate: info.startDate,
finishDate: info.finishDate, finishDate: info.finishDate,
menuCategory: info.menuCategory?.id ?? '', saleCategory: info.saleCategory?.id ?? '',
regular: info.regular, regular: info.regular,
happy: info.happy, happy: info.happy,
staff: info.staff, staff: info.staff,
@@ -104,8 +104,8 @@ export class BeerSaleReportComponent implements OnInit {
} }
filterOn(id: string) { filterOn(id: string) {
const mc = this.menuCategories.find((x) => x.id === id); const sc = this.saleCategories.find((x) => x.id === id);
this.info.menuCategory = mc ? mc : new MenuCategory({ id }); this.info.saleCategory = sc ? sc : new SaleCategory({ id });
} }
getInfo(): BeerSaleReport { getInfo(): BeerSaleReport {
@@ -114,7 +114,7 @@ export class BeerSaleReportComponent implements OnInit {
return new BeerSaleReport({ return new BeerSaleReport({
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'), startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'), finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
menuCategory: this.info.menuCategory, saleCategory: this.info.saleCategory,
regular: formModel.regular, regular: formModel.regular,
happy: formModel.happy, happy: formModel.happy,
staff: formModel.staff, staff: formModel.staff,
@@ -7,10 +7,10 @@ import { BeerSaleReportService } from './beer-sale-report.service';
export const beerSaleReportResolver: ResolveFn<BeerSaleReport> = (route) => { export const beerSaleReportResolver: ResolveFn<BeerSaleReport> = (route) => {
const startDate = route.queryParamMap.get('startDate') ?? null; const startDate = route.queryParamMap.get('startDate') ?? null;
const finishDate = route.queryParamMap.get('finishDate') ?? null; const finishDate = route.queryParamMap.get('finishDate') ?? null;
const menuCategory = route.queryParamMap.get('menuCategory') ?? null; const saleCategory = route.queryParamMap.get('saleCategory') ?? null;
const regular = route.queryParamMap.get('regular') !== 'false'; const regular = route.queryParamMap.get('regular') !== 'false';
const happy = route.queryParamMap.get('happy') !== 'false'; const happy = route.queryParamMap.get('happy') !== 'false';
const staff = route.queryParamMap.get('staff') !== 'false'; const staff = route.queryParamMap.get('staff') !== 'false';
const nc = route.queryParamMap.get('nc') !== 'false'; const nc = route.queryParamMap.get('nc') !== 'false';
return inject(BeerSaleReportService).get(startDate, finishDate, menuCategory, regular, happy, staff, nc); return inject(BeerSaleReportService).get(startDate, finishDate, saleCategory, regular, happy, staff, nc);
}; };
@@ -1,7 +1,7 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service'; import { authGuard } from '../auth/auth-guard.service';
import { menuCategoryListResolver } from '../menu-category/menu-category-list.resolver'; import { saleCategoryListResolver } from '../sale-category/sale-category-list.resolver';
import { BeerSaleReportComponent } from './beer-sale-report.component'; import { BeerSaleReportComponent } from './beer-sale-report.component';
import { beerSaleReportResolver } from './beer-sale-report.resolver'; import { beerSaleReportResolver } from './beer-sale-report.resolver';
@@ -15,7 +15,7 @@ export const routes: Routes = [
}, },
resolve: { resolve: {
info: beerSaleReportResolver, info: beerSaleReportResolver,
menuCategories: menuCategoryListResolver, saleCategories: saleCategoryListResolver,
}, },
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
@@ -19,7 +19,7 @@ export class BeerSaleReportService {
get( get(
startDate: string | null, startDate: string | null,
finishDate: string | null, finishDate: string | null,
menuCategory: string | null, saleCategory: string | null,
regular: boolean, regular: boolean,
happy: boolean, happy: boolean,
staff: boolean, staff: boolean,
@@ -32,8 +32,8 @@ export class BeerSaleReportService {
if (finishDate !== null) { if (finishDate !== null) {
options.params = options.params.set('f', finishDate); options.params = options.params.set('f', finishDate);
} }
if (menuCategory != null) { if (saleCategory != null) {
options.params = options.params.set('m', menuCategory); options.params = options.params.set('sc', saleCategory);
} }
options.params = options.params.set('r', regular); options.params = options.params.set('r', regular);
options.params = options.params.set('h', happy); options.params = options.params.set('h', happy);
@@ -1,10 +1,10 @@
import { MenuCategory } from '../core/menu-category'; import { SaleCategory } from '../core/sale-category';
import { BeerSaleReportItem } from './beer-sale-report-item'; import { BeerSaleReportItem } from './beer-sale-report-item';
export class BeerSaleReport { export class BeerSaleReport {
startDate: string; startDate: string;
finishDate: string; finishDate: string;
menuCategory: MenuCategory; saleCategory: SaleCategory;
regular: boolean; regular: boolean;
happy: boolean; happy: boolean;
staff: boolean; staff: boolean;
@@ -15,7 +15,7 @@ export class BeerSaleReport {
public constructor(init?: Partial<BeerSaleReport>) { public constructor(init?: Partial<BeerSaleReport>) {
this.startDate = ''; this.startDate = '';
this.finishDate = ''; this.finishDate = '';
this.menuCategory = new MenuCategory(); this.saleCategory = new SaleCategory();
this.regular = true; this.regular = true;
this.happy = true; this.happy = true;
this.staff = true; this.staff = true;
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "frank" name = "frank"
version = "14.2.0" version = "14.3.1"
description = "Point of Sale for a restaurant" description = "Point of Sale for a restaurant"
authors = ["tanshu <git@tanshu.com>"] authors = ["tanshu <git@tanshu.com>"]