Пример отчета о работе отдела технической поддержки


В данном примере мы будем выводить информацию о тикетах, решение которых потребовало больше времени, чем будет запрошено в форме отчета.

Описания отчета

Для этого создадим 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 &lt; tm.id ORDER BY tm2.id DESC LIMIT 1),tm.date_post) &gt; [[interval]] 
AND e.level = 29 AND tm.date_post &gt;= [[periodstart]] AND t.project = [[project.id]] AND tm.date_post &lt;= [[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&param1=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'))

В примере на скриншоте отчет составлен за произвольный период.

Отчет за произвольный период