Compare commits

..
5 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
17 changed files with 231 additions and 84 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.1" __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 = "",
+16 -56
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,12 +209,16 @@ 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:
sku.valid_till = date_ - timedelta(days=1) 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)
continue continue
new_data_sku = data.skus.pop(index) new_data_sku = data.skus.pop(index)
sku_changed = ( sku_changed = (
@@ -285,7 +274,7 @@ def update_route(
sku=new_s, sku=new_s,
) )
db.add(new_sku) db.add(new_sku)
db.commit() db.commit()
except SQLAlchemyError as e: except SQLAlchemyError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -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,
@@ -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 = (
+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.1" 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.1", "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.1', version: '14.3.1',
}; };
export const dateFormat = { export const dateFormat = {
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "frank" name = "frank"
version = "14.2.1" 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>"]