Пример отчета о работе отдела технической поддержки
В данном примере мы будем выводить информацию о тикетах, решение которых потребовало больше времени, чем будет запрошено в форме отчета.
Описания отчета
Для этого создадим xml-файл описания отчета: /usr/local/mgr5/etc/xml/billmgr_mod_<name>.xml, где <name> — название отчета (для примера назовём отчет difficulttickets, файл будет называться billmgr_mod_difficulttickets.xml) со следующим содержимым:
<?xml version="1.0" encoding="UTF-8"?>
<mgrdata>
<handler name="reportdiff.py" type="xml">
<event name="reportlist" after="yes"/>
<event name="report.difficulttickets" after="yes" priority='after'/>
<func name="report.clienttickets" type="xml"/>
</handler>
<metadata name="report.difficulttickets" type="report" level="29">
<form>
<period name="period" default="currentmonth"/>
<field name="interval">
<input name="interval" type="text" save="yes" required="yes" check="int"/>
</field>
</form>
<band name="project" fullwidth="yes">
<query>SELECT id, name FROM project ORDER BY name</query>
<col name="name" type="data"/>
<band name="difficulttickets" fullwidth="yes">
<query>SELECT DISTINCT t.id, t.name AS messtitle, a.name AS client, t.date_last FROM ticket_message tm JOIN ticket t ON t.id = tm.ticket
JOIN account a ON a.id = t.account_client JOIN user e ON e.id = tm.user WHERE TIMESTAMPDIFF(MINUTE, (SELECT tm2.date_post FROM ticket_message tm2 JOIN user
u ON u.id = tm2.user WHERE tm2.ticket = tm.ticket AND u.level = 16 AND tm2.id < tm.id ORDER BY tm2.id DESC LIMIT 1),tm.date_post) > [[interval]]
AND e.level = 29 AND tm.date_post >= [[periodstart]] AND t.project = [[project.id]] AND tm.date_post <= [[periodend]] +INTERVAL 1 DAY
ORDER BY t.id;</query>
<col name="id" type="data" sort="digit" nestedreport="ticket_all.edit"/>
<col name="messtitle" type="data"/>
<col name="client" type="data" nestedreport="report.clienttickets"/>
<col name="date_last" type="data"/>
</band>
</band>
</metadata>
<metadata name="report.clienttickets" type="report" level="29">
<toolbar view="buttontext">
<toolgrp name="back">
<toolbtn func="report.difficulttickets" name="back" img="t-back" type="back" sprite="yes"/>
</toolgrp>
</toolbar>
<band name="clienttickets" fullwidth="yes">
<col name="id" type="data" sort="digit" nestedreport="ticket_all.edit"/>
<col name="name" type="data"/>
<col name="status" type="data"/>
</band>
</metadata>
<lang name="ru">
<messages name="project">
<msg name="id">Id</msg>
<msg name="name">Провайдер</msg>
</messages>
<messages name="report.difficulttickets">
<msg name="title">Отчет по сложным тикетам</msg>
<msg name="id">Id</msg>
<msg name="messtitle">Тема тикета</msg>
<msg name="client">Клиент</msg>
<msg name="date_last">Дата последнего ответа</msg>
<msg name="interval">Интервалы времени</msg>
<msg name="report_info">Отчет по тикетам, закрытым за более чем указанное время</msg>
<msg name="hint_interval">Интервалы времени в минутах</msg>
<msg name="periodstart">Начальная дата диапазона</msg>
<msg name="periodend">Конечная дата диапазона</msg>
</messages>
<messages name="report.clienttickets">
<include name="ticket_all"/>
<msg name="title">Тикеты клиента</msg>
<msg name="id">Id</msg>
<msg name="name">Имя тикета</msg>
<msg name="status">Статус тикета</msg>
</messages>
</lang>
</mgrdata>
Объявление обработчиков
<handler name="reportdiff.py" type="xml">
<event name="reportlist" after="yes"/>
<event name="report.difficulttickets" after="yes" priority='after'/>
<func name="report.clienttickets" type="xml"/>
</handler>
- Первое объявление обработчика говорит, о том что после вызова функции reportlist (Отчеты), будет вызван скрипт reportdiff.py добавляющий наш отчет на форму выбора отчетов.
- Второй раз этот обработчик будет вызван для заполнения полей формы нашего отчета с именем report.difficulttickets (Отчет по сложным тикетам) значениями по умолчанию.
Подробнее об обработчиках и плагинах можно прочитать по ссылке: XML
Описание метаданных отчета
<metadata name="report.difficulttickets" type="report" level="29">
Подробнее рассмотрим параметры описания нашего отчета:
- name — имя функции отчета (обязательно должно начинаться на report.);
- type — тип метаданных имеет значение report и указывает, что это отчет;
- level — уровень доступа, указывает, что данный отчет доступен только пользователям с правами администратора;
-
- super (root) доступ с уровнем 30
-
- admin доступ с уровнем 29
-
- user доступ с уровнем 16
Описание формы отчета
<form>
<period name="period" default="currentmonth"/>
<field name="interval">
<input name="interval" type="text" save="yes" required="yes" check="int"/>
</field>
</form>
- <period/> — указывает на то что необходимо сгенерировать элемент select с именем period и заполнить его стандартными периодами составления отчетов ("текущий день", "текущий месяц", "текущий день" и т.д.).
- Атрибут default="currentmonth" — значением по умолчанию для данного поля будет выбран "текущий месяц";
- <field>...</field> — стандартное поля описания для поля ввода с именем interval.
Подробнее про описание форм можно узнать по ссылке Описание форм и Валидаторы
Описание структуры данных отчета
Здесь мы описываем первый бэнд и вложенный запрос, которые при формировании отчета создадут нам несколько (по количеству провайдеров) таблиц в отчете с сортировкой тикетов по провайдеру.
<band name="project" fullwidth="yes">
<query>SELECT id, name FROM project ORDER BY name</query>
<col name="name" type="data"/
Подробное описание структуры данных отчета можно узнать перейдя по ссылке Описание отчетов .
В данном описании нужно обратить внимание на описание колонки идентификаторов тикетов <col name="id"...>. Атрибут nestedreport="ticket_all.edit" говорит о том что при нажатии на элемент выборки отчета, будет открыт дочерний отчет.
<band name="difficulttickets" fullwidth="yes">
<query>...</query>
<col name="id" type="data" sort="digit" nestedreport="ticket_all.edit"/>
<col name="client" type="data" nestedreport="report.clienttickets"/>
<col name="messtitle" type="data"/>
<col name="date_last" type="data"/>
</band>
Строкой <include name="ticket_all"/> мы добавляем в основной отчет сообщения из описания отчета "ticket_all", среди них есть описания статусов тикетов.
Описание дочернего отчета
Дочерний или вложенный отчет может быть реализован как через создание отдельного файла xml-описания, так и через описание в основном отчете. Например, нужна информация по тикету, содержащая id, автора и т.д. В первом варианте создадим файл /usr/local/mgr5/etc/xml/billmgr_mod_nested.xml со следующим содержанием:
<?xml version="1.0" encoding="UTF-8"?>
<mgrdata>
<metadata name="report.difficulttickets.ticketinfo" type="report" level="29">
<toolbar view="buttontext">
<toolgrp name="back">
<toolbtn func="report.difficulttickets" name="back" img="t-back" type="back" sprite="yes"/>
</toolgrp>
</toolbar>
<band name="function" fullwidth="yes">
<query>SELECT DISTINCT t.id, t.name, a.name AS client, IF(u.level = 28, u.name_ru, IFNULL(u.realname_ru, u.name_ru)) AS responsible, (t.date_last)
AS last_message, proj.name AS project_name , IF(tf.user IS NOT NULL, 'on', NULL) AS favorite FROM ticket t JOIN account a ON a.id = t.account_client
LEFT JOIN ticket2user t2u ON t2u.ticket = t.id LEFT JOIN ticket_favorite tf ON tf.ticket = t.id AND tf.user = '1' LEFT JOIN user u
ON u.id = t.responsible LEFT JOIN abuse_task at ON at.ticket = t.id LEFT JOIN project proj ON proj.id = t.project LEFT JOIN user bu ON bu.id =
t.user_block LEFT JOIN item i ON i.id = t.item LEFT JOIN pricelist p ON p.id = i.pricelist WHERE t.id=[[elid]];</query>
<col name="id" type="data" sort="digit"/>
<col name="name" type="data"/>
<col name="client" type="data"/>
<col name="responsible" type="data"/>
<col name="last_message" type="data"/>
</band>
</metadata>
<lang name="ru">
<messages name="report.difficulttickets.ticketinfo">
<msg name="title">Информация по запросу</msg>
<msg name="id">Id тикета</msg>
<msg name="name">Тема</msg>
<msg name="client">Клиент</msg>
<msg name="responsible">Ответственный</msg>
<msg name="last_message">Последнее сообщение</msg>
</messages>
</lang>
</mgrdata>
Рекомендуем для проверки синтаксиса вашего xml-файла использовать онлайн-сервис XmlGrid или утилиту Xmllint , как правило предустановленную во всех популярных дистрибутивах.
Во втором варианте описание включено в файл основного отчета. Здесь также возможны 2 варианта — с запросом к базе данных, указанным непосредственно в xml (см.выше) или с запросом, вынесенным в обработчик (внешнее подключение). Описываем колонки для формирования таблицы:
<band name="clienttickets" fullwidth="yes">
<col name="id" type="data" sort="digit" nestedreport="ticket_all.edit"/>
<col name="name" type="data"/>
<col name="status" type="data"/>
</band>
Пример такого подключения в описании обработчика ниже.
Переход из отчета на страницу биллинга
Другой вариант использования вложенного отчета nested — указание в нем одной функций биллинга. Например, в нашем основном отчете есть колонка "ID тикета". Если мы хотим сделать так, чтобы при клике на этой колонке открывался определенный тикет с этим же ID, в файле xml-описания основного отчета в строку с описанием колонки ID добавляем параметр ticket_all.edit . Т.е вся строка будет выглядеть:
<col name="id" type="data" sort="digit" nestedreport="ticket_all.edit"/>
В этом случае вам не нужно никакое отдельное xml-описание для этого отчета, так как происходит простой редирект на одну из страниц биллинга.
Описание скрипта обработчика
Обработчик должен находится в директории: /usr/local/mgr5/addon/ и иметь права на выполнение.
Дать права на выполнение можно командой: chmod +x /usr/local/mgr5/addon/reportdiff.py
Представленный обработчик выполняет следующие действия:
1) Добавляет наш отчет на форму "Отчеты"
2) Заполняет форму нашего отчета значениями по умолчанию
3) Выполняет подключение к базе данных панели и запрашивает данные для вложенного (nested) отчета.
Как говорилось выше, для запросов можно использовать элемент <query>...</query> прямо в описании xml, по умолчанию, будет установлено соединение с текущей базой данных. Если вам этот вариант не подходит, используйте для подключения возможности языка, на котором написан ваш обработчик. В Python для этого устанавливаем пакет python-mysqldb (для ОС CentOS — yum install python-mysqldb, для Debian -apt-get install python-mysqldb).
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import os
import subprocess
import xml.etree.ElementTree as ET
import MySQLdb
# Читаем содержимое stdin (stdin содержит xml формы отчета вместе с переданными параметрами)
xmlin = sys.stdin.read()
ses = ET.fromstring(xmlin)
# Словарь пользовательских параметров в данном случае, если в отправленной нам XML нет узла /doc/interval, в поле "interval" будет подставлено значение равное 60
# При наличии нескольких необходимых к заполнению параметров можно указать form_params = {'param0' : 'value0', 'param1' : 'value1', ...}
form_params = {'interval' : '60'}
# Имя нашего отчета, определенное в XML см. выше
report_name = 'difficulttickets'
# Описание отчета для отображения на форме "Отчеты"
report_desc = 'Отчет по сложным тикетам'
# Получаем параметры запроса одной строкой вида "param0=value0¶m1=value1..."
query_string = os.getenv('QUERY_STRING', '')
# Определяем нажата ли кнопка на обрабатываемой форме.
# При нажатии любой кнопки на обрабатываемой форме в параметрах запроса будет указан параметр 'sok=ok' (действие GET) в противном случае его не будет (действие SET).
action = 'SET' if 'sok=ok' in query_string else 'GET'
#GET only
if action == 'GET':
# Добавляем наш отчет на форму выбора отчетов
if ses.get('func') == 'reportlist':
elem_node = ET.SubElement(ses, 'elem');
id_node = ET.SubElement(elem_node, 'id')
id_node.text = report_name
report_node = ET.SubElement(elem_node, 'report', orig=report_name)
report_node.text = report_desc.decode('UTF-8')
type_node = ET.SubElement(elem_node, 'type')
type_node.text = 'report'
# Заполняем форму нашего отчета значениями по умолчанию
elif ses.get('func') == 'report.difficulttickets':
for key, value in form_params.items():
if ses.find('./' + key) == None:
param_node = ET.SubElement(ses, key)
param_node.text = value;
# Формируем форму дочернего отчета 'report.clienttickets'
elif ses.get('func') == 'report.clienttickets' and ses.find('./doc/doc') == None:
elid = ses.find('./doc/elid')
if elid != None:
# Подключаемся к MySQL. Для подключения запрашиваем у панели с помощью функции mgrctl данные о доступах к базе — пользователь, пароль, хост —
# ответ получаем в виде xml, из которого выбираем нужные параметры
proc = subprocess.Popen('sbin/mgrctl -m billmgr paramlist out=xml', stdout=subprocess.PIPE, shell=True)
xml_param = ET.fromstring(proc.stdout.read())
dbhost = xml_param.find('./elem/DBHost').text
dbname = xml_param.find('./elem/DBName').text
dbpassword = xml_param.find('./elem/DBPassword').text
dbuser = xml_param.find('./elem/DBUser').text
db = MySQLdb.connect(host=dbhost, user=dbuser, passwd=dbpassword, db=dbname)
cursor = db.cursor()
# Выполняем запрос на выборку данных из БД
cursor.execute("SELECT t.id, t.name, t.status FROM ticket t JOIN account a ON t.account_client = a.id WHERE a.name ='" + elid.text + "'")
# Получаем результат выполнения запроса
data = cursor.fetchall()
# Формируем содержимое формы отчета
reportdata_node = ET.SubElement(ses, 'reportdata')
# Добавляем узел с именем band-а описанного в xml отчета
report_node = ET.SubElement(reportdata_node, 'clienttickets')
# Перебираем результаты запроса:
for record in data:
# Получаем результаты одной строки выборки 't.id', 't.name' и 't.status'
t_id, t_name, t_status = record
# Добавляем описание одной строки нашего отчета
elem_node = ET.SubElement(report_node, 'elem')
# Заполняем поле 'id'
id_node = ET.SubElement(elem_node, 'id')
id_node.text = str(t_id)
# Заполняем поле 'name'
name_node = ET.SubElement(elem_node, 'name')
name_node.text = str(t_name)
# Заполняем поле 'status'
status_node = ET.SubElement(elem_node, 'status')
# Т.к. статус в таблице 'ticket' представлен в цифровой форме, необходимо получить его описание из xml
status_node.text = ses.find("./messages/msg[@name='tstatus_" + str(t_status) + "']").text
# Заканчиваем работу с БД
db.close()
# Отправляем отредактированную XML обратно в BILLmanager через stdout
sys.stdout.write(ET.tostring(ses, 'UTF-8'))
В примере на скриншоте отчет составлен за произвольный период.
Отчет за произвольный период