The Jerusalem PostTucker Carlson claims UK had no reason to join WWII, appears to defend Hitler's invasion of PolandInquirerP1M in contraband cigarettes intercepted at Zamboanga Sibugay checkpointESPN DeportesModric tras goleada: "Me siento avergonzado"ESPNQB Taylor 'day-to-day' after Heisman contender struggles vs. TideRTP DesportoMovimento liderado por ex-candidato ao Vitória de Guimarães pede auditoria à SADColliderNear-Perfect Zombie Classic Officially Rises From the Dead on StreamingZDF heuteAktuelle Pressemitteilungen des ZDFسكاي نيوز عربيةإسبانيا تضرب التشيك بثلاثية وتنفرد بالصدارةn-tv"Absurde" Aussagen vom Kremlchef: ISW: Russland verliert mehr Territorium als es gewinnt - und Putin halluziniertCBS SportsUFC 332 -- Natalia Silva vs. Wang Cong: Fight card, odds, date, location and more for Salt Lake CityBBC Sport'It means everything' - numbers behind Robertson's 100th cap Scotland careerynetכולם מתנערים, משרד התחבורה "לא משך נתונים שנתיים" ומגיב: "בטוח לטוס? אין 100%"
The Daily Newsstand · Free, Always
Saturday, October 3, 2026

Отчёты на DOCX в FARA CRM: шаблоны в Word, редактор в браузере и PDF через LibreOffice

Translate

Счёт на оплату, договор, акт, отчёт о продажах за месяц — без документов CRM остаётся записной книжкой. В FARA CRM мы закрыли эту задачу отчётами на DOCX. Шаблон — обычный файл Word с тегами Jinja, данные — функция на Python, результат — DOCX или PDF во вложении к записи.

Поверх этого выросли редактор DOCX прямо в браузере, конструктор шаблонов с каталогом полей и превью PDF на живой записи, а также рассылка отчётов по расписанию. PDF по умолчанию собирает LibreOffice в контейнере, а без него работает запасной движок на чистом Python.

Ниже — почему мы выбрали DOCX, как устроены шаблоны и данные, как получаем PDF, какой редактор встроили в браузер и на какие грабли наступили по дороге.

Почему DOCX

Главный аргумент — документы в бизнесе уже живут в Word. У бухгалтера и юриста есть готовые счёт, договор и допсоглашение, и правят их они сами. Если шаблон — это DOCX, новая формулировка в договоре не требует разработчика и релиза.

Второй аргумент — результат тоже DOCX. Менеджер скачивает договор, поправляет пункт под клиента и только потом отправляет. Когда нужен неизменяемый документ, тот же шаблон отдаёт PDF.

Шаблонизатор — docxtpl, то есть Jinja2 внутри DOCX. Теги {{ }}, {% if %} и {% for %} пишутся прямо в тексте документа, а {%tr for %} размножает строку таблицы. Разработчику не нужно учить новый язык, а пользователь видит в Word обычный текст с фигурными скобками.

Альтернативы есть, но они дороже в поддержке. В схеме HTML → PDF каждую правку шаблона делает разработчик, а дизайнеры отчётов вроде JasperReports требуют отдельной программы и своего рантайма.

Цена выбора — PDF. DOCX не хранит вёрстку страниц: переносы строк и страниц вычисляет тот, кто документ открывает. Значит, на сервере нужен движок, который верстает как Word, — про него отдельный раздел ниже.

Как это устроено

Отчёт в FARA CRM — это три вещи: DOCX-шаблон с тегами, функция на Python, которая собирает данные, и запись с настройками. В настройках указаны модель, формат по умолчанию и тип: документ по записи, как счёт по заказу, или сводный отчёт, как продажи за неделю.

Кнопка «Печать» на форме, рассылка по расписанию и превью в конструкторе вызывают один и тот же метод. Он собирает данные, подставляет их в шаблон и при необходимости делает PDF, поэтому документ везде выглядит одинаково.

Новый канал доставки — это ещё один вход: сборку писать заново не нужно.

Код разложен по трём модулям. Ядро report_docx — это движок и кнопка «Печать», sales_report_docx — функции данных и образцы документов по продажам, report_docx_design — конструктор. Без конструктора всё работает, просто шаблоны правят в Word и загружают файлом.

Шаблоны: Jinja внутри Word

Шаблон — обычный DOCX, а теги в нём — просто текст. Вот начало образца «Отчёт по продажам за период» так, как его видно в Word:

Отчёт по продажам за период
Период: с {{ date_from }} по {{ date_to }}
Сделок: {{ count }}, на сумму {{ total }} руб.

Таблица сделок в том же шаблоне:

№

Заказ

Дата

Клиент

Менеджер

Сумма

{%tr for row in rows %}

{{ loop.index }}

{{ row.name }}

{{ row.date }}

{{ row.partner }}

{{ row.user }}

{{ row.amount }}

{%tr endfor %}

Префикс tr — расширение docxtpl: тег управляет всей строкой таблицы, а не абзацем в ячейке. Строки с for и endfor исчезают из результата, а строка между ними повторяется для каждой сделки. В договорах тем же способом скрываются пустые реквизиты: {% if c_ogrn %}…{% endif %}.

Даты и деньги форматируют фильтры: {{ amount_total|money }} превращается в «1 234 567,80», а {{ date_order|date }} — в «30.09.2026». Печати и подписи — это картинки-заглушки в шаблоне, которые при сборке подменяются настоящими.

Данные: поля записи плюс функция на Python

Для документа по записи большая часть данных берётся сама: в шаблон попадают все поля записи, связи на один уровень вглубь ({{ partner_id.name }}) и строки таблиц вроде позиций заказа. Поля, закрытые правами доступа, в шаблон не попадают.

Функция данных нужна для вычисляемого: НДС, суммы прописью, реквизитов и сводок. Это @staticmethod вида async def name(env, **params) -> dict на классе модели, и ключи словаря, который она возвращает, — это теги шаблона. Предметный модуль добавляет её к чужой модели через @extend(Sale), не трогая модуль продаж.

Вот упрощённая функция сводного отчёта за период. Записи у неё нет, есть параметр days из cron-задачи или из запроса:

@extend(Sale)
class SaleReportMixin:
    @staticmethod
    async def sales_period_data(env, days: int = 30) -> dict:
        date_from = datetime.now(timezone.utc) - timedelta(days=days)
        sales = await env.models.sale.search(
            filter=[("active", "=", True), ("date_order", ">=", date_from)],
            fields=["id", "name", "date_order", "amount_total", "partner_id", "user_id"],
        )
        rows = [...]     # строка на каждую сделку
        by_user = [...]  # итоги по менеджерам
        return {"count": len(rows), "total": ..., "rows": rows, "by_user": by_user}

Ключи словаря снаружи не видны, пока функцию не запустишь. Поэтому их описывает декоратор @report_fields: подпись, тип вроде «деньги» или «дата» и поля списков. По этим ключам конструктор показывает поля в каталоге, а движок сам решает, какую функцию вызвать: если в шаблоне стоит {{ bik }}, вызывается функция, которая объявила bik.

PDF: LibreOffice и запасной движок на чистом Python

Первая версия умела PDF только через LibreOffice: нет его на сервере — нет и PDF. А LibreOffice Writer добавляет к образу около 400 МБ. Поэтому мы написали запасной конвертер на python-docx и fpdf2, а потом всё равно включили LibreOffice в Docker-образ по умолчанию. Движок выбирается сам:

Режим

Когда включается

Что даёт

LibreOffice headless

В системе нашлись libreoffice или soffice

Вёрстку как в Word; образ тяжелее примерно на 400 МБ

Встроенный: python-docx + fpdf2

LibreOffice не найден

Текст, списки, таблицы, картинки, печати; без колонтитулов, цветов, фигур и обтекания

В Docker-образе LibreOffice включён по умолчанию, а выключается аргументом сборки WITH_LIBREOFFICE=0. Вместе с ним ставится пакет fonts-liberation: у этих шрифтов те же ширины букв, что у Times New Roman и Arial. Без них строки переносились бы не так, как в Word.

Самая интересная ловушка — параллельные конверсии. Два soffice с общим профилем мешают друг другу: второй молча передаёт документ первому и выходит без PDF. Свежий профиль на каждый вызов лечит это, но стоит около двух секунд на инициализацию. Мы выбрали середину: свой профиль на каждый воркер и одна конверсия за раз внутри процесса.

