from __future__ import annotations
import base64
import json
import logging
from decimal import Decimal
from io import BytesIO
from typing import TYPE_CHECKING
from typing import Iterable
from typing import TypedDict
import qrcode
from django.core.paginator import Paginator
from django_renderpdf.helpers import render_pdf
if TYPE_CHECKING:
from typing import IO
from PIL.Image import Image
from django_afip.models import Receipt
from django_afip.models import ReceiptEntry
from django_afip.models import ReceiptPDF
logger = logging.getLogger(__name__)
class ReceiptQrCode:
"""A QR code for receipt
See: https://www.afip.gob.ar/fe/qr/especificaciones.asp"""
BASE_URL = "https://www.afip.gob.ar/fe/qr/?p="
def __init__(self, receipt: Receipt) -> None:
self._receipt = receipt
# The examples on the website say that "importe" and "ctz" are both "decimal"
# type. JS/JSON has no decimal type. The examples use integeres.
#
# Using integers would drop cents, which would result in mismatching
# information. Using strings would add quotes, which likely breaks.
#
# Using floats seems to be the only viable solution, and SHOULD be fine for
# values in the range supported.
self._data = {
"ver": 1,
"fecha": receipt.issued_date.strftime("%Y-%m-%d"),
"cuit": receipt.point_of_sales.owner.cuit,
"ptoVta": receipt.point_of_sales.number,
"tipoCmp": int(receipt.receipt_type.code),
"nroCmp": receipt.receipt_number,
"importe": float(receipt.total_amount),
"moneda": receipt.currency.code,
"ctz": float(receipt.currency_quote),
"tipoDocRec": int(receipt.document_type.code),
"nroDocRec": receipt.document_number,
"tipoCodAut": "E", # TODO: need to implement CAEA
"codAut": int(receipt.validation.cae),
}
def as_png(self) -> Image:
json_data = json.dumps(self._data)
encoded_json = base64.b64encode(json_data.encode()).decode()
url = f"{self.BASE_URL}{encoded_json}"
qr = qrcode.QRCode(version=1, border=4)
qr.add_data(url)
qr.make()
return qr.make_image()
def get_encoded_qrcode(receipt_pdf: ReceiptPDF) -> str:
"""Return a QRCode encoded for embeding in HTML."""
img_data = BytesIO()
qr_img = ReceiptQrCode(receipt_pdf.receipt).as_png()
qr_img.save(img_data, format="PNG")
return base64.b64encode(img_data.getvalue()).decode()
# Note: When updating this, be sure to update the docstring of the method that uses
# these below.
TEMPLATE_NAMES = [
"receipts/{taxpayer}/pos_{point_of_sales}/code_{code}.html",
"receipts/{taxpayer}/code_{code}.html",
"receipts/code_{code}.html",
"receipts/{code}.html",
]
class EntriesForPage(TypedDict):
previous_subtotal: Decimal
subtotal: Decimal
entries: Iterable[ReceiptEntry]
def create_entries_context_for_render(
paginator: Paginator,
) -> dict[int, EntriesForPage]:
entries: dict[int, EntriesForPage] = {}
subtotal = Decimal(0)
for i in paginator.page_range:
previous_subtotal = subtotal
page = paginator.get_page(i)
for entry in page.object_list:
subtotal += entry.total_price
entries[i] = {
"previous_subtotal": previous_subtotal,
"subtotal": subtotal,
"entries": paginator.get_page(i).object_list,
}
return entries
[docs]
class PdfBuilder:
"""Builds PDF files for Receipts.
Creating a new instance of a builder does nothing; use :meth:`~render_pdf` to
actually render the file.
This type can be subclassed to add custom behaviour or data into PDF files.
"""
[docs]
def __init__(self, entries_per_page: int = 15) -> None:
self.entries_per_page = entries_per_page
[docs]
def get_template_names(self, receipt: Receipt) -> list[str]:
"""Return the templates use to render the Receipt PDF.
Template discovery tries to find any of the below receipts::
receipts/{taxpayer}/pos_{point_of_sales}/code_{code}.html
receipts/{taxpayer}/code_{code}.html
receipts/code_{code}.html
receipts/{code}.html
To override, for example, the "Factura C" template for point of sales 0002 for
Taxpayer 20-32964233-0, use::
receipts/20329642330/pos_2/code_6.html
"""
return [
template.format(
taxpayer=receipt.point_of_sales.owner.cuit,
point_of_sales=receipt.point_of_sales.number,
code=receipt.receipt_type.code,
)
for template in TEMPLATE_NAMES
]
[docs]
def get_context(self, receipt: Receipt) -> dict:
"""Returns the context used to render the PDF file."""
from django_afip.models import Receipt
from django_afip.models import ReceiptPDF
context: dict = {}
receipt_pdf = (
ReceiptPDF.objects.select_related(
"receipt",
"receipt__receipt_type",
"receipt__document_type",
"receipt__validation",
"receipt__point_of_sales",
"receipt__point_of_sales__owner",
)
.prefetch_related(
"receipt__entries",
)
.get(receipt=receipt)
)
# Prefetch required data in a single query:
receipt_pdf.receipt = (
Receipt.objects.select_related(
"receipt_type",
"document_type",
"validation",
"point_of_sales",
"point_of_sales__owner",
)
.prefetch_related(
"entries",
)
.get(
pk=receipt_pdf.receipt_id,
)
)
taxpayer = receipt_pdf.receipt.point_of_sales.owner
paginator = Paginator(receipt_pdf.receipt.entries.all(), self.entries_per_page)
context["entries"] = create_entries_context_for_render(paginator)
context["pdf"] = receipt_pdf
context["taxpayer"] = taxpayer
context["qrcode"] = get_encoded_qrcode(receipt_pdf)
return context
[docs]
def render_pdf(self, receipt: Receipt, file_: IO) -> None:
"""Renders the PDF into ``file_``."""
render_pdf(
template=self.get_template_names(receipt),
file_=file_,
context=self.get_context(receipt),
)