Browse Source

Fixes #2699 - Excel export of report or time accounting drops additional fields in row if integer field contain `null/nil/undefined`.

Martin Edenhofer 5 years ago
parent
commit
ba2afc0c16

+ 9 - 148
app/controllers/reports_controller.rb

@@ -61,7 +61,7 @@ class ReportsController < ApplicationController
 
     # get data
     result = {}
-    content = nil
+    excel = nil
     filename = nil
     get_params[:metric][:backend].each do |backend|
       next if params[:downloadBackendSelected] != backend[:name]
@@ -89,14 +89,19 @@ class ReportsController < ApplicationController
 
       # generate sheet
       if params[:sheet]
-        content = sheet(get_params[:profile], backend[:display], result)
+        excel = ExcelSheet::Ticket.new(
+          title:      "#{get_params[:profile].name} (#{backend[:display]})",
+          ticket_ids: result[:ticket_ids],
+          timezone:   params[:timezone],
+          locale:     current_user.locale,
+        )
         filename = "tickets-#{get_params[:profile].name}-#{backend[:display]}.xls"
       end
       break
     end
-    if content
+    if excel
       send_data(
-        content,
+        excel.content,
         filename:    filename,
         type:        'application/vnd.ms-excel',
         disposition: 'attachment'
@@ -176,148 +181,4 @@ class ReportsController < ApplicationController
     }
   end
 
-  def sheet(profile, title, result)
-
-    params[:timezone] ||= Setting.get('timezone_default')
-
-    # Create a new Excel workbook
-    temp_file = Tempfile.new('time_tracking.xls')
-    workbook = WriteExcel.new(temp_file)
-
-    # Add a worksheet
-    worksheet = workbook.add_worksheet
-    worksheet.set_row(0, 18)
-    worksheet.set_column(0, 0, 10)
-    worksheet.set_column(1, 1, 34)
-    worksheet.set_column(2, 2, 10)
-    worksheet.set_column(3, 3, 10)
-    worksheet.set_column(4, 8, 20)
-    worksheet.set_column(11, 0, 20)
-    worksheet.set_column(12, 0, 20)
-    worksheet.set_column(13, 0, 20)
-
-    # Add and define a format
-    format = workbook.add_format
-    format.set_bold
-    format.set_size(14)
-    format.set_color('black')
-
-    # Write a formatted and unformatted string, row and column notation.
-    worksheet.write_string(0, 0, "Tickets: #{profile.name} (#{title})", format)
-
-    format_header = workbook.add_format
-    format_header.set_italic
-    format_header.set_bg_color('gray')
-    format_header.set_color('white')
-
-    format_time = workbook.add_format(num_format: 'yyyy-mm-dd hh:mm:ss')
-    format_date = workbook.add_format(num_format: 'yyyy-mm-dd')
-
-    format_footer = workbook.add_format
-    format_footer.set_italic
-    format_footer.set_color('gray')
-    format_footer.set_size(8)
-
-    worksheet.write_string(2, 0, '#', format_header)
-    worksheet.write_string(2, 1, 'Title', format_header)
-    worksheet.write_string(2, 2, 'State', format_header)
-    worksheet.write_string(2, 3, 'Priority', format_header)
-    worksheet.write_string(2, 4, 'Group', format_header)
-    worksheet.write_string(2, 5, 'Owner', format_header)
-    worksheet.write_string(2, 6, 'Customer', format_header)
-    worksheet.write_string(2, 7, 'Organization', format_header)
-    worksheet.write_string(2, 8, 'Create Channel', format_header)
-    worksheet.write_string(2, 9, 'Sender', format_header)
-    worksheet.write_string(2, 10, 'Tags', format_header)
-    worksheet.write_string(2, 11, 'Created at', format_header)
-    worksheet.write_string(2, 12, 'Updated at', format_header)
-    worksheet.write_string(2, 13, 'Closed at', format_header)
-
-    # ObjectManager attributes
-    header_column = 14
-    # needs to be skipped
-    objects = ObjectManager::Attribute.where(editable:         true,
-                                             active:           true,
-                                             to_create:        false,
-                                             object_lookup_id: ObjectLookup.lookup(name: 'Ticket').id)
-                                      .pluck(:name, :display, :data_type, :data_option)
-                                      .map { |name, display, data_type, data_option| { name: name, display: display, data_type: data_type, data_option: data_option } }
-    objects.each do |object|
-      worksheet.set_column(header_column, 0, 16)
-      worksheet.write_string(2, header_column, object[:display].capitalize, format_header)
-      header_column += 1
-    end
-
-    row = 2
-    result[:ticket_ids].each do |ticket_id|
-
-      ticket = Ticket.lookup(id: ticket_id)
-      row += 1
-      worksheet.write_string(row, 0, ticket.number)
-      worksheet.write_string(row, 1, ticket.title)
-      worksheet.write_string(row, 2, ticket.state.name)
-      worksheet.write_string(row, 3, ticket.priority.name)
-      worksheet.write_string(row, 4, ticket.group.name)
-      worksheet.write_string(row, 5, ticket.owner.fullname)
-      worksheet.write_string(row, 6, ticket.customer.fullname)
-      worksheet.write_string(row, 7, ticket.try(:organization).try(:name))
-      worksheet.write_string(row, 8, ticket.create_article_type.name)
-      worksheet.write_string(row, 9, ticket.create_article_sender.name)
-      worksheet.write_string(row, 10, ticket.tag_list.join(','))
-
-      worksheet.write_date_time(row, 11, time_in_localtime_for_excel(ticket.created_at, params[:timezone]), format_time)
-      worksheet.write_date_time(row, 12, time_in_localtime_for_excel(ticket.updated_at, params[:timezone]), format_time)
-      worksheet.write_date_time(row, 13, time_in_localtime_for_excel(ticket.close_at, params[:timezone]), format_time) if ticket.close_at.present?
-
-      # Object Manager attributes
-      column = 14
-      # We already queried ObjectManager::Attributes, so we just use objects
-      objects.each do |object|
-        key = object[:name]
-        case object[:data_type]
-        when 'boolean', 'select'
-          value = ticket.send(key.to_sym)
-          if object[:data_option] && object[:data_option]['options'] && object[:data_option]['options'][ticket.send(key.to_sym)]
-            value = object[:data_option]['options'][ticket.send(key.to_sym)]
-          end
-          worksheet.write_string(row, column, value)
-        when 'datetime'
-          worksheet.write_date_time(row, column, time_in_localtime_for_excel(ticket.send(key.to_sym), params[:timezone]), format_time) if ticket.send(key.to_sym).present?
-        when 'date'
-          worksheet.write_date_time(row, column, ticket.send(key.to_sym).to_s, format_date) if ticket.send(key.to_sym).present?
-        when 'integer'
-          worksheet.write_number(row, column, ticket.send(key.to_sym))
-        else
-          # for text, integer and tree select
-          worksheet.write_string(row, column, ticket.send(key.to_sym).to_s)
-        end
-        column += 1
-      end
-    rescue => e
-      Rails.logger.error "SKIP: #{e.message}"
-
-    end
-
-    row += 2
-    worksheet.write_string(row, 0, "#{Translation.translate(current_user.locale, 'Timezone')}: #{params[:timezone]}", format_footer)
-
-    workbook.close
-
-    # read file again
-    file = File.new(temp_file, 'r')
-    contents = file.read
-    file.close
-    contents
-  end
-
-  def time_in_localtime_for_excel(time, timezone)
-    return if time.blank?
-
-    if timezone.present?
-      offset = time.in_time_zone(timezone).utc_offset
-      time += offset
-    end
-    local_time = time.utc.iso8601.to_s.sub(/Z$/, '')
-    local_time.sub(/T/, ' ')
-  end
 end

+ 90 - 297
app/controllers/time_accountings_controller.rb

@@ -21,216 +21,79 @@ class TimeAccountingsController < ApplicationController
       end
       time_unit[record[0]][:time_unit] += record[1]
     end
-    customers = {}
-    organizations = {}
-    agents = {}
-    results = []
-    time_unit.each do |ticket_id, local_time_unit|
-      ticket = Ticket.lookup(id: ticket_id)
-      next if !ticket
 
-      if !customers[ticket.customer_id]
-        customers[ticket.customer_id] = '-'
-        if ticket.customer_id
-          customer_user = User.lookup(id: ticket.customer_id)
-          if customer_user
-            customers[ticket.customer_id] = customer_user.fullname
+    if !params[:download]
+      customers = {}
+      organizations = {}
+      agents = {}
+      results = []
+      time_unit.each do |ticket_id, local_time_unit|
+        ticket = Ticket.lookup(id: ticket_id)
+        next if !ticket
+
+        if !customers[ticket.customer_id]
+          customers[ticket.customer_id] = '-'
+          if ticket.customer_id
+            customer_user = User.lookup(id: ticket.customer_id)
+            if customer_user
+              customers[ticket.customer_id] = customer_user.fullname
+            end
           end
         end
-      end
-      if !organizations[ticket.organization_id]
-        organizations[ticket.organization_id] = '-'
-        if ticket.organization_id
-          organization = Organization.lookup(id: ticket.organization_id)
-          if organization
-            organizations[ticket.organization_id] = organization.name
+        if !organizations[ticket.organization_id]
+          organizations[ticket.organization_id] = '-'
+          if ticket.organization_id
+            organization = Organization.lookup(id: ticket.organization_id)
+            if organization
+              organizations[ticket.organization_id] = organization.name
+            end
           end
         end
-      end
-      if !agents[local_time_unit[:agent_id]]
-        agent_user = User.lookup(id: local_time_unit[:agent_id])
-        if agent_user
-          agents[local_time_unit[:agent_id]] = agent_user.fullname
-        end
-      end
-      result = {
-        ticket:       ticket.attributes,
-        time_unit:    local_time_unit[:time_unit],
-        customer:     customers[ticket.customer_id],
-        organization: organizations[ticket.organization_id],
-        agent:        agents[local_time_unit[:agent_id]],
-      }
-      results.push result
-    end
-
-    if params[:download]
-      header = [
-        {
-          name:  'Ticket#',
-          width: 15,
-        },
-        {
-          name:  'Title',
-          width: 30,
-        },
-        {
-          name:  'Customer',
-          width: 20,
-        },
-        {
-          name:  'Organization',
-          width: 20,
-        },
-        {
-          name:  'Agent',
-          width: 20,
-        },
-        {
-          name:  'Time Units',
-          width: 10,
-        },
-        {
-          name:  'Time Units Total',
-          width: 10,
-        },
-        {
-          name:  'Created at',
-          width: 18,
-        },
-        {
-          name:  'Closed at',
-          width: 18,
-        },
-        {
-          name:  'Close Escalation At',
-          width: 18,
-        },
-        {
-          name:  'Close In Min',
-          width: 10,
-        },
-        {
-          name:  'Close Diff In Min',
-          width: 10,
-        },
-        {
-          name:  'First Response At',
-          width: 18,
-        },
-        {
-          name:  'First Response Escalation At',
-          width: 18,
-        },
-        {
-          name:  'First Response In Min',
-          width: 10,
-        },
-        {
-          name:  'First Response Diff In Min',
-          width: 10,
-        },
-        {
-          name:  'Update Escalation At',
-          width: 18,
-        },
-        {
-          name:  'Update In Min',
-          width: 10,
-        },
-        {
-          name:  'Update Diff In Min',
-          width: 10,
-        },
-        {
-          name:  'Last Contact At',
-          width: 18,
-        },
-        {
-          name:  'Last Contact Agent At',
-          width: 18,
-        },
-        {
-          name:  'Last Contact Customer At',
-          width: 18,
-        },
-        {
-          name:  'Article Count',
-          width: 10,
-        },
-        {
-          name:  'Escalation At',
-          width: 18,
-        },
-      ]
-      objects = ObjectManager::Attribute.where(editable:         true,
-                                               active:           true,
-                                               to_create:        false,
-                                               object_lookup_id: ObjectLookup.lookup(name: 'Ticket').id)
-                                        .pluck(:name, :display, :data_type, :data_option)
-                                        .map { |name, display, data_type, data_option| { name: name, display: display, data_type: data_type, data_option: data_option } }
-      objects.each do |object|
-        header.push({ name: object[:display], width: 18 })
-      end
-
-      result = []
-      results.each do |row|
-        result_row = [
-          row[:ticket]['number'],
-          row[:ticket]['title'],
-          row[:customer],
-          row[:organization],
-          row[:agent],
-          row[:time_unit],
-          row[:ticket]['time_unit'],
-          row[:ticket]['created_at'],
-          row[:ticket]['close_at'],
-          row[:ticket]['close_escalation_at'],
-          row[:ticket]['close_in_min'],
-          row[:ticket]['close_diff_in_min'],
-          row[:ticket]['first_response_at'],
-          row[:ticket]['first_response_escalation_at'],
-          row[:ticket]['first_response_in_min'],
-          row[:ticket]['first_response_diff_in_min'],
-          row[:ticket]['update_escalation_at'],
-          row[:ticket]['update_in_min'],
-          row[:ticket]['update_diff_in_min'],
-          row[:ticket]['last_contact_at'],
-          row[:ticket]['last_contact_agent_at'],
-          row[:ticket]['last_contact_customer_at'],
-          row[:ticket]['article_count'],
-          row[:ticket]['escalation_at'],
-        ]
-
-        # Object Manager attributes
-        # We already queried ObjectManager::Attributes, so we just use objects
-        objects.each do |object|
-          key = object[:name]
-          case object[:data_type]
-          when 'boolean', 'select'
-            value = row[:ticket][key]
-            if object[:data_option] && object[:data_option]['options'] && object[:data_option]['options'][row[:ticket][key]]
-              value = object[:data_option]['options'][row[:ticket][key]]
-            end
-            value.present? ? result_row.push(value) : result_row.push('')
-          else
-            # for text, integer and tree select
-            row[:ticket][key].present? ? result_row.push(row[:ticket][key]) : result_row.push('')
+        if !agents[local_time_unit[:agent_id]]
+          agent_user = User.lookup(id: local_time_unit[:agent_id])
+          if agent_user
+            agents[local_time_unit[:agent_id]] = agent_user.fullname
           end
         end
