company_banner

Опенсорс-решение для автоматизации отчетности

    Разработчикам сайтов и мобильных приложений часто нужно управлять подготовкой PDF-страниц к выводу на печать или их отправкой клиентам на почту.

    У PDF-файлов есть полный контроль над отображением текста и графическими изображениями на странице. К сожалению, библиотеки для генерации динамически заполняющихся PDF-файлов не входят в стандартный инструментарий PHP, JS (Web), Java или Swift (Android и iOS соответственно). В этой статье хочу вам рассказать об опенсорс-решении для генерации PDF-файлов.



    JasperReports — это открытая Java-библиотека для генерации динамически заполняющихся файлов. В ней есть много инструментов для создания сложных отчетных форм, в том числе в формате PDF, но также доступны и другие форматы: RTF, DOCX, HTML, XLS, XLS, CSV и XML. Иными словами, достаточно разработать одну форму, сделать одну верстку — и можно будет экспортировать его в любой из вышеперечисленных форматов.

    Есть еще хорошие библиотеки, например PDFLib (коммерческая версия) для PHP и ее версия с открытыми исходными кодами PDFLib-Lite. Правда, библиотека довольно дорогая, а облегченная версия распространяется только в исходных кодах, и при ее установке в среде разработки это ограничение может стать проблемой.

    PDFbox — еще одна библиотека Java с открытым исходным кодом для работы с документами PDF. Она позволяет создавать новые PDF-документы, управлять существующими документами с возможностью извлекать контент из них. Но у нее нет UI (User Interface), в отличие от JasperReports.

    Думаю, JasperReports особо полезен в больших проектах, связанных с отчетностью и не только в PDF-формате. Здесь есть все необходимое, чтобы внедрить ее в ваш проект: простое создание сложных отчетных форм, UI для удобной верстки, серверное приложение и простая интеграция с фронтом.

    В статье я раскрою следующие темы:

    • Установка среды разработки и серверного приложения.
    • Создание PDF-файла автоматически заполняющегося из базы данных.
    • Интеграция серверного приложения с фронтендом для получения созданного PDF.

    Чтобы начать использовать JasperReports в вашем проекте, необходимо скачать два приложения: JaspersoftStudio — далее будем называть его рабочей средой — и JasperServer — будем называть серверным приложением.

    JaspersoftStudio — среда разработки на основе Eclipse со встроенной Java-библиотекой JasperReports, где разрабатываются динамические или статичные PDF-файлы: например, билеты, квитанции, договора, аналитические чарты и другие.

    JasperServer — это серверное приложение, куда из JaspersoftStudio деплоятся и хранятся файлы. К ним можно получить доступ из мобильного или веб-приложения. В JasperServer есть UI, с его помощью можно просматривать отчеты, создавать учетные записи для разных пользователей и давать им соответствующие доступы. Также можно настроить рассылку на электронную почту (Scheduler).

    Настраиваем рабочую среду и серверное приложение


    Скачать и установить приложения можно по ссылкам выше. После сетапа двух приложений необходимо установить соединение из рабочей среды к серверному приложению.

    Servers → Create JasperReports Server Connection → Указать предпочитаемое имя сервера, URL, логин и пароль. Нажмите Test Connection, чтобы проверить, что соединение с сервером установлено. Если видите надпись Successful — едем дальше.



    Создаем динамически заполняющийся PDF и публикуем его


    Мы установили среду разработки, сервер и наладили соединение между ними. Теперь давайте создадим примитивный динамически заполняющийся PDF-файл, который при его запуске (генерации) будет брать данные из PostgreSQL, и задеплоим его на серверное приложение.

    В первую очередь следует прикрутить к рабочей среде источник данных (в нашем случае — PostgreSQL), откуда PDF будет брать данные. Затем приступим к разработке нашего первого PDF-файла.

    Data Adapters → Create Data Adapter → Database JDBS Connection и указываем данные соединения:

    1. JDBC Driver — PostgreSQL (org.postgresql.Driver). Если нет драйвера вашего СУБД, можно установить нужный драйвер во вкладке Driver Classpath.
    2. JDBC URL — он состоит их хоста, порта и названия БД.
    3. Username и password — логпасс от вашей учетной записи СУБД.



    Нажимаем на уже знакомую нам кнопку Test и при успешном соединении (Successful) с БД — Finish.

    Из этой БД PDF будет заполняться в рабочей среде. Так как мы планируем задеплоить наш первый PDF-файл еще и на сервер, то давайте прикрутим тот же источник данных к серверному приложению:

    Data Sources → Add Resource → Data Source и повторяем все из пункта выше.

    Теперь можно приступать к созданию PDF. Исходники в JasperReports хранятся в формате JRXML — это XML с зашитыми тегами и атрибутами, с которыми работает API JasperReports.

    Нажимаем File → New → Jasper Report → Blank A4 → Указываем имя JRXML-файла → Finish.



    После создания нового проекта вы увидите следующую картину:



    Семь разных блоков — для каждого блока прописано свое поведение, отличающееся от других. Более подробно об этом можно почитать в документации. Нажав на Source можно увидеть структуру этих блоков:

    <?xml version="1.0" encoding="UTF-8"?>
    <jasperReport
    	xmlns="http://jasperreports.sourceforge.net/jasperreports"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Example" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="ae9517f6-ff0b-41bb-a8dc-82196190e940">
    	<queryString>
    		<![CDATA[]]>
    	</queryString>
    	<background>
    		<band splitType="Stretch"/>
    	</background>
    	<title>
    		<band height="79" splitType="Stretch"/>
    	</title>
    	<pageHeader>
    		<band height="35" splitType="Stretch"/>
    	</pageHeader>
    	<columnHeader>
    		<band height="61" splitType="Stretch"/>
    	</columnHeader>
    	<detail>
    		<band height="125" splitType="Stretch"/>
    	</detail>
    	<columnFooter>
    		<band height="45" splitType="Stretch"/>
    	</columnFooter>
    	<pageFooter>
    		<band height="54" splitType="Stretch"/>
    	</pageFooter>
    	<summary>
    		<band height="42" splitType="Stretch"/>
    	</summary>
    </jasperReport>
    

    Итак, давайте удалим пять лишних блоков и оставим только два: Title и Detail. В этом нам поможет кнопка Delete (Windows) или Backspace (OS X).

    Теперь добавим два элемента. Добавить новый элемент можно двумя способами: прописать контейнер в XML-структуре (кнопка Source) либо перетащить нужный элемент из правого верхнего окна Pallette — Static Text, где будут названия полей и Text Field, внутрь которого зашьем переменные поля, вытащенные из БД:

    <?xml version="1.0" encoding="UTF-8"?>
    <jasperReport
    	xmlns="http://jasperreports.sourceforge.net/jasperreports"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Example" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="ae9517f6-ff0b-41bb-a8dc-82196190e940">
    	<queryString>
    		<![CDATA[]]>
    	</queryString>
    	<background>
    		<band splitType="Stretch"/>
    	</background>
    	<title>
    		<band height="30" splitType="Stretch">
    			<staticText>
    				<reportElement x="0" y="0" width="100" height="30" uuid="7b697ed9-f52a-483c-965e-f0b2dc6130c1"/>
    				<text>
    					<![CDATA[Static Text]]>
    				</text>
    			</staticText>
    		</band>
    	</title>
    	<detail>
    		<band height="169" splitType="Stretch">
    			<textField>
    				<reportElement x="0" y="0" width="100" height="20" uuid="41002e0b-ddb2-4e4b-a049-10810ab51208"/>
    				<textFieldExpression>
    					<![CDATA["Text Field"]]>
    				</textFieldExpression>
    			</textField>
    		</band>
    	</detail>
    </jasperReport>

    Запрос в БД можно написать в теге queryString либо нажать на кнопку DataSet and Query editor dialog. После этого откроется новое окно, где необходимо выбрать источник данных (1), написать запрос (2) и объявить переменные поля. Кнопка Read Fields (3) прочитает все поля автоматически при валидном запросе. Чтобы просмотреть данные, нужно нажать на Data preview (4).



    Отлично! Мы получили четыре поля с типом String, теперь можем совершать с ними практически любые манипуляции. Для примера мы просто выведем их списком и пропишем небольшую логику.

    Напечатаем названия нужных полей в Static Text элементах и поместим их в контейнер Title. Переменные поля укажем в Text Field элементах в контейнере Detail, так как они будут множиться. Наш PDF будет выводить имя, город и адрес электронной почты. Чтобы не было совсем скучно, давайте в Text Field элементе пропишем простую логику, используя четвертое поле — пол клиента, Sex.

    Сделаем следующее: если клиент — женщина, то перед именем будем добавлять Mrs., если мужчина — Mr. Для этого используем тернарный оператор Java:

    <textFieldExpression>
          <![CDATA[$F{sex}.equals( "male" )?"Mr. "+$F{name}:"Mrs. "+$F{name}]]>
    </textFieldExpression> 

    Нажав на Preview рядом с кнопкой Source, можно увидеть результат:



    Как видно на скриншоте, PDF успешно собрался: забрал все значения и применил логику, правильно проставив Mrs. и Mr.

    Еще заведем входной параметр City, чтобы была возможность фильтровать данные по городам. Это можно сделать либо нажав Parameters → Create Parameter в окне Outline, либо добавив новый тег parameter с атрибутами name и class:

    <parameter name="City" class="java.lang.String"/>

    Осталось только добавить параметр в SQL-запрос:

    SELECT 
    Id, name, sex, city, email
    FROM users WHERE city = $P{City}
    

    Передадим в параметр City значение San Francisco (о том, как это сделать, расскажу в следующем пункте) и нажмем Data Preview для просмотра результата.



    PDF собрался, успешно отфильтровав данные. Едем дальше

    Так как у нас уже есть динамически заполняющийся PDF-файл, можем задеплоить его на сервер для дальнейшей интеграции с нашими фронтенд-приложениями. Для этого нажимаем кнопку Publish Report to JasperReports Server → двойным кликом открываем сервер → Выбираем серверную папку, куда нужно загрузить PDF (в нашем случае — reports) → Next → Data Source from Repository → выбираем источник данных, который создали ранее на серверном приложении → Finish.

    Интеграция с фронтендом


    В JasperReports API входит своя RESTful-реализация для взаимодействия клиента с сервером — REST v2. Если вам она не подходит, можете использовать простой протокол доступа к объектам — SOAP.

    Мы рассмотрим REST v2.

    Доступны основные четыре метода для действий CRUD (Create-Read-Update-Delete): GET (получить), POST (добавить, изменить, удалить), PUT (добавить, заменить), DELETE (удалить). Вся подробная информация имеется в документации по ссылкам выше.

    Мы рассмотрим более распространенный и актуальный для этой статьи метод GET.

    http://<host>:<port>/jasperserver[pro]/rest_v2/reports/path/to/report.<format>?<arguments> 

    Выше — синхронный запрос, с помощью которого можно получить выходные данные файла (готовый PDF) в одном запросе-ответе (с асинхронным вызовом можно ознакомиться здесь).

    Думаю, с хостом и портом все понятно, а /reports/path/to/report — это URI того файла, который вызывается. Так как мы задеплоили исходник PDF-файла (Example.jrxml) в серверную папку reports, заполненный вариант URI будет таким: /reports/reports/Example.

    format — это формат (в нашем случае PDF).
    arguments — параметры.

    Выше мы добавили параметр City, его и передадим в запросе со значением San Francisco для фильтрации данных по этому городу.

    Если вызов делается не с авторизованной зоны, нужно добавить еще два параметра/атрибута: j_username и j_password (логпасс для авторизации). По умолчанию логин и пароль на сервере — jasperadmin.

    Таким образом, получаем следующий URL:

    http://localhost:8080/jasperserver/rest_v2/reports/reports/Example.PDF?city=San Francisco&j_username=jasperadmin&j_password=jasperadmin

    Так мы получим уже сформированный PDF. Например, при вызове этого URL через адресную строку браузера файл должен скачаться автоматически.

    Может возникнуть необходимость отобразить PDF картинкой. Например, если клиент хочет просто просмотреть файл, можно показать документ в PNG-формате, если же хочет скачать его, то в PDF.

    На примере Java с использованием библиотеки PDFbox рассмотрим, как можно из внешнего приложения сгенерировать и забрать PDF-файл, а затем сконвертировать его в PNG.

    Ниже — класс PullPDF с одним методом, принимающим в качестве аргумента URL-адрес.

    import java.awt.image.BufferedImage;
    import java.io.BufferedInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.net.URL;
    import java.util.Base64;
    import org.apache.pdfbox.pdmodel.PDDocument;
    import org.apache.pdfbox.rendering.ImageType;
    import org.apache.pdfbox.rendering.PDFRenderer;
    import org.apache.pdfbox.tools.imageio.ImageIOUtil;
    
    public class PullPDF {
     public String ConvertPDF2PNG(String valuefromParam) throws IOException {
      BufferedInputStream input_file = new BufferedInputStream(new URL(valuefromParam).openStream());
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
    
      try {
       PDDocument document = PDDocument.load(input_file);
       PDFRenderer pdfRenderer = new PDFRenderer(document);
       for (int page = 0; page < document.getNumberOfPages(); ++page) {
        BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
        ImageIOUtil.writeImage(bim, "png", baos);
        baos.flush();
        byte[] encodedBytes = Base64.getEncoder().encode(baos.toByteArray());
        valuefromParam = new String(encodedBytes);
       }
      } catch (Exception e) {
    
      }
      return valuefromParam;
     }
    }

    Можно получить тот же результат, используя, например, Spring Framework. Но я постарался показать универсальный способ, который можно применить и в Android, и в вебе при работе с Java.

    Заключение


    Если вы хотите автоматизировать генерацию простого чека для интернет-магазина и у вас ограничено время на его создание, то я рекомендую использовать нативный инструментарий вашего проекта или знакомый вам фреймворк. Поскольку затраты времени на установку JasperReports, ознакомление с документацией для разработки более комплексных отчетных форм, могут не оправдать себя. В остальных случаях JasperReports — хорошее опенсорс-решение для автоматизации отчетности.
    Tinkoff.ru
    120,76
    IT’s Tinkoff.ru — просто о сложном
    Поделиться публикацией

    Комментарии 22

      0
      Добрый день, вы пишите что это Опенсоурс и потом, что это довольно дорогая библиотека. В итоге опен или за деньги уточните пожалуйста?
        0
        Добрый день! Спасибо за Ваше замечание. Сейчас поправлю строение предложения для наглядности.

        В данном абзаце речь шла не про JasperReports, а про коммерческую версию PDFLib, как об аналоге. JasperReports — это опенсорс библиотека.
          0
          Еше подчеркните итоговые выводы: порекомендовали вы бы данные продукт, тк в итоге не совсем понятно. Если продукт стоящий, то и время потратить на его изучение не жалко. Почему JavaScript, репортинг это не совсем та вещь которую нужно размещать в JavaScript или я не прав?
          0
          1. У jaspersoft несколько редакций. Я правда не знаю, дают к платным версиям все исходники или нет. Это в основном про сервер.
          2. Библиотека вроде бы вся «опен за бесплатно». Можно использовать ее без сервера от Tibco
          3. Вообще не совсем правильно спрашивать «опен или за деньги». Бывает open за деньги, бывает closed бесплатно
            0
            Все верно.

            Платная версия сервера тоже предоставляется с открытым исходным кодом и на МОЙ взгляд, не имеет никаких ощутимых преимуществ над бесплатной.

            С разницей между ними можно ознакомиться здесь.
          0
          Все верно.

          Платная версия сервера тоже предоставляется с открытым исходным кодом и на МОЙ взгляд, не имеет никаких ощутимых преимуществ над бесплатной.

          С разницей между ними можно ознакомиться здесь.
            +1
            Добрый день.
            Почему выбор пал именно на JasperReports, какие варианты рассматривались и почему они не подошли (к примеру pentaho, talend, knowage все так же бесплатные и на java)?
              0
              Добрый день.

              JasperReports неплохо покрывает требования наших проектов, работает с любыми источниками данных, в том числе с web-сервисами, позволяет создавать формы со сложной бизнес логикой, делать красивую верстку в соответствии с дизайном и структурировано хранить все проекты на серверном приложении.

              Серверное приложение — хорошее решение, позволяет разработать форму и запрашивать ее из разных систем не внедряя ее в каждую из них — это позволяет, при доработках формы, делать изменения только в одном месте.

              В статье я не отделяю JasperReports как лучшее решение из существующих. У нас она хорошо справляется с сотнями тысяч запросов ежедневно и достойна внимания.

              Что выбрать Вам? — зависит от ваших требований и специфичности проекта. Надеюсь, такие статьи как эта, помогут остановиться на подходящем решении.
              0
              В вашем примере Jasper берет данные непосредственно из БД, а можно ли настроить на формирование отчетов по произвольным данным, когда данные собираются сторонними средствами/микросервисами, и подаются в Jasper для формирование отчета на основе них?
                0
                Когда заморочился с jasper — передавал исходные данные в json.
                  0
                  Как вариант!
                  0
                  Да, можно.

                  Для этого нужно завести переменные поля (параметры) на стороне Jasper'a и передавать из сторонней платформы вызывающей Jasper, данные в эти параметры, как указано в статье. Далее, вы можете использовать полученные данные для заполнения формы/документа.

                  Есть еще одно решение:

                  Jasper может принимать на вход json (строкой) и не ходить никуда за данными. Для этого достаточно объявить одно переменное поле (параметр):

                  <parameter name="JSON" class="java.lang.String"/>


                  который принимать json и передать его в другой дефолтный параметр библиотеки JasperReports:

                  <parameter name="JSON_INPUT_STREAM" class="java.io.InputStream">
                       <defaultValueExpression>
                            <![CDATA[new ByteArrayInputStream($P{JSON}.getBytes())]]>
                      </defaultValueExpression>
                  </parameter>


                  вам останется распарсить Json и использовать данные. Прочитать про парсинг Json'a можно здесь.
                    0
                    а JSON лучше нормально, как и все остальные источники передавать (данные из БД,XML и т.д.)
                    посмотреть можно здесь и существует давно, с 4.0.1 версии
                    0
                    можно как угодно, можно из Явы заполнять ассоциативный массив данных, а в отчёте принимать как параметры. можно отдавать данные как xml, а в отчёте в разделе запроса обрабатывать как XPath… в общем вариантов несколько. если интересно — пиши, примерно расскажу, что и как
                      0
                      Интересно, если есть желание, можете поделиться как вы принимаете XML в отчете.
                        0
                        вот такая XML
                        <?xml version="1.0" encoding="UTF-8" ?>
                        <object name="department_monitoring" method="report_number">
                        	<recordset>
                        		<record>
                        			<NN>1.</NN>
                        			<IF_ROW>1</IF_ROW>
                        			<EM_MOMENT_DATE>06.03.2015</EM_MOMENT_DATE>
                        			<EM_MOMENT_TIME>10:00</EM_MOMENT_TIME>
                        			<PLACE_PATH>ПФО, Самарская область, Самара, Красноглинский, с Ясная Поляна</PLACE_PATH>
                        			<EM_TYPE_NAME>Столкновение</EM_TYPE_NAME>
                        			<EMTP_NUMBER>№ 00000000000</EMTP_NUMBER>
                        			<LOSS_STR>Погибло: 0. </LOSS_STR>
                        			<SUFFER_STR>Ранено: 2. </SUFFER_STR>
                        		</record>
                        		<record>
                        			<NN>ТС № 1</NN>
                        			<IF_ROW>2</IF_ROW>
                        			<REG_NUMBER></REG_NUMBER>
                        			<MODEL_NAME></MODEL_NAME>
                        			<CU_NAME>Не заполнено</CU_NAME>
                        			<VL_VIN>111</VL_VIN>
                        			<DEP_NAME></DEP_NAME>
                        			<IF_PD></IF_PD>
                        			<PERSON_NAME></PERSON_NAME>
                        			<PERSON_AGE></PERSON_AGE>
                        			<HV_TYPE_SUPERTYPE></HV_TYPE_SUPERTYPE>
                        			<IF_DRIVER_MED>1</IF_DRIVER_MED>
                        			<DRIVER_MED>Не проводилось</DRIVER_MED>
                        			<IF_MD>1</IF_MD>
                        			<MAIN_PDD_DERANGEMENTS>
                        				<recordset>
                        					<record>
                        						<state></state>
                        						<DERANG_ID/>
                        						<DERANG_NAME>Не выявлены</DERANG_NAME>
                        						<DERANG_CODE/>
                        					</record>
                        				</recordset>
                        			</MAIN_PDD_DERANGEMENTS>
                        			<IF_AD>1</IF_AD>
                        			<ATTENDANT_PDD_DERANGEMENTS>
                        				<recordset>
                        					<record>
                        						<state></state>
                        						<DERANG_ID/>
                        						<DERANG_NAME>Не выявлены</DERANG_NAME>
                        						<DERANG_CODE/>
                        					</record>
                        				</recordset>
                        			</ATTENDANT_PDD_DERANGEMENTS>
                        		</record>
                        	</recordset>
                        	<USER_RESPONSIBILITY>Российская Федерация</USER_RESPONSIBILITY>
                        </object>
                        


                        так переводим XML как данные для отчетов
                        doc = JRXmlUtils.parse(new ByteArrayInputStream(data.getBytes(codePage)));
                        params.put(JRXPathQueryExecuterFactory.PARAMETER_XML_DATA_DOCUMENT, doc);
                        

                        а потом в отчёте работаем с источником данных как
                        <queryString language="xPath">
                        	<![CDATA[/object/recordset/record]]>
                        </queryString>
                        

                        так можно прочитать значение ноды, которая находится за пределами указанного в источнике пути
                        <field name="REPORT_FILTER" class="java.lang.String">
                        	<fieldDescription><![CDATA[//REPORT_FILTER]]></fieldDescription>
                        </field>
                        

                        так можно передать вложенный сорс данных для списков/таблиц/подотчётов и т.д.
                        <datasetRun subDataset="MAIN_data">
                        	<dataSourceExpression><![CDATA[((net.sf.jasperreports.engine.data.JRXmlDataSource)$P{REPORT_DATA_SOURCE}).subDataSource("//MAIN_PDD_DERANGEMENTS/recordset/record")]]></dataSourceExpression>
                        </datasetRun>
                        

                        и соответственно его же обрабатываем в «подзапросе»
                        <subDataset name="MAIN_data">
                        	<queryString language="xPath">
                        		<![CDATA[//MAIN_PDD_DERANGEMENTS/recordset/record]]>
                        	</queryString>
                        </subDataset>
                        

                        остальное вы знаете, скорее всего, а по XPath отдельная тема

                        возможно передать в подотчёты конкретный путь-источник можно, а в датасете вложенном проще обработать (но так работает как надо)
                    0
                    Использовал 8 лет… потом плюнул и перенес все на jsreports, который в докере теперь крутится. Для моих задач оказалось гороаздо более легковесным решением. внешний вид настраивается проще, гораздо больше возможностей для форматирования, Ексель воообще отлично получается, В jasper самой большой болью были под-отчеты. К тому же jasper как-то последнее время очень вяло развивается
                      0

                      На java для динамического формирования без использования какого-либо сервера над Jasper есть обертка — https://github.com/dynamicreports/dynamicreports. До API самого Jasper спускаюсь только в очень сложных случаях.

                        0
                        Добрый день.

                        Согласен, в небольших проектах с простой отчетностью первый вариант тоже справится и времени меньше потребует.
                          0
                          не знаю, работаю с JRL… всё неплохо, проект вроде развивается достаточно хорошо, хоть и не очень много людей его делает, на сколько я знаю. в статье говорится только про PDF, проэтому всё вроде хорошо, а вот как только начинаешь работать с редактируемыми форматами — вот там такие плавающие косяки всплывают, приходится по-всякому извращаться. а использовать обёртку над JR в виде DR не вижу сильно смысла (тем более если проект маленький, тащить ещё библиотеки, вместо чистого JR). если вдруг не помогает вариант отчёта через jrxml, то «нарисаовать» отчёт через API не состовляет труда. тут про подотчёты писали… у кого как, но я стараюсь использовать не отдельные файлы, если в одном отчёте одна форма, а списки (по сути, тот же подотчёт, только вложенный)
                            0
                            Добрый день!
                            Какие именно возникали проблемы с другими форматами? Возможно, сможем предложить Вам хорошее решение.
                              0
                              начинают ездить таблицы, смещаются… обрезаются при переносе, высота и ширина ячеек в экселе не всегда работает

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое