Merge branch 'feature/bundle'
This commit is contained in:
commit
ae8975b29f
154 files changed
+3759
-1366
No files matched your search
@@ -0,0 +1 @@
|
||||
3.14
|
||||
+1
-9
@@ -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 && \
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ###
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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_
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
):
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import enum
|
||||
|
||||
|
||||
class InventoryType(str, enum.Enum):
|
||||
regular = "regular"
|
||||
bundle = "bundle"
|
||||
bundle_item = "bundle_item"
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
):
|
||||
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_
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,6 +43,7 @@ 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:
|
||||
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)
|
||||
@@ -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,8 +163,13 @@ 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
|
||||
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}"
|
||||
@@ -163,21 +180,27 @@ def design_bill(
|
||||
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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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=[
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
}
|
||||
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 = 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,
|
||||
)
|
||||
ft["amount"] = round(item.status.voucher.amount, 0)
|
||||
if item.status is not None:
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
],
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 = 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)
|
||||
.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()
|
||||
.scalar_one()
|
||||
)
|
||||
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),
|
||||
)
|
||||
).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,17 +195,22 @@ def voucher_info(item: Voucher, db: Session) -> VoucherOut:
|
||||
narration=item.narration,
|
||||
reason=item.reason,
|
||||
voucher_type=item.voucher_type,
|
||||
kots=[
|
||||
KotOut(
|
||||
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=[
|
||||
InventoryOut(
|
||||
inventories=[],
|
||||
)
|
||||
voucher_out.kots.append(kot)
|
||||
for i in k.inventories:
|
||||
inventory = InventoryOut(
|
||||
id_=i.id,
|
||||
sort_order=i.sort_order,
|
||||
product=InventoryProduct(
|
||||
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})",
|
||||
@@ -311,6 +223,7 @@ def voucher_info(item: Voucher, db: Session) -> VoucherOut:
|
||||
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,
|
||||
@@ -318,6 +231,7 @@ def voucher_info(item: Voucher, db: Session) -> VoucherOut:
|
||||
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,
|
||||
@@ -326,13 +240,19 @@ def voucher_info(item: Voucher, db: Session) -> VoucherOut:
|
||||
)
|
||||
for m in i.modifiers
|
||||
],
|
||||
children=[],
|
||||
)
|
||||
for i in k.inventories
|
||||
],
|
||||
)
|
||||
for k in item.kots
|
||||
],
|
||||
)
|
||||
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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
database: str
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
#! /usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo arq barker.worker.WorkerSettings
|
||||
arq barker.worker.WorkerSettings
|
||||
@@ -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
Reference in new issue
Block a user