-
-        result.push result_row
+        result = {
+          ticket:       ticket.attributes,
+          time_unit:    local_time_unit[:time_unit],
+          customer:     customers[ticket.customer_id],
+          organization: organizations[ticket.organization_id],
+          agent:        agents[local_time_unit[:agent_id]],
+        }
+        results.push result
       end
-      content = sheet("By Ticket #{year}-#{month}", header, result)
-      send_data(
-        content,
-        filename:    "by_ticket-#{year}-#{month}.xls",
-        type:        'application/vnd.ms-excel',
-        disposition: 'attachment'
-      )
+      render json: results
       return
     end
 
-    render json: results
+    ticket_ids = []
+    additional_attributes = []
+    additional_attributes_header = [{ display: 'Time Units', name: 'time_unit_for_range', width: 10, data_type: 'float' }]
+    time_unit.each do |ticket_id, local_time_unit|
+      ticket_ids.push ticket_id
+      additional_attribute = {
+        time_unit_for_range: local_time_unit[:time_unit],
+      }
+      additional_attributes.push additional_attribute
+    end
+
+    excel = ExcelSheet::Ticket.new(
+      title:                        "Tickets: #{year}-#{month}",
+      ticket_ids:                   ticket_ids,
+      additional_attributes:        additional_attributes,
+      additional_attributes_header: additional_attributes_header,
+      timezone:                     params[:timezone],
+      locale:                       current_user.locale,
+    )
+
+    send_data(
+      excel.content,
+      filename:    "by_ticket-#{year}-#{month}.xls",
+      type:        'application/vnd.ms-excel',
+      disposition: 'attachment'
+    )
   end
 
   def by_customer
@@ -287,11 +150,12 @@ class TimeAccountingsController < ApplicationController
           width: 30,
         },
         {
-          name:  'Time Units',
-          width: 10,
+          name:      'Time Units',
+          width:     10,
+          data_type: 'float'
         }
       ]
-      result = []
+      records = []
       results.each do |row|
         customer_name = User.find(row[:customer]['id']).fullname
         organization_name = ''
@@ -299,11 +163,18 @@ class TimeAccountingsController < ApplicationController
           organization_name = row[:organization]['name']
         end
         result_row = [customer_name, organization_name, row[:time_unit]]
-        result.push result_row
+        records.push result_row
       end
