barker/barker/barker/printing/bill.py

182 lines
7.0 KiB
Python

import asyncio
import locale
import re
import uuid
from datetime import timedelta
from decimal import Decimal
from arq import ArqRedis, create_pool
from barker.core.config import settings
from sqlalchemy import and_, or_, select
from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
from ..models.db_setting import DbSetting
from ..models.inventory import Inventory
from ..models.printer import Printer
from ..models.product_version import ProductVersion
from ..models.section_printer import SectionPrinter
from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType
from . import currency_format, format_no_decimals
def print_bill(voucher_id: uuid.UUID, db: Session):
locale.setlocale(locale.LC_MONETARY, "en_IN")
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == voucher_id)).scalar_one()
printer = db.execute(
select(Printer)
.join(SectionPrinter.printer)
.where(SectionPrinter.section_id == voucher.food_table.section_id)
.where(SectionPrinter.sale_category_id == None) # noqa: E711
).scalar_one()
items_dict: dict[tuple[uuid.UUID, bool, Decimal, set[uuid.UUID]], Inventory] = {}
tax: dict[tuple[uuid.UUID, int], tuple[str, Decimal]] = {}
for i in [i for k in voucher.kots for i in k.inventories]:
key = (
i.product_id,
i.is_happy_hour,
i.discount,
frozenset(m.modifier_id for m in i.modifiers if m.modifier.show_in_bill),
)
if key in items_dict:
items_dict[key].quantity += i.quantity
else:
items_dict[key] = Inventory(
None,
i.product_id,
i.quantity,
i.price,
i.discount,
i.is_happy_hour,
None,
i.tax_rate,
0,
i.product,
i.tax,
)
for i in [it for it in items_dict.values() if it.quantity != 0]:
get_tax_item(i.tax.id, i.tax.name, i.tax_amount, tax)
data = design_bill(voucher, list(items_dict.values()), list(tax.values()), db)
loop = asyncio.new_event_loop()
redis: ArqRedis = loop.run_until_complete(create_pool(redis_settings))
loop.run_until_complete(
redis.enqueue_job(
"sent_to_printer", data, printer.address, printer.cut_code, _queue_name=f"barker:print:{printer.name}"
)
)
loop.close()
def get_tax_item(
id_: uuid.UUID, tax_name: str, amount: Decimal, tax_dict: dict[tuple[uuid.UUID, int], tuple[str, Decimal]]
) -> None:
items = tax_name.split(";")
if len(items) == 1:
key = (id_, 0)
old_amount = tax_dict[key][1] if key in tax_dict else Decimal(0)
tax_dict[key] = tax_name, round(amount + old_amount, 2)
else:
for i, item in enumerate(it.strip() for it in items):
key = (id_, i)
old_amount = tax_dict[key][1] if key in tax_dict else Decimal(0)
match = re.match(r"(^.*)\s+\((.*?)/(.*?)\)[^(]*$", item)
if not match or len(match.groups()) != 3:
raise Exception("Error in tax as it has multiple items, but the format is wrong.")
sub_amount: Decimal = round(amount * Decimal(match.group(2)) / Decimal(match.group(3)), 2)
tax_dict[(id_, i)] = match.group(1), round(sub_amount + old_amount, 2)
def design_bill(
voucher: Voucher,
items: list[Inventory],
tax: list[tuple[str, Decimal]],
db: Session,
):
# Header
s = "\n\r" + db.execute(select(DbSetting).where(DbSetting.name == "Header")).scalar_one().data["Text"]
if voucher.voucher_type == VoucherType.REGULAR_BILL:
s += "\n\r" + "Retail Invoice".center(42)
s += "\n\r"
elif voucher.voucher_type == VoucherType.NO_CHARGE:
s += "\n\r" + "NO CHARGE - THIS IS NOT A BILL - DON'T PAY".center(42)
s += "\n\r"
elif voucher.voucher_type == VoucherType.STAFF:
s += "\n\r" + "STAFF CONSUMPTION -- THIS IS NOT A BILL".center(42)
s += "\n\r"
# Products
product_date = (
voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES)
).date()
voucher_date = voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
s += "\n\r" + f"Bill No: {voucher.full_bill_id:>12} {voucher_date:%d-%b-%Y %H:%M}"
s += "\n\r" + "Table No.: " + voucher.food_table.name
s += "\n\r" + "-" * 42
s += "\n\r" + "Qty. Particulars Price Amount"
s += "\n\r" + "-" * 42
for item in [i for i in items if i.quantity != 0]:
product: ProductVersion = db.execute(
select(ProductVersion).where(
and_(
ProductVersion.product_id == item.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,
),
)
)
).scalar_one()
name = "H H " + product.full_name if item.is_happy_hour else product.full_name
s += (
f"\n\r"
f"{item.quantity: >5.2f} {name:<22.22} {format_no_decimals(item.price): >6}"
f" {format_no_decimals(item.price * item.quantity): >6}"
)
for m in [m for m in item.modifiers if m.modifier.show_in_bill]:
s += f"\n\r -- {m.modifier.name: <38.38}"
s += "\n\r" + "------------------------------------------"
# Totals
amount = sum(round(i.quantity * i.price, 2) for i in items)
s += f"\n\r{'Subtotal :': >32} {currency_format(amount): >9}"
amount = sum(round(i.quantity * i.price, 2) for i in items if i.is_happy_hour)
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 items)
if amount != 0:
s += f"\n\r{'Discount :': >32} {currency_format(amount): >9}"
for t in tax:
if t[1] != 0:
s += f"\n\r{t[0]: >30} : {currency_format(t[1]): >9}"
s += f"\n\r{'Total Amount :': >32} {currency_format(round(voucher.amount)): >9}"
s += "\n\r" + "-" * 42
if voucher.voucher_type != VoucherType.REGULAR_BILL:
s += "\n\r" + "THIS IS NOT A BILL - DON'T PAY".center(42)
s += "\n\r" + "-" * 42
if voucher.narration:
s += f"\n\r{voucher.narration: ^42}"
s += "\n\r" + "-" * 42
if voucher.customer and voucher.customer.print_in_bill:
s += f"\n\r{voucher.customer.name or ''}\n\r{voucher.customer.phone or ''}\n\r{voucher.customer.address or ''}"
s += "\n\r" + "-" * 42
s += "\n\r" + "Cashier : " + voucher.user.name
s += "\n\r" + db.execute(select(DbSetting).where(DbSetting.name == "Footer")).scalar_one().data["Text"]
return s