mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
771 lines
24 KiB
Ruby
771 lines
24 KiB
Ruby
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
|
|
|
class Ticket < ApplicationModel
|
|
include CanBeImported
|
|
include HasActivityStreamLog
|
|
include ChecksClientNotification
|
|
include CanCsvImport
|
|
include ChecksHtmlSanitized
|
|
include ChecksHumanChanges
|
|
include HasHistory
|
|
include HasTags
|
|
include HasSearchIndexBackend
|
|
include HasOnlineNotifications
|
|
include HasLinks
|
|
include HasObjectManagerAttributes
|
|
include HasTaskbars
|
|
include HasRecentCloses
|
|
include Ticket::CallsStatsTicketReopenLog
|
|
include Ticket::EnqueuesUserTicketCounterJob
|
|
include Ticket::ResetsPendingTimeSeconds
|
|
include Ticket::SetsCloseTime
|
|
include Ticket::SetsOnlineNotificationSeen
|
|
include Ticket::TouchesAssociations
|
|
include Ticket::TriggersSubscriptions
|
|
include Ticket::ChecksReopenAfterCertainTime
|
|
include Ticket::Checklists
|
|
include Ticket::HasDailyEventLocks
|
|
|
|
include ::Ticket::Escalation
|
|
include ::Ticket::Subject
|
|
include ::Ticket::Assets
|
|
include ::Ticket::SearchIndex
|
|
include ::Ticket::CanSelector
|
|
include ::Ticket::Search
|
|
include ::Ticket::MergeHistory
|
|
include ::Ticket::PerformChanges
|
|
|
|
store :preferences
|
|
after_initialize :check_defaults, if: :new_record?
|
|
before_create :check_generate, :check_defaults, :check_title, :set_default_state, :set_default_priority
|
|
before_update :check_defaults, :check_title, :reset_pending_time, :check_owner_active
|
|
|
|
# This must be loaded late as it depends on the internal before_create and before_update handlers of ticket.rb.
|
|
include Ticket::SetsLastOwnerUpdateTime
|
|
|
|
# workflow checks should run after before_create and before_update callbacks
|
|
# the transaction dispatcher must be run after the workflow checks!
|
|
include ChecksCoreWorkflow
|
|
include HasTransactionDispatcher
|
|
|
|
transaction_ignore_changes_attributes :updated_at,
|
|
:article_count,
|
|
:create_article_type_id,
|
|
:create_article_sender_id,
|
|
:ai_agent_running
|
|
|
|
validates :group_id, presence: true
|
|
|
|
activity_stream_permission 'ticket.agent'
|
|
|
|
core_workflow_screens 'create_middle', 'edit', 'overview_bulk'
|
|
core_workflow_admin_screens 'create_middle', 'edit'
|
|
|
|
taskbar_entities 'TicketZoom', 'TicketCreate'
|
|
taskbar_ignore_state_updates_entities 'TicketZoom'
|
|
|
|
activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
|
|
:create_article_type_id,
|
|
:create_article_sender_id,
|
|
:article_count,
|
|
:first_response_at,
|
|
:first_response_escalation_at,
|
|
:first_response_in_min,
|
|
:first_response_diff_in_min,
|
|
:close_at,
|
|
:close_escalation_at,
|
|
:close_in_min,
|
|
:close_diff_in_min,
|
|
:update_escalation_at,
|
|
:update_in_min,
|
|
:update_diff_in_min,
|
|
:last_close_at,
|
|
:last_contact_at,
|
|
:last_contact_agent_at,
|
|
:last_contact_customer_at,
|
|
:last_owner_update_at,
|
|
:preferences,
|
|
:ai_agent_running
|
|
|
|
search_index_attributes_relevant :organization_id,
|
|
:group_id,
|
|
:state_id,
|
|
:priority_id,
|
|
:customer_id,
|
|
:owner_id
|
|
|
|
history_attributes_ignored :create_article_type_id,
|
|
:create_article_sender_id,
|
|
:article_count,
|
|
:preferences,
|
|
:ai_agent_running
|
|
|
|
history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom', 'Checklist', 'Checklist::Item'
|
|
|
|
validates :note, length: { maximum: 250 }
|
|
sanitized_html :note
|
|
|
|
belongs_to :group, optional: true
|
|
belongs_to :organization, optional: true
|
|
|
|
has_many :articles, -> { reorder(:created_at, :id) }, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
|
|
has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
|
|
has_many :mentions, as: :mentionable, dependent: :destroy
|
|
has_one :shared_draft, class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy
|
|
belongs_to :state, class_name: 'Ticket::State', optional: true
|
|
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
|
|
belongs_to :owner, class_name: 'User', optional: true
|
|
belongs_to :customer, class_name: 'User', optional: true
|
|
belongs_to :created_by, class_name: 'User', optional: true
|
|
belongs_to :updated_by, class_name: 'User', optional: true
|
|
belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true
|
|
belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true
|
|
|
|
has_many :ai_stored_results, class_name: 'AI::StoredResult', as: :related_object, dependent: :destroy
|
|
|
|
association_attributes_ignored :flags, :mentions
|
|
|
|
attr_accessor :callback_loop
|
|
|
|
=begin
|
|
|
|
processes tickets which have reached their pending time and sets next state_id
|
|
|
|
processed_tickets = Ticket.process_pending
|
|
|
|
returns
|
|
|
|
processed_tickets = [<Ticket>, ...]
|
|
|
|
=end
|
|
|
|
def self.process_pending
|
|
result = []
|
|
|
|
# process pending action tickets
|
|
pending_action = Ticket::StateType.find_by(name: 'pending action')
|
|
ticket_states_pending_action = Ticket::State.where(state_type_id: pending_action)
|
|
.where.not(next_state_id: nil)
|
|
if ticket_states_pending_action.present?
|
|
next_state_map = {}
|
|
ticket_states_pending_action.each do |state|
|
|
next_state_map[state.id] = state.next_state_id
|
|
end
|
|
|
|
where(state_id: next_state_map.keys, pending_time: ..Time.current)
|
|
.find_each(batch_size: 500) do |ticket|
|
|
Transaction.execute do
|
|
ticket.state_id = next_state_map[ticket.state_id]
|
|
ticket.updated_at = Time.zone.now
|
|
ticket.updated_by_id = 1
|
|
ticket.save!
|
|
end
|
|
result.push ticket
|
|
end
|
|
end
|
|
|
|
# process pending reminder tickets
|
|
pending_reminder = Ticket::StateType.find_by(name: 'pending reminder')
|
|
ticket_states_pending_reminder = Ticket::State.where(state_type_id: pending_reminder)
|
|
|
|
if ticket_states_pending_reminder.present?
|
|
reminder_state_map = {}
|
|
ticket_states_pending_reminder.each do |state|
|
|
reminder_state_map[state.id] = state.next_state_id
|
|
end
|
|
|
|
where(state_id: reminder_state_map.keys, pending_time: ..Time.current)
|
|
.find_each(batch_size: 500) do |ticket|
|
|
|
|
article_id = nil
|
|
article = Ticket::Article.last_customer_agent_article(ticket.id)
|
|
if article
|
|
article_id = article.id
|
|
end
|
|
|
|
# send notification
|
|
TransactionJob.perform_now(
|
|
object: 'Ticket',
|
|
type: 'reminder_reached',
|
|
object_id: ticket.id,
|
|
article_id: article_id,
|
|
user_id: 1,
|
|
)
|
|
|
|
result.push ticket
|
|
end
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def auto_assign(user)
|
|
return if !persisted?
|
|
return if Setting.get('ticket_auto_assignment').blank?
|
|
return if owner_id != 1
|
|
return if !TicketPolicy.new(user, self).full?
|
|
|
|
user_ids_ignore = Array(Setting.get('ticket_auto_assignment_user_ids_ignore')).map(&:to_i)
|
|
return if user_ids_ignore.include?(user.id)
|
|
|
|
ticket_auto_assignment_selector = Setting.get('ticket_auto_assignment_selector')
|
|
return if ticket_auto_assignment_selector.blank?
|
|
|
|
condition = ticket_auto_assignment_selector[:condition].merge(
|
|
'ticket.id' => {
|
|
'operator' => 'is',
|
|
'value' => id,
|
|
}
|
|
)
|
|
|
|
ticket_count, = Ticket.selectors(condition, limit: 1, current_user: user, access: 'full')
|
|
return if ticket_count.to_i.zero?
|
|
|
|
update!(owner: user)
|
|
end
|
|
|
|
=begin
|
|
|
|
processes escalated tickets
|
|
|
|
processed_tickets = Ticket.process_escalation
|
|
|
|
returns
|
|
|
|
processed_tickets = [<Ticket>, ...]
|
|
|
|
=end
|
|
|
|
def self.process_escalation
|
|
result = []
|
|
|
|
# fetch all escalated and soon to be escalating tickets
|
|
where(escalation_at: ..15.minutes.from_now)
|
|
.find_each(batch_size: 500) do |ticket|
|
|
article_id = nil
|
|
article = Ticket::Article.last_customer_agent_article(ticket.id)
|
|
if article
|
|
article_id = article.id
|
|
end
|
|
|
|
# send escalation
|
|
if ticket.escalation_at < Time.zone.now
|
|
TransactionJob.perform_now(
|
|
object: 'Ticket',
|
|
type: 'escalation',
|
|
object_id: ticket.id,
|
|
article_id: article_id,
|
|
user_id: 1,
|
|
)
|
|
result.push ticket
|
|
next
|
|
end
|
|
|
|
# check if warning needs to be sent
|
|
TransactionJob.perform_now(
|
|
object: 'Ticket',
|
|
type: 'escalation_warning',
|
|
object_id: ticket.id,
|
|
article_id: article_id,
|
|
user_id: 1,
|
|
)
|
|
result.push ticket
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
=begin
|
|
|
|
processes tickets which auto unassign time has reached
|
|
|
|
processed_tickets = Ticket.process_auto_unassign
|
|
|
|
returns
|
|
|
|
processed_tickets = [<Ticket>, ...]
|
|
|
|
=end
|
|
|
|
def self.process_auto_unassign
|
|
|
|
# process pending action tickets
|
|
state_ids = Ticket::State.by_category_ids(:work_on)
|
|
return [] if state_ids.blank?
|
|
|
|
result = []
|
|
groups = Group.where(active: true).where('assignment_timeout IS NOT NULL AND groups.assignment_timeout != 0')
|
|
return [] if groups.blank?
|
|
|
|
groups.each do |group|
|
|
next if group.assignment_timeout.blank?
|
|
|
|
ticket_ids = Ticket.where('state_id IN (?) AND owner_id != 1 AND group_id = ? AND last_owner_update_at IS NOT NULL', state_ids, group.id).limit(600).pluck(:id)
|
|
ticket_ids.each do |ticket_id|
|
|
ticket = Ticket.find_by(id: ticket_id)
|
|
next if !ticket
|
|
|
|
minutes_since_last_assignment = Time.zone.now - ticket.last_owner_update_at
|
|
next if (minutes_since_last_assignment / 60) <= group.assignment_timeout
|
|
|
|
Transaction.execute do
|
|
ticket.owner_id = 1
|
|
ticket.updated_at = Time.zone.now
|
|
ticket.updated_by_id = 1
|
|
ticket.save!
|
|
end
|
|
result.push ticket
|
|
end
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
=begin
|
|
|
|
get article_count for a ticket, excluding system generated articles
|
|
|
|
=end
|
|
|
|
def compute_articles_count
|
|
self.article_count = articles.non_system.count
|
|
end
|
|
|
|
=begin
|
|
|
|
merge tickets
|
|
|
|
ticket = Ticket.find(123)
|
|
result = ticket.merge_to(
|
|
ticket_id: 123,
|
|
user_id: 123,
|
|
)
|
|
|
|
returns
|
|
|
|
result = true|false
|
|
|
|
=end
|
|
|
|
def merge_to(data)
|
|
|
|
# prevent cross merging tickets
|
|
target_ticket = Ticket.find_by(id: data[:ticket_id])
|
|
raise 'no target ticket given' if !target_ticket
|
|
raise Exceptions::UnprocessableContent, __('It is not possible to merge into an already merged ticket.') if target_ticket.state.state_type.name == 'merged'
|
|
|
|
# check different ticket ids
|
|
raise Exceptions::UnprocessableContent, __('A ticket cannot be merged into itself.') if id == target_ticket.id
|
|
|
|
# update articles
|
|
Transaction.execute context: 'merge' do
|
|
|
|
Ticket::Article.where(ticket_id: id).each(&:touch)
|
|
|
|
# quiet update of reassign of articles
|
|
Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]]) # rubocop:disable Rails/SkipsModelValidations
|
|
|
|
# mark target ticket as updated before logging the merge
|
|
target_ticket.compute_articles_count
|
|
target_ticket.update!(
|
|
updated_at: Time.zone.now,
|
|
)
|
|
|
|
# add merge event to both ticket's history (Issue #2469 - Add information "Ticket merged" to History)
|
|
target_ticket.history_log(
|
|
'received_merge',
|
|
data[:user_id],
|
|
id_to: target_ticket.id,
|
|
id_from: id,
|
|
)
|
|
history_log(
|
|
'merged_into',
|
|
data[:user_id],
|
|
id_to: target_ticket.id,
|
|
id_from: id,
|
|
)
|
|
|
|
# create new merge article
|
|
Ticket::Article.create(
|
|
ticket_id: id,
|
|
type_id: Ticket::Article::Type.lookup(name: 'note').id,
|
|
sender_id: Ticket::Article::Sender.lookup(name: 'Agent').id,
|
|
body: 'merged',
|
|
internal: false,
|
|
created_by_id: data[:user_id],
|
|
updated_by_id: data[:user_id],
|
|
)
|
|
|
|
compute_articles_count
|
|
|
|
# search for mention duplicates and destroy them before moving mentions
|
|
Mention.duplicates(self, target_ticket).destroy_all
|
|
Mention.where(mentionable: self).update_all(mentionable_id: target_ticket.id) # rubocop:disable Rails/SkipsModelValidations
|
|
|
|
# reassign links to the new ticket
|
|
# rubocop:disable Rails/SkipsModelValidations
|
|
ticket_source_id = Link::Object.find_by(name: 'Ticket').id
|
|
|
|
# search for all duplicate source and target links and destroy them
|
|
# before link merging
|
|
Link.duplicates(
|
|
object1_id: ticket_source_id,
|
|
object1_value: id,
|
|
object2_value: data[:ticket_id]
|
|
).destroy_all
|
|
Link.where(
|
|
link_object_source_id: ticket_source_id,
|
|
link_object_source_value: id,
|
|
).update_all(link_object_source_value: data[:ticket_id])
|
|
Link.where(
|
|
link_object_target_id: ticket_source_id,
|
|
link_object_target_value: id,
|
|
).update_all(link_object_target_value: data[:ticket_id])
|
|
# rubocop:enable Rails/SkipsModelValidations
|
|
|
|
# link tickets
|
|
Link.add(
|
|
link_type: 'parent',
|
|
link_object_source: 'Ticket',
|
|
link_object_source_value: data[:ticket_id],
|
|
link_object_target: 'Ticket',
|
|
link_object_target_value: id
|
|
)
|
|
|
|
# external sync references
|
|
ExternalSync.migrate('Ticket', id, target_ticket.id)
|
|
|
|
# set state to 'merged'
|
|
state_type = Ticket::StateType.lookup(name: 'merged')
|
|
self.state_id = Ticket::State.lookup(state_type_id: state_type.id).id
|
|
|
|
# rest owner
|
|
self.owner_id = 1
|
|
|
|
# save ticket
|
|
save!
|
|
|
|
# touch new ticket (to broadcast change)
|
|
target_ticket.touch # rubocop:disable Rails/SkipsModelValidations
|
|
|
|
EventBuffer.add('transaction', {
|
|
object: target_ticket.class.name,
|
|
type: 'update.received_merge',
|
|
data: target_ticket,
|
|
changes: {},
|
|
id: target_ticket.id,
|
|
user_id: UserInfo.current_user_id,
|
|
created_at: Time.zone.now,
|
|
})
|
|
|
|
EventBuffer.add('transaction', {
|
|
object: self.class.name,
|
|
type: 'update.merged_into',
|
|
data: self,
|
|
changes: {},
|
|
id: id,
|
|
user_id: UserInfo.current_user_id,
|
|
created_at: Time.zone.now,
|
|
})
|
|
end
|
|
true
|
|
end
|
|
|
|
=begin
|
|
|
|
perform active triggers on ticket
|
|
|
|
Ticket.perform_triggers(ticket, article, triggers, item, triggers, options)
|
|
|
|
=end
|
|
|
|
def self.perform_triggers(ticket, article, triggers, item, options = {})
|
|
recursive = Setting.get('ticket_trigger_recursive')
|
|
type = options[:type] || item[:type]
|
|
local_options = options.clone
|
|
local_options[:type] = type
|
|
local_options[:reset_user_id] = true
|
|
local_options[:disable] = ['Transaction::Notification']
|
|
local_options[:trigger_ids] ||= {}
|
|
local_options[:trigger_ids][ticket.id.to_s] ||= []
|
|
local_options[:loop_count] ||= 0
|
|
local_options[:loop_count] += 1
|
|
|
|
ticket_trigger_recursive_max_loop = Setting.get('ticket_trigger_recursive_max_loop')&.to_i || 10
|
|
if local_options[:loop_count] > ticket_trigger_recursive_max_loop
|
|
message = "Stopped perform_triggers for this object (Ticket/#{ticket.id}), because loop count was #{local_options[:loop_count]}!"
|
|
logger.info { message }
|
|
return [false, message]
|
|
end
|
|
|
|
return [true, __('No triggers active')] if triggers.blank?
|
|
|
|
# check if notification should be send because of customer emails
|
|
send_notification = true
|
|
if local_options[:send_notification] == false
|
|
send_notification = false
|
|
elsif item[:article_id]
|
|
article = Ticket::Article.lookup(id: item[:article_id])
|
|
if article&.preferences && article.preferences['send-auto-response'] == false
|
|
send_notification = false
|
|
end
|
|
end
|
|
|
|
Transaction.execute(local_options) do
|
|
triggers.each do |trigger|
|
|
logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
|
|
|
|
user_id = ticket.updated_by_id
|
|
if article
|
|
user_id = article.updated_by_id
|
|
end
|
|
|
|
user = User.lookup(id: user_id)
|
|
|
|
# verify is condition is matching
|
|
ticket_count, tickets = Ticket.selectors(
|
|
trigger.condition,
|
|
limit: 1,
|
|
execution_time: true,
|
|
current_user: user,
|
|
access: 'ignore',
|
|
ticket_action: type,
|
|
ticket_id: ticket.id,
|
|
article_id: article&.id,
|
|
changes: item[:changes],
|
|
changes_required: trigger.condition_changes_required?
|
|
)
|
|
|
|
next if ticket_count.blank?
|
|
next if ticket_count.zero?
|
|
next if tickets.take.id != ticket.id
|
|
|
|
return [true, message] if recursive == false && local_options[:loop_count] > 1
|
|
|
|
if article && send_notification == false && trigger.perform['notification.email'] && trigger.perform['notification.email']['recipient']
|
|
recipient = trigger.perform['notification.email']['recipient']
|
|
local_options[:send_notification] = false
|
|
if recipient.include?('ticket_customer') || recipient.include?('article_last_sender')
|
|
logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because sender do not want to get auto responder for object (Ticket/#{ticket.id}/Article/#{article.id})" }
|
|
next
|
|
end
|
|
end
|
|
|
|
if local_options[:trigger_ids][ticket.id.to_s].include?(trigger.id)
|
|
logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because was already executed for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
|
|
next
|
|
end
|
|
local_options[:trigger_ids][ticket.id.to_s].push trigger.id
|
|
logger.info { "Execute trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
|
|
|
|
ticket.perform_changes(trigger, 'trigger', item, user_id, activator_type: type)
|
|
|
|
if recursive == true
|
|
TransactionDispatcher.commit(local_options)
|
|
end
|
|
end
|
|
end
|
|
[true, ticket, local_options]
|
|
end
|
|
|
|
=begin
|
|
|
|
get all email references headers of a ticket, to exclude some, parse it as array into method
|
|
|
|
references = ticket.get_references
|
|
|
|
result
|
|
|
|
['message-id-1234', 'message-id-5678']
|
|
|
|
ignore references header(s)
|
|
|
|
references = ticket.get_references(['message-id-5678'])
|
|
|
|
result
|
|
|
|
['message-id-1234']
|
|
|
|
=end
|
|
|
|
# limited by 32kb (https://github.com/zammad/zammad/issues/5334)
|
|
# https://learn.microsoft.com/en-us/office365/servicedescriptions/exchange-online-service-description/exchange-online-limits
|
|
def get_references(ignore = [], max_length: 30_000)
|
|
references = []
|
|
counter = 0
|
|
Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).reorder(id: :desc).each do |article|
|
|
new_references = []
|
|
if article.message_id.present?
|
|
new_references.push article.message_id
|
|
end
|
|
if article.in_reply_to.present?
|
|
new_references.push article.in_reply_to
|
|
end
|
|
new_references -= ignore
|
|
|
|
counter += new_references.join.length
|
|
break if counter > max_length
|
|
|
|
references.unshift(*new_references)
|
|
end
|
|
references
|
|
end
|
|
|
|
# Get whichever #last_contact_* was later
|
|
# This is not identical to #last_contact_at
|
|
# It returns time to last original (versus follow up) contact
|
|
# @return [Time, nil]
|
|
def last_original_update_at
|
|
[last_contact_agent_at, last_contact_customer_at].compact.max
|
|
end
|
|
|
|
# true if conversation did happen and agent responded
|
|
# false if customer is waiting for response or agent reached out and customer did not respond yet
|
|
# @return [Bool]
|
|
def agent_responded?
|
|
return false if last_contact_customer_at.blank?
|
|
return false if last_contact_agent_at.blank?
|
|
|
|
last_contact_customer_at < last_contact_agent_at
|
|
end
|
|
|
|
=begin
|
|
|
|
Get the color of the state the current ticket is in
|
|
|
|
ticket.current_state_color
|
|
|
|
returns a hex color code
|
|
|
|
=end
|
|
def current_state_color
|
|
return '#f35912' if escalation_at && escalation_at < Time.zone.now
|
|
|
|
case state.state_type.name
|
|
when 'new', 'open'
|
|
return '#faab00'
|
|
when 'closed'
|
|
return '#38ad69'
|
|
when 'pending reminder'
|
|
return '#faab00' if pending_time && pending_time < Time.zone.now
|
|
end
|
|
|
|
'#000000'
|
|
end
|
|
|
|
def mention_user_ids
|
|
mentions.pluck(:user_id)
|
|
end
|
|
|
|
def ai_summary_unread?(user, ai_analytics_run)
|
|
return false if !ai_analytics_run
|
|
|
|
usage = ai_analytics_run.usages.find_by(user:)
|
|
|
|
return false if usage
|
|
|
|
articles
|
|
.without_system_notifications
|
|
.last
|
|
&.author_id != user.id
|
|
end
|
|
|
|
private
|
|
|
|
def check_generate
|
|
return true if number
|
|
|
|
self.number = Ticket::Number.generate
|
|
true
|
|
end
|
|
|
|
def check_title
|
|
return true if !title
|
|
|
|
title.gsub!(%r{\s|\t|\r}, ' ')
|
|
true
|
|
end
|
|
|
|
def check_defaults
|
|
check_default_owner
|
|
check_default_organization
|
|
true
|
|
end
|
|
|
|
def check_default_owner
|
|
return if !has_attribute?(:owner_id)
|
|
return if owner_id || owner
|
|
|
|
self.owner_id = 1
|
|
end
|
|
|
|
def check_default_organization
|
|
return if !has_attribute?(:organization_id)
|
|
return if !customer_id
|
|
|
|
customer = User.find_by(id: customer_id)
|
|
return if !customer
|
|
return if organization_id.present? && customer.organization_id?(organization_id)
|
|
return if organization.present? && customer.organization_id?(organization.id)
|
|
|
|
self.organization_id = customer.organization_id
|
|
end
|
|
|
|
def reset_pending_time
|
|
|
|
# ignore if no state has changed
|
|
return true if !changes_to_save['state_id']
|
|
|
|
# ignore if new state is blank and
|
|
# let handle ActiveRecord the error
|
|
return if state_id.blank?
|
|
|
|
# check if new state isn't pending*
|
|
current_state = Ticket::State.lookup(id: state_id)
|
|
current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id)
|
|
|
|
# in case, set pending_time to nil
|
|
return true if current_state_type.name.match?(%r{^pending}i)
|
|
|
|
self.pending_time = nil
|
|
true
|
|
end
|
|
|
|
def set_default_state
|
|
return true if state_id
|
|
|
|
default_ticket_state = Ticket::State.find_by(default_create: true)
|
|
return true if !default_ticket_state
|
|
|
|
self.state_id = default_ticket_state.id
|
|
true
|
|
end
|
|
|
|
def set_default_priority
|
|
return true if priority_id
|
|
|
|
default_ticket_priority = Ticket::Priority.find_by(default_create: true)
|
|
return true if !default_ticket_priority
|
|
|
|
self.priority_id = default_ticket_priority.id
|
|
true
|
|
end
|
|
|
|
def check_owner_active
|
|
return true if Setting.get('import_mode')
|
|
|
|
# only change the owner for non closed Tickets for historical/reporting reasons
|
|
return true if state.present? && Ticket::StateType.lookup(id: state.state_type_id)&.name == 'closed'
|
|
|
|
# return when ticket is unassigned
|
|
return true if owner_id.blank?
|
|
return true if owner_id == 1
|
|
|
|
# return if owner is active, is agent and has access to group of ticket
|
|
return true if owner.active? && owner.permissions?('ticket.agent') && owner.group_access?(group_id, 'full')
|
|
|
|
# else set the owner of the ticket to the default user as unassigned
|
|
self.owner_id = 1
|
|
true
|
|
end
|
|
end
|