-      content = sheet("By Customer #{year}-#{month}", header, result)
+
+      excel = ExcelSheet.new(
+        title:    "By Customer #{year}-#{month}",
+        header:   header,
+        records:  records,
+        timezone: params[:timezone],
+        locale:   current_user.locale,
+      )
       send_data(
-        content,
+        excel.content,
         filename:    "by_customer-#{year}-#{month}.xls",
         type:        'application/vnd.ms-excel',
         disposition: 'attachment'
@@ -360,22 +231,30 @@ class TimeAccountingsController < ApplicationController
           width: 40,
         },
         {
-          name:  'Time Units',
-          width: 20,
+          name:      'Time Units',
+          width:     20,
+          data_type: 'float',
         }
       ]
-      result = []
+      records = []
       results.each do |row|
         organization_name = ''
         if row[:organization].present?
           organization_name = row[:organization]['name']
         end
         result_row = [organization_name, row[:time_unit]]
-        result.push result_row
+        records.push result_row
       end
-      content = sheet("By Organization #{year}-#{month}", header, result)
+
+      excel = ExcelSheet.new(
+        title:    "By Organization #{year}-#{month}",
+        header:   header,
+        records:  records,
+        timezone: params[:timezone],
+        locale:   current_user.locale,
+      )
       send_data(
-        content,
+        excel.content,
         filename:    "by_organization-#{year}-#{month}.xls",
         type:        'application/vnd.ms-excel',
         disposition: 'attachment'
@@ -385,90 +264,4 @@ class TimeAccountingsController < ApplicationController
 
     render json: results
   end
-
-  private
-
-  def sheet(title, header, result)
-
-    params[:timezone] ||= Setting.get('timezone_default')
-
-    # Create a new Excel workbook
-    temp_file = Tempfile.new('time_tracking.xls')
-    workbook = WriteExcel.new(temp_file)
-
-    # Add a worksheet
-    worksheet = workbook.add_worksheet
-
-    # Add and define a format
-    format = workbook.add_format  # Add a format
-    format.set_bold
-    format.set_size(14)
-    format.set_color('black')
-
-    format_time = workbook.add_format(num_format: 'yyyy-mm-dd hh:mm:ss')
-    format_date = workbook.add_format(num_format: 'yyyy-mm-dd')
-
-    format_footer = workbook.add_format
-    format_footer.set_italic
-    format_footer.set_color('gray')
-    format_footer.set_size(8)
-
-    worksheet.set_row(0, 18, header.count)
-
-    # Write a formatted and unformatted string, row and column notation.
-    worksheet.write_string(0, 0, title, format)
-
-    format_header = workbook.add_format  # Add a format
-    format_header.set_italic
-    format_header.set_bg_color('gray')
-    format_header.set_color('white')
-    count = 0
-    header.each do |item|
-      if item[:width]
-        worksheet.set_column(count, count, item[:width])
-      end
-      worksheet.write_string(2, count, item[:name], format_header)
-      count += 1
-    end
-
-    row_count = 2
-    result.each do |row|
-      row_count += 1
-      row_item_count = 0
-      row.each do |item|
-        if item.acts_like?(:time)
-          worksheet.write_date_time(row_count, row_item_count, time_in_localtime_for_excel(item, params[:timezone]), format_time) if item.present?
-        elsif item.acts_like?(:date)
-          worksheet.write_date_time(row_count, row_item_count, item.to_s, format_date) if item.present?
-        elsif item.is_a?(Integer) || item.is_a?(Float)
-          worksheet.write_number(row_count, row_item_count, item)
-        else
-          worksheet.write_string(row_count, row_item_count, item.to_s)
-        end
-        row_item_count += 1
-      end
-    end
-
-    row_count += 2
-    worksheet.write_string(row_count, 0, "#{Translation.translate(current_user.locale, 'Timezone')}: #{params[:timezone]}", format_footer)
-
-    workbook.close
-
-    # read file again
-    file = File.new(temp_file, 'r')
-    contents = file.read
-    file.close
-    contents
-  end
-
-  def time_in_localtime_for_excel(time, timezone)
-    return if time.blank?
-
-    if timezone.present?
-      offset = time.in_time_zone(timezone).utc_offset
-      time += offset
-    end
-    local_time = time.utc.iso8601.to_s.sub(/Z$/, '')
-    local_time.sub(/T/, ' ')
-  end
 end

+ 173 - 0
lib/excel_sheet.rb

@@ -0,0 +1,173 @@
+class ExcelSheet
+
+  def initialize(title:, header:, records:, timezone: nil, locale:)
+    @title           = title
+    @header          = header
+    @records         = records
+    @timezone        = timezone.presence || Setting.get('timezone_default')
+    @timezone_offset = @timezone.present? ? Time.now.in_time_zone(@timezone).utc_offset : 0
+    @locale          = locale || 'en-en'
+    @tempfile        = Tempfile.new('excel-export.xls')
+    @workbook        = WriteExcel.new(@tempfile)
+    @worksheet       = @workbook.add_worksheet
+    @contents        = nil
+    @current_row     = 0
+    @current_column  = 0
+
+    @lookup_cache = {}
+
+    @format_time = @workbook.add_format(num_format: 'yyyy-mm-dd hh:mm:ss')
+    @format_date = @workbook.add_format(num_format: 'yyyy-mm-dd')
+
+    @format_headline = @workbook.add_format
+    @format_headline.set_bold
+    @format_headline.set_size(14)
+    @format_headline.set_color('black')
+
+    @format_header = @workbook.add_format
+    @format_header.set_italic
+    @format_header.set_bg_color('gray')
+    @format_header.set_color('white')
+
+    @format_footer = @workbook.add_format
+    @format_footer.set_italic
+    @format_footer.set_color('gray')
+    @format_footer.set_size(8)
+  end
+
+  def contents
+    file = File.new(@tempfile, 'r')
+    contents = file.read
+    file.close
+    contents
+  end
+
+  def content
+    gen_header
+    gen_rows
+    gen_footer
+    contents
+  end
+
+  def gen_header
+    @worksheet.write_string(@current_row, @current_column, @title, @format_headline)
+    @worksheet.set_row(0, 18)
+
+    @current_row += 2
+    @current_column = 0
+    @header.each do |header|
+      if header[:width]
+        @worksheet.set_column(@current_column, @current_column, header[:width])
+      end
+      @worksheet.write_string(@current_row, @current_column, header[:display] || header[:name], @format_header)
+      @current_column += 1
+    end
+  end
+
+  def gen_rows
+    @records.each do |record|
+      gen_row_by_array(record)
+    end
+  end
+
+  def gen_row_by_array(record)
+    @current_row += 1
+    @current_column = 0
+    record.each do |item|
+      begin
+        if item.acts_like?(:time)
+          value_convert(item, nil, { data_type: 'datetime' })
+        elsif item.acts_like?(:date)
+          value_convert(item, nil, { data_type: 'datetime' })
+        elsif item.is_a?(Integer) || item.is_a?(Float)
+          value_convert(item, nil, { data_type: 'integer' })
+        else
+          value_convert(item, nil, { data_type: 'string' })
+        end
+      rescue => e
+        Rails.logger.error e
+      end
+      @current_column += 1
+    end
+  end
+
+  def gen_row_by_header(record, additional = {})
+    @current_row += 1
+    @current_column = 0
+    @header.each do |header|
+      begin
+        value_convert(record, header[:name], header, additional)
+      rescue => e
+        Rails.logger.error e
+      end
+      @current_column += 1
+    end
+  end
+
+  def gen_footer
+    @current_row += 2
+    @worksheet.write_string(@current_row, 0, "#{Translation.translate(@locale, 'Timezone')}: #{@timezone}", @format_footer)
+    @workbook.close
+  end
+
+  def timestamp_in_localtime(time)
+    return if time.blank?
+
+    (time + @timezone_offset).utc.strftime('%F %T') # "2019-08-19 16:21:52"
+  end
+
+  def value_lookup(record, attribute, additional)
+    value = record[attribute.to_sym]
+    if attribute[-3, 3] == '_id'
+      ref = attribute[0, attribute.length - 3]
+      if record.respond_to?(ref.to_sym)
+        @lookup_cache[attribute] ||= {}
+        return @lookup_cache[attribute][value] if @lookup_cache[attribute][value]
+
+        ref_object = record.send(ref.to_sym)
+        ref_name = value
+        if ref_object.respond_to?(:fullname)
+          ref_name = ref_object.fullname
+        elsif ref_object.respond_to?(:name)
+          ref_name = ref_object.name
+        end
+        @lookup_cache[attribute][value] = ref_name
+        return ref_name
+      end
+    end
+    value = record.try(attribute)
+
+    # if no value exists, check additional values
+    if !value && additional && additional[attribute.to_sym]
+      value = additional[attribute.to_sym]
+    end
+    if value.is_a?(Array)
+      value = value.join(',')
+    end
+    value
+  end
+
+  def value_convert(record, attribute, object, additional = {})
+    value = if attribute
+              value_lookup(record, attribute, additional)
+            else
+              record
+            end
+    case object[:data_type]
+    when 'boolean', 'select'
+      if object[:data_option] && object[:data_option]['options'] && object[:data_option]['options'][value]
+        value = object[:data_option]['options'][value]
+      end
+      @worksheet.write_string(@current_row, @current_column, value) if value.present?
+    when 'datetime'
+      @worksheet.write_date_time(@current_row, @current_column, timestamp_in_localtime(value), @format_time) if value.present?
+    when 'date'
+      @worksheet.write_date_time(@current_row, @current_column, value.to_s, @format_date) if value.present?
+    when 'integer'
+      @worksheet.write_number(@current_row, @current_column, value) if value.present?
+    else
+      @worksheet.write_string(@current_row, @current_column, value.to_s) if value.present?
+    end
+  end
+
+end

+ 103 - 0
lib/excel_sheet/ticket.rb

@@ -0,0 +1,103 @@
+class ExcelSheet::Ticket < ExcelSheet
+
+=begin
+
+  excel = ExcelSheet::Ticket.new(
+    title:                        "#{year}-#{month}",
+    ticket_ids:                   ticket_ids,
+    additional_attributes:        additional_attributes,
+    additional_attributes_header: additional_attributes_header,
+    timezone:                     params[:timezone],
+    locale:                       current_user.locale,
+  )
+
+  excel.content
+
+=end
+
+  def initialize(params)
+    @ticket_ids                   = params[:ticket_ids] || []
+    @additional_attributes        = params[:additional_attributes] || []
+    @additional_attributes_header = params[:additional_attributes_header] || []
+
+    super(
+      title:    params[:title],
+      header:   ticket_header,
+      records:  [],
+      timezone: params[:timezone],
+      locale:   params[:locale]
+    )
+  end
+
+  def ticket_header
+    header = [
+      { display: '#', name: 'number', width: 18, data_type: 'string' },
+      { display: 'Title', name: 'title', width: 34, data_type: 'string' },
+      { display: 'State', name: 'state_id', width: 14, data_type: 'string' },
+      { display: 'Priority', name: 'priority_id', width: 14, data_type: 'string' },
+      { display: 'Group', name: 'group_id', width: 20, data_type: 'string' },
+      { display: 'Owner', name: 'owner_id', width: 20, data_type: 'string' },
+      { display: 'Customer', name: 'customer_id', width: 20, data_type: 'string' },
+      { display: 'Organization', name: 'organization_id', width: 20, data_type: 'string' },
+      { display: 'Create Channel', name: 'create_article_type_id', width: 10, data_type: 'string' },
+      { display: 'Sender', name: 'create_article_sender_id', width: 14, data_type: 'string' },
+      { display: 'Tags', name: 'tag_list', width: 20, data_type: 'string' },
+      { display: 'Time Units Total', name: 'time_unit', width: 10, data_type: 'float' },
+    ]
+
+    header = header.concat(@additional_attributes_header) if @additional_attributes_header
+
+    # ObjectManager attributes
+    objects = ObjectManager::Attribute.where(active:           true,
+                                             to_create:        false,
+                                             object_lookup_id: ObjectLookup.lookup(name: 'Ticket').id)
+                                      .pluck(:name, :display, :data_type, :data_option)
+                                      .map { |name, display, data_type, data_option| { name: name, display: display, data_type: data_type, data_option: data_option, width: 20 } }
+    objects.each do |object|
+      already_exists = false
+      header.each do |local_header|
+        next if local_header[:name] != object[:name]
+
+        already_exists = true
+        break
+      end
+      next if already_exists
+
+      header.push object
+    end
+
+    header = header.concat([
+                             { display: 'Created At', name: 'created_at', width: 18, data_type: 'datetime' },
+                             { display: 'Updated At', name: 'updated_at', width: 18, data_type: 'datetime' },
+                             { display: 'Closed At', name: 'close_at', width: 18, data_type: 'datetime' },
+                             { display: 'Close Escalation At', name: 'close_escalation_at', width: 18, data_type: 'datetime' },
+                             { display: 'Close In Min', name: 'close_in_min', width: 10, data_type: 'integer' },
+                             { display: 'Close Diff In Min', name: 'close_diff_in_min', width: 10, data_type: 'integer' },
+                             { display: 'First Response At', name: 'first_response_at', width: 18, data_type: 'datetime' },
+                             { display: 'First Response Escalation At', name: 'first_response_escalation_at', width: 18, data_type: 'datetime' },
+                             { display: 'First Response In Min', name: 'first_response_in_min', width: 10, data_type: 'integer' },
+                             { display: 'First Response Diff In Min', name: 'first_response_diff_in_min', width: 10, data_type: 'integer' },
+                             { display: 'Update Escalation At', name: 'update_escalation_at', width: 18, data_type: 'datetime' },
+                             { display: 'Update In Min', name: 'update_in_min', width: 10, data_type: 'integer' },
+                             { display: 'Update Diff In Min', name: 'update_diff_in_min', width: 10, data_type: 'integer' },
+                             { display: 'Last Contact At', name: 'last_contact_at', width: 18, data_type: 'datetime' },
+                             { display: 'Last Contact Agent At', name: 'last_contact_agent_at', width: 18, data_type: 'datetime' },
+                             { display: 'Last Contact Customer At', name: 'last_contact_customer_at', width: 18, data_type: 'datetime' },
+                             { display: 'Article Count', name: 'article_count', width: 10, data_type: 'integer' },
+                             { display: 'Escalation At', name: 'escalation_at', width: 18, data_type: 'datetime' },
+                           ])
+    header
+  end
+
+  def gen_rows
+    @ticket_ids.each_with_index do |ticket_id, index|
+      ticket = ::Ticket.lookup(id: ticket_id)
+      raise "Can't find Ticket with ID #{ticket_id} for '#{@title}' #{self.class.name} generation" if !ticket
+
+      gen_row_by_header(ticket, @additional_attributes[index])
+    rescue => e
+      Rails.logger.error e
+    end
+  end
+
+end

+ 14 - 0
spec/lib/excel_sheet_spec.rb

@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+RSpec.describe ExcelSheet do
+
+  describe '.timestamp_in_localtime' do
+
+    let(:document) { described_class.new(title: 'some title', header: [], records: [], timezone: 'Europe/Berlin', locale: 'de-de') }
+
+    it 'does convert UTC timestamp to local system based timestamp' do
+      expect(document.timestamp_in_localtime(Time.parse('2019-08-08T01:00:05Z').in_time_zone)).to eq('2019-08-08 03:00:05')
+    end
+
+  end
+end

+ 0 - 4
spec/requests/report_spec.rb

@@ -81,10 +81,6 @@ RSpec.describe 'Report', type: :request, searchindex: true do
       expect(@response['Content-Type']).to eq('application/vnd.ms-excel')
     end
 
-    it 'does convert UTC timestamp to local system based timestamp' do
-      expect(ReportsController.new.time_in_localtime_for_excel(Time.parse('2019-08-08T01:00:05Z').in_time_zone, 'Europe/Berlin')).to eq('2019-08-08 03:00:05')
-    end
-
     it 'does report example - deliver result' do
       skip('No ES configured') if !SearchIndexBackend.enabled?
 

+ 0 - 3
spec/requests/time_accounting_spec.rb

@@ -62,8 +62,5 @@ RSpec.describe 'Time Accounting API endpoints', type: :request do
       end
     end
 
-    it 'does convert UTC timestamp to local system based timestamp' do
-      expect(TimeAccountingsController.new.instance_eval { time_in_localtime_for_excel(Time.parse('2019-08-08T01:00:05Z').in_time_zone, 'Europe/Berlin') }).to eq('2019-08-08 03:00:05')
-    end
   end
 end