Вступление
Формат переносимого документа (PDF) не является форматом WYSIWYG («Что видишь, то и получаешь»). Он был разработан как платформенно-независимый, независимый от базовой операционной системы и механизмов рендеринга.
Чтобы достичь этого, PDF был создан для взаимодействия с помощью чего-то, больше похожего на язык программирования, и полагается на серию инструкций и операций для достижения результата. Фактически, PDF основан на языке сценариев - PostScript , который был первым независимым от устройства языком описания страниц .
В этом руководстве мы будем использоватьpText
- библиотеку Python, предназначенную для чтения, управления и создания PDF-документов. Он предлагает как низкоуровневую модель (позволяющую получить доступ к точным координатам и макету, если вы решите их использовать), так и высокоуровневую модель (где вы можете делегировать точные расчеты полей, позиций и т. Д. Менеджеру по макету) .
Мы рассмотрим, как создать счет-фактуру в формате PDF на Python с помощью pText .
Установка pText
pText можно загрузить из источника
наGitHub или
установить через pip
:
$ pip install ptext-joris-schellekens
Создание счета в формате PDF на Python с помощью pText
pText имеет два интуитивно понятных ключевых класса - Document
и
Page
, которые представляют документ и страницы в нем. Кроме того,
PDF
представляет собой API для загрузки и сохранения Document
нами
документов.
Давайте создадим Document()
и Page()
как пустой холст, на который мы
можем добавить счет-фактуру:
from ptext.pdf.document import Document
from ptext.pdf.page.page import Page
# Create document
pdf = Document()
# Add page
page = Page()
pdf.append_page(page)
Поскольку мы не хотим заниматься вычислением координат - мы можем
делегировать это PageLayout
который управляет всем контентом и его
позициями:
# New imports
from ptext.pdf.canvas.layout.page_layout import SingleColumnLayout
from ptext.io.read.types import Decimal
page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)
Здесь мы используем SingleColumnLayout
поскольку все содержимое должно
быть в одном столбце - у нас не будет левой и правой стороны счета.
Здесь мы также уменьшаем вертикальное поле. Значение по умолчанию -
обрезать верхние 10% высоты страницы в качестве поля, и мы уменьшаем его
до 2%, поскольку мы хотим использовать это пространство для логотипа /
названия компании.
Кстати, давайте добавим на макет логотип компании:
# New import
from ptext.pdf.canvas.layout.image import Image
page_layout.add(
Image(
"https://s3.amazonaws.com/s3.stackabuse.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",
width=Decimal(128),
height=Decimal(128),
))
Здесь мы добавляем элемент в макет - Image()
. Через его конструктор
мы добавляем URL-адрес, указывающий на ресурс изображения, и
устанавливаем его width
и height
.
Под изображением мы хотим добавить информацию о нашей воображаемой компании (имя, адрес, веб-сайт, телефон), а также информацию о счете (номер счета, дату, срок оплаты). Распространенным форматом для краткости (который, кстати, также делает код более чистым) является использование таблицы для хранения данных счетов. Давайте создадим отдельный вспомогательный метод для создания информации о счете в таблице, которую мы затем можем использовать, чтобы просто добавить таблицу в счет в нашем основном методе:
# New imports
from ptext.pdf.canvas.layout.table import Table
from ptext.pdf.canvas.layout.paragraph import Paragraph, Alignment
from datetime import datetime
import random
def _build_invoice_information():
table_001 = Table(number_of_rows=5, number_of_columns=3)
table_001.add(Paragraph("[Street Address]"))
table_001.add(Paragraph("Date", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))
now = datetime.now()
table_001.add(Paragraph("%d/%d/%d" % (now.day, now.month, now.year)))
table_001.add(Paragraph("[City, State, ZIP Code]"))
table_001.add(Paragraph("Invoice #", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))
table_001.add(Paragraph("%d" % random.randint(1000, 10000)))
table_001.add(Paragraph("[Phone]"))
table_001.add(Paragraph("Due Date", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))
table_001.add(Paragraph("%d/%d/%d" % (now.day, now.month, now.year)))
table_001.add(Paragraph("[Email Address]"))
table_001.add(Paragraph(" "))
table_001.add(Paragraph(" "))
table_001.add(Paragraph("[Company Website]"))
table_001.add(Paragraph(" "))
table_001.add(Paragraph(" "))
table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))
table_001.no_borders()
return table_001
Здесь мы делаем простую Table
с 5 строками и 3 столбцами. Строки
соответствуют почтовому адресу, городу / области, телефону, адресу
электронной почты и веб-сайту компании. Каждая строка будет иметь 0..3
значений (столбцов). Каждый текстовый элемент добавляется как
Paragraph
, который мы выровняли по правому краю с помощью
Alignment.RIGHT
и принимаем аргументы стиля, такие как font
.
Наконец, мы добавили отступы ко всем ячейкам, чтобы убедиться, что мы не размещаем текст неудобно рядом с путями ячеек.
Теперь, вернувшись к нашему основному методу, мы можем вызвать
_build_invoice_information()
чтобы заполнить таблицу и добавить ее в
наш макет:
page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)
page_layout.add(
Image(
"https://s3.amazonaws.com/s3.stackabuse.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",
width=Decimal(128),
height=Decimal(128),
))
# Invoice information table
page_layout.add(_build_invoice_information())
# Empty paragraph for spacing
page_layout.add(Paragraph(" "))
Теперь давайте быстро создадим этот PDF-документ, чтобы увидеть, как он
выглядит. Для этого воспользуемся модулем PDF
# New import
from ptext.pdf.pdf import PDF
with open("output.pdf", "wb") as pdf_file_handle:
PDF.dumps(pdf_file_handle, document)
{.ezlazyload}
Большой! Теперь мы также хотим добавить информацию о выставлении счетов и доставке. Его удобно разместить в таблице, как и информацию о компании. Для краткости мы также предпочтем создать отдельную вспомогательную функцию для создания этой информации, а затем мы можем просто добавить ее в наш основной метод:
# New imports
from ptext.pdf.canvas.color.color import HexColor, X11Color
def _build_billing_and_shipping_information():
table_001 = Table(number_of_rows=6, number_of_columns=2)
table_001.add(
Paragraph(
"BILL TO",
background_color=HexColor("263238"),
font_color=X11Color("White"),
)
)
table_001.add(
Paragraph(
"SHIP TO",
background_color=HexColor("263238"),
font_color=X11Color("White"),
)
)
table_001.add(Paragraph("[Recipient Name]")) # BILLING
table_001.add(Paragraph("[Recipient Name]")) # SHIPPING
table_001.add(Paragraph("[Company Name]")) # BILLING
table_001.add(Paragraph("[Company Name]")) # SHIPPING
table_001.add(Paragraph("[Street Address]")) # BILLING
table_001.add(Paragraph("[Street Address]")) # SHIPPING
table_001.add(Paragraph("[City, State, ZIP Code]")) # BILLING
table_001.add(Paragraph("[City, State, ZIP Code]")) # SHIPPING
table_001.add(Paragraph("[Phone]")) # BILLING
table_001.add(Paragraph("[Phone]")) # SHIPPING
table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))
table_001.no_borders()
return table_001
Мы установили background_color
начальных абзацев на #263238
(серо-синий), чтобы соответствовать цвету логотипа, и font_color
на
White
.
Назовем это также и в основном методе:
# Invoice information table
page_layout.add(_build_invoice_information())
# Empty paragraph for spacing
page_layout.add(Paragraph(" "))
# Billing and shipping information table
page_layout.add(_build_billing_and_shipping_information())
Как только мы снова запустим скрипт, это приведет к созданию нового PDF-файла, содержащего дополнительную информацию:
{.ezlazyload}
Когда наша основная информация разобрана (информация о компании и информация о выставлении счетов / доставке), мы хотим добавить подробное описание. Это будут товары / услуги, которые наша предполагаемая компания предложила кому-то и которые также обычно выполняются в виде таблицы под информацией, которую мы уже добавили.
Опять же, давайте создадим вспомогательную функцию, которая генерирует таблицу и заполняет ее данными, которые мы можем просто добавить в наш макет позже:
# New import
from ptext.pdf.canvas.layout.table import Table, TableCell
def _build_itemized_description_table(self):
table_001 = Table(number_of_rows=15, number_of_columns=4)
for h in ["DESCRIPTION", "QTY", "UNIT PRICE", "AMOUNT"]:
table_001.add(
TableCell(
Paragraph(h, font_color=X11Color("White")),
background_color=HexColor("016934"),
)
)
odd_color = HexColor("BBBBBB")
even_color = HexColor("FFFFFF")
for row_number, item in enumerate([("Product 1", 2, 50), ("Product 2", 4, 60), ("Labor", 14, 60)]):
c = even_color if row_number % 2 == 0 else odd_color
table_001.add(TableCell(Paragraph(item[0]), background_color=c))
table_001.add(TableCell(Paragraph(str(item[1])), background_color=c))
table_001.add(TableCell(Paragraph("$ " + str(item[2])), background_color=c))
table_001.add(TableCell(Paragraph("$ " + str(item[1] * item[2])), background_color=c))
# Optionally add some empty rows to have a fixed number of rows for styling purposes
for row_number in range(3, 10):
c = even_color if row_number % 2 == 0 else odd_color
for _ in range(0, 4):
table_001.add(TableCell(Paragraph(" "), background_color=c))
table_001.add(TableCell(Paragraph("Subtotal", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT,), col_span=3,))
table_001.add(TableCell(Paragraph("$ 1,180.00", horizontal_alignment=Alignment.RIGHT)))
table_001.add(TableCell(Paragraph("Discounts", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT,),col_span=3,))
table_001.add(TableCell(Paragraph("$ 177.00", horizontal_alignment=Alignment.RIGHT)))
table_001.add(TableCell(Paragraph("Taxes", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT), col_span=3,))
table_001.add(TableCell(Paragraph("$ 100.30", horizontal_alignment=Alignment.RIGHT)))
table_001.add(TableCell(Paragraph("Total", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT ), col_span=3,))
table_001.add(TableCell(Paragraph("$ 1163.30", horizontal_alignment=Alignment.RIGHT)))
table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))
table_001.no_borders()
return table_001
На практике вы бы заменили жестко запрограммированные строки, связанные
с промежуточным итогом, налогами и общими ценами, расчетами фактических
цен - хотя это в значительной степени зависит от базовой реализации
ваших Product
, поэтому мы добавили подставку. в для абстракции. Как
только мы добавим и эту таблицу в документ, мы сможем перестроить ее и
посмотреть.
Теперь весь основной метод должен выглядеть примерно так:
# Create document
pdf = Document()
# Add page
page = Page()
pdf.append_page(page)
page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)
page_layout.add(
Image(
"https://s3.amazonaws.com/s3.stackabuse.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",
width=Decimal(128),
height=Decimal(128),
))
# Invoice information table
page_layout.add(_build_invoice_information())
# Empty paragraph for spacing
page_layout.add(Paragraph(" "))
# Billing and shipping information table
page_layout.add(_build_billing_and_shipping_information())
# Itemized description
page_layout.add(_build_itemized_description_table())
with open("output2.pdf", "wb") as pdf_file_handle:
PDF.dumps(pdf_file_handle, pdf)
Выполнение этого фрагмента кода приводит к:
{.ezlazyload}
Создание контура
Наш PDF-файл готов и готов к работе, но мы можем поднять его еще выше, добавив два небольших дополнения. Во-первых, мы можем добавить Outline , который помогает читателям, таким как Adobe, перемещаться и создавать меню для ваших PDF-файлов:
# New import
from ptext.pdf.page.page import DestinationType
# Outline
pdf.add_outline("Your Invoice", 0, DestinationType.FIT, page_nr=0)
Функция add_outline()
принимает несколько аргументов:
title
: заголовок, который будет отображаться в боковом менюlevel
: насколько глубоко в дереве что-то будет. Уровень 0 - это корневой уровень.- Несколько аргументов, составляющих «пункт назначения»
Пункты назначения можно рассматривать как цели для гиперссылок. Вы можете ссылаться на всю страницу (это то, что мы делаем в этом примере), но вы также можете ссылаться на определенные части страницы (например, точно по координате Y 350 ).
Кроме того, вам нужно указать, как читатель должен представлять эту страницу - например, вы хотите просто прокрутить до этой страницы, а не увеличивать масштаб? Вы хотите отображать только целевую область с полностью увеличенным масштабом читателя в этой конкретной области?
В этой строке кода мы просим читателя отобразить страницу 0 (первую страницу) и убедиться, что она соответствует окну читателя (при необходимости увеличивая / уменьшая масштаб).
После того, как вы добавили схему, вы должны увидеть ее в выбранной вами программе чтения:
{.ezlazyload}
С несколькими страницами - вы можете создать более сложную схему и
ссылаться на них через add_outline()
для упрощения навигации.
Встраивание документов JSON в счета-фактуры PDF
Поскольку PDF-файлы не очень удобны для компьютера (с точки зрения чтения и однозначного декодирования), иногда нам может потребоваться добавить более удобные для компьютера форматы, если кто-то хочет обрабатывать счета автоматически.
Созданный в Германии стандарт счетов-фактур под названием ZUGFeRD (позже принятый ЕС) позволяет нам составлять счета-фактуры в формате PDF с использованием более удобочитаемых компьютерных форматов файлов, таких как XML, который описывает счет и легко поддается синтаксическому анализу. В дополнение к этому вы также можете встроить другие документы, относящиеся к вашему счету, такие как условия и соглашения, политика возврата и т. Д.
Чтобы встроить какой-либо дополнительный файл в файл PDF с помощью pText, мы можем использовать
append_embedded_file()
.
Давайте сначала создадим словарь для хранения данных счета в JSON,
который мы затем сохраним в файл invoice_json
import json
# Creating a JSON file
invoice_json = {
"items": [
{
"Description": "Product1",
"Quantity": 2,
"Unit Price": 50,
"Amount": 100,
},
{
"Description": "Product2",
"Quantity": 4,
"Unit Price": 60,
"Amount": 100,
},
{
"Description": "Labor",
"Quantity": 14,
"Unit Price": 60,
"Amount": 100,
},
],
"Subtotal": 1180,
"Discounts": 177,
"Taxes": 100.30,
"Total": 1163.30,
}
invoice_json_bytes = bytes(json.dumps(invoice_json, indent=4), encoding="latin1")
Теперь мы можем просто вставить этот файл в наш счет в формате PDF:
pdf.append_embedded_file("invoice.json", invoice_json_bytes, apply_compression=False)
Как только мы снова запустим скрипт и сохраним документ, мы идем:
{.ezlazyload}
Заключение
В этом руководстве мы рассмотрели, как создать счет в Python с помощью pText. Затем мы добавили схему в файл PDF для упрощения навигации и рассмотрели, как добавлять вложения / встроенные файлы для программного доступа к содержимому PDF.