"""
Models for receipt data.
"""
import datetime
import re
from typing import final
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import (
MappedColumn,
Relationship,
mapped_column,
relationship,
)
from typing_extensions import override
from .base import Base, Price, Quantity, Unit
from .product import Product
from .shop import Shop
[docs]
@final
class Receipt(Base):
"""
Receipt model for a receipt from a certain date at a shop with products
and possibly discounts.
"""
__tablename__ = "receipt"
filename: MappedColumn[str] = mapped_column(String(255), primary_key=True)
updated: MappedColumn[datetime.datetime] = mapped_column()
date: MappedColumn[datetime.date] = mapped_column()
shop: MappedColumn[str] = mapped_column("shop", ForeignKey("shop.key"))
shop_meta: Relationship[Shop] = relationship()
products: Relationship[list["ProductItem"]] = relationship(
back_populates="receipt",
cascade="all, delete-orphan",
passive_deletes=True,
order_by="ProductItem.position",
)
discounts: Relationship[list["Discount"]] = relationship(
back_populates="receipt",
cascade="all, delete-orphan",
passive_deletes=True,
order_by="Discount.position",
)
@property
def total_price(self) -> Price:
"""
Retrieve the total cost of the receipt after discounts.
"""
total = sum(product.price for product in self.products)
return Price(total + self.total_discount)
@property
def total_discount(self) -> Price:
"""
Retrieve the total discount of the receipt.
"""
total = sum(discount.price_decrease for discount in self.discounts)
return Price(total)
@override
def __repr__(self) -> str:
return f"Receipt(date={self.date.isoformat()!r}, shop={self.shop!r})"
[docs]
@final
class DiscountItems(Base): # pylint: disable=too-few-public-methods
"""
Association table for products involved in discounts.
"""
__tablename__ = "receipt_discount_products"
discount_id: MappedColumn[int] = mapped_column(
"discount_id",
ForeignKey("receipt_discount.id", ondelete="CASCADE"),
primary_key=True,
)
product_id: MappedColumn[int] = mapped_column(
"product_id",
ForeignKey("receipt_product.id", ondelete="CASCADE"),
primary_key=True,
)
[docs]
@final
class ProductItem(Base): # pylint: disable=too-few-public-methods
"""
Product model for a product item mentioned on a receipt.
"""
__tablename__ = "receipt_product"
id: MappedColumn[int] = mapped_column(primary_key=True)
receipt_key: MappedColumn[str] = mapped_column(
ForeignKey("receipt.filename", ondelete="CASCADE")
)
receipt: Relationship[Receipt] = relationship(back_populates="products")
quantity: MappedColumn[Quantity] = mapped_column()
label: MappedColumn[str] = mapped_column()
price: MappedColumn[Price] = mapped_column()
discount_indicator: MappedColumn[str | None] = mapped_column()
discounts: Relationship[list["Discount"]] = relationship(
secondary=DiscountItems.__table__,
back_populates="items",
passive_deletes=True,
)
product_id: MappedColumn[int | None] = mapped_column(
ForeignKey("product.id", ondelete="SET NULL")
)
product: Relationship[Product | None] = relationship()
position: MappedColumn[int] = mapped_column()
# Extracted fields from quantity
amount: MappedColumn[float] = mapped_column()
unit: MappedColumn[Unit | None] = mapped_column()
@property
def discount_indicators(self) -> list[str]:
"""
Retrieve a list of discrete portions of the discount indicator.
"""
if self.discount_indicator is None:
return []
pattern = "|".join(
indicator.pattern
for indicator in self.receipt.shop_meta.discount_indicators
)
return [
part
for part in re.split(rf"({pattern})", self.discount_indicator)
if part != ""
]
@override
def __repr__(self) -> str:
return (
f"ProductItem(receipt={self.receipt_key!r}, "
f"quantity='{self.quantity!s}', label={self.label!r}, "
f"price={self.price!s}, "
f"discount_indicator={self.discount_indicator!r}, "
f"product={self.product_id!r})"
)
[docs]
@final
class Discount(Base): # pylint: disable=too-few-public-methods
"""
Discount model for a discount action mentioned on a receipt.
"""
__tablename__ = "receipt_discount"
id: MappedColumn[int] = mapped_column(primary_key=True)
receipt_key: MappedColumn[str] = mapped_column(
ForeignKey("receipt.filename", ondelete="CASCADE")
)
receipt: Relationship[Receipt] = relationship(back_populates="discounts")
label: MappedColumn[str] = mapped_column()
price_decrease: MappedColumn[Price] = mapped_column()
items: Relationship[list[ProductItem]] = relationship(
secondary=DiscountItems.__table__,
back_populates="discounts",
passive_deletes=True,
)
position: MappedColumn[int] = mapped_column()
@override
def __repr__(self) -> str:
return (
f"Discount(receipt={self.receipt_key!r}, label={self.label!r}, "
f"price_decrease={self.price_decrease!s}, "
f"items={[item.label for item in self.items]!r})"
)