Какой движок сейчас работает, администратор видит прямо в списке шаблонов: зелёный бейдж «PDF: LibreOffice» или жёлтый «PDF: встроенный конвертер» с подсказкой, как включить точную вёрстку.

Запасной движок — 720 строк на python-docx и fpdf2. Он переносит текст, нумерованные списки, таблицы с объединёнными ячейками, печати и подписи. Колонтитулов, цветов и обтекания текстом нет, но для счетов, договоров и сводок этого хватает.

Больше всего времени ушло на то, что Word делает незаметно. Номера пунктов он хранит не в тексте, а в отдельном numbering.xml, и считать их приходится самим. А в формах вроде счёта часть границ таблицы спрятана белыми линиями, и их нужно не рисовать.

Редактор DOCX прямо в браузере

Без редактора правка шаблона выглядит так: скачать файл, открыть Word, загрузить обратно. Мы встроили в CRM редактор docx-editor и обернули его React-компонент в свой DocxEditorFrame. Наружу он отдаёт три действия: сохранить документ, вставить текст и вставить поле.

Открытой части редактора под Apache-2.0 нам хватило. Платные модули, например конвертация в PDF, не понадобились: PDF мы и так делаем на сервере.

Редактор весит около 3,5 МБ, поэтому грузится отдельным чанком через React.lazy — только когда пользователь открыл документ. Пригодился он не только для отчётов: у любого DOCX-вложения в CRM появилась кнопка «Редактировать».

Конструктор шаблона

Конструктор — это страница из трёх колонок: слева шаблон в редакторе, посередине каталог полей, справа превью с данными настоящей записи.

  • Клик по полю вставляет его в место курсора. В документе оно выглядит как поле Word с подписью, например «БИК», а внутри лежит готовый тег {{ bik }}. Для денег и дат фильтр подставляется сам.

  • У списков есть кнопки «начало цикла» и «конец цикла» для строки таблицы, так что писать {%tr for %} руками не нужно.

  • Когда пользователь делает паузу в правках на 1,2 секунды, редактор отдаёт документ серверу, а тот собирает его с данными выбранной записи. Результат можно смотреть как DOCX или как PDF.

Для сводного отчёта вместо записи задаются параметры, например {"days": 7}. Администратор может открыть конструктор прямо из меню «Печать» на форме записи — тогда эта запись сразу попадёт в превью.

Отчёт по расписанию

Сводные отчёты удобно получать, не заходя в CRM. Cron у нас исполняет только пару «модель + метод»: произвольный Python-код в задачах мы отключили ради безопасности. Поэтому рассылка — это два метода модели шаблона, cron_send_report_email и cron_send_report_telegram, а задача хранит только аргументы:

{"template_id": 1, "to": "manager@example.com", "subject": "Продажи за неделю", "params": {"days": 7}}

params уходят в функцию данных, поэтому отчёт при каждом запуске собирается заново. Письмо уходит через email-коннектор, файл в Telegram — через бота. Две задачи-примера создаются выключенными: достаточно поправить адрес и включить.

Печать шаблона из любой формы, в данном примере заказ на продажу

Печать шаблона из любой формы, в данном примере заказ на продажу

Что получилось

Основной коммит — 63 файла и около 4,9 тысячи новых строк вместе с документацией. В него вошли редактор, конструктор, запасной PDF-конвертер, рассылка и семь образцов документов: счёт, отчёт за период и пять документов по договору.

Три урока, которые пригодятся и вам:

  • Снимайте служебные обёртки перед рендером. Конструктор вставляет поля как элементы управления Word: в редакторе это удобно, а в готовом документе и PDF они мешают.

  • Запускайте LibreOffice с отдельным профилем на процесс. Иначе параллельные конвертации молча теряют PDF.

  • Проверьте, какой fpdf стоит в окружении. Старый PyFPDF и fpdf2 ставятся под одним именем модуля, и мы на этом споткнулись.

Дальше хотим научить конструктор спискам и агрегациям, чтобы простые сводные отчёты собирались без функции на Python.

Исходники — на GitHub, демо — demo.faracrm.com, документация — docs.faracrm.com.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.