Feature: Now a tax can be broken up into multiple components while bill printing.

This is especially for GST. The format for the tax name is
"Display name 1 (Ratio eg. 1/2) ; Display name 1 (Ratio eg. 1/2)"
eg.
"SGST on Food @ 2.5% (1/2) ; CGST on Food @ 2.5% (1/2)"
or
"UTGST on Food @ 2.5% (1/2) ; CGST on Food @ 2.5% (1/2)"

Feature:
All report printing now uses local aware number formatting for Indian locale.
This commit is contained in:
2020-12-13 13:14:19 +05:30
parent 27aa4d12a6
commit 3ad73dff1c
35 changed files with 130 additions and 60 deletions
+12 -2
View File
@@ -1,2 +1,12 @@
from ..printing.bill import print_bill # noqa: F401
from ..printing.kot import print_kot # noqa: F401
import locale
from decimal import Decimal
from typing import Union
def currency_format(amount: Union[Decimal, int]) -> str:
return locale.currency(amount, symbol=False, grouping=True)
def format_no_decimals(amount: Union[Decimal, int]) -> str:
return locale.format_string("%d", amount, grouping=True, monetary=True)
+34 -11
View File
@@ -1,4 +1,6 @@
import asyncio
import locale
import re
import uuid
from datetime import timedelta
@@ -19,9 +21,11 @@ from ..models.master import (
VoucherType,
)
from ..models.voucher import Inventory, Voucher
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.query(Voucher).filter(Voucher.id == voucher_id).first()
printer = (
@@ -44,15 +48,30 @@ def print_bill(voucher_id: uuid.UUID, db: Session):
items_dict[key].quantity += i.quantity
else:
items_dict[key] = i
if i.tax_id in tax:
tax[i.tax_id] = (i.tax.name, tax[i.tax_id][1] + i.tax_amount)
else:
tax[i.tax_id] = (i.tax.name, i.tax_amount)
get_tax_item(i.tax.id, i.tax.name, i.tax_amount, tax)
data = design_bill(voucher, items_dict.values(), tax.values(), db)
redis: ArqRedis = asyncio.run(create_pool(redis_settings))
asyncio.run(redis.enqueue_job("sent_to_printer", data, printer.address, printer.cut_code))
def get_tax_item(id_: uuid.UUID, tax_name: str, amount: Decimal, tax_dict: {}) -> None:
items = tax_name.split(";")
if len(items) == 1:
key = (id_, 0)
old_amount = tax_dict[key][1] if key in tax_dict else 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 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 = 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[Tuple[Inventory, Decimal]],
@@ -76,7 +95,7 @@ def design_bill(
s += "\n\r" + f"Bill No: {voucher.full_bill_id:>12} {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" + "Qty. Particulars Price Amount"
s += "\n\r" + "-" * 42
for item in [i for i in items if i.quantity != 0]:
product: ProductVersion = (
@@ -99,28 +118,32 @@ def design_bill(
.first()
)
name = "H H " + product.full_name if item.is_happy_hour else product.full_name
s += "\n\r" + f"{item.quantity: >5.2f} {name:<22.22} {item.price: >6.2f} {item.price * item.quantity: >6.2f}"
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(i.quantity * i.price for i in items)
s += f"\n\r{'Subtotal :': >32} {amount: >9.2f}"
s += f"\n\r{'Subtotal :': >32} {currency_format(amount): >9}"
amount = sum(i.quantity * i.price for i in items if i.is_happy_hour)
if amount != 0:
s += f"\n\r{'Happy Hour Discount :': >32} {amount: >9.2f}"
s += f"\n\r{'Happy Hour Discount :': >32} {currency_format(amount): >9}"
amount = sum(i.quantity * i.effective_price * i.discount for i in items)
if amount != 0:
s += f"\n\r{'Discount :': >32} {amount: >9.2f}"
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} : {t[1]: >9.2f}"
s += f"\n\r{t[0]: >30} : {currency_format(t[1]): >9}"
s += f"\n\r{'Total Amount :', 32} {round(voucher.amount): >9.2f}"
s += f"\n\r{'Total Amount :': >32} {currency_format(round(voucher.amount)): >9}"
s += "\n\r" + "-" * 42
if voucher.voucher_type != VoucherType.REGULAR_BILL:
+5 -2
View File
@@ -1,4 +1,5 @@
import asyncio
import locale
import uuid
from datetime import datetime
@@ -10,9 +11,11 @@ from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
from ..models.auth import Device
from ..models.master import Printer, SectionPrinter
from . import currency_format
def print_cashier_report(report: CashierReport, device_id: uuid.UUID, db: Session):
locale.setlocale(locale.LC_MONETARY, "en_IN")
data = design_cashier_report(report)
section_id = db.query(Device.section_id).filter(Device.id == device_id).scalar()
printer = (
@@ -33,13 +36,13 @@ def design_cashier_report(report: CashierReport):
s += "\n\r" f"{report.start_date:%d-%b-%Y} To {report.finish_date:%d-%b-%Y}".center(42)
s += "\n\r" + "-" * 42
for item in report.amounts:
s += f"\n\r{item.name} : {item.amount: >26.2f}"
s += f"\n\r{item.name} : {currency_format(item.amount): >26}"
s += "\n\r" + "=" * 42
s += f"\n\rActive Cashiers : {report.cashiers}"
for so, it in report.info.items():
s += f"\n\r" f"--- {so} ".ljust(42, "-")
for i in it:
s += f"\n\r{i.date} {i.bill_id} {i.customer}"
s += f"\n\rAmount: {i.amount:9.2f}"
s += f"\n\rAmount: {currency_format(i.amount):9}"
s += "\n\r" + "-" * 42
return s
+4 -1
View File
@@ -1,4 +1,5 @@
import asyncio
import locale
import uuid
from arq import ArqRedis, create_pool
@@ -8,9 +9,11 @@ from ..core.arq import settings as redis_settings
from ..models.auth import Device
from ..models.master import Printer, SectionPrinter
from ..schemas.discount_report import DiscountReport
from . import currency_format
def print_discount_report(report: DiscountReport, device_id: uuid.UUID, db: Session):
locale.setlocale(locale.LC_MONETARY, "en_IN")
data = design_discount_report(report)
section_id = db.query(Device.section_id).filter(Device.id == device_id).scalar()
printer = (
@@ -30,6 +33,6 @@ def design_discount_report(report: DiscountReport):
s += "\n\r" + "-" * 42
s += f"\n\r{'Name': <29.29} {'Amount': >12.12}"
for item in report.amounts:
s += f"\n\r{item.name: <29.29} {item.amount: >12.2f}"
s += f"\n\r{item.name: <29.29} {currency_format(item.amount): >12}"
s += "\n\r" + "=" * 42
return s
+4 -1
View File
@@ -1,4 +1,5 @@
import asyncio
import locale
import uuid
from datetime import datetime
@@ -10,9 +11,11 @@ from ..core.arq import settings as redis_settings
from ..models.auth import Device
from ..models.master import Printer, SectionPrinter
from ..schemas.sale_report import SaleReport
from . import currency_format
def print_sale_report(report: SaleReport, device_id: uuid.UUID, db: Session):
locale.setlocale(locale.LC_MONETARY, "en_IN")
data = design_sale_report(report)
section_id = db.query(Device.section_id).filter(Device.id == device_id).scalar()
printer = (
@@ -33,6 +36,6 @@ def design_sale_report(report: SaleReport):
s += "\n\r" f"{report.start_date:%d-%b-%Y} To {report.finish_date:%d-%b-%Y}".center(42)
s += "\n\r" + "-" * 42
for item in report.amounts:
s += f"\n\r{item.name: <29.22} {item.amount: >12.2f}"
s += f"\n\r{item.name: <29.22} {currency_format(item.amount): >12}"
s += "\n\r" + "=" * 42
return s
+26
View File
@@ -1,3 +1,4 @@
import re
import uuid
from decimal import Decimal
@@ -35,6 +36,11 @@ def save(
):
try:
item = Tax(name=data.name, rate=round(data.rate / Decimal(100), 4))
if not name_valid(data.name):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="The name is not valid",
)
db.add(item)
db.commit()
return tax_info(item)
@@ -63,6 +69,11 @@ def update(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
if not name_valid(data.name):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="The name is not valid",
)
item.name = data.name
item.rate = round(data.rate / Decimal(100), 4)
db.commit()
@@ -147,3 +158,18 @@ def tax_info(item: Optional[Tax]):
"isFixture": item.is_fixture,
}
return tax
def name_valid(name: str) -> bool:
items = name.split(";")
if len(items) == 1:
return True
total = 0
for i, item in enumerate(it.strip() for it in items):
match = re.match(r"(^.*)\s+\((.*?)/(.*?)\)[^(]*$", item)
if not match or len(match.groups()) != 3:
return False
total += round(Decimal(match.group(2)) / Decimal(match.group(3)), 5)
if round(total, 2) != 1:
return False
return True
+2 -1
View File
@@ -15,7 +15,8 @@ from ...core.security import get_current_active_user as get_user
from ...db.session import SessionLocal
from ...models.master import ProductVersion, VoucherType
from ...models.voucher import GuestBook, Inventory, InventoryModifier, Kot, Voucher
from ...printing import print_bill, print_kot
from ...printing.bill import print_bill
from ...printing.kot import print_kot
from ...routers.voucher import (
check_permissions,
do_update_settlements,
+2 -1
View File
@@ -15,7 +15,8 @@ from ...core.security import get_current_active_user as get_user
from ...db.session import SessionLocal
from ...models.master import ProductVersion, VoucherType
from ...models.voucher import Inventory, InventoryModifier, Kot, Voucher
from ...printing import print_bill, print_kot
from ...printing.bill import print_bill
from ...printing.kot import print_kot
from ...routers.voucher import (
check_permissions,
do_update_settlements,
+1 -1
View File
@@ -25,7 +25,7 @@ export class AuthGuard implements CanActivate {
return false;
}
if (permission !== undefined && !this.authService.allowed(permission)) {
this.toaster.show('Danger', 'You do not have the permission to access this area.');
this.toaster.show('Error', 'You do not have the permission to access this area.');
return false;
}
// logged in so return true
+1 -1
View File
@@ -69,7 +69,7 @@ export class LoginComponent implements OnInit, AfterViewInit {
this.showOtp = true;
this.clientId = this.cs.getCookie('client_id');
}
this.toaster.show('Danger', error.error.details);
this.toaster.show('Error', error.error.details);
},
);
}
@@ -77,7 +77,7 @@ export class CashierReportComponent implements OnInit {
this.toaster.show('', 'Successfully Printed');
},
(error) => {
this.toaster.show('Error', error.error);
this.toaster.show('Error', error);
},
);
}
+1 -1
View File
@@ -40,7 +40,7 @@ export class ErrorInterceptor implements HttpInterceptor {
}
// auto logout if 401 response returned from api
this.authService.logout();
this.toaster.show('Danger', 'User has been logged out');
this.toaster.show('Error', 'User has been logged out');
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '250px',
data: {
@@ -68,7 +68,7 @@ export class DeviceDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/devices');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -80,7 +80,7 @@ export class DeviceDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/devices');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -75,7 +75,7 @@ export class DiscountReportComponent implements OnInit {
this.toaster.show('', 'Successfully Printed');
},
(error) => {
this.toaster.show('Error', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -64,7 +64,7 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/guest-book');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -67,7 +67,7 @@ export class GuestBookListComponent implements OnInit {
this.router.navigateByUrl('/guest-book');
},
(error) => {
this.toaster.show('Danger', error);
this.toaster.show('Error', error);
},
);
}
@@ -62,7 +62,7 @@ export class HeaderFooterComponent implements OnInit {
this.showItem(result);
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -65,7 +65,7 @@ export class MenuCategoryDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/menu-categories');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -77,7 +77,7 @@ export class MenuCategoryDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/menu-categories');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -51,7 +51,7 @@ export class MenuCategoryListComponent implements OnInit {
this.toaster.show('Success', '');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -107,7 +107,7 @@ export class ModifierCategoryDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/modifier-categories');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -119,7 +119,7 @@ export class ModifierCategoryDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/modifier-categories');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -72,7 +72,7 @@ export class ModifierDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/modifiers');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -84,7 +84,7 @@ export class ModifierDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/modifiers');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -65,7 +65,7 @@ export class PrinterDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/printers');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -77,7 +77,7 @@ export class PrinterDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/printers');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -87,7 +87,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/products');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -99,7 +99,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/products');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -73,7 +73,7 @@ export class ProductListComponent implements OnInit {
this.loadData(result, this.menuCategories);
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -66,7 +66,7 @@ export class RoleDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/roles');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -78,7 +78,7 @@ export class RoleDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/roles');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -66,7 +66,7 @@ export class SaleCategoryDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/sale-categories');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -78,7 +78,7 @@ export class SaleCategoryDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/sale-categories');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -75,7 +75,7 @@ export class SaleReportComponent implements OnInit {
this.toaster.show('', 'Successfully Printed');
},
(error) => {
this.toaster.show('Error', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -84,7 +84,7 @@ export class SectionPrinterComponent implements OnInit {
this.showItem(result);
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -95,7 +95,7 @@ export class SectionPrinterComponent implements OnInit {
this.toaster.show('Success', '');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -61,7 +61,7 @@ export class SectionDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/sections');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -73,7 +73,7 @@ export class SectionDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/sections');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -77,7 +77,7 @@ export class SettleOptionDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/settle-options');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -89,7 +89,7 @@ export class SettleOptionDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/settle-options');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -70,7 +70,7 @@ export class TableDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/tables');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -82,7 +82,7 @@ export class TableDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/tables');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -49,7 +49,7 @@ export class TableListComponent implements OnInit {
this.toaster.show('Success', '');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -63,7 +63,7 @@ export class TaxDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/taxes');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -75,7 +75,7 @@ export class TaxDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/taxes');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -129,7 +129,7 @@ export class UpdateProductPricesComponent implements OnInit {
this.loadData(result);
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -78,7 +78,7 @@ export class UserDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/users');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}
@@ -90,7 +90,7 @@ export class UserDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/users');
},
(error) => {
this.toaster.show('Danger', error.error);
this.toaster.show('Error', error);
},
);
}