Merge branch 'feature/bundle'

This commit is contained in:
tanshu committed 2026-02-11 10:42:01 +05:30
commit ae8975b29f
154 files changed
+3833 -1440

No files matched your search

+1
View File
@@ -0,0 +1 @@
3.14
+1 -9
View File
@@ -25,17 +25,9 @@ RUN \
else echo "Lockfile not found." && exit 1; \
fi
FROM python:3.13 AS runner
FROM python:3.14 AS runner
LABEL maintainer="Amritanshu <docker@tanshu.com>"
RUN apt-get update && \
apt-get install -y locales && \
sed --in-place --expression='s/# en_IN UTF-8/en_IN UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales
ENV LANG=en_IN
ENV LC_ALL=en_IN
# Install Poetry
RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python - && \
cd /usr/local/bin && \
+12 -3
View File
@@ -46,13 +46,13 @@ libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-d
## 2. Install Python 3.14 using pyenv
```zsh
pyenv install 3.14.0
pyenv install 3.14
```
Set the local Python version for this project:
```zsh
pyenv local 3.14.0
pyenv local 3.14
```
Verify:
@@ -79,7 +79,16 @@ export PATH="$HOME/.local/bin:$PATH"
Enable tab completion for Zsh
```zsh
poetry completions zsh > ~/.zfunc/_poetry
mkdir $ZSH_CUSTOM/plugins/poetry
poetry completions zsh > $ZSH_CUSTOM/plugins/poetry/_poetry
```
You must then add poetry to your plugins array in ~/.zshrc:
```zsh
plugins(
poetry
...
)
```
Verify:
+2 -2
View File
@@ -75,7 +75,7 @@ def upgrade():
["sku_id"], ["stock_keeping_units.id"], name=op.f("fk_sku_versions_sku_id_stock_keeping_units")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_sku_versions")),
sa.CheckConstraint("length(trim(units)) >= 1", name="ck_sku_versions_units_not_blank"),
sa.CheckConstraint("length(trim(units)) >= 1", name="units_not_blank"),
)
# ---------------------------------------------------------------------
@@ -223,7 +223,7 @@ def upgrade():
op.alter_column("product_versions", "units", new_column_name="fraction_units")
op.drop_constraint(op.f("fk_products_menu_category_id_menu_categories"), "product_versions", type_="foreignkey")
op.create_check_constraint(
"ck_product_versions_name_not_blank",
"name_not_blank",
"product_versions",
"length(trim(name)) >= 1",
)
@@ -0,0 +1,147 @@
"""bundles
Revision ID: 367ecf7b898f
Revises: 1e0daf6bc1ae
Create Date: 2026-01-28 16:11:21.874409
"""
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision = "367ecf7b898f"
down_revision = "1e0daf6bc1ae"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"bundle_items",
sa.Column("id", sa.Uuid(), server_default=sa.text("gen_random_uuid()"), nullable=False),
sa.Column("bundle_id", sa.Uuid(), nullable=False),
sa.Column("item_id", sa.Uuid(), nullable=False),
sa.Column("quantity", sa.Numeric(precision=15, scale=2), nullable=False),
sa.Column("sale_price", sa.Numeric(precision=15, scale=2), nullable=False),
sa.Column("print_in_bill", sa.Boolean(), server_default=sa.text("true"), nullable=False),
sa.Column("valid_from", sa.Date(), nullable=True),
sa.Column("valid_till", sa.Date(), nullable=True),
postgresql.ExcludeConstraint(
(sa.column("bundle_id"), "="),
(sa.column("item_id"), "="),
(sa.text("daterange(valid_from, valid_till, '[]')"), "&&"),
using="gist",
name=op.f("uq_bundle_items_bundle_id_item_id"),
),
sa.ForeignKeyConstraint(
["bundle_id"], ["stock_keeping_units.id"], name=op.f("fk_bundle_items_bundle_id_stock_keeping_units")
),
sa.ForeignKeyConstraint(
["item_id"], ["stock_keeping_units.id"], name=op.f("fk_bundle_items_item_id_stock_keeping_units")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_bundle_items")),
)
inventory_type = sa.Enum("regular", "bundle", "bundle_item", name="inventorytype")
inventory_type.create(op.get_bind())
op.add_column("inventories", sa.Column("type", inventory_type, server_default=sa.text("'regular'"), nullable=False))
op.add_column("inventories", sa.Column("parent_id", sa.Uuid(), nullable=True))
op.drop_constraint(op.f("uq_inventories_kot_id"), "inventories", type_="unique")
op.create_unique_constraint(
op.f("uq_inventories_kot_id"),
"inventories",
["kot_id", "sku_id", "is_happy_hour", "type", "parent_id"],
postgresql_nulls_not_distinct=True,
)
op.create_foreign_key(
op.f("fk_inventories_parent_id_inventories"), "inventories", "inventories", ["parent_id"], ["id"]
)
op.add_column(
"stock_keeping_units", sa.Column("is_bundle", sa.Boolean(), server_default=sa.text("false"), nullable=False)
)
mc = sa.table(
"menu_categories",
sa.column("id", sa.Uuid()),
sa.column("name", sa.Unicode(length=255)),
sa.column("is_active", sa.Boolean()),
sa.column("is_fixture", sa.Boolean()),
sa.column("sort_order", sa.Integer()),
)
op.execute(
mc.insert().values(
id="6752ed9d-6f1a-4941-940a-17759dcc6720",
name="Bundles",
is_active=True,
is_fixture=True,
sort_order=0,
)
)
customers = sa.table(
"customers",
sa.column("id", sa.Uuid),
sa.column("name", sa.Text),
sa.column("phone", sa.Text),
)
guest_book = sa.table(
"guest_book",
sa.column("customer_id", sa.Uuid),
)
voucher = sa.table(
"vouchers",
sa.column("customer_id", sa.Uuid),
)
bad_customer_ids = sa.select(customers.c.id).where(
sa.func.length(sa.func.trim(customers.c.phone)) == 0,
)
op.execute(guest_book.delete().where(guest_book.c.customer_id.in_(bad_customer_ids)))
op.execute(voucher.update().where(voucher.c.customer_id.in_(bad_customer_ids)).values(customer_id=None))
op.execute(customers.delete().where(customers.c.id.in_(bad_customer_ids)))
op.execute(
customers.update()
.where(
sa.func.length(sa.func.trim(customers.c.name)) == 0,
)
.values(name=customers.c.phone)
)
with op.batch_alter_table("customers") as batch_op:
batch_op.create_check_constraint("name_not_blank", sa.text("length(trim(both from name)) >= 1"))
batch_op.create_check_constraint("phone_not_blank", sa.text("length(trim(both from phone)) >= 1"))
def downgrade():
op.drop_table("bundle_items")
op.drop_column("stock_keeping_units", "is_bundle")
op.drop_constraint(op.f("fk_inventories_parent_id_inventories"), "inventories", type_="foreignkey")
op.drop_constraint(op.f("uq_inventories_kot_id"), "inventories", type_="unique")
op.create_unique_constraint(
op.f("uq_inventories_kot_id"),
"inventories",
["kot_id", "sku_id", "is_happy_hour", "price"],
postgresql_nulls_not_distinct=False,
)
op.drop_column("inventories", "parent_id")
op.drop_column("inventories", "type")
mc = sa.table(
"menu_categories",
sa.column("id", sa.Uuid()),
)
op.execute(mc.delete().where(mc.c.id == "6752ed9d-6f1a-4941-940a-17759dcc6720"))
with op.batch_alter_table("customers") as batch_op:
batch_op.drop_constraint("name_not_blank", type_="check")
batch_op.drop_constraint("phone_not_blank", type_="check")
@@ -106,7 +106,7 @@ def upgrade():
op.execute(gbt.update().values(type="walk_in"))
with op.batch_alter_table("guest_book") as batch_op:
batch_op.alter_column("type", existing_type=guestbooktype, nullable=False)
batch_op.create_check_constraint("ck_guest_book_nulls", sa.text("num_nonnulls(booking_date, arrival_date) > 0"))
batch_op.create_check_constraint("nulls", sa.text("num_nonnulls(booking_date, arrival_date) > 0"))
# ### end Alembic commands ###
+4
View File
@@ -1,6 +1,7 @@
# Import all the models, so that Base has them before being
# imported by Alembic
from ..models.bill import Bill
from ..models.bundle_item import BundleItem
from ..models.customer import Customer
from ..models.customer_discount import CustomerDiscount
from ..models.db_setting import DbSetting
@@ -9,6 +10,7 @@ from ..models.food_table import FoodTable
from ..models.guest_book import GuestBook
from ..models.inventory import Inventory
from ..models.inventory_modifier import InventoryModifier
from ..models.inventory_type import InventoryType
from ..models.kot import Kot
from ..models.login_history import LoginHistory
from ..models.menu_category import MenuCategory
@@ -43,6 +45,7 @@ from .base_class import reg
__all__ = [
"Bill",
"BundleItem",
"Customer",
"CustomerDiscount",
"DbSetting",
@@ -51,6 +54,7 @@ __all__ = [
"GuestBook",
"Inventory",
"InventoryModifier",
"InventoryType",
"Kot",
"LoginHistory",
"MenuCategory",
+2
View File
@@ -8,6 +8,7 @@ from starlette.middleware.sessions import SessionMiddleware
from .core.config import settings
from .db.base import reg # noqa: F401
from .routers import (
bundle,
customer,
customer_discount,
db_settings,
@@ -72,6 +73,7 @@ app.include_router(printer.router, prefix="/api/printers", tags=["printers"])
app.include_router(menu_category.router, prefix="/api/menu-categories", tags=["products"])
app.include_router(product.router, prefix="/api/products", tags=["products"])
app.include_router(bundle.router, prefix="/api/bundles", tags=["products"])
app.include_router(temporal_product.router, prefix="/api/temporal-products", tags=["products"])
app.include_router(device.router, prefix="/api/devices", tags=["devices"])
app.include_router(sale_category.router, prefix="/api/sale-categories", tags=["products"])
+76
View File
@@ -0,0 +1,76 @@
from __future__ import annotations
import uuid
from datetime import date
from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, Date, ForeignKey, Numeric, Uuid, func, text
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
if TYPE_CHECKING:
from .stock_keeping_unit import StockKeepingUnit
@reg.mapped_as_dataclass(unsafe_hash=True)
class BundleItem:
__tablename__ = "bundle_items"
id: Mapped[uuid.UUID] = mapped_column(
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()")
)
bundle_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
item_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
quantity: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
sale_price: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
print_in_bill: Mapped[bool] = mapped_column(Boolean, server_default=text("true"), nullable=False)
valid_from: Mapped[date | None] = mapped_column(Date(), nullable=True)
valid_till: Mapped[date | None] = mapped_column(Date(), nullable=True)
bundle: Mapped[StockKeepingUnit] = relationship(
"StockKeepingUnit", back_populates="bundle_items", foreign_keys=[bundle_id]
)
item: Mapped[StockKeepingUnit] = relationship("StockKeepingUnit", foreign_keys=[item_id])
__table_args__ = (
postgresql.ExcludeConstraint(
(bundle_id, "="),
(item_id, "="),
(func.daterange(valid_from, valid_till, text("'[]'")), "&&"),
),
)
def __init__(
self,
quantity: Decimal,
sale_price: Decimal,
print_in_bill: bool,
bundle_id: uuid.UUID,
item_id: uuid.UUID,
bundle: StockKeepingUnit | None = None,
item: StockKeepingUnit | None = None,
valid_from: date | None = None,
valid_till: date | None = None,
id_: uuid.UUID | None = None,
):
self.quantity = quantity
self.sale_price = sale_price
self.print_in_bill = print_in_bill
self.valid_from = valid_from
self.valid_till = valid_till
if bundle_id is not None:
self.bundle_id = bundle_id
if item_id is not None:
self.item_id = item_id
if bundle is not None:
self.bundle = bundle
if item is not None:
self.item = item
if id_ is not None:
self.id = id_
+11 -2
View File
@@ -4,7 +4,7 @@ import uuid
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, Text, Uuid, text
from sqlalchemy import Boolean, CheckConstraint, Text, Uuid, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import expression
@@ -18,7 +18,16 @@ if TYPE_CHECKING:
@reg.mapped_as_dataclass(unsafe_hash=True)
class Customer:
__tablename__ = "customers"
__table_args__ = (
CheckConstraint(
"length(trim(both from name)) >= 1",
name="name_not_blank",
),
CheckConstraint(
"length(trim(both from phone)) >= 1",
name="phone_not_blank",
),
)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
name: Mapped[str] = mapped_column(Text, nullable=False)
phone: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
+1 -1
View File
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
@reg.mapped_as_dataclass(unsafe_hash=True)
class GuestBook:
__tablename__ = "guest_book"
__table_args__ = (CheckConstraint("num_nonnulls(booking_date, arrival_date) > 0", name="ck_guest_book_nulls"),)
__table_args__ = (CheckConstraint("num_nonnulls(booking_date, arrival_date) > 0", name="nulls"),)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
customer_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("customers.id"), nullable=False)
+33 -11
View File
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
from sqlalchemy import (
Boolean,
ColumnElement,
Enum,
ForeignKey,
Integer,
Numeric,
@@ -22,6 +23,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
from .inventory_type import InventoryType
from .tax import Tax
@@ -34,7 +36,9 @@ if TYPE_CHECKING:
@reg.mapped_as_dataclass(unsafe_hash=True)
class Inventory:
__tablename__ = "inventories"
__table_args__ = (UniqueConstraint("kot_id", "sku_id", "is_happy_hour", "price"),)
__table_args__ = (
UniqueConstraint("kot_id", "sku_id", "is_happy_hour", "type", "parent_id", postgresql_nulls_not_distinct=True),
)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
kot_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("kots.id"), nullable=False, index=True)
@@ -46,28 +50,41 @@ class Inventory:
tax_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("taxes.id"), nullable=False)
discount: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
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)
kot: Mapped[Kot] = relationship(back_populates="inventories")
tax: Mapped[Tax] = relationship(back_populates="inventories")
sku: Mapped[StockKeepingUnit] = relationship(back_populates="inventories")
parent: Mapped[Inventory | None] = relationship("Inventory", remote_side="Inventory.id", back_populates="children")
children: Mapped[list[Inventory]] = relationship("Inventory", back_populates="parent")
modifiers: Mapped[list[InventoryModifier]] = relationship(back_populates="inventory")
def __init__(
self,
kot_id: uuid.UUID,
sku_id: uuid.UUID | None,
quantity: Decimal,
price: Decimal,
discount: Decimal,
is_hh: bool,
tax_id: uuid.UUID | None,
tax_rate: Decimal,
sort_order: int,
kot_id: uuid.UUID | None = None,
kot: Kot | None = None,
sku_id: uuid.UUID | None = None,
quantity: Decimal = Decimal(0),
price: Decimal = Decimal(0),
discount: Decimal = Decimal(0),
is_hh: bool = False,
tax_id: uuid.UUID | None = None,
tax_rate: Decimal = Decimal(0),
sort_order: int = 0,
type_: InventoryType = InventoryType.regular,
parent_id: uuid.UUID | None = None,
parent: Inventory | None = None,
sku: StockKeepingUnit | None = None,
tax: Tax | None = None,
):
self.kot_id = kot_id
if kot_id is not None:
self.kot_id = kot_id
if kot is not None:
self.kot = kot
if sku_id is not None:
self.sku_id = sku_id
self.quantity = quantity
@@ -78,6 +95,11 @@ class Inventory:
self.tax_id = tax_id
self.tax_rate = tax_rate
self.sort_order = sort_order
self.type_ = type_
if parent_id is not None:
self.parent_id = parent_id
if parent is not None:
self.parent = parent
if sku is not None:
self.sku = sku
if tax is not None:
+7
View File
@@ -0,0 +1,7 @@
import enum
class InventoryType(str, enum.Enum):
regular = "regular"
bundle = "bundle"
bundle_item = "bundle_item"
+1 -1
View File
@@ -28,7 +28,7 @@ class ModifierCategory:
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
products: Mapped[list[Product]] = relationship(
secondary=ModifierCategoryProduct.__table__,
secondary=ModifierCategoryProduct.__table__, # type: ignore[attr-defined]
back_populates="modifier_categories",
)
modifiers: Mapped[list[Modifier]] = relationship(back_populates="modifier_category")
+1 -1
View File
@@ -23,7 +23,7 @@ class Permission:
name: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
roles: Mapped[list[Role]] = relationship(
secondary=RolePermission.__table__,
secondary=RolePermission.__table__, # type: ignore[attr-defined]
back_populates="permissions",
)
+1 -1
View File
@@ -26,7 +26,7 @@ class Product:
skus: Mapped[list[StockKeepingUnit]] = relationship("StockKeepingUnit", back_populates="product")
modifier_categories: Mapped[list[ModifierCategory]] = relationship(
"ModifierCategory",
secondary=ModifierCategoryProduct.__table__,
secondary=ModifierCategoryProduct.__table__, # type: ignore[attr-defined]
order_by="ModifierCategory.sort_order",
back_populates="products",
)
+9 -2
View File
@@ -57,20 +57,27 @@ class ProductVersion:
def __init__(
self,
product_id: uuid.UUID,
product_id: uuid.UUID | None,
name: str = "",
fraction_units: str = "",
sale_category_id: uuid.UUID | None = None,
valid_from: date | None = None,
valid_till: date | None = None,
product: Product | None = None,
sale_category: SaleCategory | None = None,
id_: uuid.UUID | None = None,
):
self.product_id = product_id
if product_id is not None:
self.product_id = product_id
self.name = name
self.fraction_units = fraction_units
if product is not None:
self.product = product
if sale_category_id is not None:
self.sale_category_id = sale_category_id
self.valid_from = valid_from
self.valid_till = valid_till
if sale_category is not None:
self.sale_category = sale_category
if id_ is not None:
self.id = id_
+1 -1
View File
@@ -24,7 +24,7 @@ class Role:
permissions: Mapped[list[Permission]] = relationship(
"Permission",
secondary=RolePermission.__table__,
secondary=RolePermission.__table__, # type: ignore[attr-defined]
back_populates="roles",
)
@@ -11,6 +11,7 @@ from ..db.base_class import reg
if TYPE_CHECKING:
from .bundle_item import BundleItem
from .inventory import Inventory
from .product import Product
from .sku_version import SkuVersion
@@ -25,21 +26,27 @@ class StockKeepingUnit:
)
product_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("products.id"), nullable=False)
is_not_available: Mapped[bool] = mapped_column(Boolean, nullable=False)
is_bundle: Mapped[bool] = mapped_column(Boolean, nullable=False)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
product: Mapped[Product] = relationship("Product", back_populates="skus")
inventories: Mapped[list[Inventory]] = relationship(back_populates="sku")
versions: Mapped[list[SkuVersion]] = relationship(back_populates="sku")
bundle_items: Mapped[list[BundleItem]] = relationship(
"BundleItem", back_populates="bundle", foreign_keys="BundleItem.bundle_id"
)
def __init__(
self,
is_not_available: bool = False,
is_bundle: bool = False,
sort_order: int = 0,
product_id: uuid.UUID | None = None,
product: Product | None = None,
id_: uuid.UUID | None = None,
) -> None:
self.is_not_available = is_not_available
self.is_bundle = is_bundle
self.sort_order = sort_order
if product_id is not None:
self.product_id = product_id
+1 -1
View File
@@ -28,7 +28,7 @@ class User:
roles: Mapped[list[Role]] = relationship(
"Role",
secondary=UserRole.__table__,
secondary=UserRole.__table__, # type: ignore[attr-defined]
order_by="Role.name",
)
login_history: Mapped[list[LoginHistory]] = relationship(order_by=desc(LoginHistory.date), back_populates="user")
+1 -1
View File
@@ -91,4 +91,4 @@ class Voucher:
@property
def amount(self) -> Decimal:
return round(sum([i.amount for k in self.kots for i in k.inventories], Decimal(0)), 2)
return round(sum([i.amount for k in self.kots for i in k.inventories if i.parent_id is None], Decimal(0)), 2)
+11 -50
View File
@@ -1,59 +1,20 @@
import locale
import uuid
from datetime import date
from decimal import Decimal
from sqlalchemy import and_, or_, select
from sqlalchemy.orm import Session, contains_eager
from ..models.product import Product
from ..models.product_version import ProductVersion
from ..models.sku_version import SkuVersion
from ..models.stock_keeping_unit import StockKeepingUnit
def currency_format(amount: Decimal | int) -> str:
return locale.currency(amount, symbol=False, grouping=True)
return format_indian(amount)
def format_no_decimals(amount: Decimal | int) -> str:
return locale.format_string("%d", amount, grouping=True, monetary=True)
return format_indian(amount).split(".")[0]
def get_product_name(sku_id: uuid.UUID, date_: date, db: Session) -> str:
item = get_product(sku_id, date_, db)
return f"{item.sku.product.versions[0].name} ({item.units})"
def get_product(sku_id: uuid.UUID, date_: date, db: Session) -> SkuVersion:
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_,
),
)
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
)
return (
db.execute(
select(SkuVersion)
.join(StockKeepingUnit, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.where(SkuVersion.sku_id == sku_id)
.options(
contains_eager(SkuVersion.sku).contains_eager(StockKeepingUnit.product).contains_eager(Product.versions)
)
)
.unique()
.scalar_one()
)
def format_indian(amount: Decimal | int) -> str:
s = f"{abs(amount):.2f}"
integer, fraction = s.split(".")
remainder = integer[-3:]
integer = integer[:-3]
while integer:
remainder = integer[-2:] + "," + remainder
integer = integer[:-2]
return ("-" if amount < 0 else "") + remainder + "." + fraction
+44 -21
View File
@@ -1,5 +1,4 @@
import asyncio
import locale
import re
import uuid
@@ -7,7 +6,7 @@ from datetime import timedelta
from decimal import Decimal
from arq import ArqRedis, create_pool
from sqlalchemy import select
from sqlalchemy import Date, func, select
from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
@@ -15,16 +14,22 @@ from ..core.config import settings
from ..models.bill import Bill
from ..models.db_setting import DbSetting
from ..models.inventory import Inventory
from ..models.inventory_type import InventoryType
from ..models.kot import Kot
from ..models.printer import Printer
from ..models.section_printer import SectionPrinter
from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType
from . import currency_format, format_no_decimals, get_product_name
from ..routers import _pv_onclause, _sv_onclause
from ..routers.voucher import get_voucher
from . import currency_format, format_no_decimals
def print_bill(voucher_id: uuid.UUID, db: Session) -> None:
locale.setlocale(locale.LC_MONETARY, "en_IN")
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == voucher_id)).scalar_one()
day = func.cast(
Kot.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
voucher: Voucher = get_voucher(voucher_id, db, _pv_onclause(day), _sv_onclause(day))
printer = db.execute(
select(Printer)
@@ -38,7 +43,8 @@ def print_bill(voucher_id: uuid.UUID, db: Session) -> None:
for regime_id, inventories in regimes:
taxes_dict: dict[str, Decimal] = {}
for i in inventories:
get_tax_item(i.tax.id, i.tax.name, i.tax_amount, taxes_dict)
if i.type_ != InventoryType.bundle_item:
get_tax_item(i.tax.id, i.tax.name, i.tax_amount, taxes_dict)
taxes_list = [(t, a) for t, a in taxes_dict.items() if a != 0]
bill_text = design_bill(voucher, regime_id, inventories, taxes_list, db)
bills.append(bill_text)
@@ -62,12 +68,16 @@ def print_bill(voucher_id: uuid.UUID, db: Session) -> None:
def get_inventories(voucher: Voucher) -> list[Inventory]:
items_dict: dict[tuple[uuid.UUID, bool, Decimal, frozenset[uuid.UUID]], Inventory] = {}
items_dict: dict[
tuple[uuid.UUID, bool, Decimal, InventoryType, uuid.UUID | None, frozenset[uuid.UUID]], Inventory
] = {}
for i in [i for k in voucher.kots for i in k.inventories]:
key = (
i.sku_id,
i.is_happy_hour,
i.discount,
i.type_,
None if i.parent is None else i.parent.sku_id,
frozenset(m.modifier_id for m in i.modifiers if m.modifier.show_in_bill),
)
if key in items_dict:
@@ -85,6 +95,8 @@ def get_inventories(voucher: Voucher) -> list[Inventory]:
kot_id=i.kot_id,
tax_id=i.tax_id,
sort_order=i.sort_order,
parent_id=i.parent_id,
type_=i.type_,
)
return list([item for item in items_dict.values() if item.quantity != 0])
@@ -98,9 +110,10 @@ def get_regimes(
]
items_dict: dict[int, list[Inventory]] = {}
previous_regime_id: int = inventories[0].tax.regime_id
for inv in inventories:
key = inv.tax.regime_id
key = previous_regime_id if inv.type_ == InventoryType.bundle_item else inv.tax.regime_id
previous_regime_id = key
if key not in items_dict:
items_dict[key] = []
items_dict[key].append(inv)
@@ -142,7 +155,6 @@ def design_bill(
s += "\n\r" + bill_object.regime.header
# Products
voucher_date = voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
product_date = (voucher_date - timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES)).date()
bill_number = f"{bill_object.regime.prefix}-{bill_object.bill_number}"
s += "\n\r" + f"Bill No: {bill_number:>12} {voucher_date:%d-%b-%Y %H:%M}"
@@ -151,33 +163,44 @@ def design_bill(
s += "\n\r" + "Qty. Particulars Price Amount"
s += "\n\r" + "-" * 42
for inv in inventories:
name = get_product_name(inv.sku_id, product_date, db)
name = "H H " + name if inv.is_happy_hour else name
s += (
f"\n\r"
f"{inv.quantity: >5.2f} {name:<22.22} {format_no_decimals(inv.price): >6}"
f" {format_no_decimals(inv.price * inv.quantity): >6}"
)
name = (
"H H " if inv.is_happy_hour else ""
) + f"{inv.sku.product.versions[0].name} ({inv.sku.versions[0].units})"
if inv.parent_id:
info = f" ({inv.quantity:f}) {name}"
s += "\n\r" + info[:42]
else:
s += (
f"\n\r"
f"{inv.quantity: >5.2f} {name:<22.22} {format_no_decimals(inv.price): >6}"
f" {format_no_decimals(inv.price * inv.quantity): >6}"
)
for m in [m for m in inv.modifiers if m.modifier.show_in_bill]:
s += f"\n\r -- {m.modifier.name: <38.38}"
s += "\n\r" + "------------------------------------------"
# Totals
amount = sum(round(i.quantity * i.price, 2) for i in inventories)
amount = sum(round(i.quantity * i.price, 2) for i in inventories if i.type_ != InventoryType.bundle_item)
s += f"\n\r{'Subtotal :': >32} {currency_format(amount): >9}"
amount = sum(round(i.quantity * i.price, 2) for i in inventories if i.is_happy_hour)
amount = sum(
round(i.quantity * i.price, 2) for i in inventories if i.is_happy_hour and i.type_ != InventoryType.bundle_item
)
if amount != 0:
s += f"\n\r{'Happy Hour Discount :': >32} {currency_format(amount): >9}"
amount = sum(round(i.quantity * i.effective_price * i.discount, 2) for i in inventories)
amount = sum(
round(i.quantity * i.effective_price * i.discount, 2)
for i in inventories
if i.type_ != InventoryType.bundle_item
)
if amount != 0:
s += f"\n\r{'Discount :': >32} {currency_format(amount): >9}"
for t in taxes:
s += f"\n\r{t[0]: >30} : {currency_format(t[1]): >9}"
s += f"\n\r{'Total Amount :': >32} {currency_format(round(sum([i.amount for i in inventories]))): >9}"
s += f"\n\r{'Total Amount :': >32} {currency_format(round(sum([i.amount for i in inventories if i.type_ != InventoryType.bundle_item]))): >9}"
s += "\n\r" + "-" * 42
if voucher.narration:
-2
View File
@@ -1,5 +1,4 @@
import asyncio
import locale
import uuid
from datetime import datetime, timedelta
@@ -18,7 +17,6 @@ from . import currency_format
def print_cashier_report(report: CashierReport, device_id: uuid.UUID, db: Session) -> None:
locale.setlocale(locale.LC_MONETARY, "en_IN")
summary = design_cashier_report_summary(report)
details = design_cashier_report_details(report)
section_id = db.execute(select(Device.section_id).where(Device.id == device_id)).scalar_one()
@@ -1,5 +1,4 @@
import asyncio
import locale
import uuid
from arq import ArqRedis, create_pool
@@ -15,7 +14,6 @@ from . import currency_format
def print_discount_report(report: DiscountReport, device_id: uuid.UUID, db: Session) -> None:
locale.setlocale(locale.LC_MONETARY, "en_IN")
data = design_discount_report(report)
section_id = db.execute(select(Device.section_id).where(Device.id == device_id)).scalar_one()
printer = db.execute(
+16 -14
View File
@@ -5,7 +5,7 @@ from datetime import timedelta
from typing import Any
from arq import ArqRedis, create_pool
from sqlalchemy import or_, select
from sqlalchemy import Date, func, or_, select
from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
@@ -15,14 +15,12 @@ from ..models.kot import Kot
from ..models.printer import Printer
from ..models.section_printer import SectionPrinter
from ..models.voucher import Voucher
from . import get_product, get_product_name
from ..routers import _pv_onclause, _sv_onclause
from ..routers.voucher import get_voucher
def design_kot(voucher: Voucher, kot: Kot, items: list[Inventory], copy_number: int, db: Session) -> str:
date_ = kot.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
product_date = (
voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES)
).date()
s = (
"KOT / BOT".center(42)
+ "\n\r"
@@ -41,9 +39,13 @@ def design_kot(voucher: Voucher, kot: Kot, items: list[Inventory], copy_number:
+ "".ljust(42, "-")
)
for item in items:
name = get_product_name(item.sku_id, product_date, db)
name = "H H " + name if item.is_happy_hour else name
s += "\n\r" + f"{item.quantity:6.2} x {name:<33}"
name = (
"H H " if item.is_happy_hour else ""
) + f"{item.sku.product.versions[0].name} ({item.sku.versions[0].units})"
if item.parent_id:
s += "\n\r" + f"{item.quantity:6.2f} #= {name:<32}"
else:
s += "\n\r" + f"{item.quantity:6.2f} x {name:<33}"
for m in item.modifiers:
s += "\n\r" + f" --- {m.modifier.name:<32}"
s += "\n\r" + "".ljust(42, "-")
@@ -51,14 +53,14 @@ def design_kot(voucher: Voucher, kot: Kot, items: list[Inventory], copy_number:
def print_kot(voucher_id: uuid.UUID, db: Session) -> None:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == voucher_id)).scalar_one()
day = func.cast(
Kot.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
voucher: Voucher = get_voucher(voucher_id, db, _pv_onclause(day), _sv_onclause(day))
my_hash: dict[tuple[uuid.UUID, int], tuple[Printer, list[Any]]] = {}
kot: Kot = voucher.kots[-1]
product_date = (
voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES)
).date()
for item in kot.inventories:
product = get_product(item.sku_id, product_date, db)
sku = item.sku
row = db.execute(
select(Printer, SectionPrinter.copies)
@@ -66,7 +68,7 @@ def print_kot(voucher_id: uuid.UUID, db: Session) -> None:
.where(SectionPrinter.section_id == voucher.food_table.section_id)
.where(
or_(
SectionPrinter.sale_category_id == product.sku.product.versions[0].sale_category_id,
SectionPrinter.sale_category_id == sku.product.versions[0].sale_category_id,
SectionPrinter.sale_category_id == None, # noqa: E711
)
)
@@ -1,5 +1,4 @@
import asyncio
import locale
import uuid
from datetime import datetime, timedelta
@@ -17,7 +16,6 @@ from ..schemas.product_sale_report import ProductSaleReport
def print_product_sale_report(report: ProductSaleReport, device_id: uuid.UUID, db: Session) -> None:
locale.setlocale(locale.LC_MONETARY, "en_IN")
data = design_product_sale_report(report)
section_id = db.execute(select(Device.section_id).where(Device.id == device_id)).scalar_one()
printer = db.execute(
-2
View File
@@ -1,5 +1,4 @@
import asyncio
import locale
import uuid
from datetime import datetime, timedelta
@@ -18,7 +17,6 @@ from . import currency_format
def print_sale_report(report: SaleReport, device_id: uuid.UUID, db: Session) -> None:
locale.setlocale(locale.LC_MONETARY, "en_IN")
data = design_sale_report(report)
section_id = db.execute(select(Device.section_id).where(Device.id == device_id)).scalar_one()
printer = db.execute(
+51
View File
@@ -1,6 +1,14 @@
from datetime import UTC, date, datetime, timedelta
from typing import Any
from sqlalchemy import ColumnElement, Label, and_, or_
from ..core.config import settings
from ..models.bundle_item import BundleItem
from ..models.product import Product
from ..models.product_version import ProductVersion
from ..models.sku_version import SkuVersion
from ..models.stock_keeping_unit import StockKeepingUnit
def query_date(d: str | None = None) -> date:
@@ -32,3 +40,46 @@ def dates_overlap(start1: date | None, end1: date | None, start2: date | None, e
if end2 is None:
end2 = date.max
return start1 <= end2 and start2 <= end1
def _pv_active(date_: date | Label[Any]) -> ColumnElement[bool]:
return and_(
or_(ProductVersion.valid_from == None, ProductVersion.valid_from <= date_), # noqa: E711
or_(ProductVersion.valid_till == None, ProductVersion.valid_till >= date_), # noqa: E711
)
def _sv_active(date_: date | Label[Any]) -> ColumnElement[bool]:
return and_(
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= date_), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= date_), # noqa: E711
)
def _bundle_active(date_: date) -> ColumnElement[bool]:
return and_(
or_(BundleItem.valid_from == None, BundleItem.valid_from <= date_), # noqa: E711
or_(BundleItem.valid_till == None, BundleItem.valid_till >= date_), # noqa: E711
)
def _pv_onclause(date_: date | Label[Any]) -> ColumnElement[bool]:
return 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_,
),
)
def _sv_onclause(date_: date | Label[Any]) -> ColumnElement[bool]:
return 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
)
+529
View File
@@ -0,0 +1,529 @@
import uuid
from collections.abc import Sequence
from datetime import date, timedelta
from decimal import Decimal
from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy import Date, func, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import contains_eager
from ..core.config import settings
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..models.bundle_item import BundleItem as BundleItemModel
from ..models.inventory import Inventory
from ..models.kot import Kot
from ..models.product import Product
from ..models.product_version import ProductVersion
from ..models.sku_version import SkuVersion
from ..models.stock_keeping_unit import StockKeepingUnit
from ..models.voucher import Voucher
from ..schemas import bundle as schemas
from ..schemas.menu_category import MenuCategoryLink
from ..schemas.sale_category import SaleCategoryLink
from ..schemas.user_token import UserToken
from . import _pv_onclause, _sv_onclause, effective_date
router = APIRouter()
FRACTION_1 = Decimal("1")
YIELD_1 = Decimal("1")
COST_0 = Decimal("0")
# ---------- helpers ----------
def bundle_blank() -> schemas.BundleBlank:
return schemas.BundleBlank(
name="",
units="",
sale_price=Decimal("0.00"),
sale_category=None,
has_happy_hour=False,
is_not_available=False,
sort_order=0,
items=[],
)
def _bundle_info(pv: ProductVersion, sv: SkuVersion, items: Sequence[BundleItemModel]) -> schemas.Bundle:
sku = sv.sku
return schemas.Bundle(
id_=sku.id,
version_id=sv.id,
name=pv.name,
units=pv.fraction_units,
sale_price=sv.sale_price,
sale_category=SaleCategoryLink(id_=pv.sale_category_id, name=pv.sale_category.name),
has_happy_hour=sv.has_happy_hour,
is_not_available=sku.is_not_available,
sort_order=sku.sort_order,
menu_category=MenuCategoryLink(
id_=sv.menu_category_id,
name=sv.menu_category.name,
skus=[],
)
if sv.menu_category is not None
else None,
items=[
schemas.BundleItem(
id_=bi.id,
name=f"{bi.item.product.versions[0].name} ({bi.item.versions[0].units})",
item_id=bi.item_id,
sale_price=bi.sale_price,
quantity=bi.quantity,
print_in_bill=bi.print_in_bill,
)
for bi in items
],
)
# ---------- routes ----------
@router.post("", response_model=None)
def save(
data: schemas.BundleIn,
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
) -> None:
try:
with SessionFuture() as db:
if not data.items:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough bundle items.",
)
sale_price = round(
sum((round(it.sale_price, 2) * round(it.quantity, 5) for it in data.items), start=Decimal(0)), 2
)
product = Product(sort_order=data.sort_order)
db.add(product)
db.flush()
pv = ProductVersion(
product_id=None,
product=product,
name=data.name,
fraction_units=data.units, # per your rule
sale_category_id=data.sale_category.id_,
sale_category=None,
valid_from=date_,
valid_till=None,
)
db.add(pv)
sku = StockKeepingUnit(
product=product,
is_not_available=data.is_not_available,
is_bundle=True,
sort_order=data.sort_order,
)
db.add(sku)
db.flush()
sv = SkuVersion(
sku_id=sku.id,
units=data.units,
fraction=FRACTION_1,
product_yield=YIELD_1,
cost_price=COST_0,
sale_price=sale_price,
has_happy_hour=data.has_happy_hour,
menu_category_id=data.menu_category.id_,
valid_from=date_,
valid_till=None,
)
db.add(sv)
# bundle items
for it in data.items:
db.add(
BundleItemModel(
bundle_id=sku.id,
item_id=it.item_id,
quantity=round(it.quantity, 5),
sale_price=round(it.sale_price, 2),
print_in_bill=it.print_in_bill,
valid_from=date_,
valid_till=None,
)
)
db.commit()
except SQLAlchemyError as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.put("/{id_}", response_model=None)
def update_route(
id_: uuid.UUID, # bundle header SKU id
data: schemas.BundleIn,
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
) -> None:
try:
with SessionFuture() as db:
if not data.items:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough bundle items.",
)
sale_price = round(
sum((round(it.sale_price, 2) * round(it.quantity, 5) for it in data.items), start=Decimal(0)), 2
)
# Load header SKU + active header SkuVersion + menu category + product + active ProductVersion
sv: SkuVersion | None = (
db.execute(
select(SkuVersion)
.join(StockKeepingUnit, onclause=_sv_onclause(date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(SkuVersion.menu_category)
.where(
StockKeepingUnit.id == id_,
StockKeepingUnit.is_bundle == True, # noqa: E712
)
.options(
contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions),
contains_eager(SkuVersion.menu_category),
)
)
.unique()
.scalars()
.one_or_none()
)
if sv is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found.")
sku = sv.sku
product = sku.product
pv = product.versions[0]
pv_changed = pv.name != data.name or pv.sale_category_id != data.sale_category.id_
sku.is_not_available = data.is_not_available
sku.sort_order = data.sort_order
product.sort_order = data.sort_order
if pv_changed:
if pv.valid_from == date_:
pv.name = data.name
pv.sale_category_id = data.sale_category.id_
else:
pv.valid_till = date_ - timedelta(days=1)
db.add(
ProductVersion(
product_id=product.id,
product=product,
name=data.name,
fraction_units=pv.fraction_units,
sale_category_id=data.sale_category.id_,
valid_from=date_,
valid_till=None,
)
)
sv_changed = (
sv.units != data.units
or Decimal(sv.sale_price).quantize(Decimal("0.01")) != sale_price
or sv.has_happy_hour != data.has_happy_hour
or sv.menu_category_id != data.menu_category.id_
)
if sv_changed:
if sv.valid_from == date_:
sv.units = data.units
sv.sale_price = sale_price
sv.has_happy_hour = data.has_happy_hour
sv.menu_category_id = data.menu_category.id_
# # enforce bundle constants
# sv.fraction = FRACTION_1
# sv.product_yield = YIELD_1
# sv.cost_price = COST_0
else:
sv.valid_till = date_ - timedelta(days=1)
db.add(
SkuVersion(
sku_id=sku.id,
units=data.units,
fraction=FRACTION_1,
product_yield=YIELD_1,
cost_price=COST_0,
sale_price=sale_price,
has_happy_hour=data.has_happy_hour,
menu_category_id=data.menu_category.id_,
valid_from=date_,
valid_till=None,
)
)
# ---- Bundle items replace/update
existing = db.execute(select(BundleItemModel).where(BundleItemModel.bundle_id == sku.id)).scalars().all()
existing_by_item = {x.item_id: x for x in existing}
incoming_ids = {x.item_id for x in data.items}
existing_by_item = {x.item_id: x for x in existing}
incoming_ids = {x.item_id for x in data.items}
# delete removed
for ex_d in existing:
if ex_d.item_id not in incoming_ids:
if ex_d.valid_from == date_:
db.delete(ex_d)
else:
ex_d.valid_till = date_ - timedelta(days=1)
# add/update
for it in data.items:
ex = existing_by_item.get(it.item_id)
if ex is None:
db.add(
BundleItemModel(
bundle_id=sku.id,
item_id=it.item_id,
sale_price=round(it.sale_price, 2),
quantity=round(it.quantity, 5),
print_in_bill=it.print_in_bill,
valid_from=date_,
valid_till=None,
)
)
elif ex.valid_from == date_:
ex.sale_price = round(it.sale_price, 2)
ex.quantity = round(it.quantity, 5)
else:
ex.valid_till = date_ - timedelta(days=1)
db.add(
BundleItemModel(
bundle_id=sku.id,
item_id=it.item_id,
sale_price=round(it.sale_price, 2),
quantity=round(it.quantity, 5),
print_in_bill=it.print_in_bill,
valid_from=date_,
valid_till=None,
)
)
db.commit()
except SQLAlchemyError as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.get("", response_model=schemas.BundleBlank)
def show_blank(
user: UserToken = Security(get_user, scopes=["products"]),
) -> schemas.BundleBlank:
return bundle_blank()
@router.get("/list", response_model=list[schemas.Bundle])
def show_list(
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
) -> list[schemas.Bundle]:
with SessionFuture() as db:
sv_onclause = _sv_onclause(date_)
pv_onclause = _pv_onclause(date_)
rows = (
db.execute(
select(SkuVersion)
# .select_from(SkuVersion)
.join(StockKeepingUnit, onclause=sv_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=pv_onclause)
.join(SkuVersion.menu_category)
.where(StockKeepingUnit.is_bundle == True) # noqa: E712
.options(
contains_eager(SkuVersion.sku).contains_eager(StockKeepingUnit.product),
contains_eager(SkuVersion.menu_category),
)
.order_by(StockKeepingUnit.sort_order, SkuVersion.units)
)
.unique()
.scalars()
.all()
)
out: list[schemas.Bundle] = []
for sv in rows:
pv = sv.sku.product.versions[0]
items = (
db.execute(
select(BundleItemModel)
.join(BundleItemModel.item)
.join(SkuVersion, onclause=sv_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=pv_onclause)
.join(ProductVersion.sale_category)
.where(
BundleItemModel.bundle_id == sv.sku_id,
or_(BundleItemModel.valid_from == None, BundleItemModel.valid_from <= date_), # noqa: E711
or_(BundleItemModel.valid_till == None, BundleItemModel.valid_till >= date_), # noqa: E711
)
.options(
contains_eager(BundleItemModel.item).contains_eager(StockKeepingUnit.versions),
contains_eager(BundleItemModel.item)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.sale_category),
)
)
.unique()
.scalars()
.all()
)
out.append(_bundle_info(pv=pv, sv=sv, items=items))
return out
@router.get("/{id_}", response_model=schemas.Bundle)
def show_id(
id_: uuid.UUID, # bundle header SKU id
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
) -> schemas.Bundle:
with SessionFuture() as db:
sv_onclause = _sv_onclause(date_)
pv_onclause = _pv_onclause(date_)
sv = (
db.execute(
select(SkuVersion)
.join(StockKeepingUnit, onclause=sv_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=pv_onclause)
.join(SkuVersion.menu_category)
.where(
StockKeepingUnit.id == id_,
StockKeepingUnit.is_bundle == True, # noqa: E712
)
.options(
contains_eager(SkuVersion.sku).contains_eager(StockKeepingUnit.product),
contains_eager(SkuVersion.menu_category),
)
)
.unique()
.scalar_one_or_none()
)
if sv is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found.")
pv = sv.sku.product.versions[0]
items = (
db.execute(
select(BundleItemModel)
.join(BundleItemModel.item)
.join(SkuVersion, onclause=sv_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=pv_onclause)
.join(ProductVersion.sale_category)
.where(
BundleItemModel.bundle_id == sv.sku_id,
or_(BundleItemModel.valid_from == None, BundleItemModel.valid_from <= date_), # noqa: E711
or_(BundleItemModel.valid_till == None, BundleItemModel.valid_till >= date_), # noqa: E711
)
.options(
contains_eager(BundleItemModel.item).contains_eager(StockKeepingUnit.versions),
contains_eager(BundleItemModel.item)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.sale_category),
)
)
.unique()
.scalars()
.all()
)
return _bundle_info(pv=pv, sv=sv, items=items)
@router.delete("/{id_}", response_model=None)
def delete_route(
id_: uuid.UUID, # bundle header SKU id
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
) -> None:
with SessionFuture() as db:
sv_onclause = _sv_onclause(date_)
pv_onclause = _pv_onclause(date_)
day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
billed = db.execute(
select(func.count(Inventory.id))
.join(Inventory.kot)
.join(Kot.voucher)
.where(Inventory.sku_id == id_, day >= date_)
).scalar_one()
if billed > 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="The cannot delete this product as it was billed",
)
sv = (
db.execute(
select(SkuVersion)
.join(StockKeepingUnit, onclause=sv_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=pv_onclause)
.where(
StockKeepingUnit.id == id_,
StockKeepingUnit.is_bundle == True, # noqa: E712
)
.options(contains_eager(SkuVersion.sku).contains_eager(StockKeepingUnit.product))
)
.unique()
.scalar_one_or_none()
)
if sv is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found.")
pv = sv.sku.product.versions[0]
# close/delete sku version
if sv.valid_from == date_:
db.delete(sv)
else:
sv.valid_till = date_ - timedelta(days=1)
# close/delete product version
if pv.valid_from == date_:
db.delete(pv)
else:
pv.valid_till = date_ - timedelta(days=1)
items = (
db.execute(
select(BundleItemModel).where(
BundleItemModel.bundle_id == id_,
or_(BundleItemModel.valid_from == None, BundleItemModel.valid_from <= date_), # noqa: E711
or_(BundleItemModel.valid_till == None, BundleItemModel.valid_till >= date_), # noqa: E711
)
)
.scalars()
.all()
)
for bi in items:
if bi.valid_from == date_:
db.delete(bi)
else:
bi.valid_till = date_ - timedelta(days=1)
db.commit()
+2 -1
View File
@@ -9,6 +9,7 @@ from ..db.session import SessionFuture
from ..models.device import Device
from ..models.login_history import LoginHistory
from ..schemas import device as schemas
from ..schemas.section import SectionLink
from ..schemas.user_token import UserToken
@@ -74,7 +75,7 @@ def device_info(item: Device) -> schemas.Device:
id_=item.id,
name=item.name,
enabled=item.enabled,
section=schemas.SectionLink(id_=item.section_id, name=item.section.name),
section=SectionLink(id_=item.section_id, name=item.section.name),
creation_date=item.creation_date,
last_user=item.login_history[0].user.name if len(item.login_history) else "Never",
last_date=item.login_history[0].date if len(item.login_history) else None,
+8 -8
View File
@@ -5,29 +5,29 @@ from sqlalchemy.exc import SQLAlchemyError
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..models.db_setting import DbSetting
from ..schemas import header_footer as schemas
from ..schemas.header_footer import HeaderFooter
from ..schemas.user_token import UserToken
router = APIRouter()
@router.get("")
@router.get("", response_model=list[HeaderFooter])
def get(
user: UserToken = Security(get_user, scopes=["owner"]),
):
) -> list[HeaderFooter]:
with SessionFuture() as db:
return [
{"id": item.id, "name": item.name, "text": item.data["Text"]}
HeaderFooter(id_=item.id, name=item.name, text=item.data["Text"])
for item in db.execute(select(DbSetting).where(DbSetting.name.in_(["Header", "Footer"]))).scalars().all()
]
@router.post("")
@router.post("", response_model=list[HeaderFooter])
def save(
data: schemas.HeaderFooter,
data: HeaderFooter,
user: UserToken = Security(get_user, scopes=["owner"]),
):
) -> list[HeaderFooter]:
try:
with SessionFuture() as db:
item: DbSetting = db.execute(
@@ -36,7 +36,7 @@ def save(
item.data = {"Text": data.text}
db.commit()
return [
{"id": item.id, "name": item.name, "text": item.data["Text"]}
HeaderFooter(id_=item.id, name=item.name, text=item.data["Text"])
for item in db.execute(select(DbSetting).where(DbSetting.name.in_(["Header", "Footer"])))
.scalars()
.all()
+4 -3
View File
@@ -2,16 +2,17 @@ from fastapi import APIRouter, HTTPException
from sqlalchemy import text
from ..db.session import SessionFuture
from ..schemas.health_response import HealthResponse
router = APIRouter()
@router.get("")
def health_check():
@router.get("", response_model=HealthResponse)
def health_check() -> HealthResponse:
try:
with SessionFuture() as db: # lightweight DB check
db.execute(text("SELECT 1"))
return {"status": "ok", "database": "connected"}
return HealthResponse(status="ok", database="connected")
except Exception as e:
raise HTTPException(status_code=500, detail={"status": "error", "database": str(e)}) from e
+2 -1
View File
@@ -10,6 +10,7 @@ from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..models.modifier import Modifier
from ..schemas import modifier as schemas
from ..schemas.modifier_category import ModifierCategoryLink
from ..schemas.user_token import UserToken
@@ -111,7 +112,7 @@ def modifier_info(item: Modifier) -> schemas.Modifier:
show_in_bill=item.show_in_bill,
price=item.price,
is_active=item.is_active,
modifier_category=schemas.ModifierCategoryLink(
modifier_category=ModifierCategoryLink(
id_=item.modifier_category.id,
name=item.modifier_category.name,
),
+6 -5
View File
@@ -18,7 +18,8 @@ from ..models.sku_version import SkuVersion
from ..models.stock_keeping_unit import StockKeepingUnit
from ..schemas import modifier_category as schemas
from ..schemas.menu_category import MenuCategoryLink
from ..schemas.modifier_category_for_product import ModifierCategoryForProduct, ModifierLink
from ..schemas.modifier import ModifierLink
from ..schemas.modifier_category_for_product import ModifierCategoryForProduct
from ..schemas.product_link import ProductLink
from ..schemas.product_link import ProductLink as ProductLinkSchema
from ..schemas.user_token import UserToken
@@ -172,7 +173,7 @@ def show_list(
is_active=item.is_active,
sort_order=item.sort_order,
menu_categories=[
schemas.MenuCategoryLink(
MenuCategoryLink(
id_=mc.id,
name=mc.name,
enabled=reduce(lambda x, y: x and (y.sku.product_id in products), mc.skus, True),
@@ -190,7 +191,7 @@ def show_list(
return modifier_categories
@router.get("/for-product/{sku_id}")
@router.get("/for-sku/{sku_id}")
def for_product(
sku_id: uuid.UUID,
date_: date = Depends(effective_date),
@@ -283,7 +284,7 @@ def modifier_category_info(item: ModifierCategory, date_: date, db: Session) ->
maximum=item.maximum,
is_active=item.is_active,
menu_categories=[
schemas.MenuCategoryLink(
MenuCategoryLink(
id_=mc.id,
name=mc.name,
skus=[
@@ -354,7 +355,7 @@ def modifier_category_blank(date_: date, db: Session) -> schemas.ModifierCategor
maximum=0,
is_active=True,
menu_categories=[
schemas.MenuCategoryLink(
MenuCategoryLink(
id_=mc.id,
name=mc.name,
skus=[
+82 -107
View File
@@ -1,5 +1,6 @@
import uuid
from collections.abc import Sequence
from datetime import date, timedelta
from decimal import Decimal
@@ -12,6 +13,7 @@ from sqlalchemy.sql.functions import count, func
from ..core.config import settings
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..models.bundle_item import BundleItem
from ..models.inventory import Inventory
from ..models.kot import Kot
from ..models.menu_category import MenuCategory
@@ -25,11 +27,12 @@ from ..models.stock_keeping_unit import StockKeepingUnit
from ..models.voucher import Voucher
from ..schemas import product as schemas
from ..schemas.menu_category import MenuCategoryLink
from ..schemas.product_query import ProductQuery
from ..schemas.product_query import BundleItemQuery, ProductQuery
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 effective_date
from . import _pv_onclause, _sv_onclause, effective_date
router = APIRouter()
@@ -168,22 +171,8 @@ def update_route(
) -> None:
try:
with SessionFuture() as db:
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_,
),
)
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
)
product_version_onclause = _pv_onclause(date_)
sku_version_onclause = _sv_onclause(date_)
version: ProductVersion = db.execute(
select(ProductVersion)
.join(Product, onclause=product_version_onclause)
@@ -446,46 +435,61 @@ def product_list(date_: date, db: Session) -> list[schemas.Product]:
# ]
@router.get("/query")
def bundle_items(sku_id: uuid.UUID, date_: date, db: Session) -> Sequence[BundleItem]:
return (
db.execute(
select(BundleItem)
.join(BundleItem.item)
.join(SkuVersion, onclause=_sv_onclause(date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.where(
BundleItem.bundle_id == sku_id,
or_(BundleItem.valid_from == None, BundleItem.valid_from <= date_), # noqa: E711
or_(BundleItem.valid_till == None, BundleItem.valid_till >= date_), # noqa: E711
)
.options(
contains_eager(BundleItem.item).contains_eager(StockKeepingUnit.versions),
contains_eager(BundleItem.item)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions),
)
)
.unique()
.scalars()
.all()
)
@router.get("/query", response_model=list[ProductQuery])
def show_term(
q: str | None = None,
mc: uuid.UUID | None = None,
sc: uuid.UUID | None = None,
date_: date = Depends(effective_date),
current_user: UserToken = Depends(get_user),
):
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_,
),
)
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
)
) -> 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)
.join(SkuVersion.menu_category)
.join(StockKeepingUnit, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
# .join(SkuVersion.menu_category)
# .join(ProductVersion.sale_category)
.join(ProductVersion.sale_category)
.join(SaleCategory.tax)
.options(
contains_eager(SkuVersion.sku).contains_eager(StockKeepingUnit.product).contains_eager(Product.versions)
contains_eager(SkuVersion.menu_category),
contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.sale_category)
.contains_eager(SaleCategory.tax),
)
)
if mc is not None:
query = query.join(SkuVersion.menu_category)
if sc is not None:
query = query.join(SkuVersion.menu_category).join(ProductVersion.sale_category).join(SaleCategory.tax)
if mc is not None:
query = query.where(SkuVersion.menu_category_id == mc).order_by(
Product.sort_order, ProductVersion.name, StockKeepingUnit.sort_order, SkuVersion.units
@@ -498,72 +502,23 @@ def show_term(
StockKeepingUnit.sort_order,
SkuVersion.units,
)
if mc is not None:
query = query.options(
contains_eager(SkuVersion.menu_category),
)
if sc is not None:
query = query.options(
contains_eager(ProductVersion.sale_category).contains_eager(SaleCategory.tax),
)
if q is not None:
for name in q.split():
query = query.where(or_(ProductVersion.name.ilike(f"%{name}%"), SkuVersion.units.ilike(f"%{name}%")))
with SessionFuture() as db:
for item in db.execute(query).unique().scalars().all():
if sc is not None:
list_.append(
ProductQuery(
id_=item.sku_id,
name=f"{item.sku.product.versions[0].name} ({item.units})",
menu_category=MenuCategoryLink(
id_=item.menu_category_id, name=item.menu_category.name, skus=[]
),
price=item.sale_price,
has_happy_hour=item.has_happy_hour,
is_not_available=item.sku.is_not_available,
)
)
if mc is not None:
list_.append(query_product_info(item, False))
if item.has_happy_hour:
list_.append(query_product_info(item, True))
for sv in db.execute(query).unique().scalars().all():
bi: Sequence[BundleItem] = []
if sv.sku.is_bundle:
bi = bundle_items(sv.sku_id, date_, db)
info = query_product_info(sv, False, bi)
list_.append(info)
if mc is not None and sv.has_happy_hour:
info = query_product_info(sv, True, bi)
list_.append(info)
return list_
# def product_list_of_sale_category(date_: date, db: Session) -> list[schemas.Product]:
# return [
# product_info(item)
# for item in db.execute(
# select(ProductVersion)
# .join(ProductVersion.menu_category)
# .join(ProductVersion.sale_category)
# .where(
# and_(
# or_(
# ProductVersion.valid_from == None, # noqa: E711
# ProductVersion.valid_from <= date_,
# ),
# or_(
# ProductVersion.valid_till == None, # noqa: E711
# ProductVersion.valid_till >= date_,
# ),
# )
# )
# .order_by(MenuCategory.sort_order)
# .order_by(MenuCategory.name)
# .order_by(ProductVersion.sort_order)
# .order_by(ProductVersion.name)
# .options(
# contains_eager(ProductVersion.menu_category),
# contains_eager(ProductVersion.sale_category),
# )
# )
# .scalars()
# .all()
# ]
@router.get("/{id_}", response_model=schemas.Product)
def show_id(
id_: uuid.UUID,
@@ -610,10 +565,15 @@ def show_id(
return product_info(version)
def query_product_info(item: SkuVersion, happy_hour: bool) -> ProductQuery:
def query_product_info(item: SkuVersion, happy_hour: bool, bundle_items: Sequence[BundleItem]) -> ProductQuery:
return ProductQuery(
id_=item.sku_id,
name=("H H " if happy_hour else "") + f"{item.sku.product.versions[0].name} ({item.units})",
menu_category=MenuCategoryLink(
id_=item.menu_category.id,
name=item.menu_category.name,
skus=[],
),
sale_category=SaleCategoryLink(
id_=item.sku.product.versions[0].sale_category_id,
name=item.sku.product.versions[0].sale_category.name,
@@ -627,6 +587,21 @@ def query_product_info(item: SkuVersion, happy_hour: bool) -> ProductQuery:
has_happy_hour=happy_hour,
is_not_available=item.sku.is_not_available,
sort_order=item.sku.sort_order,
is_bundle=item.sku.is_bundle,
bundle_items=[
BundleItemQuery(
id_=bi.item_id,
name=f"{bi.item.product.versions[0].name} ({bi.item.versions[0].units})",
price=bi.sale_price,
quantity=bi.quantity,
tax=TaxLink(
id_=bi.item.product.versions[0].sale_category.tax.id,
name=bi.item.product.versions[0].sale_category.tax.name,
rate=bi.item.product.versions[0].sale_category.tax.rate,
),
)
for bi in bundle_items
],
)
@@ -636,12 +611,12 @@ def product_info(version: ProductVersion) -> schemas.Product:
version_id=version.id,
name=version.name,
fraction_units=version.fraction_units,
sale_category=schemas.SaleCategoryLink(
sale_category=SaleCategoryLink(
id_=version.sale_category_id,
name=version.sale_category.name,
),
skus=[
schemas.StockKeepingUnit(
StockKeepingUnitSchema(
id_=sku.id,
units=sku_version.units,
fraction=sku_version.fraction,
@@ -1,16 +1,15 @@
from datetime import date, timedelta
from operator import or_
from fastapi import APIRouter, Depends, Security
from sqlalchemy import Date, and_
from sqlalchemy import Date
from sqlalchemy.sql.expression import func, select
from ...core.config import settings
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.inventory import Inventory
from ...models.inventory_type import InventoryType
from ...models.kot import Kot
from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
@@ -18,6 +17,7 @@ from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas.beer_consumption_report import BeerConsumptionReport, BeerConsumptionReportItem
from ...schemas.user_token import UserToken
from .. import _pv_onclause, _sv_onclause
from . import check_audit_permission, report_finish_date, report_start_date
@@ -39,22 +39,8 @@ def beer_consumption(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
sum_ = func.sum(Inventory.quantity * SkuVersion.fraction).label("sum")
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= day,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= day,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= day), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= day), # noqa: E711
)
product_version_onclause = _pv_onclause(day)
sku_version_onclause = _sv_onclause(day)
query = (
select(day, ProductVersion.name, sum_, ProductVersion.fraction_units)
.join(Voucher.kots)
@@ -64,6 +50,7 @@ def beer_consumption(
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.where(
Inventory.type_ != InventoryType.bundle,
day >= start_date,
day <= finish_date,
)
@@ -3,24 +3,24 @@ import uuid
from datetime import date, timedelta
from fastapi import APIRouter, Cookie, Depends, Security
from sqlalchemy import Date, and_, func, or_, select
from sqlalchemy import Date, func, select
from sqlalchemy.orm import Session
from ...core.config import settings
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.inventory import Inventory
from ...models.inventory_type import InventoryType
from ...models.kot import Kot
from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sale_category import SaleCategory
from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...printing.discount_report import print_discount_report
from ...schemas.discount_report import DiscountReport, DiscountReportItem
from ...schemas.user_token import UserToken
from .. import _pv_onclause
from . import check_audit_permission, report_finish_date, report_start_date
@@ -47,33 +47,18 @@ def get_discount_report(start_date: date, finish_date: date, db: Session) -> lis
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
amount = func.sum(Inventory.quantity * Inventory.effective_price * Inventory.discount).label("Amount")
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= day,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= day,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= day), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= day), # noqa: E711
)
product_version_onclause = _pv_onclause(day)
list_ = db.execute(
select(SaleCategory.name, amount)
.join(Voucher.kots)
.join(Kot.inventories)
.join(Inventory.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.sale_category)
.where(
Inventory.discount != 0,
Inventory.type_ != InventoryType.bundle,
day >= start_date,
day <= finish_date,
Voucher.voucher_type.in_([VoucherType.REGULAR_BILL, VoucherType.KOT]),
@@ -4,7 +4,7 @@ from datetime import date, timedelta
from decimal import Decimal
from fastapi import APIRouter, Depends, Security
from sqlalchemy import Date, and_, func, nulls_last, or_, select
from sqlalchemy import Date, func, nulls_last, select
from sqlalchemy.orm import Session
from ...core.config import settings
@@ -22,6 +22,7 @@ from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas.menu_engineering_report import MeItem, MeReport
from ...schemas.user_token import UserToken
from .. import _pv_active, _sv_active
from . import check_audit_permission, report_finish_date, report_start_date
@@ -48,20 +49,8 @@ def menu_engineering_report(start_date: date, finish_date: date, db: Session) ->
day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
product_version_valid = and_(
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= day,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= day,
),
)
sku_version_valid = and_(
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= day), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= day), # noqa: E711
)
product_version_active = _pv_active(day)
sku_version_active = _sv_active(day)
list_ = db.execute(
select(
SaleCategory.name,
@@ -87,8 +76,8 @@ def menu_engineering_report(start_date: date, finish_date: date, db: Session) ->
day >= start_date,
day <= finish_date,
Voucher.voucher_type == VoucherType.REGULAR_BILL,
product_version_valid,
sku_version_valid,
product_version_active,
sku_version_active,
)
.group_by(
SaleCategory.name,
@@ -4,7 +4,7 @@ from datetime import date, timedelta
from typing import Annotated
from fastapi import APIRouter, Cookie, Depends, Query, Security
from sqlalchemy import Date, and_, func, or_, select
from sqlalchemy import Date, func, select
from sqlalchemy.orm import Session
from ...core.config import settings
@@ -14,7 +14,6 @@ from ...models.food_table import FoodTable
from ...models.inventory import Inventory
from ...models.kot import Kot
from ...models.menu_category import MenuCategory
from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sale_category import SaleCategory
from ...models.sku_version import SkuVersion
@@ -22,8 +21,10 @@ from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...printing.product_sale_report import print_product_sale_report
from ...schemas.product_sale_report import ProductSaleReport, ProductSaleReportItem, UserLink
from ...schemas.product_sale_report import ProductSaleReport, ProductSaleReportItem
from ...schemas.user import UserLink
from ...schemas.user_token import UserToken
from .. import _pv_onclause, _sv_onclause
from . import check_audit_permission, report_finish_date, report_start_date
@@ -54,22 +55,8 @@ def product_sale_report(
day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= day,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= day,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= day), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= day), # noqa: E711
)
product_version_onclause = _pv_onclause(day)
sku_version_onclause = _sv_onclause(day)
query = (
select(
ProductVersion.id,
@@ -1,6 +1,6 @@
from datetime import date, timedelta
from fastapi import APIRouter, Depends, Security
from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy import and_, nullsfirst, or_, select
from sqlalchemy.orm import Session, contains_eager
@@ -11,6 +11,7 @@ from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
from ...schemas.product_updates_report import ProductUpdatesReport
from ...schemas.user_token import UserToken
from . import report_finish_date, report_start_date
@@ -18,21 +19,24 @@ from . import report_finish_date, report_start_date
router = APIRouter()
@router.get("")
@router.get("", response_model=ProductUpdatesReport)
def product_updates_report_view(
start_date: date = Depends(report_start_date),
finish_date: date = Depends(report_finish_date),
user: UserToken = Security(get_user, scopes=["product-sale-report"]),
):
) -> ProductUpdatesReport:
try:
with SessionFuture() as db:
return {
"startDate": start_date.strftime("%d-%b-%Y"),
"finishDate": finish_date.strftime("%d-%b-%Y"),
"report": product_updates_report(start_date, finish_date, db),
}
return ProductUpdatesReport(
start_date=start_date,
finish_date=finish_date,
report=product_updates_report(start_date, finish_date, db),
)
except Exception as e:
return {"error": str(e)}
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
def product_updates_report(start_date: date, finish_date: date, db: Session) -> list[str]:
+9 -14
View File
@@ -5,7 +5,7 @@ from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Cookie, Depends, Query, Security
from sqlalchemy import Date, and_, func, or_, select
from sqlalchemy import Date, func, select
from sqlalchemy.orm import Session
from ...core.config import settings
@@ -13,18 +13,20 @@ from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.food_table import FoodTable
from ...models.inventory import Inventory
from ...models.inventory_type import InventoryType
from ...models.kot import Kot
from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sale_category import SaleCategory
from ...models.settle_option import SettleOption
from ...models.settlement import Settlement
from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...printing.sale_report import print_sale_report
from ...schemas.sale_report import SaleReport, SaleReportItem
from ...schemas.user import UserLink
from ...schemas.user_token import UserToken
from .. import _pv_onclause
from . import check_audit_permission, report_finish_date, report_start_date
from .tax_report import get_tax
@@ -60,26 +62,19 @@ def get_sale(start_date: date, finish_date: date, id_: uuid.UUID | None, db: Ses
day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= day,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= day,
),
)
product_version_onclause = _pv_onclause(day)
query = (
select(SaleCategory.name, func.sum(Inventory.net))
.select_from(Inventory)
.join(Inventory.kot)
.join(Kot.voucher)
.join(Inventory.product)
.join(Inventory.sku)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.sale_category)
.join(Voucher.food_table)
.where(
Inventory.type_ != InventoryType.bundle,
day >= start_date,
day <= finish_date,
Voucher.voucher_type == VoucherType.REGULAR_BILL,
@@ -13,6 +13,7 @@ from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.food_table import FoodTable
from ...models.inventory import Inventory
from ...models.inventory_type import InventoryType
from ...models.kot import Kot
from ...models.tax import Tax
from ...models.voucher import Voucher
@@ -60,6 +61,7 @@ def get_tax(s: date, f: date, id_: uuid.UUID | None, db: Session) -> list[TaxRep
.join(Inventory.tax)
.join(Voucher.food_table)
.where(
Inventory.type_ != InventoryType.bundle_item,
Voucher.date >= start_date,
Voucher.date <= finish_date,
Voucher.voucher_type == VoucherType.REGULAR_BILL,
+2 -2
View File
@@ -119,7 +119,7 @@ def role_info(item: Role, db: Session) -> schemas.Role:
id_=item.id,
name=item.name,
permissions=[
schemas.PermissionItem(
PermissionItem(
id_=p.id,
name=p.name,
enabled=p in item.permissions,
@@ -133,7 +133,7 @@ def role_blank(db: Session) -> schemas.RoleBlank:
return schemas.RoleBlank(
name="",
permissions=[
schemas.PermissionItem(id_=p.id, name=p.name, enabled=False)
PermissionItem(id_=p.id, name=p.name, enabled=False)
for p in db.execute(select(Permission).order_by(Permission.name)).scalars().all()
],
)
+2 -1
View File
@@ -13,6 +13,7 @@ from ..db.session import SessionFuture
from ..models.product_version import ProductVersion
from ..models.sale_category import SaleCategory
from ..schemas import sale_category as schemas
from ..schemas.tax import TaxLink
from ..schemas.user_token import UserToken
@@ -118,7 +119,7 @@ def sale_category_info(item: SaleCategory) -> schemas.SaleCategory:
id_=item.id,
name=item.name,
discount_limit=item.discount_limit,
tax=schemas.TaxLink(id_=item.tax_id, name=item.tax.name, rate=item.tax.rate),
tax=TaxLink(id_=item.tax_id, name=item.tax.name, rate=item.tax.rate),
)
+4 -2
View File
@@ -13,6 +13,8 @@ from ..db.session import SessionFuture
from ..models.sale_category import SaleCategory
from ..models.section_printer import SectionPrinter
from ..schemas import section_printer as schemas
from ..schemas.printer import PrinterLink
from ..schemas.sale_category import SaleCategoryLink
from ..schemas.user_token import UserToken
@@ -126,8 +128,8 @@ def report(section_id: uuid.UUID | None, db: Session) -> list[schemas.SectionPri
)
list_.append(
schemas.SectionPrinter(
sale_category=None if sc_id is None else schemas.SaleCategoryLink(id_=sc_id, name=sc_name),
printer=None if section_printer is None else schemas.PrinterLink(id_=section_printer.printer_id),
sale_category=None if sc_id is None else SaleCategoryLink(id_=sc_id, name=sc_name),
printer=None if section_printer is None else PrinterLink(id_=section_printer.printer_id),
copies=0 if section_printer is None else section_printer.copies,
)
)
+4 -3
View File
@@ -6,17 +6,18 @@ from sqlalchemy import select
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..models.db_setting import DbSetting
from ..schemas.db_setting import DbSetting as DbSettingSchema
from ..schemas.user_token import UserToken
router = APIRouter()
@router.get("/{id_}")
@router.get("/{id_}", response_model=DbSettingSchema)
def get(
id_: uuid.UUID,
user: UserToken = Security(get_user),
):
) -> DbSettingSchema:
with SessionFuture() as db:
data = db.execute(select(DbSetting).where(DbSetting.name == id_)).scalar_one()
return {"id": data.id, "name": data.name, "details": data.data}
return DbSettingSchema(id_=data.id, name=data.name, details=data.data)
+22 -22
View File
@@ -12,6 +12,8 @@ from ..db.session import SessionFuture
from ..models.food_table import FoodTable
from ..models.overview import Overview
from ..schemas import table as schemas
from ..schemas.running_table import RunningTable
from ..schemas.section import SectionLink
from ..schemas.user_token import UserToken
@@ -33,7 +35,7 @@ def sort_order(
id_=item.id,
name=item.name,
seats=item.seats,
section=schemas.SectionLink(id_=item.section_id, name=item.section.name),
section=SectionLink(id_=item.section_id, name=item.section.name),
is_active=item.is_active,
sort_order=item.sort_order,
)
@@ -125,8 +127,8 @@ def show_list(user: UserToken = Depends(get_user)) -> list[schemas.Table]:
]
@router.get("/running")
def show_running(user: UserToken = Depends(get_user)):
@router.get("/running", response_model=list[RunningTable])
def show_running(user: UserToken = Depends(get_user)) -> list[RunningTable]:
with SessionFuture() as db:
list_ = (
db.execute(
@@ -136,27 +138,25 @@ def show_running(user: UserToken = Depends(get_user)):
.all()
)
food_tables = []
food_tables: list[RunningTable] = []
for item in list_:
ft = {
"id": item.id,
"name": item.name,
"seats": item.seats,
"section": {"id": item.section_id, "name": item.section.name},
"isActive": item.is_active,
"sortOrder": item.sort_order,
}
ft = RunningTable(
id_=item.id,
name=item.name,
seats=item.seats,
section=SectionLink(id_=item.section_id, name=item.section.name),
is_active=item.is_active,
sort_order=item.sort_order,
)
if item.status is not None:
ft["status"] = item.status.status
ft["voucherId"] = item.status.voucher_id
ft["pax"] = item.status.voucher.pax
ft["date"] = (item.status.voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime(
"%d-%b-%Y %H:%M"
)
ft["amount"] = round(item.status.voucher.amount, 0)
ft.status = item.status.status
ft.voucher_id = item.status.voucher_id
ft.pax = item.status.voucher.pax
ft.date_ = item.status.voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
ft.amount = int(round(item.status.voucher.amount, 0))
if item.status.guest is not None:
ft["guest"] = item.status.guest.customer.name
ft["bold"] = item.status.guest.customer.print_in_bill
ft.guest = item.status.guest.customer.name
ft.bold = item.status.guest.customer.print_in_bill
food_tables.append(ft)
return food_tables
@@ -188,7 +188,7 @@ def food_table_info(item: FoodTable) -> schemas.Table:
id_=item.id,
name=item.name,
seats=item.seats,
section=schemas.SectionLink(id_=item.section_id, name=item.section.name),
section=SectionLink(id_=item.section_id, name=item.section.name),
is_active=item.is_active,
sort_order=item.sort_order,
)
+2 -1
View File
@@ -14,6 +14,7 @@ from ..models.inventory import Inventory
from ..models.sale_category import SaleCategory
from ..models.tax import Tax
from ..schemas import tax as schemas
from ..schemas.regime import RegimeLink
from ..schemas.user_token import UserToken
@@ -129,7 +130,7 @@ def tax_info(item: Tax) -> schemas.Tax:
id_=item.id,
name=item.name,
rate=item.rate,
regime=schemas.RegimeLink(id_=item.regime_id, name=item.regime.name),
regime=RegimeLink(id_=item.regime_id, name=item.regime.name),
is_fixture=item.is_fixture,
)
+2 -2
View File
@@ -102,14 +102,14 @@ def check_gaps(data: TemporalProduct, db: Session) -> None:
detail="Product and SKU versions validity do not match",
)
for i, p_item in enumerate(data.products[1:], start=1):
if data.products[i - 1].valid_till + timedelta(days=1) != p_item.valid_from:
if data.products[i - 1].valid_till + timedelta(days=1) != p_item.valid_from: # type: ignore[operator]
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gaps in product versions exist",
)
for sku_list in skus.values():
for i, s_item in enumerate(sku_list[1:], start=1):
if sku_list[i - 1].valid_till + timedelta(days=1) != s_item.valid_from:
if sku_list[i - 1].valid_till + timedelta(days=1) != s_item.valid_from: # type: ignore[operator]
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gaps in sku versions exist",
+2 -2
View File
@@ -166,7 +166,7 @@ def user_info(item: User, db: Session, user: UserToken) -> schemas.User:
password="",
locked_out=item.locked_out,
roles=[
schemas.RoleItem(
RoleItem(
id_=r.id,
name=r.name,
enabled=r in item.roles,
@@ -194,7 +194,7 @@ def blank_user_info(db: Session) -> schemas.UserIn:
password="",
locked_out=False,
roles=[
schemas.RoleItem(id_=r.id, name=r.name, enabled=False)
RoleItem(id_=r.id, name=r.name, enabled=False)
for r in db.execute(select(Role).order_by(Role.name)).scalars().all()
],
)
+79 -2
View File
@@ -4,18 +4,24 @@ from datetime import datetime
from decimal import Decimal
from fastapi import HTTPException, status
from sqlalchemy import func
from sqlalchemy.orm import Session
from sqlalchemy import ColumnElement, func, select
from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql import expression
from ...models.bill import Bill
from ...models.guest_book import GuestBook
from ...models.guest_book_type import GuestBookType
from ...models.inventory import Inventory
from ...models.kot import Kot
from ...models.overview import Overview
from ...models.overview_status import OverviewStatus
from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.regime import Regime
from ...models.settle_option import SettleOption
from ...models.settlement import Settlement
from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas import voucher as schemas
@@ -180,3 +186,74 @@ def happy_hour_items_more_than_regular(kots: list[schemas.Kot]) -> bool:
else:
inventories[inventory.sku.id_]["normal"] += inventory.quantity
return any(value["happy"] > value["normal"] for value in inventories.values())
def get_voucher(
voucher_id: uuid.UUID,
db: Session,
product_version_onclause: ColumnElement[bool],
sku_version_onclause: ColumnElement[bool],
) -> Voucher:
item: Voucher = (
db.execute(
select(Voucher)
.join(Voucher.food_table)
.join(Voucher.customer, isouter=True)
.join(Voucher.kots)
.join(Kot.inventories)
.join(Inventory.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.where(Voucher.id == voucher_id)
.order_by(Kot.code, Inventory.sort_order)
.options(
contains_eager(Voucher.food_table),
contains_eager(Voucher.customer),
contains_eager(Voucher.kots)
.contains_eager(Kot.inventories)
.contains_eager(Inventory.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions),
contains_eager(Voucher.kots)
.contains_eager(Kot.inventories)
.contains_eager(Inventory.sku)
.contains_eager(StockKeepingUnit.versions),
)
)
.unique()
.scalar_one()
)
if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Voucher not found",
)
return item
def get_sku(
sku_id: uuid.UUID,
db: Session,
product_version_onclause: ColumnElement[bool],
sku_version_onclause: ColumnElement[bool],
) -> StockKeepingUnit:
sku: StockKeepingUnit = (
db.execute(
select(StockKeepingUnit)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.sale_category)
.where(StockKeepingUnit.id == sku_id)
.options(
contains_eager(StockKeepingUnit.versions),
contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.sale_category),
)
)
.unique()
.scalar_one()
)
return sku
+1 -1
View File
@@ -49,7 +49,7 @@ def change(
)
bill_changed: bool = (
len(
set((i.id, i.amount) for k in old.kots for i in k.inventories)
set((i.id, i.amount) for k in old.kots for i in k.inventories if not i.parent_id)
^ set((i.id_, i.amount) for k in data.kots for i in k.inventories)
)
!= 0
+88 -59
View File
@@ -5,21 +5,19 @@ from datetime import UTC, datetime, timedelta
from decimal import Decimal
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import and_, func, or_, select
from sqlalchemy import func, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.orm import Session
from ...core.config import settings
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.bundle_item import BundleItem
from ...models.guest_book import GuestBook
from ...models.inventory import Inventory
from ...models.inventory_modifier import InventoryModifier
from ...models.inventory_type import InventoryType
from ...models.kot import Kot
from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...printing.bill import print_bill
@@ -30,12 +28,14 @@ from ...routers.voucher import (
do_update_settlements,
do_update_table,
get_guest_book,
get_sku,
get_tax,
happy_hour_has_discount,
happy_hour_items_balanced,
)
from ...schemas import voucher as schemas
from ...schemas.user_token import UserToken
from .. import _bundle_active, _pv_onclause, _sv_onclause
router = APIRouter()
@@ -82,27 +82,13 @@ def do_save(
) -> Voucher:
now = datetime.now(UTC).replace(tzinfo=None)
product_date = (now + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES)).date()
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= product_date,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= product_date,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= product_date), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= product_date), # noqa: E711
)
product_version_onclause = _pv_onclause(product_date)
sku_version_onclause = _sv_onclause(product_date)
check_permissions(None, voucher_type, user.permissions)
kot_id = db.execute(select(func.coalesce(func.max(Voucher.kot_id), 0) + 1)).scalar_one()
item = Voucher(
voucher = Voucher(
now,
guest_book.pax if guest_book is not None else data.pax,
kot_id,
@@ -111,86 +97,129 @@ def do_save(
voucher_type,
user.id_,
)
db.add(item)
db.add(voucher)
for dk in data.kots:
# Filter out nil inventories
dk.inventories = [dki for dki in dk.inventories if round(dki.quantity, 2) != 0]
# Filter out nil kots
data.kots = [k for k in data.kots if len(k.inventories) > 0]
for k in data.kots:
if not happy_hour_items_balanced(k.inventories):
for data_kot in data.kots:
if not happy_hour_items_balanced(data_kot.inventories):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Happy hour products are not balanced.",
)
if happy_hour_has_discount(k.inventories):
if happy_hour_has_discount(data_kot.inventories):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Discount is not allowed on happy hour products.",
)
code = db.execute(select(func.coalesce(func.max(Kot.code), 0) + 1)).scalar_one()
kot = Kot(item.id, code, item.food_table_id, item.date, item.user_id)
item.kots.append(kot)
kot = Kot(voucher.id, code, voucher.food_table_id, voucher.date, voucher.user_id)
voucher.kots.append(kot)
db.add(kot)
for index, i in enumerate(k.inventories):
if round(i.quantity, 2) == 0:
index = 0
for data_inv in data_kot.inventories:
index += 1
if round(data_inv.quantity, 2) == 0:
continue
total_quantity: Decimal = round(
Decimal(sum(inv.quantity for ko in data.kots for inv in ko.inventories if inv.sku.id_ == i.sku.id_)),
Decimal(
sum(inv.quantity for ko in data.kots for inv in ko.inventories if inv.sku.id_ == data_inv.sku.id_)
),
2,
)
sku: StockKeepingUnit = (
db.execute(
select(StockKeepingUnit)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.sale_category)
.where(StockKeepingUnit.id == i.sku.id_)
.options(
contains_eager(StockKeepingUnit.versions),
contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.sale_category),
)
)
.unique()
.scalar_one()
)
sku = get_sku(data_inv.sku.id_, db, product_version_onclause, sku_version_onclause)
if total_quantity < 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Quantity of {sku.product.versions[0].name} ({sku.versions[0].units}) cannot be less than 0",
)
if round(i.quantity, 2) < 0 and "edit-printed-product" not in user.permissions:
if round(data_inv.quantity, 2) < 0 and "edit-printed-product" not in user.permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"You are not allowed to delete printed products.\n In this case {sku.product.versions[0].name} ({sku.versions[0].units})",
)
if data_inv.type_ == InventoryType.bundle_item:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Bundle items cannot be added directly. Please add the bundle parent item.",
)
if data_inv.type_ == InventoryType.bundle and len(data_inv.children) == 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Bundle items must have children items.",
)
tax_rate = get_tax(sku.product.versions[0].sale_category.tax.rate, voucher_type)
inv = Inventory(
kot_id=kot.id,
sku_id=sku.id,
quantity=round(i.quantity, 2),
quantity=round(data_inv.quantity, 2),
price=sku.versions[0].sale_price,
discount=round(min(i.discount, sku.product.versions[0].sale_category.discount_limit), 5),
is_hh=i.is_happy_hour,
discount=round(min(data_inv.discount, sku.product.versions[0].sale_category.discount_limit), 5),
is_hh=data_inv.is_happy_hour,
tax_id=sku.product.versions[0].sale_category.tax_id,
tax_rate=tax_rate,
sort_order=index,
type_=data_inv.type_,
)
kot.inventories.append(inv)
db.add(inv)
for m in i.modifiers:
for m in data_inv.modifiers:
mod = InventoryModifier(None, m.id_, Decimal(0))
inv.modifiers.append(mod)
db.add(mod)
if data_inv.type_ == InventoryType.bundle:
if (data_inv.price * data_inv.quantity) != sum(
child.price * child.quantity for child in data_inv.children
):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Bundle price for {data_inv.sku.name} must be equal to sum of child item prices.",
)
for child in data_inv.children:
index += 1
sku_child = get_sku(child.sku.id_, db, product_version_onclause, sku_version_onclause)
tax_rate_child = get_tax(sku_child.product.versions[0].sale_category.tax.rate, voucher_type)
bundle = db.execute(
select(BundleItem).where(
BundleItem.bundle_id == data_inv.sku.id_,
BundleItem.item_id == child.sku.id_,
_bundle_active(product_date),
)
).scalar_one_or_none()
if not bundle:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Bundle item {child.sku.name} is not part of bundle {data_inv.sku.name}",
)
inv_child = Inventory(
kot=kot,
sku_id=sku_child.id,
quantity=round(data_inv.quantity * bundle.quantity, 2),
price=bundle.sale_price, # TODO: Fix
discount=round(
min(data_inv.discount, sku_child.product.versions[0].sale_category.discount_limit), 5
),
is_hh=data_inv.is_happy_hour,
tax_id=sku_child.product.versions[0].sale_category.tax_id,
tax_rate=tax_rate_child,
sort_order=index,
type_=InventoryType.bundle_item,
parent=inv,
)
inv.children.append(inv_child)
db.add(inv_child)
for m in child.modifiers:
mod = InventoryModifier(None, m.id_, Decimal(0))
inv_child.modifiers.append(mod)
db.add(mod)
db.flush()
do_update_settlements(item, [], db)
do_update_bill_numbers(item, db)
do_update_settlements(voucher, [], db)
do_update_bill_numbers(voucher, db)
if (
sum(len(k.inventories) for k in item.kots) == 0
sum(len(k.inventories) for k in voucher.kots) == 0
and guest_book is None
and data.pax == 0
and data.customer is None
@@ -199,4 +228,4 @@ def do_save(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Please add some products",
)
return item
return voucher
+99 -179
View File
@@ -4,7 +4,7 @@ import uuid
from datetime import timedelta
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import Date, and_, func, or_, select
from sqlalchemy import Date, func, select
from sqlalchemy.orm import Session, contains_eager
from ...core.config import settings
@@ -14,6 +14,7 @@ from ...models.bill import Bill
from ...models.food_table import FoodTable
from ...models.guest_book import GuestBook
from ...models.inventory import Inventory
from ...models.inventory_type import InventoryType
from ...models.kot import Kot
from ...models.overview import Overview
from ...models.product import Product
@@ -23,25 +24,27 @@ from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas.customer import CustomerLink
from ...schemas.modifier import ModifierLink
from ...schemas.table import TableLink
from ...schemas.tax import TaxLink
from ...schemas.user import UserLink
from ...schemas.user_token import UserToken
from ...schemas.voucher_out import (
CustomerLink,
InventoryProduct,
ModifierLink,
ProductMenuCategory,
ProductSaleCategory,
TableLink,
TaxLink,
UserLink,
VoucherBlank,
VoucherOut,
)
from ...schemas.voucher_out import (
Inventory as InventoryOut,
)
from ...schemas.voucher_out import (
InventoryProduct,
ProductMenuCategory,
ProductSaleCategory,
VoucherBlank,
VoucherOut,
)
from ...schemas.voucher_out import (
Kot as KotOut,
)
from .. import _pv_onclause, _sv_onclause
from . import get_voucher
router = APIRouter()
@@ -49,59 +52,16 @@ router = APIRouter()
@router.get("/from-id/{id_}")
def from_id(
id_: str,
id_: uuid.UUID,
user: UserToken = Security(get_user),
) -> VoucherOut:
with SessionFuture() as db:
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 = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= day,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= day,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= day), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= day), # noqa: E711
)
item: Voucher = (
db.execute(
select(Voucher)
.join(Voucher.food_table)
.join(Voucher.customer, isouter=True)
.join(Voucher.kots)
.join(Kot.inventories)
.join(Inventory.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.where(Voucher.id == id_)
.order_by(Kot.code)
.options(
contains_eager(Voucher.food_table),
contains_eager(Voucher.customer),
contains_eager(Voucher.kots)
.contains_eager(Kot.inventories)
.contains_eager(Inventory.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions),
contains_eager(Voucher.kots)
.contains_eager(Kot.inventories)
.contains_eager(Inventory.sku)
.contains_eager(StockKeepingUnit.versions),
)
)
.unique()
.scalar_one()
)
product_version_onclause = _pv_onclause(day)
sku_version_onclause = _sv_onclause(day)
item = get_voucher(id_, db, product_version_onclause, sku_version_onclause)
return voucher_info(item, db)
@@ -112,24 +72,10 @@ def from_bill(
) -> VoucherOut:
with SessionFuture() as db:
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 = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= day,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= day,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= day), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= day), # noqa: E711
)
product_version_onclause = _pv_onclause(day)
sku_version_onclause = _sv_onclause(day)
match = re.compile(r"^(\w+)-(\d+)$").match(id_)
if not match or len(match.groups()) != 2:
raise HTTPException(
@@ -185,24 +131,10 @@ def from_table(
) -> VoucherOut | VoucherBlank:
with SessionFuture() as db:
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 = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= day,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= day,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= day), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= day), # noqa: E711
)
product_version_onclause = _pv_onclause(day)
sku_version_onclause = _sv_onclause(day)
guest = None if g is None else db.execute(select(GuestBook).where(GuestBook.id == g)).scalar_one()
if v is not None:
overview = (
@@ -221,47 +153,22 @@ def from_table(
detail="Voucher not found",
)
else:
item = (
db.execute(
select(Voucher)
.join(Voucher.food_table)
.join(Voucher.customer, isouter=True)
.join(Voucher.kots)
.join(Kot.inventories)
.join(Inventory.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.where(Voucher.id == v)
.order_by(Kot.code)
.options(
contains_eager(Voucher.food_table),
contains_eager(Voucher.customer),
contains_eager(Voucher.kots)
.contains_eager(Kot.inventories)
.contains_eager(Inventory.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions),
contains_eager(Voucher.kots)
.contains_eager(Kot.inventories)
.contains_eager(Inventory.sku)
.contains_eager(StockKeepingUnit.versions),
)
)
.unique()
.scalar_one_or_none()
)
item = get_voucher(v, db, product_version_onclause, sku_version_onclause)
if item is None:
item = db.execute(
select(Voucher)
.join(Voucher.food_table)
.join(Voucher.customer, isouter=True)
.where(Voucher.id == v)
.options(
contains_eager(Voucher.food_table),
contains_eager(Voucher.customer),
item = (
db.execute(
select(Voucher)
.join(Voucher.food_table)
.join(Voucher.customer, isouter=True)
.where(Voucher.id == v)
.options(
contains_eager(Voucher.food_table),
contains_eager(Voucher.customer),
)
)
).scalar_one()
.unique()
.scalar_one()
)
if guest is not None:
item.customer = guest.customer
@@ -271,7 +178,7 @@ def from_table(
def voucher_info(item: Voucher, db: Session) -> VoucherOut:
return VoucherOut(
voucher_out = VoucherOut(
id_=item.id,
date_=item.date,
date_tip=item.date,
@@ -288,51 +195,64 @@ def voucher_info(item: Voucher, db: Session) -> VoucherOut:
narration=item.narration,
reason=item.reason,
voucher_type=item.voucher_type,
kots=[
KotOut(
id_=k.id,
code=k.code,
date_=k.date,
user=UserLink(id_=k.user_id, name=k.user.name),
inventories=[
InventoryOut(
id_=i.id,
sort_order=i.sort_order,
product=InventoryProduct(
id_=i.sku_id,
name=("H H " if i.is_happy_hour else "")
+ f"{i.sku.product.versions[0].name} ({i.sku.versions[0].units})",
menu_category=ProductMenuCategory(
id_=i.sku.versions[0].menu_category_id,
name=i.sku.versions[0].menu_category.name,
),
sale_category=ProductSaleCategory(
id_=i.sku.product.versions[0].sale_category_id,
name=i.sku.product.versions[0].sale_category.name,
discount_limit=i.sku.product.versions[0].sale_category.discount_limit,
),
),
quantity=i.quantity,
price=i.price,
is_happy_hour=i.is_happy_hour,
tax_rate=i.tax_rate,
tax=TaxLink(id_=i.tax_id, name=i.tax.name),
discount=i.discount,
modifiers=[
ModifierLink(
id_=m.modifier.id,
name=m.modifier.name,
price=m.price,
)
for m in i.modifiers
],
)
for i in k.inventories
],
)
for k in item.kots
],
kots=[],
)
for k in item.kots:
kot = KotOut(
id_=k.id,
code=k.code,
date_=k.date,
user=UserLink(id_=k.user_id, name=k.user.name),
inventories=[],
)
voucher_out.kots.append(kot)
for i in k.inventories:
inventory = InventoryOut(
id_=i.id,
sort_order=i.sort_order,
sku=InventoryProduct(
id_=i.sku_id,
name=("H H " if i.is_happy_hour else "")
+ f"{i.sku.product.versions[0].name} ({i.sku.versions[0].units})",
menu_category=ProductMenuCategory(
id_=i.sku.versions[0].menu_category_id,
name=i.sku.versions[0].menu_category.name,
),
sale_category=ProductSaleCategory(
id_=i.sku.product.versions[0].sale_category_id,
name=i.sku.product.versions[0].sale_category.name,
discount_limit=i.sku.product.versions[0].sale_category.discount_limit,
),
is_bundle=i.sku.is_bundle,
),
quantity=i.quantity,
price=i.price,
is_happy_hour=i.is_happy_hour,
tax_rate=i.tax_rate,
tax=TaxLink(id_=i.tax_id, name=i.tax.name),
discount=i.discount,
type_=i.type_,
modifiers=[
ModifierLink(
id_=m.modifier.id,
name=m.modifier.name,
price=m.price,
)
for m in i.modifiers
],
children=[],
)
if i.parent_id and inventory.type_ != InventoryType.bundle_item:
raise Exception("Inventory type and parent ID mismatch")
if i.parent_id is not None and inventory.type_ == InventoryType.bundle_item:
parent = next((inv for inv in kot.inventories if inv.id_ == i.parent_id), None)
if parent is not None:
parent.children.append(inventory)
else:
raise Exception("Parent inventory not found")
else:
kot.inventories.append(inventory)
return voucher_out
def voucher_blank(table: FoodTable, guest: GuestBook | None) -> VoucherBlank:
+34 -6
View File
@@ -1,28 +1,32 @@
import uuid
from collections import defaultdict
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import delete, func, select
from sqlalchemy import Date, delete, func, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ...core.config import settings
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.inventory import Inventory
from ...models.inventory_modifier import InventoryModifier
from ...models.inventory_type import InventoryType
from ...models.kot import Kot
from ...models.overview import Overview
from ...models.settle_option import SettleOption
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...routers import _pv_onclause, _sv_onclause
from ...routers.voucher import (
check_permissions,
do_update_bill_numbers,
do_update_settlements,
do_update_table,
get_voucher,
)
from ...schemas import split as schemas
from ...schemas.receive_payment import ReceivePaymentItem as SettleSchema
@@ -43,7 +47,12 @@ def split(
with SessionFuture() as db:
now = datetime.now(UTC).replace(tzinfo=None)
update_table = u
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
day = func.cast(
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)
item: Voucher = get_voucher(id_, db, product_version_onclause, sku_version_onclause)
original_voucher_type = item.voucher_type
item.voucher_type = VoucherType.VOID
item.reason = "Bill Split"
@@ -53,8 +62,18 @@ def split(
db.execute(delete(Overview).where(Overview.voucher_id == item.id))
check_permissions(None, item.voucher_type, user.permissions)
one_inventories = [i for k in item.kots for i in k.inventories if i.id in data.inventories]
two_inventories = [i for k in item.kots for i in k.inventories if i.id not in data.inventories]
one_inventories = [
i
for k in item.kots
for i in k.inventories
if i.id in data.inventories or i.parent_id in data.inventories
]
two_inventories = [
i
for k in item.kots
for i in k.inventories
if i.id not in data.inventories and i.parent_id not in data.inventories
]
one = save(
one_inventories,
@@ -128,7 +147,9 @@ def save(
item.kots.append(kot)
db.add(kot)
db.flush()
parent_map: dict[uuid.UUID, Inventory] = {}
for old_inventory in split_inventories:
parent = parent_map.get(old_inventory.parent_id) if old_inventory.parent_id else None
inv = Inventory(
kot_id=kot.id,
sku_id=old_inventory.sku_id,
@@ -139,13 +160,20 @@ def save(
tax_id=old_inventory.tax_id,
tax_rate=old_inventory.tax_rate,
sort_order=old_inventory.sort_order,
type_=old_inventory.type_,
parent=parent,
)
kot.inventories.append(inv)
if not parent:
kot.inventories.append(inv)
else:
parent.children.append(inv)
db.add(inv)
for m in old_inventory.modifiers:
mod = InventoryModifier(None, m.modifier_id, m.price)
inv.modifiers.append(mod)
db.add(mod)
if old_inventory.type_ == InventoryType.bundle:
parent_map[old_inventory.id] = inv
db.flush()
do_update_bill_numbers(item, db)
do_update_settlements(item, [], db)
+132 -68
View File
@@ -4,31 +4,34 @@ from datetime import UTC, datetime, timedelta
from decimal import Decimal
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import and_, func, or_, select
from sqlalchemy import ColumnElement, Date, func, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import contains_eager
from sqlalchemy.orm import Session, contains_eager
from ...core.config import settings
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.bundle_item import BundleItem
from ...models.inventory import Inventory
from ...models.inventory_modifier import InventoryModifier
from ...models.inventory_type import InventoryType
from ...models.kot import Kot
from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...printing.bill import print_bill
from ...printing.kot import print_kot
from ...routers import _bundle_active, _pv_onclause, _sv_onclause
from ...routers.voucher import (
check_permissions,
do_update_bill_numbers,
do_update_settlements,
do_update_table,
get_guest_book,
get_sku,
get_tax,
get_voucher,
happy_hour_has_discount,
happy_hour_items_balanced,
happy_hour_items_more_than_regular,
@@ -40,6 +43,26 @@ from ...schemas.user_token import UserToken
router = APIRouter()
def discount_limit(sku_id: uuid.UUID, db: Session, product_version_onclause: ColumnElement[bool]) -> Decimal:
sku: StockKeepingUnit = (
db.execute(
select(StockKeepingUnit)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.sale_category)
.where(StockKeepingUnit.id == sku_id)
.options(
contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.sale_category),
)
)
.unique()
.scalar_one()
)
return sku.product.versions[0].sale_category.discount_limit
@router.put("/update/{id_}")
def update_route(
id_: uuid.UUID,
@@ -52,68 +75,32 @@ def update_route(
try:
with SessionFuture() as db:
now = datetime.now(UTC).replace(tzinfo=None)
day = func.cast(
Kot.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
product_date = (
now + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES)
).date()
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= product_date,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= product_date,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= product_date), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= product_date), # noqa: E711
)
product_version_onclause = _pv_onclause(product_date)
sku_version_onclause = _sv_onclause(product_date)
update_table = u
voucher_type = VoucherType(p)
guest_book = get_guest_book(g, db)
need_to_print_kot = False
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
item = get_voucher(id_, db, _pv_onclause(day), _sv_onclause(day))
check_permissions(item, voucher_type, user.permissions)
item.pax = data.pax
if data.customer is not None:
item.customer_id = data.customer.id_
else:
item.customer_id = None
if guest_book is not None:
item.pax = guest_book.pax
item.customer_id = guest_book.customer_id
item.food_table_id = data.table.id_
if item.voucher_type != VoucherType.KOT:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal error, reprints should not reach here",
)
if item.voucher_type == VoucherType.KOT and voucher_type != VoucherType.KOT:
item.date = now
item.voucher_type = voucher_type
item.user_id = user.id_
item.last_edit_date = now
if happy_hour_items_more_than_regular(data.kots):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="When product has happy hours\n"
"Minimum same number of regular items also needed in the whole bill.",
)
for ik in item.kots:
for iki in ik.inventories:
iki.tax_rate = get_tax(iki.tax_rate, voucher_type)
# TODO: Need to check from the database product for the max discount
# However simple relationship does not work as we need the product validity as well
# Still we should not fret too much as we are checking this in the frontend.
iki.discount = next(
round(inv.discount, 5) for ko in data.kots for inv in ko.inventories if inv.id_ == iki.id
)
for dk in data.kots:
# Filter out nil inventories
dk.inventories = [dki for dki in dk.inventories if round(dki.quantity, 2) != 0]
@@ -130,27 +117,48 @@ def update_route(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Happy hour products are not balanced.",
)
for nk in data.kots:
if nk.id_ is not None:
item.pax = data.pax
if data.customer is not None:
item.customer_id = data.customer.id_
else:
item.customer_id = None
if guest_book is not None:
item.pax = guest_book.pax
item.customer_id = guest_book.customer_id
item.food_table_id = data.table.id_
if item.voucher_type == VoucherType.KOT and voucher_type != VoucherType.KOT:
item.date = now
item.voucher_type = voucher_type
item.user_id = user.id_
item.last_edit_date = now
flat_inventories: list[schemas.Inventory] = []
for d_kot in data.kots:
for d_inv in d_kot.inventories:
if d_inv.type_ != InventoryType.bundle_item:
flat_inventories.append(d_inv)
if len(d_inv.children) > 0:
for d_child in d_inv.children:
flat_inventories.append(d_child)
for ik in item.kots:
for iki in ik.inventories:
iki.tax_rate = get_tax(iki.tax_rate, voucher_type)
new_discount = next(round(inv.discount, 5) for inv in flat_inventories if inv.id_ == iki.id)
iki.discount = round(min(new_discount, discount_limit(iki.sku_id, db, product_version_onclause)), 5)
for data_kot in data.kots:
if data_kot.id_ is not None:
continue
need_to_print_kot = True
code = db.execute(select(func.coalesce(func.max(Kot.code), 0) + 1)).scalar_one()
kot = Kot(item.id, code, item.food_table_id, now, item.user_id)
item.kots.append(kot)
db.add(kot)
for index, nki in enumerate(nk.inventories):
sku: StockKeepingUnit = db.execute(
select(StockKeepingUnit)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.where(StockKeepingUnit.id == nki.sku.id_)
.options(
contains_eager(StockKeepingUnit.versions),
contains_eager(StockKeepingUnit.product).contains_eager(Product.versions),
)
).scalar_one()
if round(nki.quantity, 2) < 0:
index = 0
for data_inv in data_kot.inventories:
index += 1
sku = get_sku(data_inv.sku.id_, db, product_version_onclause, sku_version_onclause)
if round(data_inv.quantity, 2) < 0:
if "edit-printed-product" not in user.permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -169,30 +177,86 @@ def update_route(
* -1,
2,
)
if round(nki.quantity, 2) < minimum:
if round(data_inv.quantity, 2) < minimum:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Quantity of {sku.product.versions[0].name} ({sku.versions[0].units}) cannot be less than {minimum}",
)
if data_inv.type_ == InventoryType.bundle_item:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Bundle items cannot be added directly. Please add the bundle parent item.",
)
if data_inv.type_ == InventoryType.bundle and len(data_inv.children) == 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Bundle items must have children items.",
)
tax_rate = get_tax(sku.product.versions[0].sale_category.tax.rate, voucher_type)
inv = Inventory(
kot_id=kot.id,
kot=kot,
sku_id=sku.id,
quantity=round(nki.quantity, 2),
quantity=round(data_inv.quantity, 2),
price=sku.versions[0].sale_price,
discount=round(min(nki.discount, sku.product.versions[0].sale_category.discount_limit), 5),
is_hh=nki.is_happy_hour,
discount=round(min(data_inv.discount, sku.product.versions[0].sale_category.discount_limit), 5),
is_hh=data_inv.is_happy_hour,
tax_rate=tax_rate,
sort_order=index,
tax_id=sku.product.versions[0].sale_category.tax_id,
tax=sku.product.versions[0].sale_category.tax,
type_=data_inv.type_,
)
kot.inventories.append(inv)
db.add(inv)
for m in nki.modifiers:
for m in data_inv.modifiers:
mod = InventoryModifier(None, m.id_, Decimal(0))
inv.modifiers.append(mod)
db.add(mod)
if data_inv.type_ == InventoryType.bundle:
if (data_inv.price * data_inv.quantity) != sum(
child.price * child.quantity for child in data_inv.children
):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Bundle price for {data_inv.sku.name} must be equal to sum of child item prices.",
)
for child in data_inv.children:
index += 1
sku_child = get_sku(child.sku.id_, db, product_version_onclause, sku_version_onclause)
tax_rate_child = get_tax(sku_child.product.versions[0].sale_category.tax.rate, voucher_type)
bundle = db.execute(
select(BundleItem).where(
BundleItem.bundle_id == data_inv.sku.id_,
BundleItem.item_id == child.sku.id_,
_bundle_active(product_date),
)
).scalar_one_or_none()
if not bundle:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Bundle item {child.sku.name} is not part of bundle {data_inv.sku.name}",
)
inv_child = Inventory(
kot=kot,
sku_id=sku_child.id,
quantity=round(data_inv.quantity * bundle.quantity, 2),
price=bundle.sale_price, # TODO: Fix
discount=round(
min(data_inv.discount, sku_child.product.versions[0].sale_category.discount_limit),
5,
),
is_hh=data_inv.is_happy_hour,
tax_id=sku_child.product.versions[0].sale_category.tax_id,
tax_rate=tax_rate_child,
sort_order=index,
type_=InventoryType.bundle_item,
parent=inv,
)
inv.children.append(inv_child)
db.add(inv_child)
for m in child.modifiers:
mod = InventoryModifier(None, m.id_, Decimal(0))
inv_child.modifiers.append(mod)
db.add(mod)
do_update_bill_numbers(item, db)
do_update_settlements(item, [], db)
if update_table:
@@ -1,8 +1,16 @@
from datetime import date, datetime
from decimal import Decimal
from typing import Any
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_serializer
from pydantic import (
BaseModel,
ConfigDict,
Field,
SerializationInfo,
field_serializer,
field_validator,
model_serializer,
)
from . import Daf, to_camel
@@ -18,11 +26,11 @@ class BeerConsumptionReportItem(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, _info: Any) -> str:
def serialize_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
# dynamic fields (e.g., regular, nc, kot)
dynamic_amounts: dict[str, Daf] = Field(default_factory=dict)
dynamic_amounts: Annotated[dict[str, Daf], Field(default_factory=dict)]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -64,7 +72,7 @@ class BeerConsumptionReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, _info: Any) -> str:
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@@ -75,5 +83,5 @@ class BeerConsumptionReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, _info: Any) -> str:
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@@ -1,7 +1,6 @@
from datetime import date, datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
from . import Daf, to_camel
@@ -21,7 +20,7 @@ class BillSettlementItem(BaseModel):
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("date_")
def serialize_date(self, value: datetime, _info: Any) -> str:
def serialize_date(self, value: datetime, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
@@ -39,7 +38,7 @@ class BillSettlement(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, _info: Any) -> str:
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@@ -50,5 +49,5 @@ class BillSettlement(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, _info: Any) -> str:
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+60
View File
@@ -0,0 +1,60 @@
import uuid
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import Daf, to_camel
from .menu_category import MenuCategoryLink
from .sale_category import SaleCategoryLink
class BundleItem(BaseModel):
id_: uuid.UUID | None = None
name: Annotated[str, Field(min_length=1)]
item_id: Annotated[uuid.UUID, Field(description="StockKeepingUnit ID of the item")]
sale_price: Annotated[Daf, Field(ge=Decimal(0))]
quantity: Annotated[Daf, Field(gt=Decimal(0))]
print_in_bill: bool
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
class BundleIn(BaseModel):
name: Annotated[str, Field(min_length=1)]
units: Annotated[str, Field(min_length=1)]
sale_price: Annotated[Daf, Field(ge=Decimal(0))]
sale_category: SaleCategoryLink
has_happy_hour: bool
is_not_available: bool
sort_order: int
menu_category: MenuCategoryLink
items: list[BundleItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
class Bundle(BundleIn):
id_: uuid.UUID # Sku ID
version_id: uuid.UUID # SkuVersion ID
items: list[BundleItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
class BundleBlank(BaseModel):
name: str
units: str
sale_price: Daf
sale_category: SaleCategoryLink | None
has_happy_hour: bool
is_not_available: bool
sort_order: int
menu_category: MenuCategoryLink | None = None
items: list[BundleItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
+3 -4
View File
@@ -1,7 +1,6 @@
from datetime import date, datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
from . import Daf, to_camel
from .user import UserLink
@@ -60,7 +59,7 @@ class CashierReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, _info: Any) -> str:
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@@ -71,5 +70,5 @@ class CashierReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, _info: Any) -> str:
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+6 -5
View File
@@ -1,6 +1,7 @@
import uuid
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
@@ -10,14 +11,14 @@ from . import Daf, to_camel
class CustomerDiscount(BaseModel):
id_: uuid.UUID
name: str
discount: Daf = Field(ge=Decimal(0), default=Decimal(0), le=Decimal(1))
limit: Daf = Field(ge=Decimal(0), default=Decimal(0), le=Decimal(1))
discount: Annotated[Daf, Field(ge=Decimal(0), default=Decimal(0), le=Decimal(1))]
limit: Annotated[Daf, Field(ge=Decimal(0), default=Decimal(0), le=Decimal(1))]
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
class CustomerIn(BaseModel):
name: str = Field(..., min_length=1)
# phone: str = Field(..., min_length=1)
name: Annotated[str, Field(min_length=1)]
# phone: Annotated[str, Field(min_length=1)
phone: str
address: str | None = None
print_in_bill: bool
@@ -32,7 +33,7 @@ class Customer(CustomerIn):
class CustomerLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+19
View File
@@ -0,0 +1,19 @@
import uuid
from typing import Any
from pydantic import BaseModel, ConfigDict
from . import to_camel
class DbSetting(BaseModel):
id_: uuid.UUID
name: str
details: dict[str, Any]
model_config = ConfigDict(
str_strip_whitespace=True,
alias_generator=to_camel,
populate_by_name=True,
)
+6 -6
View File
@@ -1,16 +1,16 @@
import uuid
from datetime import datetime
from typing import Any
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, field_serializer
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer
from . import to_camel
from .section import SectionLink
class DeviceIn(BaseModel):
name: str = Field(..., min_length=1)
name: Annotated[str, Field(min_length=1)]
enabled: bool
section: SectionLink
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -24,15 +24,15 @@ class Device(DeviceIn):
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_serializer("creation_date")
def serialize_creation_date(self, value: datetime, _info: Any) -> str:
def serialize_creation_date(self, value: datetime, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
@field_serializer("last_date")
def serialize_last_date(self, value: datetime | None, _info: Any) -> str | None:
def serialize_last_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
class DeviceLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+3 -4
View File
@@ -1,7 +1,6 @@
from datetime import date, datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
from . import Daf, to_camel
@@ -26,7 +25,7 @@ class DiscountReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, _info: Any) -> str:
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@@ -37,5 +36,5 @@ class DiscountReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, _info: Any) -> str:
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+10 -9
View File
@@ -1,12 +1,13 @@
import uuid
from datetime import date, datetime
from typing import Any
from typing import Annotated
from pydantic import (
BaseModel,
ConfigDict,
Field,
SerializationInfo,
field_serializer,
field_validator,
model_validator,
@@ -20,7 +21,7 @@ class GuestBookIn(BaseModel):
name: str
phone: str
address: str | None = None
pax: int = Field(ge=0)
pax: Annotated[int, Field(ge=0)]
booking_date: datetime | None = None
arrival_date: datetime | None = None
notes: str | None = None
@@ -38,7 +39,7 @@ class GuestBookIn(BaseModel):
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("booking_date")
def serialize_booking_date(self, value: datetime | None, _info: Any) -> str | None:
def serialize_booking_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
@field_validator("arrival_date", mode="before")
@@ -51,7 +52,7 @@ class GuestBookIn(BaseModel):
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("arrival_date")
def serialize_arrival_date(self, value: datetime | None, _info: Any) -> str | None:
def serialize_arrival_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
@model_validator(mode="after")
@@ -75,7 +76,7 @@ class GuestBook(GuestBookIn):
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime, _info: Any) -> str:
def serialize_last_edit_date(self, value: datetime, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
@@ -107,7 +108,7 @@ class GuestBookListItem(BaseModel):
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("booking_date")
def serialize_booking_date(self, value: datetime | None, _info: Any) -> str | None:
def serialize_booking_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
@field_validator("arrival_date", mode="before")
@@ -120,7 +121,7 @@ class GuestBookListItem(BaseModel):
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("arrival_date")
def serialize_arrival_date(self, value: datetime | None, _info: Any) -> str | None:
def serialize_arrival_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
@field_validator("last_edit_date", mode="before")
@@ -131,7 +132,7 @@ class GuestBookListItem(BaseModel):
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime, _info: Any) -> str:
def serialize_last_edit_date(self, value: datetime, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
@@ -148,5 +149,5 @@ class GuestBookList(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, _info: Any) -> str:
def serialize_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+6
View File
@@ -0,0 +1,6 @@
from pydantic import BaseModel
class HealthResponse(BaseModel):
status: str
database: str
+3 -1
View File
@@ -1,12 +1,14 @@
import uuid
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
class AccountBase(BaseModel):
name: str = Field(..., min_length=1)
name: Annotated[str, Field(min_length=1)]
is_starred: bool
is_active: bool
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
+4 -2
View File
@@ -1,5 +1,7 @@
import uuid
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
@@ -7,7 +9,7 @@ from .product_link import ProductLink
class MenuCategoryIn(BaseModel):
name: str = Field(..., min_length=1)
name: Annotated[str, Field(min_length=1)]
is_active: bool
is_fixture: bool
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -25,7 +27,7 @@ class MenuCategoryBlank(MenuCategoryIn):
class MenuCategoryLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
enabled: bool | None = None
skus: list[ProductLink]
@@ -1,9 +1,8 @@
import uuid
from datetime import date, datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
from . import Daf, to_camel
@@ -37,7 +36,7 @@ class MeReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, _info: Any) -> str:
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@@ -48,5 +47,5 @@ class MeReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, _info: Any) -> str:
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+4 -3
View File
@@ -1,6 +1,7 @@
import uuid
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
@@ -9,9 +10,9 @@ from .modifier_category import ModifierCategoryLink
class ModifierIn(BaseModel):
name: str = Field(..., min_length=1)
name: Annotated[str, Field(min_length=1)]
show_in_bill: bool
price: Daf = Field(ge=Decimal(0), default=Decimal(0))
price: Annotated[Daf, Field(ge=Decimal(0), default=Decimal(0))]
is_active: bool
modifier_category: ModifierCategoryLink
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -22,7 +23,7 @@ class Modifier(ModifierIn):
class ModifierLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
price: Daf | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+6 -4
View File
@@ -1,5 +1,7 @@
import uuid
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
@@ -7,9 +9,9 @@ from .menu_category import MenuCategoryLink
class ModifierCategoryIn(BaseModel):
name: str = Field(..., min_length=1)
minimum: int = Field(ge=0)
maximum: int | None = Field(ge=0)
name: Annotated[str, Field(min_length=1)]
minimum: Annotated[int, Field(ge=0)]
maximum: Annotated[int | None, Field(ge=0)]
is_active: bool
menu_categories: list[MenuCategoryLink]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -27,6 +29,6 @@ class ModifierCategoryBlank(ModifierCategoryIn):
class ModifierCategoryLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@@ -1,5 +1,7 @@
import uuid
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
@@ -8,9 +10,9 @@ from .modifier import ModifierLink
class ModifierCategoryForProduct(BaseModel):
id_: uuid.UUID
name: str = Field(..., min_length=1)
minimum: int = Field(ge=0)
maximum: int | None = Field(ge=0)
name: Annotated[str, Field(min_length=1)]
minimum: Annotated[int, Field(ge=0)]
maximum: Annotated[int | None, Field(ge=0)]
is_active: bool
modifiers: list[ModifierLink]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
+5 -3
View File
@@ -1,13 +1,15 @@
import uuid
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
class PrinterIn(BaseModel):
name: str = Field(..., min_length=1)
address: str = Field(..., min_length=1)
name: Annotated[str, Field(min_length=1)]
address: Annotated[str, Field(min_length=1)]
cut_code: str
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -24,5 +26,5 @@ class PrinterBlank(PrinterIn):
class PrinterLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+7 -7
View File
@@ -1,9 +1,9 @@
import uuid
from datetime import date, datetime
from typing import Any
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer, field_validator
from . import to_camel
from .sale_category import SaleCategoryLink
@@ -11,9 +11,9 @@ from .stock_keeping_unit import StockKeepingUnit
class ProductIn(BaseModel):
name: str = Field(..., min_length=1)
fraction_units: str = Field(..., min_length=1)
sale_category: SaleCategoryLink = Field(...)
name: Annotated[str, Field(min_length=1)]
fraction_units: Annotated[str, Field(min_length=1)]
sale_category: SaleCategoryLink
sort_order: int
skus: list[StockKeepingUnit]
@@ -37,7 +37,7 @@ class Product(ProductIn):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_from")
def serialize_valid_from(self, value: date | None, _info: Any) -> str | None:
def serialize_valid_from(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("valid_till", mode="before")
@@ -50,7 +50,7 @@ class Product(ProductIn):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_till")
def serialize_valid_till(self, value: date | None, _info: Any) -> str | None:
def serialize_valid_till(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
+2 -2
View File
@@ -1,12 +1,12 @@
import uuid
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict
from . import to_camel
class ProductLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
enabled: bool | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+18 -10
View File
@@ -1,6 +1,7 @@
import uuid
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
@@ -10,23 +11,30 @@ from .sale_category import SaleCategoryLink
from .tax import TaxLink
class BundleItemQuery(BaseModel):
id_: uuid.UUID | None = None
name: Annotated[str, Field()]
price: Annotated[Daf, Field()]
quantity: Annotated[Daf, Field()]
tax: TaxLink | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
class ProductQuery(BaseModel):
# one row per SKU version
id_: uuid.UUID = Field(..., description="SkuVersion ID")
# product_id: uuid.UUID = Field(..., description="Product ID")
name: str = Field(..., min_length=1)
# units: str = Field(..., min_length=1)
price: Daf = Field(ge=Decimal(0))
id_: Annotated[uuid.UUID, Field(description="SkuVersion ID")]
name: Annotated[str, Field(min_length=1)]
price: Annotated[Daf, Field(ge=Decimal(0))]
has_happy_hour: bool
is_not_available: bool
sort_order: int = Field(ge=0, default=0)
is_bundle: bool
sort_order: Annotated[int, Field(ge=0, default=0)]
menu_category: MenuCategoryLink | None = None
sale_category: SaleCategoryLink | None = None
tax: TaxLink | None = None
bundle_items: list[BundleItemQuery] = []
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
+13 -5
View File
@@ -2,9 +2,17 @@ import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import Any
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_serializer
from pydantic import (
BaseModel,
ConfigDict,
Field,
SerializationInfo,
field_serializer,
field_validator,
model_serializer,
)
from . import Daf, to_camel
from .user import UserLink
@@ -16,7 +24,7 @@ class ProductSaleReportItem(BaseModel):
is_happy_hour: bool
# dynamic fields (e.g., regular, nc, kot)
dynamic_amounts: dict[str, Daf] = Field(default_factory=dict)
dynamic_amounts: Annotated[dict[str, Daf], Field(default_factory=dict)]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -57,7 +65,7 @@ class ProductSaleReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, _info: Any) -> str:
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@@ -68,5 +76,5 @@ class ProductSaleReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, _info: Any) -> str:
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@@ -0,0 +1,35 @@
from datetime import date, datetime
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
from . import to_camel
class ProductUpdatesReport(BaseModel):
start_date: date
finish_date: date
report: list[str]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: str | date) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: str | date) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+5 -3
View File
@@ -1,12 +1,14 @@
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
class RegimeIn(BaseModel):
name: str = Field(..., min_length=1)
name: Annotated[str, Field(min_length=1)]
header: str
prefix: str = Field(..., min_length=1)
prefix: Annotated[str, Field(min_length=1)]
is_fixture: bool
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@@ -24,6 +26,6 @@ class RegimeBlank(RegimeIn):
class RegimeLink(BaseModel):
id_: int = Field(...)
id_: int
name: str | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+3 -1
View File
@@ -1,5 +1,7 @@
import uuid
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
@@ -7,7 +9,7 @@ from .permission import PermissionItem
class RoleIn(BaseModel):
name: str = Field(..., min_length=1)
name: Annotated[str, Field(min_length=1)]
permissions: list[PermissionItem]
model_config = ConfigDict(str_strip_whitespace=True)
+43
View File
@@ -0,0 +1,43 @@
import uuid
from datetime import datetime
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
from . import to_camel
from .section import SectionLink
class RunningTable(BaseModel):
id_: uuid.UUID
name: str
seats: int
section: SectionLink
is_active: bool
sort_order: int
# present only when status exists in DB
status: str | None = None
voucher_id: uuid.UUID | None = None
pax: int | None = None
date_: datetime | None = None
amount: float | None = None # you round to 0 decimals; keep Decimal if you prefer
guest: str | None = None
bold: bool | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: str | datetime | None) -> datetime | None:
if value is None:
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("date_")
def serialize_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
if value is None:
return None
return value.strftime("%d-%b-%Y %H:%M")
+5 -4
View File
@@ -1,6 +1,7 @@
import uuid
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
@@ -9,9 +10,9 @@ from .tax import TaxLink
class SaleCategoryIn(BaseModel):
name: str = Field(..., min_length=1)
discount_limit: Daf = Field(ge=Decimal(0), default=Decimal(0), le=Decimal(1))
tax: TaxLink = Field(...)
name: Annotated[str, Field(min_length=1)]
discount_limit: Annotated[Daf, Field(ge=Decimal(0), default=Decimal(0), le=Decimal(1))]
tax: TaxLink
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -35,6 +36,6 @@ class SaleCategoryForDiscount(BaseModel):
class SaleCategoryLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+3 -4
View File
@@ -1,9 +1,8 @@
import uuid
from datetime import date, datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
from . import Daf, to_camel
from .user import UserLink
@@ -31,7 +30,7 @@ class SaleReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, _info: Any) -> str:
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@@ -42,5 +41,5 @@ class SaleReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, _info: Any) -> str:
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+4 -2
View File
@@ -1,12 +1,14 @@
import uuid
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
class SectionIn(BaseModel):
name: str = Field(..., min_length=1)
name: Annotated[str, Field(min_length=1)]
model_config = ConfigDict(str_strip_whitespace=True)
@@ -21,6 +23,6 @@ class SectionBlank(BaseModel):
class SectionLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+3 -1
View File
@@ -1,3 +1,5 @@
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
@@ -8,5 +10,5 @@ from .sale_category import SaleCategoryLink
class SectionPrinter(BaseModel):
sale_category: SaleCategoryLink | None = None
printer: PrinterLink | None = None
copies: int = Field(ge=0)
copies: Annotated[int, Field(ge=0)]
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+11 -11
View File
@@ -2,9 +2,9 @@ import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import Any
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer, field_validator
from . import Daf, to_camel
from .menu_category import MenuCategoryLink
@@ -13,13 +13,13 @@ from .menu_category import MenuCategoryLink
class StockKeepingUnit(BaseModel):
id_: uuid.UUID | None = None
version_id: uuid.UUID | None = None
units: str = Field(..., min_length=1)
fraction: Daf = Field(ge=Decimal(1), default=Decimal(1))
product_yield: Daf = Field(gt=Decimal(0), le=Decimal(1), default=Decimal(1))
cost_price: Daf = Field(ge=Decimal(0), default=Decimal(0))
sale_price: Daf = Field(ge=Decimal(0), default=Decimal(0))
menu_category: MenuCategoryLink = Field(...)
sort_order: int = Field(ge=0, default=0)
units: Annotated[str, Field(min_length=1)]
fraction: Annotated[Daf, Field(ge=Decimal(1), default=Decimal(1))]
product_yield: Annotated[Daf, Field(gt=Decimal(0), le=Decimal(1), default=Decimal(1))]
cost_price: Annotated[Daf, Field(ge=Decimal(0), default=Decimal(0))]
sale_price: Annotated[Daf, Field(ge=Decimal(0), default=Decimal(0))]
menu_category: MenuCategoryLink
sort_order: Annotated[int, Field(ge=0, default=0)]
has_happy_hour: bool
is_not_available: bool
@@ -38,7 +38,7 @@ class StockKeepingUnit(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_from")
def serialize_valid_from(self, value: date | None, _info: Any) -> str | None:
def serialize_valid_from(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("valid_till", mode="before")
@@ -51,5 +51,5 @@ class StockKeepingUnit(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_till")
def serialize_valid_till(self, value: date | None, _info: Any) -> str | None:
def serialize_valid_till(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
+5 -3
View File
@@ -1,5 +1,7 @@
import uuid
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
@@ -7,8 +9,8 @@ from .section import SectionLink
class TableIn(BaseModel):
name: str = Field(..., min_length=1)
seats: int = Field(ge=0)
name: Annotated[str, Field(min_length=1)]
seats: Annotated[int, Field(ge=0)]
section: SectionLink
is_active: bool
sort_order: int
@@ -29,6 +31,6 @@ class TableBlank(BaseModel):
class TableLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+6 -5
View File
@@ -1,6 +1,7 @@
import uuid
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
@@ -9,9 +10,9 @@ from .regime import RegimeLink
class TaxIn(BaseModel):
name: str = Field(..., min_length=1)
rate: Daf = Field(ge=Decimal(0), default=Decimal(0))
regime: RegimeLink = Field(...)
name: Annotated[str, Field(min_length=1)]
rate: Annotated[Daf, Field(ge=Decimal(0), default=Decimal(0))]
regime: RegimeLink
is_fixture: bool
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -23,13 +24,13 @@ class Tax(TaxIn):
class TaxBlank(BaseModel):
name: str
rate: Daf = Field(ge=Decimal(0), default=Decimal(0))
rate: Annotated[Daf, Field(ge=Decimal(0), default=Decimal(0))]
is_fixture: bool
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
class TaxLink(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
rate: Daf | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+3 -4
View File
@@ -1,7 +1,6 @@
from datetime import date, datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
from . import Daf, to_camel
@@ -28,7 +27,7 @@ class TaxReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, _info: Any) -> str:
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@@ -39,5 +38,5 @@ class TaxReport(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, _info: Any) -> str:
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+18 -18
View File
@@ -2,9 +2,9 @@ import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import Any
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer, field_validator
from . import Daf, to_camel
from .menu_category import MenuCategoryLink
@@ -14,9 +14,9 @@ from .sale_category import SaleCategoryLink
class Product(BaseModel):
id_: uuid.UUID
version_id: uuid.UUID
name: str = Field(..., min_length=1)
fraction_units: str = Field(..., min_length=1)
sale_category: SaleCategoryLink = Field(...)
name: Annotated[str, Field(min_length=1)]
fraction_units: Annotated[str, Field(min_length=1)]
sale_category: SaleCategoryLink
sort_order: int
valid_from: date | None = None
@@ -33,7 +33,7 @@ class Product(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_from")
def serialize_valid_from(self, value: date | None, _info: Any) -> str | None:
def serialize_valid_from(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("valid_till", mode="before")
@@ -46,20 +46,20 @@ class Product(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_till")
def serialize_valid_till(self, value: date | None, _info: Any) -> str | None:
def serialize_valid_till(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
class StockKeepingUnit(BaseModel):
id_: uuid.UUID
version_id: uuid.UUID
units: str = Field(..., min_length=1)
fraction: Daf = Field(ge=Decimal(1), default=Decimal(1))
product_yield: Daf = Field(gt=Decimal(0), le=Decimal(1), default=Decimal(1))
cost_price: Daf = Field(ge=Decimal(0), default=Decimal(0))
sale_price: Daf = Field(ge=Decimal(0), default=Decimal(0))
menu_category: MenuCategoryLink = Field(...)
sort_order: int = Field(ge=0, default=0)
units: Annotated[str, Field(min_length=1)]
fraction: Annotated[Daf, Field(ge=Decimal(1), default=Decimal(1))]
product_yield: Annotated[Daf, Field(gt=Decimal(0), le=Decimal(1), default=Decimal(1))]
cost_price: Annotated[Daf, Field(ge=Decimal(0), default=Decimal(0))]
sale_price: Annotated[Daf, Field(ge=Decimal(0), default=Decimal(0))]
menu_category: MenuCategoryLink
sort_order: Annotated[int, Field(ge=0, default=0)]
has_happy_hour: bool
is_not_available: bool
@@ -78,7 +78,7 @@ class StockKeepingUnit(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_from")
def serialize_valid_from(self, value: date | None, _info: Any) -> str | None:
def serialize_valid_from(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("valid_till", mode="before")
@@ -91,11 +91,11 @@ class StockKeepingUnit(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("valid_till")
def serialize_valid_till(self, value: date | None, _info: Any) -> str | None:
def serialize_valid_till(self, value: date | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
class TemporalProduct(BaseModel):
products: list[Product] = Field(...)
skus: list[StockKeepingUnit] = Field(...)
products: list[Product]
skus: list[StockKeepingUnit]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -1,9 +1,8 @@
import uuid
from datetime import date, datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
from . import Daf, to_camel
@@ -30,5 +29,5 @@ class UpdateProductPrices(BaseModel):
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, _info: Any) -> str:
def serialize_date(self, value: date, _info: SerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
+2 -3
View File
@@ -1,9 +1,8 @@
import uuid
from datetime import datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, field_serializer
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer
from . import to_camel
from .role import RoleItem
@@ -44,5 +43,5 @@ class UserList(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_serializer("last_date")
def serialize_last_date(self, value: datetime | None, _info: Any) -> str | None:
def serialize_last_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
+12 -4
View File
@@ -1,9 +1,11 @@
import uuid
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from ..models.inventory_type import InventoryType
from . import Daf, to_camel
from .customer import CustomerLink
from .modifier import ModifierLink
@@ -16,12 +18,15 @@ class Inventory(BaseModel):
id_: uuid.UUID | None = None
sku: ProductLink
quantity: Daf
price: Daf | None = None
price: Daf
is_happy_hour: bool
type_: InventoryType
parent_id: uuid.UUID | None = None
tax: TaxLink | None = None
tax_rate: Daf | None = None
discount: Daf = Field(ge=0, le=1)
is_happy_hour: bool
discount: Annotated[Daf, Field(ge=0, le=1)]
modifiers: list[ModifierLink]
children: Annotated[list["Inventory"], Field(default_factory=list)]
amount: Daf | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@@ -55,7 +60,7 @@ class Inventory(BaseModel):
@model_validator(mode="after")
def calculate_amount(self) -> "Inventory":
price = 0 if self.is_happy_hour else self.price
price = Decimal(0) if self.is_happy_hour else (self.price or Decimal(0))
self.amount = round(
Decimal(price * self.quantity * (1 - self.discount) * (1 + (self.tax_rate or Decimal(0)))),
2,
@@ -63,6 +68,9 @@ class Inventory(BaseModel):
return self
Inventory.model_rebuild()
class Kot(BaseModel):
id_: uuid.UUID | None = None
inventories: list[Inventory]
+22 -13
View File
@@ -2,10 +2,12 @@ import uuid
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer, field_validator, model_validator
from ..core.config import settings
from ..models.inventory_type import InventoryType
from . import Daf, to_camel
from .customer import CustomerLink
from .modifier import ModifierLink
@@ -15,21 +17,22 @@ from .user import UserLink
class ProductSaleCategory(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
discount_limit: Daf
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
class ProductMenuCategory(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
class InventoryProduct(BaseModel):
id_: uuid.UUID = Field(...)
id_: uuid.UUID
name: str | None = None
is_bundle: bool
menu_category: ProductMenuCategory | None = None
sale_category: ProductSaleCategory | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@@ -38,13 +41,16 @@ class InventoryProduct(BaseModel):
class Inventory(BaseModel):
id_: uuid.UUID | None = None
sort_order: int | None = None
product: InventoryProduct
sku: InventoryProduct
quantity: Daf
price: Daf | None = None
tax: TaxLink | None = None
tax_rate: Daf | None = None
discount: Daf = Field(ge=0, le=1)
discount: Annotated[Daf, Field(ge=0, le=1)]
is_happy_hour: bool
type_: InventoryType
parent_id: uuid.UUID | None = None
children: Annotated[list["Inventory"], Field(default_factory=list)]
modifiers: list[ModifierLink]
amount: Daf | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@@ -92,6 +98,9 @@ class Inventory(BaseModel):
return self
Inventory.model_rebuild()
class Kot(BaseModel):
id_: uuid.UUID | None = None
code: int | None = None
@@ -101,7 +110,7 @@ class Kot(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_serializer("date_")
def serialize_date(self, value: datetime, _info):
def serialize_date(self, value: datetime, _info: SerializationInfo) -> str:
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%d-%b-%Y %H:%M:%S")
@@ -140,25 +149,25 @@ class VoucherOut(BaseModel):
)
@field_serializer("date_")
def serialize_date(self, value: datetime, _info):
def serialize_date(self, value: datetime, _info: SerializationInfo) -> str:
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%H:%M")
@field_serializer("date_tip")
def serialize_date_tip(self, value: datetime, _info):
def serialize_date_tip(self, value: datetime, _info: SerializationInfo) -> str:
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%d-%b-%Y %H:%M:%S")
@field_serializer("creation_date")
def serialize_creation_date(self, value: datetime, _info):
def serialize_creation_date(self, value: datetime, _info: SerializationInfo) -> str:
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%H:%M")
@field_serializer("creation_date_tip")
def serialize_creation_date_tip(self, value: datetime, _info):
def serialize_creation_date_tip(self, value: datetime, _info: SerializationInfo) -> str:
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%d-%b-%Y %H:%M:%S")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime, _info):
def serialize_last_edit_date(self, value: datetime, _info: SerializationInfo) -> str:
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%H:%M")
@field_serializer("last_edit_date_tip")
def serialize_last_edit_date_tip(self, value: datetime, _info):
def serialize_last_edit_date_tip(self, value: datetime, _info: SerializationInfo) -> str:
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%d-%b-%Y %H:%M:%S")
-33
View File
@@ -1,33 +0,0 @@
import re # noqa: F401
import socket # noqa: F401
import sys # noqa: F401
from arq import Retry
async def sent_to_printer(ctx: dict, data: str, address: str, cut_code: str):
print(ctx, data, address, cut_code)
try:
with open(address, "w") as printer:
printer.write(data)
printer.write("\n")
print(cut_code)
printer.write(cut_code)
except LookupError as e:
print("Lookup error:", e)
except: # noqa: E722
print("Unexpected error:", sys.exc_info()[0])
# retry the job with increasing back-off
# delays will be 5s, 10s, 15s, 20s
# after max_tries (default 5) the job will permanently fail
raise Retry(defer=ctx["job_try"] * 5) from None
# GS = "\x1d"
# PAPER_CUT = GS + "V"
# # For the printer ESC/POS Reference
# # https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=87
# # The m codes are in decimal and can be converted using the following table
# # https://www.eso.org/~ndelmott/ascii.html
# # The \x03 in all the following is to feed 3 lines before cut it can be increased or reduced
# FUNCTION_B_FULL_CUT = "\x41\x03"
# FUNCTION_B_PARTIAL_CUT = "\x42\x03"
-35
View File
@@ -1,35 +0,0 @@
import sys
from aiohttp import ClientSession
from barker.core.arq import settings
from barker.core.config import settings as sett
from barker.tasks.printing import sent_to_printer
sys.path.extend(["./"])
async def startup(ctx):
ctx["session"] = ClientSession()
print(f"Worker listening for: {sett.WORKER_QUEUE_NAME}")
# print(f"Worker printing on: {sett.WORKER_PRINTER_ADDRESS}")
async def shutdown(ctx):
await ctx["session"].close()
class WorkerSettings:
"""
Settings for the ARQ worker.
"""
queue_name = f"barker:print:{sett.WORKER_QUEUE_NAME}"
redis_settings = settings
functions: list = [sent_to_printer]
on_startup = startup
on_shutdown = shutdown
# command to run arq barker.worker.WorkerSettings
+7 -2
View File
@@ -5,7 +5,7 @@ description = "Point of Sale for a restaurant"
authors = ["tanshu <git@tanshu.com>"]
[tool.poetry.dependencies]
python = "^3.13"
python = "^3.14"
uvicorn = {extras = ["standard"], version = "^0.35.0"}
fastapi = {extras = ["all"], version = "^0.115.14"}
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
@@ -124,5 +124,10 @@ disallow_subclassing_any = true
disallow_untyped_defs = true
disallow_untyped_decorators = true
plugins = ["sqlalchemy.ext.mypy.plugin"]
plugins = ["sqlalchemy.ext.mypy.plugin", "pydantic.mypy"]
# --strict end
[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
-5
View File
@@ -1,5 +0,0 @@
#! /usr/bin/env bash
set -e
echo arq barker.worker.WorkerSettings
arq barker.worker.WorkerSettings
+4
View File
@@ -13,6 +13,10 @@ export const routes: Routes = [
path: 'bill-settlement-report',
loadChildren: () => import('./bill-settlement-report/bill-settlement-report.routes').then((mod) => mod.routes),
},
{
path: 'bundles',
loadChildren: () => import('./bundles/bundles.routes').then((mod) => mod.routes),
},
{
path: 'cashier-report',
loadChildren: () => import('./cashier-report/cashier-report.routes').then((mod) => mod.routes),
Loaded 100 of 154 files, more files were not shown because too many files have changed in this diff. Show more