From 61f5f2f1af1c0af898bd841bcf94a621cd77865d Mon Sep 17 00:00:00 2001 From: Amritanshu Date: Wed, 28 Jan 2026 03:53:52 +0000 Subject: [PATCH] Product update sort order working Beer Sale report should work. But it has all the products in it now. All reports working --- barker/barker/printing/product_sale_report.py | 3 +- barker/barker/routers/product.py | 30 +---- .../routers/reports/beer_sale_report.py | 28 +++-- .../barker/routers/reports/discount_report.py | 12 +- .../reports/menu_engineering_report.py | 85 +++++++------ .../routers/reports/product_sale_report.py | 26 ++-- .../routers/reports/product_updates_report.py | 118 +++++++++++++----- barker/barker/schemas/temporal_product.py | 4 +- .../product-list/product-list.component.ts | 2 +- bookie/src/app/product/product.service.ts | 4 +- 10 files changed, 196 insertions(+), 116 deletions(-) diff --git a/barker/barker/printing/product_sale_report.py b/barker/barker/printing/product_sale_report.py index 251c8f7d..adb1dc62 100644 --- a/barker/barker/printing/product_sale_report.py +++ b/barker/barker/printing/product_sale_report.py @@ -13,11 +13,10 @@ from ..core.config import settings from ..models.device import Device from ..models.printer import Printer from ..models.section_printer import SectionPrinter -from ..schemas.menu_engineering_report import MeReport from ..schemas.product_sale_report import ProductSaleReport -def print_product_sale_report(report: ProductSaleReport | MeReport, device_id: uuid.UUID, db: Session) -> None: +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() diff --git a/barker/barker/routers/product.py b/barker/barker/routers/product.py index 7bdaa4b4..1da24290 100644 --- a/barker/barker/routers/product.py +++ b/barker/barker/routers/product.py @@ -35,38 +35,18 @@ from . import effective_date router = APIRouter() -@router.post("/list", response_model=list[schemas.Product]) +@router.post("/list/{id_}", response_model=list[schemas.Product]) def sort_order( + id_: uuid.UUID, data: list[schemas.Product], date_: date = Depends(effective_date), user: UserToken = Security(get_user, scopes=["products"]), ) -> list[schemas.Product]: - raise NotImplementedError("Sorting products is not yet implemented.") try: with SessionFuture() as db: - indexes: dict[uuid.UUID, int] = {} - for item in data: - if item.menu_category.id_ in indexes: - indexes[item.menu_category.id_] += 1 - else: - indexes[item.menu_category.id_] = 0 - db.execute( - update(ProductVersion) - .where( - and_( - ProductVersion.product_id == item.id_, - or_( - ProductVersion.valid_from == None, # noqa: E711 - ProductVersion.valid_from <= date_, - ), - or_( - ProductVersion.valid_till == None, # noqa: E711 - ProductVersion.valid_till >= date_, - ), - ) - ) - .values(sort_order=indexes[item.menu_category.id_]) - ) + skus = [sku for product in data for sku in product.skus if sku.menu_category.id_ == id_] + for i, sku in enumerate(skus): + db.execute(update(StockKeepingUnit).where(StockKeepingUnit.id == sku.id_).values(sort_order=i)) db.commit() return product_list(date_, db) except SQLAlchemyError as e: diff --git a/barker/barker/routers/reports/beer_sale_report.py b/barker/barker/routers/reports/beer_sale_report.py index cd4e1c7f..a4cb6e65 100644 --- a/barker/barker/routers/reports/beer_sale_report.py +++ b/barker/barker/routers/reports/beer_sale_report.py @@ -5,6 +5,9 @@ from fastapi import APIRouter, Depends, Security from sqlalchemy import Date, and_ from sqlalchemy.sql.expression import func, select +from barker.models.sku_version import SkuVersion +from barker.models.stock_keeping_unit import StockKeepingUnit + from ...core.config import settings from ...core.security import get_current_active_user as get_user from ...db.session import SessionFuture @@ -36,7 +39,7 @@ def beer_consumption( day = func.cast( Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date ).label("day") - sum_ = func.sum(Inventory.quantity * ProductVersion.quantity).label("sum") + sum_ = func.sum(Inventory.quantity * SkuVersion.fraction).label("sum") product_version_onclause = and_( ProductVersion.product_id == Product.id, or_( @@ -48,11 +51,18 @@ def beer_consumption( 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 + ) query = ( - select(day, ProductVersion.name, sum_) + select(day, ProductVersion.name, sum_, ProductVersion.fraction_units) .join(Voucher.kots) .join(Kot.inventories) - .join(Inventory.product) + .join(Inventory.sku) + .join(SkuVersion, onclause=sku_version_onclause) + .join(StockKeepingUnit.product) .join(ProductVersion, onclause=product_version_onclause) .where( day >= start_date, @@ -74,21 +84,21 @@ def beer_consumption( with SessionFuture() as db: list_ = db.execute( query.where(Voucher.voucher_type.in_(vt)) - .group_by(day, ProductVersion.name) + .group_by(day, ProductVersion.name, ProductVersion.fraction_units) .having(sum_ != 0) .order_by(day, ProductVersion.name) ).all() headers: list[str] = [] data: list[BeerConsumptionReportItem] = [] - for date_, name, quantity in list_: - if name not in headers: - headers.append(name) + for date_, name, quantity, units in list_: + if f"{name} ({units})" not in headers: + headers.append(f"{name} ({units})") old = next((d for d in data if d.date_ == date_), None) if old: - old[name] = quantity + old[f"{name} ({units})"] = quantity else: item = BeerConsumptionReportItem(date_=date_) - item[name] = quantity + item[f"{name} ({units})"] = quantity data.append(item) return BeerConsumptionReport( start_date=start_date, diff --git a/barker/barker/routers/reports/discount_report.py b/barker/barker/routers/reports/discount_report.py index b9476f67..dc37b23c 100644 --- a/barker/barker/routers/reports/discount_report.py +++ b/barker/barker/routers/reports/discount_report.py @@ -6,6 +6,9 @@ from fastapi import APIRouter, Cookie, Depends, Security from sqlalchemy import Date, and_, func, or_, select from sqlalchemy.orm import Session +from barker.models.sku_version import SkuVersion +from barker.models.stock_keeping_unit import StockKeepingUnit + from ...core.config import settings from ...core.security import get_current_active_user as get_user from ...db.session import SessionFuture @@ -56,11 +59,18 @@ def get_discount_report(start_date: date, finish_date: date, db: Session) -> lis 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 + ) list_ = db.execute( select(SaleCategory.name, amount) .join(Voucher.kots) .join(Kot.inventories) - .join(Inventory.product) + .join(Inventory.sku) + .join(SkuVersion, onclause=sku_version_onclause) + .join(StockKeepingUnit.product) .join(ProductVersion, onclause=product_version_onclause) .join(ProductVersion.sale_category) .where( diff --git a/barker/barker/routers/reports/menu_engineering_report.py b/barker/barker/routers/reports/menu_engineering_report.py index 9377ff1e..6d236cd0 100644 --- a/barker/barker/routers/reports/menu_engineering_report.py +++ b/barker/barker/routers/reports/menu_engineering_report.py @@ -3,10 +3,13 @@ import uuid from datetime import date, timedelta from decimal import Decimal -from fastapi import APIRouter, Cookie, Depends, Security +from fastapi import APIRouter, Depends, Security from sqlalchemy import Date, and_, func, nulls_last, or_, select from sqlalchemy.orm import Session +from barker.models.sku_version import SkuVersion +from barker.models.stock_keeping_unit import StockKeepingUnit + from ...core.config import settings from ...core.security import get_current_active_user as get_user from ...db.session import SessionFuture @@ -18,7 +21,6 @@ from ...models.product_version import ProductVersion from ...models.sale_category import SaleCategory from ...models.voucher import Voucher from ...models.voucher_type import VoucherType -from ...printing.product_sale_report import print_product_sale_report from ...schemas.menu_engineering_report import MeItem, MeReport from ...schemas.user_token import UserToken from . import check_audit_permission, report_finish_date, report_start_date @@ -47,8 +49,7 @@ 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_onclause = and_( - ProductVersion.product_id == Product.id, + product_version_valid = and_( or_( ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from <= day, @@ -58,45 +59,53 @@ def menu_engineering_report(start_date: date, finish_date: date, db: Session) -> 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 + ) list_ = db.execute( select( SaleCategory.name, MenuCategory.name, ProductVersion.id, - ProductVersion.full_name, - ProductVersion.price, + ProductVersion.name, + SkuVersion.id, + SkuVersion.units, + SkuVersion.sale_price, func.sum(Inventory.quantity), func.sum(Inventory.net), ) .select_from(ProductVersion) .join(ProductVersion.sale_category) - .join(ProductVersion.menu_category) - .join(Product, onclause=product_version_onclause) - .join(Product.inventories, isouter=True) + .join(ProductVersion.product) + .join(Product.skus) + .join(StockKeepingUnit.versions) + .join(SkuVersion.menu_category) + .join(StockKeepingUnit.inventories, isouter=True) .join(Inventory.kot, isouter=True) .join(Kot.voucher, isouter=True) .where( - or_( - day == None, # noqa: E711 - and_( - day >= start_date, - day <= finish_date, - Voucher.voucher_type == VoucherType.REGULAR_BILL, - ), - ), + day >= start_date, + day <= finish_date, + Voucher.voucher_type == VoucherType.REGULAR_BILL, + product_version_valid, + sku_version_valid, ) .group_by( SaleCategory.name, MenuCategory.name, ProductVersion.id, - ProductVersion.full_name, + ProductVersion.name, + SkuVersion.id, + SkuVersion.units, + SkuVersion.sale_price, ) .order_by(SaleCategory.name, nulls_last(func.sum(Inventory.net).desc())) ).all() info: list[MeItem] = [] sc_sales: dict[str, Decimal] = {} sc_quantity: dict[str, Decimal] = {} - for sc, mc, id_, name, price, quantity, sales in list_: + for sc, mc, p_id, name, s_id, units, price, quantity, sales in list_: if sc not in sc_sales: sc_sales[sc] = Decimal(0) if sales is not None: @@ -107,8 +116,8 @@ def menu_engineering_report(start_date: date, finish_date: date, db: Session) -> sc_quantity[sc] += quantity info.append( MeItem( - id_=id_, - name=name, + id_=uuid.uuid5(uuid.NAMESPACE_DNS, f"{p_id}_{s_id}"), + name=f"{name} ({units})", price=price, average=round(sales / quantity) if sales and quantity else price, sale_category=sc, @@ -126,20 +135,20 @@ def menu_engineering_report(start_date: date, finish_date: date, db: Session) -> return info -@router.get("/print", response_model=bool) -def print_report( - start_date: date = Depends(report_start_date), - finish_date: date = Depends(report_finish_date), - device_id: uuid.UUID = Cookie(None), - user: UserToken = Security(get_user, scopes=["product-sale-report"]), -) -> bool: - check_audit_permission(start_date, user.permissions) - with SessionFuture() as db: - report = MeReport( - user_name=user.name, - start_date=start_date, - finish_date=finish_date, - amounts=menu_engineering_report(start_date, finish_date, db), - ) - print_product_sale_report(report, device_id, db) - return True +# @router.get("/print", response_model=bool) +# def print_report( +# start_date: date = Depends(report_start_date), +# finish_date: date = Depends(report_finish_date), +# device_id: uuid.UUID = Cookie(None), +# user: UserToken = Security(get_user, scopes=["product-sale-report"]), +# ) -> bool: +# check_audit_permission(start_date, user.permissions) +# with SessionFuture() as db: +# report = MeReport( +# user_name=user.name, +# start_date=start_date, +# finish_date=finish_date, +# amounts=menu_engineering_report(start_date, finish_date, db), +# ) +# print_product_sale_report(report, device_id, db) +# return True diff --git a/barker/barker/routers/reports/product_sale_report.py b/barker/barker/routers/reports/product_sale_report.py index 5aa61537..491f8d69 100644 --- a/barker/barker/routers/reports/product_sale_report.py +++ b/barker/barker/routers/reports/product_sale_report.py @@ -7,6 +7,9 @@ from fastapi import APIRouter, Cookie, Depends, Query, Security from sqlalchemy import Date, and_, func, or_, select from sqlalchemy.orm import Session +from barker.models.sku_version import SkuVersion +from barker.models.stock_keeping_unit import StockKeepingUnit + from ...core.config import settings from ...core.security import get_current_active_user as get_user from ...db.session import SessionFuture @@ -63,20 +66,28 @@ def product_sale_report( 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 + ) query = ( select( ProductVersion.id, - ProductVersion.full_name, + ProductVersion.name, + SkuVersion.units, Voucher.voucher_type, Inventory.is_happy_hour, func.sum(Inventory.quantity), ) .join(Inventory.kot) .join(Kot.voucher) - .join(Inventory.product) + .join(Inventory.sku) + .join(SkuVersion, onclause=sku_version_onclause) + .join(SkuVersion.menu_category) + .join(StockKeepingUnit.product) .join(ProductVersion, onclause=product_version_onclause) .join(ProductVersion.sale_category) - .join(ProductVersion.menu_category) .join(Voucher.food_table) .where( day >= start_date, @@ -89,20 +100,21 @@ def product_sale_report( SaleCategory.name, MenuCategory.name, ProductVersion.id, - ProductVersion.full_name, + ProductVersion.name, + SkuVersion.units, Voucher.voucher_type, Inventory.is_happy_hour, - ).order_by(SaleCategory.name, MenuCategory.name, ProductVersion.full_name) + ).order_by(SaleCategory.name, MenuCategory.name, ProductVersion.name, SkuVersion.units) list_ = db.execute(query).all() info: list[ProductSaleReportItem] = [] - for product_version_id, name, v_type, hh, quantity in list_: + for product_version_id, name, units, v_type, hh, quantity in list_: type_ = VoucherType(v_type).name old = next((i for i in info if i.product_version_id == product_version_id and i.is_happy_hour == hh), None) if old: old[type_] = old[type_] + quantity else: item = ProductSaleReportItem( - product_version_id=product_version_id, name=f"{'H H ' if hh else ''}{name}", is_happy_hour=hh + product_version_id=product_version_id, name=f"{'H H ' if hh else ''}{name} ({units})", is_happy_hour=hh ) item[type_] = quantity info.append(item) diff --git a/barker/barker/routers/reports/product_updates_report.py b/barker/barker/routers/reports/product_updates_report.py index 20149be9..ece3b486 100644 --- a/barker/barker/routers/reports/product_updates_report.py +++ b/barker/barker/routers/reports/product_updates_report.py @@ -1,9 +1,13 @@ from datetime import date, timedelta from fastapi import APIRouter, Depends, Security -from sqlalchemy import and_, or_, select +from sqlalchemy import and_, nullsfirst, or_, select from sqlalchemy.orm import Session, contains_eager +from barker.models.product import Product +from barker.models.sku_version import SkuVersion +from barker.models.stock_keeping_unit import StockKeepingUnit + from ...core.security import get_current_active_user as get_user from ...db.session import SessionFuture from ...models.menu_category import MenuCategory @@ -21,19 +25,32 @@ def product_updates_report_view( finish_date: date = Depends(report_finish_date), user: UserToken = Security(get_user, scopes=["product-sale-report"]), ): - 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), - } + 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), + } + except Exception as e: + return {"error": str(e)} def product_updates_report(start_date: date, finish_date: date, db: Session) -> list[str]: list_ = ( db.execute( - select(ProductVersion) - .join(ProductVersion.menu_category) + select(Product) + .join(Product.versions) + .join(ProductVersion.sale_category) + .join(Product.skus) + .join(StockKeepingUnit.versions) + .join(SkuVersion.menu_category) + .order_by(MenuCategory.sort_order) + .order_by(MenuCategory.name) + .order_by(Product.sort_order) + .order_by(ProductVersion.name) + .order_by(nullsfirst(ProductVersion.valid_from)) + .order_by(nullsfirst(SkuVersion.valid_from)) .where( or_( and_( @@ -44,33 +61,76 @@ def product_updates_report(start_date: date, finish_date: date, db: Session) -> ProductVersion.valid_till >= start_date - timedelta(days=1), ProductVersion.valid_till <= finish_date, ), + and_( + SkuVersion.valid_from >= start_date, + SkuVersion.valid_from <= finish_date, + ), + and_( + SkuVersion.valid_till >= start_date - timedelta(days=1), + SkuVersion.valid_till <= finish_date, + ), ) ) - .order_by( - MenuCategory.sort_order, - MenuCategory.name, - ProductVersion.sort_order, - ProductVersion.name, - ProductVersion.valid_from.nullsfirst(), - ) .options( - contains_eager(ProductVersion.menu_category), + contains_eager(Product.versions).contains_eager(ProductVersion.sale_category), + contains_eager(Product.skus) + .contains_eager(StockKeepingUnit.versions) + .contains_eager(SkuVersion.menu_category), ) ) + .unique() .scalars() .all() ) + report = {} - for item in list_: - if item.product_id not in report: - report[item.product_id] = "" - report[item.product_id] += ( - "From: " - + (f"{item.valid_from:%d-%b-%Y}" if item.valid_from is not None else "\u221e") - + " " - + "Till: " - + (f"{item.valid_till:%d-%b-%Y}" if item.valid_till is not None else "\u221e") - + "\n" - + f"{item.full_name} @ {item.price: .2f} - {'Happy Hour' if item.has_happy_hour else 'No Happy Hour'}\n" - ) + for product in list_: + dates: set[date] = set() + for p in product.versions: + dates.add(p.valid_from or date.min) + for sk in product.skus: + for s in sk.versions: + dates.add(s.valid_from or date.min) + sku_versions = [s for sk in product.skus for s in sk.versions] + if product.id not in report: + report[product.id] = "" + for d in sorted(dates): + ap = active_at_p(product.versions, d) + ass = active_at_s(sku_versions, d) + if ap is None or ass is None: + continue + v_from: date | None = ( + max(ap.valid_from, ass.valid_from) + if ap.valid_from and ass.valid_from + else ap.valid_from or ass.valid_from + ) + v_till: date | None = ( + min(ap.valid_till, ass.valid_till) + if ap.valid_till and ass.valid_till + else ap.valid_till or ass.valid_till + ) + report[product.id] += ( + "From: " + + (f"{v_from:%d-%b-%Y}" if v_from is not None else "\u221e") + + " " + + "Till: " + + (f"{v_till:%d-%b-%Y}" if v_till is not None else "\u221e") + + "\n" + + f"{ap.name} ({ass.units}) @ {ass.sale_price: .2f} - {'Happy Hour' if ass.has_happy_hour else 'No Happy Hour'}\n" + ) + return list(report.values()) + + +def active_at_p(items: list[ProductVersion], t: date) -> ProductVersion | None: + for it in items: + if (it.valid_from or date.min) <= t and (it.valid_till is None or t < it.valid_till): + return it + return None + + +def active_at_s(items: list[SkuVersion], t: date) -> SkuVersion | None: + for it in items: + if (it.valid_from or date.min) <= t and (it.valid_till is None or t < it.valid_till): + return it + return None diff --git a/barker/barker/schemas/temporal_product.py b/barker/barker/schemas/temporal_product.py index b50437a3..a8ad9f4b 100644 --- a/barker/barker/schemas/temporal_product.py +++ b/barker/barker/schemas/temporal_product.py @@ -51,8 +51,8 @@ class Product(BaseModel): class StockKeepingUnit(BaseModel): - id_: uuid.UUID = None - version_id: uuid.UUID = None + 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)) diff --git a/bookie/src/app/product/product-list/product-list.component.ts b/bookie/src/app/product/product-list/product-list.component.ts index 28aa31dc..7ba617b4 100644 --- a/bookie/src/app/product/product-list/product-list.component.ts +++ b/bookie/src/app/product/product-list/product-list.component.ts @@ -98,7 +98,7 @@ export class ProductListComponent implements OnInit { } updateSortOrder() { - this.ser.updateSortOrder(this.dataSource.filteredData).subscribe({ + this.ser.updateSortOrder(this.menuCategoryFilter.value, this.dataSource.filteredData).subscribe({ next: (result: Product[]) => { this.snackBar.open('', 'Success'); this.loadData(result, this.menuCategories); diff --git a/bookie/src/app/product/product.service.ts b/bookie/src/app/product/product.service.ts index cfc1b95f..0e6b8f73 100644 --- a/bookie/src/app/product/product.service.ts +++ b/bookie/src/app/product/product.service.ts @@ -57,9 +57,9 @@ export class ProductService { .pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable; } - updateSortOrder(list: Product[]): Observable { + updateSortOrder(menuCategoryId: string, list: Product[]): Observable { return this.http - .post(`${url}/list`, list, httpOptions) + .post(`${url}/list/${menuCategoryId}`, list, httpOptions) .pipe(catchError(this.log.handleError(serviceName, 'updateSortOrder'))) as Observable; }