Fix: Due to matching of timestamp vs date, on the day that a product valid_till expired,

comparison with voucher.date would wrongly fail.
This commit is contained in:
2025-07-17 03:28:22 +00:00
parent 01b0cbfadf
commit 2fbfe96c40
9 changed files with 58 additions and 45 deletions

View File

@ -4,7 +4,7 @@ from datetime import date, timedelta
from decimal import Decimal from decimal import Decimal
from fastapi import APIRouter, Depends, HTTPException, Security, status from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy import and_, insert, or_, select, update from sqlalchemy import Date, and_, insert, or_, select, update
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, contains_eager from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql.functions import count, func from sqlalchemy.sql.functions import count, func
@ -250,8 +250,8 @@ def delete_route(
) )
) )
).scalar_one() ).scalar_one()
day = func.date_trunc( day = func.cast(
"day", Voucher.date + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES) Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
billed = db.execute( billed = db.execute(
select(count(Inventory.id)) select(count(Inventory.id))

View File

@ -2,7 +2,7 @@ from datetime import date, timedelta
from operator import or_ from operator import or_
from fastapi import APIRouter, Depends, Security from fastapi import APIRouter, Depends, Security
from sqlalchemy import and_ from sqlalchemy import Date, and_
from sqlalchemy.sql.expression import func, select from sqlalchemy.sql.expression import func, select
from ...core.config import settings from ...core.config import settings
@ -33,19 +33,19 @@ def beer_consumption(
user: UserToken = Security(get_user, scopes=["beer-sale-report"]), user: UserToken = Security(get_user, scopes=["beer-sale-report"]),
) -> BeerConsumptionReport: ) -> BeerConsumptionReport:
check_audit_permission(start_date, user.permissions) check_audit_permission(start_date, user.permissions)
day = func.date_trunc( day = func.cast(
"day", Voucher.date + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES) Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
sum_ = func.sum(Inventory.quantity * ProductVersion.quantity).label("sum") sum_ = func.sum(Inventory.quantity * ProductVersion.quantity).label("sum")
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.product_id == Product.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date, ProductVersion.valid_from <= day,
), ),
or_( or_(
ProductVersion.valid_till == None, # noqa: E711 ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date, ProductVersion.valid_till >= day,
), ),
) )
query = ( query = (

View File

@ -3,7 +3,7 @@ import uuid
from datetime import date, timedelta from datetime import date, timedelta
from fastapi import APIRouter, Cookie, Depends, Security from fastapi import APIRouter, Cookie, Depends, Security
from sqlalchemy import and_, func, or_, select from sqlalchemy import Date, and_, func, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ...core.config import settings from ...core.config import settings
@ -41,19 +41,19 @@ def discount_report(
def get_discount_report(start_date: date, finish_date: date, db: Session) -> list[DiscountReportItem]: def get_discount_report(start_date: date, finish_date: date, db: Session) -> list[DiscountReportItem]:
day = func.date_trunc( day = func.cast(
"day", Voucher.date + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES) Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
amount = func.sum(Inventory.quantity * Inventory.effective_price * Inventory.discount).label("Amount") amount = func.sum(Inventory.quantity * Inventory.effective_price * Inventory.discount).label("Amount")
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.product_id == Product.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date, ProductVersion.valid_from <= day,
), ),
or_( or_(
ProductVersion.valid_till == None, # noqa: E711 ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date, ProductVersion.valid_till >= day,
), ),
) )
list_ = db.execute( list_ = db.execute(

View File

@ -4,7 +4,7 @@ from datetime import date, timedelta
from decimal import Decimal from decimal import Decimal
from fastapi import APIRouter, Cookie, Depends, Security from fastapi import APIRouter, Cookie, Depends, Security
from sqlalchemy import and_, func, nulls_last, or_, select from sqlalchemy import Date, and_, func, nulls_last, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ...core.config import settings from ...core.config import settings
@ -44,18 +44,18 @@ def menu_engineering_report_view(
def menu_engineering_report(start_date: date, finish_date: date, db: Session) -> list[MeItem]: def menu_engineering_report(start_date: date, finish_date: date, db: Session) -> list[MeItem]:
day = func.date_trunc( day = func.cast(
"day", Voucher.date + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES) Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.product_id == Product.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= start_date, ProductVersion.valid_from <= day,
), ),
or_( or_(
ProductVersion.valid_till == None, # noqa: E711 ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= finish_date, ProductVersion.valid_till >= day,
), ),
) )
list_ = db.execute( list_ = db.execute(

View File

@ -4,7 +4,7 @@ from datetime import date, timedelta
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Cookie, Depends, Query, Security from fastapi import APIRouter, Cookie, Depends, Query, Security
from sqlalchemy import and_, func, or_, select from sqlalchemy import Date, and_, func, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ...core.config import settings from ...core.config import settings
@ -49,18 +49,18 @@ def product_sale_report_view(
def product_sale_report( def product_sale_report(
start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session
) -> list[ProductSaleReportItem]: ) -> list[ProductSaleReportItem]:
day = func.date_trunc( day = func.cast(
"day", Voucher.date + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES) Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.product_id == Product.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date, ProductVersion.valid_from <= day,
), ),
or_( or_(
ProductVersion.valid_till == None, # noqa: E711 ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date, ProductVersion.valid_till >= day,
), ),
) )
query = ( query = (

View File

@ -5,7 +5,7 @@ from decimal import Decimal
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Cookie, Depends, Query, Security from fastapi import APIRouter, Cookie, Depends, Query, Security
from sqlalchemy import and_, func, or_, select from sqlalchemy import Date, and_, func, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ...core.config import settings from ...core.config import settings
@ -57,18 +57,18 @@ def get_sale_report(
def get_sale(start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session) -> list[SaleReportItem]: def get_sale(start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session) -> list[SaleReportItem]:
day = func.date_trunc( day = func.cast(
"day", Voucher.date + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES) Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.product_id == Product.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date, ProductVersion.valid_from <= day,
), ),
or_( or_(
ProductVersion.valid_till == None, # noqa: E711 ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date, ProductVersion.valid_till >= day,
), ),
) )
query = ( query = (
@ -89,6 +89,7 @@ def get_sale(start_date: date, finish_date: date, id_: uuid.UUID | None, db: Ses
if id_: if id_:
query = query.where(FoodTable.section_id == id_) query = query.where(FoodTable.section_id == id_)
query = query.group_by(SaleCategory.name).order_by(SaleCategory.name) query = query.group_by(SaleCategory.name).order_by(SaleCategory.name)
print(query)
list_ = db.execute(query).all() list_ = db.execute(query).all()
total = Decimal(0) total = Decimal(0)
info = [] info = []
@ -99,8 +100,8 @@ def get_sale(start_date: date, finish_date: date, id_: uuid.UUID | None, db: Ses
def get_settlements(start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session) -> list[SaleReportItem]: def get_settlements(start_date: date, finish_date: date, id_: uuid.UUID | None, db: Session) -> list[SaleReportItem]:
day = func.date_trunc( day = func.cast(
"day", Voucher.date + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES) Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
query = ( query = (
select(SettleOption.name, func.sum(Settlement.amount)) select(SettleOption.name, func.sum(Settlement.amount))

View File

@ -3,7 +3,7 @@ import uuid
from datetime import date, timedelta from datetime import date, timedelta
from fastapi import APIRouter, HTTPException, Security, status from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import and_, delete, distinct, or_, select, update from sqlalchemy import Date, and_, delete, distinct, or_, select, update
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, contains_eager from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql.functions import count, func from sqlalchemy.sql.functions import count, func
@ -96,8 +96,8 @@ def check_product(old: ProductVersion, data: schemas.Product, db: Session) -> No
def check_inventories(old: ProductVersion, data: schemas.Product, db: Session) -> None: def check_inventories(old: ProductVersion, data: schemas.Product, db: Session) -> None:
day = func.date_trunc( day = func.cast(
"day", Voucher.date + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES) Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
if data.valid_from is not None and (old.valid_from or date.min) < data.valid_from: if data.valid_from is not None and (old.valid_from or date.min) < data.valid_from:
@ -125,8 +125,8 @@ def check_inventories(old: ProductVersion, data: schemas.Product, db: Session) -
def update_inventories(old: ProductVersion, data: schemas.Product, db: Session) -> None: def update_inventories(old: ProductVersion, data: schemas.Product, db: Session) -> None:
if old.product_id != data.id_: if old.product_id != data.id_:
day = func.date_trunc( day = func.cast(
"day", Voucher.date + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES) Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
invs = select(Inventory.id).join(Inventory.kot).join(Kot.voucher).where(Inventory.product_id == old.product_id) invs = select(Inventory.id).join(Inventory.kot).join(Kot.voucher).where(Inventory.product_id == old.product_id)
if old.valid_from is not None: if old.valid_from is not None:
@ -153,8 +153,8 @@ def delete_route(
valid_from, valid_till = db.execute( valid_from, valid_till = db.execute(
select(ProductVersion.valid_from, ProductVersion.valid_till).where(ProductVersion.id == version_id) select(ProductVersion.valid_from, ProductVersion.valid_till).where(ProductVersion.id == version_id)
).one() ).one()
day = func.date_trunc( day = func.cast(
"day", Voucher.date + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES) Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
query = select(count(Inventory.id)).join(Inventory.kot).join(Kot.voucher).where(Inventory.product_id == id_) query = select(count(Inventory.id)).join(Inventory.kot).join(Kot.voucher).where(Inventory.product_id == id_)
if valid_from is not None: if valid_from is not None:

View File

@ -1,10 +1,13 @@
import re import re
import uuid import uuid
from datetime import timedelta
from fastapi import APIRouter, HTTPException, Security, status from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import and_, or_, select from sqlalchemy import Date, and_, func, or_, select
from sqlalchemy.orm import Session, 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 ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture from ...db.session import SessionFuture
from ...models.bill import Bill from ...models.bill import Bill
@ -48,15 +51,18 @@ def from_id(
user: UserToken = Security(get_user), user: UserToken = Security(get_user),
) -> VoucherOut: ) -> VoucherOut:
with SessionFuture() as db: with SessionFuture() as db:
day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.product_id == Product.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date, ProductVersion.valid_from <= day,
), ),
or_( or_(
ProductVersion.valid_till == None, # noqa: E711 ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date, ProductVersion.valid_till >= day,
), ),
) )
item: Voucher = ( item: Voucher = (
@ -91,15 +97,18 @@ def from_bill(
user: UserToken = Security(get_user), user: UserToken = Security(get_user),
) -> VoucherOut: ) -> VoucherOut:
with SessionFuture() as db: with SessionFuture() as db:
day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.product_id == Product.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date, ProductVersion.valid_from <= day,
), ),
or_( or_(
ProductVersion.valid_till == None, # noqa: E711 ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date, ProductVersion.valid_till >= day,
), ),
) )
match = re.compile(r"^(\w+)-(\d+)$").match(id_) match = re.compile(r"^(\w+)-(\d+)$").match(id_)
@ -150,15 +159,18 @@ def from_table(
user: UserToken = Security(get_user), user: UserToken = Security(get_user),
) -> VoucherOut | VoucherBlank: ) -> VoucherOut | VoucherBlank:
with SessionFuture() as db: with SessionFuture() as db:
day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day")
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.product_id == Product.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date, ProductVersion.valid_from <= day,
), ),
or_( or_(
ProductVersion.valid_till == None, # noqa: E711 ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date, ProductVersion.valid_till >= day,
), ),
) )
guest = None if g is None else db.execute(select(GuestBook).where(GuestBook.id == g)).scalar_one() guest = None if g is None else db.execute(select(GuestBook).where(GuestBook.id == g)).scalar_one()

View File

@ -13,8 +13,8 @@
<mat-icon matSuffix (click)="hide = !hide">{{ hide ? 'visibility' : 'visibility_off' }}</mat-icon> <mat-icon matSuffix (click)="hide = !hide">{{ hide ? 'visibility' : 'visibility_off' }}</mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-divider></mat-divider>
@if (unregisteredDevice) { @if (unregisteredDevice) {
<mat-divider></mat-divider>
<h2>Sorry, device {{ deviceName }} is not enabled.</h2> <h2>Sorry, device {{ deviceName }} is not enabled.</h2>
} }
</form> </form>