Создание счетов-фактур в формате PDF на Python с помощью pText

Введение Формат переносимого документа (PDF) не является форматом WYSIWYG («Что видишь, то и получаешь»). Он был разработан как платформенно-независимый, независимый от базовой операционной системы и механизмов рендеринга. Чтобы достичь этого, PDF был создан для взаимодействия с помощью чего-то, больше похожего на язык программирования, и полагается на серию инструкций и операций для достижения результата. Фактически, PDF основан на языке сценариев - PostScript [https://www.adobe.com/products/postscr

Вступление

Формат переносимого документа (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) 

ptext счет-фактура1{.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-файла, содержащего дополнительную информацию:

ptext счет-фактура2{.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) 

Выполнение этого фрагмента кода приводит к:

ptext счет-фактура3{.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 (первую страницу) и убедиться, что она соответствует окну читателя (при необходимости увеличивая / уменьшая масштаб).

После того, как вы добавили схему, вы должны увидеть ее в выбранной вами программе чтения:

ptext счет-фактура4{.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) 

Как только мы снова запустим скрипт и сохраним документ, мы идем:

ptext счет-фактура5{.ezlazyload}

Заключение

В этом руководстве мы рассмотрели, как создать счет в Python с помощью pText. Затем мы добавили схему в файл PDF для упрощения навигации и рассмотрели, как добавлять вложения / встроенные файлы для программного доступа к содержимому PDF.

comments powered by Disqus