Initial Redmine tooling and local plugin forks
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class Address < ActiveRecord::Base
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
attr_reader :country
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'street1', 'street2', 'region', 'city', 'country_code', 'postcode',
|
||||
'full_address', 'address_type', 'addressable'
|
||||
|
||||
belongs_to :addressable, :polymorphic => true
|
||||
|
||||
scope :business, lambda { where(:address_type => 'business') }
|
||||
scope :billing, lambda { where(:address_type => 'billing') }
|
||||
scope :shipping, lambda { where(:address_type => 'shipping') }
|
||||
|
||||
before_save :populate_full_address
|
||||
|
||||
def country
|
||||
@country ||= l(:label_crm_countries)[country_code.to_sym].to_s unless country_code.blank?
|
||||
end
|
||||
|
||||
def blank?
|
||||
%w(street1 street2 city region postcode country_code).all? { |attr| self.send(attr).blank? }
|
||||
end
|
||||
|
||||
#----------------------------------------------------------------------------
|
||||
# Ensure blank address records don't get created. If we have a new record and
|
||||
# address is empty then return true otherwise return false so that _destroy
|
||||
# is processed (if applicable) and the record is removed.
|
||||
# Intended to be called as follows:
|
||||
# accepts_nested_attributes_for :business_address, :allow_destroy => true, :reject_if => proc {|attributes| Address.reject_address(attributes)}
|
||||
def self.reject_address(attributes)
|
||||
exists = attributes['id'].present?
|
||||
empty = %w(street1 street2 city region postcode country_code full_address).map { |name| attributes[name].blank? }.all?
|
||||
attributes[:_destroy] = 1 if exists && empty
|
||||
!exists && empty
|
||||
end
|
||||
|
||||
def to_s
|
||||
%w(street1 street2 city postcode region country).map { |attr| send(attr) }.select { |a| !a.blank? }.join(', ')
|
||||
end
|
||||
|
||||
def post_address
|
||||
address_template = ContactsSetting.post_address_format
|
||||
address_template = address_template.gsub('%street1%', street1.to_s)
|
||||
address_template = address_template.gsub('%street2%', street2.to_s)
|
||||
address_template = address_template.gsub('%city%', city.to_s)
|
||||
address_template = address_template.gsub('%town%', city.to_s)
|
||||
address_template = address_template.gsub('%postcode%', postcode.to_s)
|
||||
address_template = address_template.gsub('%zip%', postcode.to_s)
|
||||
address_template = address_template.gsub('%region%', region.to_s)
|
||||
address_template = address_template.gsub('%state%', region.to_s)
|
||||
address_template = address_template.gsub('%country%', country.to_s)
|
||||
address_template.gsub(/\r\n?/, "\n").gsub(/^$\n/, '').gsub(/^[, ]+|[, ]+$|[,]{2,}/,'').gsub(/\s{2,}/, ' ').strip
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def populate_full_address
|
||||
self.full_address = self.to_s
|
||||
end
|
||||
end
|
||||
+519
@@ -0,0 +1,519 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class Contact < ActiveRecord::Base
|
||||
unloadable
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
CONTACT_FORMATS = {
|
||||
:firstname_lastname => {
|
||||
:string => '#{first_name} #{last_name}',
|
||||
:order => %w(first_name middle_name last_name id),
|
||||
:setting_order => 1
|
||||
},
|
||||
:lastname_firstname_middlename => {
|
||||
:string => '#{last_name} #{first_name} #{middle_name}',
|
||||
:order => %w(last_name first_name middle_name id),
|
||||
:setting_order => 1
|
||||
},
|
||||
:firstname_middlename_lastname => {
|
||||
:string => '#{first_name} #{middle_name} #{last_name}',
|
||||
:order => %w(first_name middle_name last_name id),
|
||||
:setting_order => 1
|
||||
},
|
||||
:firstname_lastinitial => {
|
||||
:string => '#{first_name} #{middle_name.to_s.chars.first + \'.\' unless middle_name.blank?} #{last_name.to_s.chars.first + \'.\' unless last_name.blank?}',
|
||||
:order => %w(first_name middle_name last_name id),
|
||||
:setting_order => 2
|
||||
},
|
||||
:firstinitial_lastname => {
|
||||
:string => '#{first_name.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{middle_name.to_s.chars.first + \'.\' unless middle_name.blank?} #{last_name}',
|
||||
:order => %w(first_name middle_name last_name id),
|
||||
:setting_order => 2
|
||||
},
|
||||
:lastname_firstinitial => {
|
||||
:string => '#{last_name} #{first_name.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{middle_name.to_s.chars.first + \'.\' unless middle_name.blank?}',
|
||||
:order => %w(last_name first_name middle_name id),
|
||||
:setting_order => 2
|
||||
},
|
||||
:firstname => {
|
||||
:string => '#{first_name}',
|
||||
:order => %w(first_name middle_name id),
|
||||
:setting_order => 3
|
||||
},
|
||||
:lastname_firstname => {
|
||||
:string => '#{last_name} #{first_name}',
|
||||
:order => %w(last_name first_name middle_name id),
|
||||
:setting_order => 4
|
||||
},
|
||||
:lastname_coma_firstname => {
|
||||
:string => '#{last_name.to_s + \',\' unless last_name.blank?} #{first_name}',
|
||||
:order => %w(last_name first_name middle_name id),
|
||||
:setting_order => 5
|
||||
},
|
||||
:lastname => {
|
||||
:string => '#{last_name}',
|
||||
:order => %w(last_name id),
|
||||
:setting_order => 6
|
||||
}
|
||||
}
|
||||
|
||||
VISIBILITY_PROJECT = 0
|
||||
VISIBILITY_PUBLIC = 1
|
||||
VISIBILITY_PRIVATE = 2
|
||||
|
||||
delegate :street1, :street2, :city, :country, :country_code, :postcode, :region, :post_address, :to => :address, :allow_nil => true
|
||||
|
||||
has_many :notes, :as => :source, :class_name => 'ContactNote', :dependent => :delete_all
|
||||
has_many :addresses, :dependent => :destroy, :as => :addressable, :class_name => 'Address'
|
||||
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
|
||||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
has_one :avatar, lambda { where("#{Attachment.table_name}.description = 'avatar'") }, :class_name => 'Attachment', :as => :container, :dependent => :destroy
|
||||
has_one :address, lambda { where(:address_type => 'business') }, :dependent => :destroy, :as => :addressable, :class_name => 'Address'
|
||||
has_many :deals, lambda { order("#{Deal.table_name}.status_id") }
|
||||
has_and_belongs_to_many :related_deals, lambda { order("#{Deal.table_name}.status_id") }, :uniq => true, :class_name => 'Deal'
|
||||
has_and_belongs_to_many :projects, :uniq => true
|
||||
has_and_belongs_to_many :issues, lambda { order("#{Issue.table_name}.due_date") }, :uniq => true
|
||||
else
|
||||
has_one :avatar, :conditions => "#{Attachment.table_name}.description = 'avatar'", :class_name => 'Attachment', :as => :container, :dependent => :destroy
|
||||
has_one :address, :conditions => { :address_type => 'business' }, :dependent => :destroy, :as => :addressable, :class_name => 'Address'
|
||||
has_many :deals, :order => "#{Deal.table_name}.status_id"
|
||||
has_and_belongs_to_many :related_deals, :order => "#{Deal.table_name}.status_id", :class_name => 'Deal', :uniq => true
|
||||
has_and_belongs_to_many :projects, :uniq => true
|
||||
has_and_belongs_to_many :issues, :order => "#{Issue.table_name}.due_date", :uniq => true
|
||||
end
|
||||
|
||||
attr_accessor :phones
|
||||
attr_accessor :emails
|
||||
acts_as_customizable
|
||||
acts_as_viewable
|
||||
rcrm_acts_as_taggable
|
||||
acts_as_watchable
|
||||
acts_as_attachable :view_permission => :view_contacts,
|
||||
:delete_permission => :edit_contacts
|
||||
|
||||
acts_as_event :datetime => :created_on,
|
||||
:url => lambda { |o| { :controller => 'contacts', :action => 'show', :id => o } },
|
||||
:type => 'icon icon-contact',
|
||||
:title => lambda { |o| o.name },
|
||||
:description => lambda { |o| [o.info, o.company, o.email, o.address, o.background].join(' ') }
|
||||
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
acts_as_activity_provider :type => 'contacts',
|
||||
:permission => :view_contacts,
|
||||
:author_key => :author_id,
|
||||
:scope => joins(:projects)
|
||||
|
||||
acts_as_searchable :columns => ["#{table_name}.first_name",
|
||||
"#{table_name}.middle_name",
|
||||
"#{table_name}.last_name",
|
||||
"#{table_name}.company",
|
||||
"#{table_name}.email",
|
||||
"#{Address.table_name}.full_address",
|
||||
"#{table_name}.background",
|
||||
"#{ContactNote.table_name}.content"],
|
||||
:project_key => "#{Project.table_name}.id",
|
||||
:scope => includes([:address, :notes]),
|
||||
:date_column => "created_on"
|
||||
else
|
||||
acts_as_activity_provider :type => 'contacts',
|
||||
:permission => :view_contacts,
|
||||
:author_key => :author_id,
|
||||
:find_options => { :include => :projects }
|
||||
|
||||
acts_as_searchable :columns => ["#{table_name}.first_name",
|
||||
"#{table_name}.middle_name",
|
||||
"#{table_name}.last_name",
|
||||
"#{table_name}.company",
|
||||
"#{table_name}.email",
|
||||
"#{Address.table_name}.full_address",
|
||||
"#{table_name}.background",
|
||||
"#{ContactNote.table_name}.content"],
|
||||
:project_key => "#{Project.table_name}.id",
|
||||
:include => [:projects, :address, :notes],
|
||||
# sort by id so that limited eager loading doesn't break with postgresql
|
||||
:order_column => "#{table_name}.id"
|
||||
end
|
||||
|
||||
accepts_nested_attributes_for :address, :allow_destroy => true, :update_only => true, :reject_if => proc { |attributes| Address.reject_address(attributes) }
|
||||
|
||||
scope :visible, lambda { |*args| eager_load(:projects).where(Contact.visible_condition(args.shift || User.current, *args)) }
|
||||
scope :deletable, lambda { |*args| eager_load(:projects).where(Contact.deletable_condition(args.shift || User.current, *args)).readonly(false) }
|
||||
scope :editable, lambda { |*args| eager_load(:projects).where(Contact.editable_condition(args.shift || User.current, *args)).readonly(false) }
|
||||
scope :by_project, lambda { |prj| joins(:projects).where("#{Project.table_name}.id = ?", prj) unless prj.blank? }
|
||||
scope :like_by, lambda { |field, search| {:conditions => ["LOWER(#{Contact.table_name}.#{field}) LIKE ?", search.downcase + "%"] }}
|
||||
scope :companies, lambda { where(:is_company => true) }
|
||||
scope :people, lambda { where(:is_company => false) }
|
||||
scope :order_by_name, lambda { order(Contact.fields_for_order_statement) }
|
||||
scope :order_by_creation, lambda { order("#{Contact.table_name}.created_on DESC") }
|
||||
|
||||
scope :by_full_name, lambda { |search| where("LOWER(CONCAT(#{Contact.table_name}.first_name,' ',#{Contact.table_name}.last_name)) = ? ", search.downcase) }
|
||||
scope :by_name, lambda { |search| where("(LOWER(#{Contact.table_name}.first_name) LIKE LOWER(:p) OR
|
||||
LOWER(#{Contact.table_name}.last_name) LIKE LOWER(:p) OR
|
||||
LOWER(#{Contact.table_name}.middle_name) LIKE LOWER(:p))",
|
||||
{ :p => '%' + search.downcase + '%' }) }
|
||||
|
||||
scope :live_search, lambda {|search| where("(LOWER(#{Contact.table_name}.first_name) LIKE LOWER(:p) OR
|
||||
LOWER(#{Contact.table_name}.last_name) LIKE LOWER(:p) OR
|
||||
LOWER(#{Contact.table_name}.middle_name) LIKE LOWER(:p) OR
|
||||
LOWER(#{Contact.table_name}.company) LIKE LOWER(:p) OR
|
||||
LOWER(#{Contact.table_name}.email) LIKE LOWER(:p) OR
|
||||
LOWER(#{Contact.table_name}.phone) LIKE LOWER(:p) OR
|
||||
LOWER(#{Contact.table_name}.job_title) LIKE LOWER(:p))",
|
||||
{ :p => '%' + search.downcase + '%' }) }
|
||||
|
||||
validates_presence_of :first_name, :project
|
||||
validate :emails_format
|
||||
# validates_uniqueness_of :first_name, :scope => [:last_name, :company, :email]
|
||||
|
||||
before_validation :strip_email
|
||||
after_create :send_notification
|
||||
before_save :update_company_contacts
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'is_company',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'middle_name',
|
||||
'company',
|
||||
'website',
|
||||
'skype_name',
|
||||
'birthday',
|
||||
'job_title',
|
||||
'background',
|
||||
'author_id',
|
||||
'assigned_to_id',
|
||||
'phone',
|
||||
'email',
|
||||
'tag_list',
|
||||
'project_ids',
|
||||
'visibility',
|
||||
'custom_field_values',
|
||||
'custom_fields',
|
||||
'watcher_user_ids',
|
||||
'address_attributes'
|
||||
|
||||
def self.visible_condition(user, options = {})
|
||||
user.reload
|
||||
user_ids = [user.id] + user.groups.map(&:id)
|
||||
|
||||
projects_allowed_to_view_contacts = Project.where(Project.allowed_to_condition(user, :view_contacts)).pluck(:id)
|
||||
allowed_to_view_condition = projects_allowed_to_view_contacts.empty? ? "(1=0)" : "#{Project.table_name}.id IN (#{projects_allowed_to_view_contacts.join(',')})"
|
||||
projects_allowed_to_view_private = Project.where(Project.allowed_to_condition(user, :view_private_contacts)).pluck(:id)
|
||||
allowed_to_view_private_condition = projects_allowed_to_view_private.empty? ? "(1=0)" : "#{Project.table_name}.id IN (#{projects_allowed_to_view_private.join(',')})"
|
||||
|
||||
cond = "(#{Project.table_name}.id <> -1 ) AND ("
|
||||
if user.admin?
|
||||
cond << "(#{table_name}.visibility = 1) OR (#{allowed_to_view_condition}) "
|
||||
else
|
||||
cond << " (#{table_name}.visibility = 1) OR" if user.allowed_to_globally?(:view_contacts, {})
|
||||
cond << " (#{allowed_to_view_condition} AND #{table_name}.visibility <> 2) "
|
||||
|
||||
if user.logged?
|
||||
cond << " OR (#{allowed_to_view_private_condition} " +
|
||||
" OR (#{allowed_to_view_condition} " +
|
||||
" AND (#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}) )))"
|
||||
end
|
||||
end
|
||||
cond << ')'
|
||||
end
|
||||
|
||||
def self.editable_condition(user, options = {})
|
||||
visible_condition(user, options) + " AND (#{Project.allowed_to_condition(user, :edit_contacts)})"
|
||||
end
|
||||
|
||||
def self.deletable_condition(user, options = {})
|
||||
visible_condition(user, options) + " AND (#{Project.allowed_to_condition(user, :delete_contacts)})"
|
||||
end
|
||||
def all_deals
|
||||
@all_deals ||= (deals + related_deals).uniq.sort! { |x, y| x.status_id <=> y.status_id }
|
||||
end
|
||||
|
||||
def all_visible_deals(usr = User.current)
|
||||
@all_deals ||= (deals.visible(usr) + related_deals.visible(usr)).uniq.sort! { |x, y| x.status_id <=> y.status_id }
|
||||
if is_company?
|
||||
company_contacts.each { |contact| @all_deals += contact.deals }
|
||||
end
|
||||
@all_deals.uniq.sort! { |x, y| x.status_id <=> y.status_id }
|
||||
end
|
||||
|
||||
def self.available_tags(options = {})
|
||||
limit = options[:limit]
|
||||
|
||||
scope = RedmineCrm::Tag.where({})
|
||||
scope = scope.where("#{Project.table_name}.id = ?", options[:project]) if options[:project]
|
||||
scope = scope.where(Contact.visible_condition(options[:user] || User.current))
|
||||
scope = scope.where("LOWER(#{RedmineCrm::Tag.table_name}.name) LIKE ?", "%#{options[:name_like].downcase}%") if options[:name_like]
|
||||
|
||||
joins = []
|
||||
joins << "JOIN #{RedmineCrm::Tagging.table_name} ON #{RedmineCrm::Tagging.table_name}.tag_id = #{RedmineCrm::Tag.table_name}.id "
|
||||
joins << "JOIN #{Contact.table_name} ON #{Contact.table_name}.id = #{RedmineCrm::Tagging.table_name}.taggable_id AND #{RedmineCrm::Tagging.table_name}.taggable_type = '#{Contact.name}' "
|
||||
joins << Contact.projects_joins
|
||||
|
||||
scope = scope.select("#{RedmineCrm::Tag.table_name}.*, COUNT(DISTINCT #{RedmineCrm::Tagging.table_name}.taggable_id) AS count")
|
||||
scope = scope.joins(joins.flatten)
|
||||
scope = scope.group("#{RedmineCrm::Tag.table_name}.id, #{RedmineCrm::Tag.table_name}.name HAVING COUNT(*) > 0")
|
||||
scope = scope.limit(limit) if limit
|
||||
scope = scope.order("#{RedmineCrm::Tag.table_name}.name")
|
||||
scope
|
||||
end
|
||||
|
||||
def duplicates(limit = 10)
|
||||
scope = Contact.where({})
|
||||
|
||||
cond = "((1=1) "
|
||||
cond << "AND LOWER(#{Contact.table_name}.first_name) LIKE LOWER('#{first_name.strip}') " unless first_name.blank?
|
||||
cond << "AND (LOWER(#{Contact.table_name}.middle_name) LIKE LOWER('#{middle_name.strip}') OR middle_name LIKE '') " unless middle_name.blank?
|
||||
cond << "AND LOWER(#{Contact.table_name}.last_name) LIKE LOWER('#{last_name.strip}') " unless last_name.blank?
|
||||
cond << " OR LOWER(#{Contact.table_name}.email) LIKE LOWER('#{primary_email.strip}') " unless primary_email.blank?
|
||||
cond << ")"
|
||||
cond << " AND #{Contact.table_name}.id <> #{id}" unless new_record?
|
||||
scope = scope.where(cond)
|
||||
@duplicates ||= (first_name.blank? && last_name.blank? && middle_name.blank?) ? [] : scope.visible.limit(limit)
|
||||
end
|
||||
|
||||
def company_contacts
|
||||
@contacts ||= Contact.order_by_name.includes(:avatar).where(["#{Contact.table_name}.is_company = ? AND #{Contact.table_name}.company = ? AND #{Contact.table_name}.id <> ?", false, first_name, id])
|
||||
end
|
||||
|
||||
alias_method :employees, :company_contacts
|
||||
|
||||
def redmine_user
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
@redmine_user ||= User.joins(:email_address).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", emails).first unless email.blank?
|
||||
else
|
||||
@redmine_user ||= User.where(:mail => emails).first unless email.blank?
|
||||
end
|
||||
end
|
||||
|
||||
def contact_company
|
||||
@contact_company ||= Contact.where(:first_name => company, :is_company => true).
|
||||
where("#{Contact.table_name}.id <> #{id.to_i}").first unless company.blank?
|
||||
end
|
||||
|
||||
def notes_attachments
|
||||
@contact_attachments ||= Attachment.where(:container_type => 'Note', :container_id => notes.map(&:id)).order(:created_on)
|
||||
end
|
||||
|
||||
# usr for mailer
|
||||
def visible?(usr = nil)
|
||||
usr ||= User.current
|
||||
if is_public?
|
||||
usr.allowed_to_globally?(:view_contacts, {})
|
||||
else
|
||||
allowed_to?(usr || User.current, :view_contacts)
|
||||
end
|
||||
end
|
||||
|
||||
def editable?(usr = nil)
|
||||
allowed_to?(usr || User.current, :edit_contacts)
|
||||
end
|
||||
|
||||
def deletable?(usr = nil)
|
||||
allowed_to?(usr || User.current, :delete_contacts)
|
||||
end
|
||||
|
||||
def allowed_to?(user, action, options = {})
|
||||
if is_private?
|
||||
(projects.map { |p| user.allowed_to?(action, p) }.compact.any? && (author == user || user.is_or_belongs_to?(assigned_to))) ||
|
||||
(projects.map { |p| user.allowed_to?(:view_private_contacts, p) }.compact.any? && projects.map { |p| user.allowed_to?(action, p) }.compact.any?)
|
||||
else
|
||||
projects.map { |p| user.allowed_to?(action, p) }.compact.any?
|
||||
end
|
||||
end
|
||||
|
||||
def is_public?
|
||||
visibility == VISIBILITY_PUBLIC
|
||||
end
|
||||
|
||||
def is_private?
|
||||
visibility == VISIBILITY_PRIVATE
|
||||
end
|
||||
|
||||
def send_mail_allowed?(usr = nil)
|
||||
usr ||= User.current
|
||||
@send_mail_allowed ||= 0 < projects.visible(usr).where(Project.allowed_to_condition(usr, :send_contacts_mail)).count
|
||||
end
|
||||
|
||||
def self.projects_joins
|
||||
joins = []
|
||||
joins << ["JOIN contacts_projects ON contacts_projects.contact_id = #{table_name}.id"]
|
||||
joins << ["JOIN #{Project.table_name} ON contacts_projects.project_id = #{Project.table_name}.id"]
|
||||
end
|
||||
|
||||
def project(current_project=nil)
|
||||
return @project if @project
|
||||
visible_projects = Project.visible.where(:id => projects.pluck(:id))
|
||||
if current_project && visible_projects.include?(current_project)
|
||||
@project = current_project
|
||||
else
|
||||
@project = visible_projects.where(Project.allowed_to_condition(User.current, :view_contacts)).first
|
||||
end
|
||||
|
||||
@project ||= projects.first
|
||||
end
|
||||
|
||||
def project=(project)
|
||||
projects << project
|
||||
end
|
||||
|
||||
def self.find_by_emails(emails)
|
||||
cond = '(1 = 0)'
|
||||
emails = emails.map(&:downcase)
|
||||
emails.each do |mail|
|
||||
cond << " OR (LOWER(#{Contact.table_name}.email) LIKE LOWER('%#{mail.gsub("'", "").gsub("\"", "")}%'))"
|
||||
end
|
||||
contacts = Contact.where(cond)
|
||||
contacts.select { |c| (c.emails.map(&:downcase) & emails).any? }
|
||||
end
|
||||
|
||||
def self.name_formatter(formatter = nil)
|
||||
CONTACT_FORMATS[formatter || ContactsSetting.contact_name_format.to_sym]
|
||||
end
|
||||
|
||||
# Returns an array of fields names than can be used to make an order statement for users
|
||||
# according to how user names are displayed
|
||||
# Examples:
|
||||
#
|
||||
# Contact.fields_for_order_statement => ['contacts.first_name', 'contacts.first_name', 'contacts.id']
|
||||
# Contact.fields_for_order_statement('customers') => ['customers.last_name', 'customers.id']
|
||||
def self.fields_for_order_statement(table = nil)
|
||||
table ||= table_name
|
||||
name_formatter[:order].map { |field| "#{table}.#{field}" }
|
||||
end
|
||||
|
||||
# Return contacts's full name for display
|
||||
def name(formatter = nil)
|
||||
unless is_company?
|
||||
f = self.class.name_formatter(formatter)
|
||||
if formatter
|
||||
eval('"' + f[:string] + '"')
|
||||
else
|
||||
@name ||= eval('"' + f[:string] + '"')
|
||||
end
|
||||
else
|
||||
first_name
|
||||
end
|
||||
end
|
||||
|
||||
def name_with_company
|
||||
return name if company.blank?
|
||||
[name, ' ', '(', company, ')'].join
|
||||
end
|
||||
|
||||
def info
|
||||
job_title
|
||||
end
|
||||
|
||||
def phones
|
||||
@phones || phone ? phone.split(/, */) : []
|
||||
end
|
||||
|
||||
def emails
|
||||
@emails || email ? email.split(/, */).map { |m| m.strip } : []
|
||||
end
|
||||
|
||||
def primary_email
|
||||
emails.first
|
||||
end
|
||||
|
||||
def age
|
||||
return nil if birthday.blank?
|
||||
now = Time.now
|
||||
# how many years?
|
||||
# has their birthday occured this year yet?
|
||||
# subtract 1 if so, 0 if not
|
||||
now.year - birthday.year - (birthday.to_time.change(:year => now.year) > now ? 1 : 0)
|
||||
end
|
||||
|
||||
def website_address
|
||||
website.match("^https?://") ? website : website.gsub(/^/, "http://") unless website.blank?
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def notified_users
|
||||
notified = []
|
||||
# Author and assignee are always notified unless they have been
|
||||
# locked or don't want to be notified
|
||||
notified << author if author
|
||||
if assigned_to
|
||||
notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
|
||||
end
|
||||
|
||||
notified += project.notified_users
|
||||
|
||||
if !is_company && !contact_company.blank?
|
||||
notified += contact_company.notified_users
|
||||
end
|
||||
|
||||
notified = notified.select { |u| u.active? }
|
||||
notified.uniq!
|
||||
# Remove users that can not view the issue
|
||||
notified.reject! { |user| !visible?(user) }
|
||||
notified
|
||||
end
|
||||
|
||||
# Returns the mail adresses of users that should be notified
|
||||
def recipients
|
||||
notified_users.collect(&:mail)
|
||||
end
|
||||
|
||||
def all_watcher_recepients
|
||||
notified = watcher_recipients
|
||||
if !is_company && !contact_company.blank?
|
||||
notified += contact_company.watcher_recipients
|
||||
end
|
||||
notified
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_phone
|
||||
if @phones
|
||||
self.phone = @phones.uniq.map { |s| s.strip.delete(',').squeeze(' ') }.join(', ')
|
||||
end
|
||||
end
|
||||
|
||||
def send_notification
|
||||
Mailer.crm_contact_add(self).deliver if Setting.notified_events.include?('crm_contact_added')
|
||||
end
|
||||
|
||||
def strip_email
|
||||
return unless email
|
||||
self.email = email.tr(' ', '')
|
||||
end
|
||||
|
||||
def emails_format
|
||||
return unless email
|
||||
validate_result = email.split(',').all? { |email| email.match(/\A[^@]+@[^@]+\z/) }
|
||||
errors.add(:email, I18n.t(:text_crm_string_incorrect_format)) unless validate_result
|
||||
end
|
||||
|
||||
def update_company_contacts
|
||||
return unless is_company
|
||||
return unless first_name_changed?
|
||||
Contact.where(["#{Contact.table_name}.is_company = ? AND #{Contact.table_name}.company = ?", false, first_name_was]).
|
||||
update_all(:company => first_name)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class ContactCustomField < CustomField
|
||||
unloadable
|
||||
|
||||
def type_name
|
||||
:label_contact_plural
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class ContactImport
|
||||
extend ActiveModel::Naming
|
||||
include ActiveModel::Conversion
|
||||
include ActiveModel::Validations
|
||||
include CSVImportable
|
||||
|
||||
attr_accessor :file, :project, :tag_list, :quotes_type
|
||||
|
||||
def klass
|
||||
Contact
|
||||
end
|
||||
|
||||
def build_from_fcsv_row(row)
|
||||
ret = Hash[row.to_hash.collect { |k, v| [k.underscore.tr(' ', '_'), force_utf8(v)] if k }].delete_if { |k, _v| !klass.column_names.include?(k) }
|
||||
ret[:birthday] = row['birthday'].to_date if row['birthday']
|
||||
ActiveRecord::VERSION::MAJOR >= 4 ? ret[:tag_list] = [row['tags'], tag_list] : ret[:tag_list] = [row['tags'], tag_list].join(',')
|
||||
ret[:assigned_to_id] = User.find_by_login(row['responsible']).try(:id) unless row['responsible'].blank?
|
||||
unless row['address'].blank? && row['city'].blank? && row['street1'].blank? && row['street2'].blank? && row['region'].blank? && row['postcode'].blank? && row['country_code'].blank?
|
||||
ret[:address_attributes] = {}
|
||||
ret[:address_attributes][:street1] = row['address'] unless row['address'].blank?
|
||||
ret[:address_attributes][:street2] = row['street2'] unless row['street2'].blank?
|
||||
ret[:address_attributes][:city] = row['city'] unless row['city'].blank?
|
||||
ret[:address_attributes][:postcode] = row['postcode'] unless row['postcode'].blank?
|
||||
ret[:address_attributes][:postcode] = row['zip'] unless row['zip'].blank?
|
||||
ret[:address_attributes][:region] = row['region'] unless row['region'].blank?
|
||||
ret[:address_attributes][:country_code] = row['country code'] unless row['country code'].blank?
|
||||
ret[:address_attributes][:country] = row['country'] unless row['country'].blank?
|
||||
ret[:address_attributes][:region] = row['state'] unless row['state'].blank? && !row["region"].blank?
|
||||
end
|
||||
ret
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,130 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class ContactKernelImport < Import
|
||||
|
||||
def klass
|
||||
Contact
|
||||
end
|
||||
|
||||
def saved_objects
|
||||
object_ids = saved_items.pluck(:obj_id)
|
||||
Contact.where(:id => object_ids).order(:id)
|
||||
end
|
||||
|
||||
def project=(project)
|
||||
settings['project'] = project.id
|
||||
end
|
||||
|
||||
def project
|
||||
settings['project']
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_object(row, _item = nil)
|
||||
contact = Contact.new
|
||||
contact.project = Project.find(settings['project'])
|
||||
contact.author = user
|
||||
|
||||
attributes = {}
|
||||
if is_company = row_value(row, 'is_company')
|
||||
attributes['is_company'] = '1' if yes?(is_company)
|
||||
end
|
||||
if first_name = row_value(row, 'first_name')
|
||||
attributes['first_name'] = first_name
|
||||
end
|
||||
if middle_name = row_value(row, 'middle_name')
|
||||
attributes['middle_name'] = middle_name
|
||||
end
|
||||
if last_name = row_value(row, 'last_name')
|
||||
attributes['last_name'] = last_name
|
||||
end
|
||||
if job_title = row_value(row, 'job_title')
|
||||
attributes['job_title'] = job_title
|
||||
end
|
||||
if company = row_value(row, 'company')
|
||||
attributes['company'] = company
|
||||
end
|
||||
if phone = row_value(row, 'phone')
|
||||
attributes['phone'] = phone
|
||||
end
|
||||
if email = row_value(row, 'email')
|
||||
attributes['email'] = email
|
||||
end
|
||||
|
||||
address_attributes = {}
|
||||
if address_street = row_value(row, 'address_street')
|
||||
address_attributes['street1'] = address_street
|
||||
end
|
||||
if address_country_code = row_value(row, 'address_country_code')
|
||||
address_attributes['country_code'] = address_country_code
|
||||
end
|
||||
if address_zip = row_value(row, 'address_zip')
|
||||
address_attributes['postcode'] = address_zip
|
||||
end
|
||||
if address_state = row_value(row, 'address_state')
|
||||
address_attributes['region'] = address_state
|
||||
end
|
||||
if address_city = row_value(row, 'address_city')
|
||||
address_attributes['city'] = address_city
|
||||
end
|
||||
attributes['address_attributes'] = address_attributes
|
||||
|
||||
if skype_name = row_value(row, 'skype_name')
|
||||
attributes['skype_name'] = skype_name
|
||||
end
|
||||
if website = row_value(row, 'website')
|
||||
attributes['website'] = website
|
||||
end
|
||||
if birthday = row_value(row, 'birthday')
|
||||
attributes['birthday'] = birthday
|
||||
end
|
||||
if tag_list = row_value(row, 'tag_list')
|
||||
attributes['tag_list'] = tag_list
|
||||
end
|
||||
if background = row_value(row, 'background')
|
||||
attributes['background'] = background
|
||||
end
|
||||
|
||||
attributes['custom_field_values'] = contact.custom_field_values.inject({}) do |h, v|
|
||||
value = case v.custom_field.field_format
|
||||
when 'date'
|
||||
row_date(row, "cf_#{v.custom_field.id}")
|
||||
when 'list'
|
||||
row_value(row, "cf_#{v.custom_field.id}").try(:split, ',')
|
||||
else
|
||||
row_value(row, "cf_#{v.custom_field.id}")
|
||||
end
|
||||
if value
|
||||
h[v.custom_field.id.to_s] =
|
||||
if value.is_a?(Array)
|
||||
value.map { |val| v.custom_field.value_from_keyword(val.strip, contact) }.compact.flatten
|
||||
else
|
||||
v.custom_field.value_from_keyword(value, contact)
|
||||
end
|
||||
end
|
||||
h
|
||||
end
|
||||
|
||||
contact.send :safe_attributes=, attributes, user
|
||||
contact
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class ContactNote < Note
|
||||
unloadable
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
belongs_to :contact, :foreign_key => :source_id
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'subject', 'type_id', 'content', 'source', 'author_id'
|
||||
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
if ActiveRecord::Base.connection.table_exists?('notes')
|
||||
acts_as_activity_provider :type => 'contacts',
|
||||
:permission => :view_contacts,
|
||||
:author_key => :author_id,
|
||||
:scope => eager_load(:contact => :projects).where(:source_type => 'Contact')
|
||||
end
|
||||
else
|
||||
acts_as_activity_provider :type => 'contacts',
|
||||
:permission => :view_contacts,
|
||||
:author_key => :author_id,
|
||||
:find_options => { :include => [:contact => :projects], :conditions => { :source_type => 'Contact' } }
|
||||
end
|
||||
|
||||
scope :visible,
|
||||
lambda { |*args| joins([:contact => :projects]).
|
||||
where(Contact.visible_condition(args.shift || User.current, *args) +
|
||||
" AND (#{ContactNote.table_name}.source_type = 'Contact')") }
|
||||
|
||||
acts_as_attachable :view_permission => :view_contacts,
|
||||
:delete_permission => :edit_contacts
|
||||
end
|
||||
@@ -0,0 +1,240 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class ContactQuery < Query
|
||||
include CrmQuery
|
||||
|
||||
class QueryMultipleValuesColumn < QueryColumn
|
||||
def value_object(object)
|
||||
value = super
|
||||
value.respond_to?(:to_a) ? value.to_a : value
|
||||
end
|
||||
end
|
||||
|
||||
self.queried_class = Contact
|
||||
self.view_permission = :view_contacts if Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch?
|
||||
|
||||
self.available_columns = [
|
||||
QueryColumn.new(:id, :sortable => "#{Contact.table_name}.id", :default_order => 'desc', :caption => '#'),
|
||||
QueryColumn.new(:name, :sortable => lambda {Contact.fields_for_order_statement}, :caption => :field_contact_full_name),
|
||||
QueryColumn.new(:first_name, :sortable => "#{Contact.table_name}.first_name"),
|
||||
QueryColumn.new(:last_name, :sortable => "#{Contact.table_name}.last_name"),
|
||||
QueryColumn.new(:middle_name, :sortable => "#{Contact.table_name}.middle_name", :caption => :field_contact_middle_name),
|
||||
QueryColumn.new(:job_title, :sortable => "#{Contact.table_name}.job_title", :caption => :field_contact_job_title, :groupable => true),
|
||||
QueryColumn.new(:company, :sortable => "#{Contact.table_name}.company", :groupable => "#{Contact.table_name}.company", :caption => :field_contact_company),
|
||||
QueryColumn.new(:phones, :sortable => "#{Contact.table_name}.phone", :caption => :field_contact_phone),
|
||||
QueryColumn.new(:emails, :sortable => "#{Contact.table_name}.email", :caption => :field_contact_email),
|
||||
QueryColumn.new(:address, :sortable => "#{Address.table_name}.full_address", :caption => :label_crm_address),
|
||||
QueryColumn.new(:street1, :sortable => "#{Address.table_name}.street1", :caption => :label_crm_street1),
|
||||
QueryColumn.new(:street2, :sortable => "#{Address.table_name}.street2", :caption => :label_crm_street2),
|
||||
QueryColumn.new(:city, :sortable => "#{Address.table_name}.city", :groupable => "#{Address.table_name}.city", :caption => :label_crm_city),
|
||||
QueryColumn.new(:region, :sortable => "#{Address.table_name}.region", :caption => :label_crm_region),
|
||||
QueryColumn.new(:postcode, :sortable => "#{Address.table_name}.postcode", :caption => :label_crm_postcode),
|
||||
QueryColumn.new(:country, :sortable => "#{Address.table_name}.country_code", :groupable => "#{Address.table_name}.country_code", :caption => :label_crm_country),
|
||||
QueryMultipleValuesColumn.new(:tags, :caption => :label_crm_tags_plural),
|
||||
QueryColumn.new(:created_on, :sortable => "#{Contact.table_name}.created_on"),
|
||||
QueryColumn.new(:updated_on, :sortable => "#{Contact.table_name}.updated_on"),
|
||||
QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
|
||||
QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")})
|
||||
]
|
||||
|
||||
|
||||
def initialize(attributes=nil, *args)
|
||||
super attributes
|
||||
self.filters ||= {}
|
||||
end
|
||||
|
||||
def initialize_available_filters
|
||||
add_available_filter "ids", :type => :integer, :label => :label_contact if Redmine::VERSION.to_s >= '3.3'
|
||||
add_available_filter "first_name", :type => :string, :order => 0
|
||||
add_available_filter "last_name", :type => :string, :order => 1
|
||||
add_available_filter "middle_name", :type => :string, :order => 2
|
||||
add_available_filter "job_title", :type => :string, :order => 3
|
||||
add_available_filter "company", :type => :string, :order => 4
|
||||
add_available_filter "phone", :type => :text, :order => 5
|
||||
add_available_filter "email", :type => :text, :order => 6
|
||||
add_available_filter "full_address", :type => :text, :order => 7, :name => l(:label_crm_address)
|
||||
add_available_filter "street1", :type => :text, :order => 8, :name => l(:label_crm_street1)
|
||||
add_available_filter "street2", :type => :text, :order => 8, :name => l(:label_crm_street2)
|
||||
add_available_filter "city", :type => :text, :order => 8, :name => l(:label_crm_city)
|
||||
add_available_filter "region", :type => :text, :order => 9, :name => l(:label_crm_region)
|
||||
add_available_filter "postcode", :type => :text, :order => 10, :name => l(:label_crm_postcode)
|
||||
add_available_filter "country", :type => :list_optional, :values => l(:label_crm_countries).map{|k, v| [v, k]}, :order => 11, :name => l(:label_crm_country)
|
||||
add_available_filter "is_company", :type => :list, :values => [[l(:general_text_yes), ActiveRecord::Base.connection.quoted_true.gsub(/'/, '')], [l(:general_text_no), ActiveRecord::Base.connection.quoted_false.gsub(/'/, '')]], :order => 12
|
||||
add_available_filter "last_note", :type => :date_past, :order => 13
|
||||
add_available_filter "has_deals", :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 14, :name => l(:label_crm_has_deals)
|
||||
add_available_filter "updated_on", :type => :date_past, :order => 20
|
||||
add_available_filter "created_on", :type => :date, :order => 21
|
||||
add_available_filter "tags", :type => :list, :values => Contact.available_tags(project.blank? ? {} : {:project => project.id}).collect{ |t| [t.name, t.name] }, :order => 12
|
||||
initialize_author_filter
|
||||
initialize_assignee_filter
|
||||
|
||||
add_available_filter("has_open_issues",
|
||||
:type => :list_optional, :values => users_values, :label => :label_crm_has_open_issues
|
||||
) unless users_values.empty?
|
||||
|
||||
add_custom_fields_filters(ContactCustomField.where(:is_filter => true))
|
||||
add_associations_custom_fields_filters :author, :assigned_to
|
||||
end
|
||||
|
||||
def available_columns
|
||||
return @available_columns if @available_columns
|
||||
@available_columns = self.class.available_columns.dup
|
||||
@available_columns += CustomField.where(:type => 'ContactCustomField').all.map {|cf| QueryCustomFieldColumn.new(cf) }
|
||||
@available_columns
|
||||
end
|
||||
|
||||
def default_columns_names
|
||||
@default_columns_names ||= [:id, :name, :job_title, :company, :phone, :email, :address]
|
||||
end
|
||||
|
||||
def sql_for_tags_field(field, operator, value)
|
||||
compare = operator_for('tags').eql?('=') ? 'IN' : 'NOT IN'
|
||||
ids_list = Contact.tagged_with(value).collect{|contact| contact.id }.push(0).join(',')
|
||||
"( #{Contact.table_name}.id #{compare} (#{ids_list}) ) "
|
||||
end
|
||||
|
||||
def sql_for_project_field(field, operator, value)
|
||||
'(' + sql_for_field(field, operator, value, Project.table_name, "id", false) + ')'
|
||||
end
|
||||
|
||||
def sql_for_country_field(field, operator, value)
|
||||
if operator == '*' # Any group
|
||||
contact_countries = l(:label_crm_countries).map{|k, v| k.to_s}
|
||||
operator = '=' # Override the operator since we want to find by assigned_to
|
||||
elsif operator == "!*"
|
||||
contact_countries = l(:label_crm_countries).map{|k, v| k.to_s}
|
||||
operator = '!' # Override the operator since we want to find by assigned_to
|
||||
else
|
||||
contact_countries = value
|
||||
end
|
||||
'(' + sql_for_field("address_id", operator, contact_countries, Address.table_name, "country_code", false) + ')'
|
||||
end
|
||||
|
||||
def sql_for_city_field(field, operator, value)
|
||||
sql_for_field(field, operator, value, Address.table_name, "city")
|
||||
end
|
||||
|
||||
def sql_for_street1_field(field, operator, value)
|
||||
sql_for_field(field, operator, value, Address.table_name, "street1")
|
||||
end
|
||||
|
||||
def sql_for_street2_field(field, operator, value)
|
||||
sql_for_field(field, operator, value, Address.table_name, "street2")
|
||||
end
|
||||
|
||||
def sql_for_full_address_field(field, operator, value)
|
||||
sql_for_field(field, operator, value, Address.table_name, "full_address")
|
||||
end
|
||||
|
||||
def sql_for_region_field(field, operator, value)
|
||||
sql_for_field(field, operator, value, Address.table_name, "region")
|
||||
end
|
||||
|
||||
def sql_for_postcode_field(field, operator, value)
|
||||
sql_for_field(field, operator, value, Address.table_name, "postcode")
|
||||
end
|
||||
|
||||
def sql_for_has_deals_field(field, operator, value)
|
||||
db_table = Deal.table_name
|
||||
if operator == "!"
|
||||
"#{Contact.table_name}.id IN (
|
||||
SELECT #{db_table}.contact_id FROM #{db_table}
|
||||
GROUP BY #{db_table}.contact_id
|
||||
HAVING COUNT(#{db_table}.id) = 0)"
|
||||
else operator == "="
|
||||
"#{Contact.table_name}.id IN (
|
||||
SELECT #{db_table}.contact_id FROM #{db_table}
|
||||
GROUP BY #{db_table}.contact_id
|
||||
HAVING COUNT(#{db_table}.id) > 0)"
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_has_open_issues_field(field, operator, value)
|
||||
db_table = ContactNote.table_name
|
||||
if operator == "!*"
|
||||
"#{Contact.table_name}.id IN (
|
||||
SELECT #{Contact.table_name}.id FROM #{Contact.table_name}
|
||||
LEFT JOIN contacts_issues ON contacts_issues.contact_id = #{Contact.table_name}.id
|
||||
LEFT JOIN #{Issue.table_name} ON contacts_issues.issue_id = #{Issue.table_name}.id
|
||||
LEFT JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id
|
||||
WHERE (#{IssueStatus.table_name}.is_closed = #{ActiveRecord::Base.connection.quoted_false}) OR (#{IssueStatus.table_name}.is_closed IS NULL)
|
||||
GROUP BY #{Contact.table_name}.id
|
||||
HAVING COUNT(#{Issue.table_name}.id) = 0)"
|
||||
elsif operator == "*"
|
||||
"#{Contact.table_name}.id IN (
|
||||
SELECT contacts_issues.contact_id FROM contacts_issues
|
||||
INNER JOIN #{Issue.table_name} ON contacts_issues.issue_id = #{Issue.table_name}.id
|
||||
INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id
|
||||
WHERE #{IssueStatus.table_name}.is_closed = #{ActiveRecord::Base.connection.quoted_false}
|
||||
GROUP BY contacts_issues.contact_id
|
||||
HAVING COUNT(#{Issue.table_name}.id) > 0)"
|
||||
else
|
||||
"#{Contact.table_name}.id IN (
|
||||
SELECT contacts_issues.contact_id FROM contacts_issues
|
||||
INNER JOIN #{Issue.table_name} ON contacts_issues.issue_id = #{Issue.table_name}.id
|
||||
INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id
|
||||
WHERE #{IssueStatus.table_name}.is_closed = #{ActiveRecord::Base.connection.quoted_false}
|
||||
AND #{sql_for_field("assigned_to_id", operator, value, Issue.table_name, 'assigned_to_id')}
|
||||
GROUP BY contacts_issues.contact_id)"
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_last_note_field(field, operator, value)
|
||||
db_table = ContactNote.table_name
|
||||
if operator == "!*"
|
||||
"#{Contact.table_name}.id IN (
|
||||
SELECT #{Contact.table_name}.id FROM #{Contact.table_name}
|
||||
LEFT JOIN #{db_table} ON #{db_table}.source_id = #{Contact.table_name}.id and #{db_table}.source_type = 'Contact'
|
||||
GROUP BY #{Contact.table_name}.id
|
||||
HAVING COUNT(#{db_table}.id) = 0)"
|
||||
elsif operator == "*"
|
||||
"#{Contact.table_name}.id IN (
|
||||
SELECT #{Contact.table_name}.id FROM #{Contact.table_name}
|
||||
INNER JOIN #{db_table} ON #{db_table}.source_id = #{Contact.table_name}.id and #{db_table}.source_type = 'Contact'
|
||||
GROUP BY #{Contact.table_name}.id
|
||||
HAVING COUNT(#{db_table}.id) > 0)"
|
||||
else
|
||||
"#{Contact.table_name}.id IN (
|
||||
SELECT #{db_table}.source_id
|
||||
FROM #{db_table}
|
||||
WHERE #{db_table}.source_type='Contact'
|
||||
AND #{db_table}.id IN
|
||||
(SELECT MAX(#{db_table}.id)
|
||||
FROM #{db_table}
|
||||
WHERE #{db_table}.source_type='Contact'
|
||||
GROUP BY #{db_table}.source_id)
|
||||
AND #{sql_for_field(field, operator, value, db_table, 'created_on')}
|
||||
)"
|
||||
end
|
||||
end
|
||||
|
||||
def objects_scope(options={})
|
||||
scope = Contact.visible
|
||||
options[:search].split(' ').collect{ |search_string| scope = scope.live_search(search_string) } unless options[:search].blank?
|
||||
scope = scope.includes((query_includes + (options[:include] || [])).uniq).
|
||||
where(statement).
|
||||
where(options[:conditions])
|
||||
scope
|
||||
end
|
||||
|
||||
def query_includes
|
||||
[:address, :projects, :assigned_to]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class ContactsIssue < ActiveRecord::Base
|
||||
include Redmine::SafeAttributes
|
||||
validates_presence_of :contact_id, :issue_id
|
||||
validates_uniqueness_of :contact_id, :scope => [:issue_id]
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'issue_id', 'contact_id'
|
||||
# after_create :send_mails
|
||||
# after_save :send_mails
|
||||
|
||||
private
|
||||
|
||||
def send_mails
|
||||
Mailer.deliver_contacts_issue_connected(Contact.find(contact_id), Issue.find(issue_id))
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,262 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class ContactsMailer < ActionMailer::Base
|
||||
include Redmine::I18n
|
||||
|
||||
class UnauthorizedAction < StandardError; end
|
||||
class MissingInformation < StandardError; end
|
||||
|
||||
helper :application
|
||||
|
||||
attr_reader :email, :user
|
||||
|
||||
def self.default_url_options
|
||||
h = Setting.host_name
|
||||
h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
|
||||
{ :host => h, :protocol => Setting.protocol }
|
||||
end
|
||||
|
||||
def bulk_mail(contact, params = {})
|
||||
raise l(:error_empty_email) if (contact.emails.empty? || params[:message].blank?)
|
||||
|
||||
@contact = contact
|
||||
@params = params
|
||||
|
||||
params[:attachments].each_value do |mail_attachment|
|
||||
if file = mail_attachment['file']
|
||||
file.rewind if file
|
||||
attachments[file.original_filename] = file.binread
|
||||
file.rewind if file
|
||||
elsif token = mail_attachment['token']
|
||||
if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
|
||||
attachment_id, attachment_digest = $1, $2
|
||||
if a = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
|
||||
attachments[a.filename] = File.binread(a.diskfile)
|
||||
end
|
||||
end
|
||||
end
|
||||
end unless params[:attachments].blank?
|
||||
|
||||
mail(:from => params[:from] || User.current.mail,
|
||||
:to => contact.emails.first,
|
||||
:cc => params[:cc],
|
||||
:bcc => params[:bcc],
|
||||
:subject => params[:subject]) do |format|
|
||||
format.text
|
||||
format.html
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def self.receive(email, options={})
|
||||
@@contacts_mailer_options = options.dup
|
||||
super email
|
||||
end
|
||||
|
||||
# Processes incoming emails
|
||||
# Returns the created object (eg. an issue, a message) or false
|
||||
def receive(email)
|
||||
# debugger
|
||||
@email = email
|
||||
sender_email = email.from.to_a.first.to_s.strip
|
||||
# Ignore emails received from the application emission address to avoid hell cycles
|
||||
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
|
||||
logger.info "ContactsMailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
|
||||
return false
|
||||
end
|
||||
@user = User.find_by_mail(sender_email) if sender_email.present?
|
||||
if @user.nil? || (@user && !@user.active?)
|
||||
logger.info "ContactsMailHandler: user not found [#{sender_email}]" if logger && logger.info
|
||||
end
|
||||
dispatch
|
||||
end
|
||||
|
||||
def dispatch
|
||||
deal_id = email.to.to_s.match(/.+\+d([0-9]*)/).to_a[1]
|
||||
deal_id ||= email.bcc.to_s.match(/.+\+d([0-9]*)/).to_a[1]
|
||||
deal_id ||= email.cc.to_s.match(/.+\+d([0-9]*)/).to_a[1]
|
||||
|
||||
if deal_id
|
||||
deal = Deal.find_by_id(deal_id)
|
||||
if deal
|
||||
return [*receive_deal_note(deal_id)]
|
||||
end
|
||||
end
|
||||
|
||||
contacts = []
|
||||
|
||||
if contacts.blank?
|
||||
contact_id = email.to.to_s.match(/.+\+c([0-9]*)/).to_a[1]
|
||||
contact_id ||= email.bcc.to_s.match(/.+\+c([0-9]*)/).to_a[1]
|
||||
contact_id ||= email.cc.to_s.match(/.+\+c([0-9]*)/).to_a[1]
|
||||
contacts = Contact.where(:id => contact_id)
|
||||
end
|
||||
|
||||
if contacts.blank?
|
||||
contacts = Contact.find_by_emails(email.to.to_a)
|
||||
end
|
||||
|
||||
if contacts.blank?
|
||||
from_key_words = get_keyword_locales(:label_crm_mail_from)
|
||||
@plain_text_body = plain_text_body.gsub(/^>\s*/, '').gsub('> ','').gsub('"', '"')
|
||||
full_address = plain_text_body.match(/^(#{from_key_words.join('|')})[ \s]*:[ \s]*(.+)\s*$/).to_a[2]
|
||||
|
||||
email_address = full_address.match(/[\w,\.,\-,\+]+@.+\.\w{2,}/) if full_address
|
||||
contacts = Contact.find_by_emails([email_address.to_s.strip]) if email_address
|
||||
end
|
||||
|
||||
if contacts.blank?
|
||||
return false
|
||||
end
|
||||
|
||||
raise MissingInformation if contacts.blank?
|
||||
|
||||
result = []
|
||||
contacts.each do |contact|
|
||||
result << receive_contact_note(contact.id)
|
||||
end
|
||||
result
|
||||
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
# TODO: send a email to the user
|
||||
logger.error e.message if logger
|
||||
false
|
||||
rescue MissingInformation => e
|
||||
logger.error "ContactsMailHandler: missing information from #{user}: #{e.message}" if logger
|
||||
false
|
||||
rescue UnauthorizedAction => e
|
||||
logger.error "ContactsMailHandler: unauthorized attempt from #{user}" if logger
|
||||
false
|
||||
end
|
||||
|
||||
# Receives a reply to a forum message
|
||||
def receive_contact_note(contact_id)
|
||||
contact = Contact.find_by_id(contact_id)
|
||||
note = nil
|
||||
# logger.error "ContactsMailHandler: receive_contact_note user: #{user},
|
||||
# contact: #{contact.name},
|
||||
# editable: #{contact.editable?(self.user)},
|
||||
# current: #{User.current}"
|
||||
raise UnauthorizedAction unless contact.editable?(self.user)
|
||||
if contact
|
||||
note = ContactNote.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
|
||||
:type_id => Note.note_types[:email],
|
||||
:content => plain_text_body,
|
||||
:created_on => email.date)
|
||||
note.author = self.user
|
||||
contact.notes << note
|
||||
add_attachments(note)
|
||||
logger.info note
|
||||
note.save
|
||||
contact.save
|
||||
end
|
||||
note
|
||||
end
|
||||
|
||||
def receive_deal_note(deal_id)
|
||||
deal = Deal.find_by_id(deal_id)
|
||||
note = nil
|
||||
# logger.error "ContactsMailHandler: receive_contact_note user: #{user},
|
||||
# contact: #{contact.name},
|
||||
# editable: #{contact.editable?(self.user)},
|
||||
# current: #{User.current}"
|
||||
raise UnauthorizedAction unless deal.editable?(self.user)
|
||||
if deal
|
||||
note = DealNote.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
|
||||
:type_id => Note.note_types[:email],
|
||||
:content => plain_text_body,
|
||||
:created_on => email.date)
|
||||
note.author = self.user
|
||||
deal.notes << note
|
||||
add_attachments(note)
|
||||
logger.info note
|
||||
note.save
|
||||
deal.save
|
||||
end
|
||||
note
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Destructively extracts the value for +attr+ in +text+
|
||||
# Returns nil if no matching keyword found
|
||||
def extract_keyword!(text, attr, format=nil)
|
||||
keys = [attr.to_s.humanize]
|
||||
if attr.is_a?(Symbol)
|
||||
keys << l("field_#{attr}", :default => '', :locale => user.language) if user && user.language.present?
|
||||
keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) if Setting.default_language.present?
|
||||
end
|
||||
keys.reject! {|k| k.blank?}
|
||||
keys.collect! {|k| Regexp.escape(k)}
|
||||
format ||= '.+'
|
||||
text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '') # /^(От:)[ \t]*:[ \t]*(.+)\s*$/i
|
||||
$2 && $2.strip
|
||||
end
|
||||
|
||||
def add_attachments(obj)
|
||||
if email.attachments && email.attachments.any?
|
||||
email.attachments.each do |attachment|
|
||||
obj.attachments << Attachment.create(:container => obj,
|
||||
:file => attachment.decoded,
|
||||
:filename => attachment.filename,
|
||||
:author => user,
|
||||
:content_type => attachment.mime_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the text/plain part of the email
|
||||
# If not found (eg. HTML-only email), returns the body with tags removed
|
||||
def plain_text_body
|
||||
|
||||
return @plain_text_body unless @plain_text_body.nil?
|
||||
|
||||
part = email.text_part || email.html_part || email
|
||||
@plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
|
||||
|
||||
# strip html tags and remove doctype directive
|
||||
@plain_text_body = ActionController::Base.helpers.strip_tags(@plain_text_body.strip) unless email.text_part
|
||||
@plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
|
||||
@plain_text_body
|
||||
|
||||
end
|
||||
|
||||
def get_keyword_locales(keyword)
|
||||
I18n.available_locales.collect{|lc| l(keyword, :locale => lc)}.uniq
|
||||
end
|
||||
|
||||
# Appends a Redmine header field (name is prepended with 'X-Redmine-')
|
||||
def redmine_headers(h)
|
||||
h.each { |k,v| headers["X-Redmine-#{k}"] = v }
|
||||
end
|
||||
|
||||
def initialize_defaults(method_name)
|
||||
super
|
||||
# Common headers
|
||||
headers 'X-Mailer' => 'Redmine Contacts',
|
||||
'X-Redmine-Host' => Setting.host_name,
|
||||
'X-Redmine-Site' => Setting.app_title
|
||||
end
|
||||
|
||||
def logger
|
||||
Rails.logger
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,171 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class ContactsSetting < ActiveRecord::Base
|
||||
unloadable
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
TAX_TYPE_EXCLUSIVE = 1
|
||||
TAX_TYPE_INCLUSIVE = 2
|
||||
|
||||
belongs_to :project
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'name', 'value', 'project_id'
|
||||
|
||||
cattr_accessor :settings
|
||||
acts_as_attachable
|
||||
|
||||
# Hash used to cache setting values
|
||||
@contacts_cached_settings = {}
|
||||
@contacts_cached_cleared_on = Time.now
|
||||
|
||||
validates_uniqueness_of :name, :scope => [:project_id]
|
||||
|
||||
# Returns the value of the setting named name
|
||||
def self.[](name, project_id)
|
||||
project_id = project_id.id if project_id.is_a?(Project)
|
||||
v = @contacts_cached_settings[hk(name, project_id)]
|
||||
v ? v : (@contacts_cached_settings[hk(name, project_id)] = find_or_default(name, project_id).value)
|
||||
end
|
||||
|
||||
def self.[]=(name, project_id, v)
|
||||
project_id = project_id.id if project_id.is_a?(Project)
|
||||
setting = find_or_default(name, project_id)
|
||||
setting.value = (v ? v : '')
|
||||
@contacts_cached_settings[hk(name, project_id)] = nil
|
||||
setting.save
|
||||
setting.value
|
||||
end
|
||||
|
||||
# Checks if settings have changed since the values were read
|
||||
# and clears the cache hash if it's the case
|
||||
# Called once per request
|
||||
def self.check_cache
|
||||
settings_updated_on = ContactsSetting.maximum(:updated_on)
|
||||
if settings_updated_on && @contacts_cached_cleared_on <= settings_updated_on
|
||||
clear_cache
|
||||
end
|
||||
end
|
||||
|
||||
# Clears the settings cache
|
||||
def self.clear_cache
|
||||
@contacts_cached_settings.clear
|
||||
@contacts_cached_cleared_on = Time.now
|
||||
logger.info 'Contacts settings cache cleared.' if logger
|
||||
end
|
||||
|
||||
def self.contact_name_format
|
||||
Setting.plugin_redmine_contacts['name_format'] || :firstname_lastname
|
||||
end
|
||||
|
||||
def self.vcard?
|
||||
Object.const_defined?(:Vcard)
|
||||
end
|
||||
|
||||
def self.spreadsheet?
|
||||
Object.const_defined?(:Spreadsheet)
|
||||
end
|
||||
|
||||
def self.monochrome_tags?
|
||||
!!Setting.plugin_redmine_contacts['monochrome_tags']
|
||||
end
|
||||
|
||||
def self.contacts_show_in_top_menu?
|
||||
!!Setting.plugin_redmine_contacts['contacts_show_in_top_menu']
|
||||
end
|
||||
|
||||
def self.contacts_show_in_app_menu?
|
||||
!!Setting.plugin_redmine_contacts['contacts_show_in_app_menu']
|
||||
end
|
||||
|
||||
def self.default_country
|
||||
Setting.plugin_redmine_contacts['default_country']
|
||||
end
|
||||
|
||||
def self.cross_project_contacts?
|
||||
Setting.plugin_redmine_contacts['cross_project_contacts'].to_i > 0
|
||||
end
|
||||
|
||||
# Finance
|
||||
|
||||
def self.default_currency
|
||||
Setting.plugin_redmine_contacts['default_currency'] || 'USD'
|
||||
end
|
||||
|
||||
def self.major_currencies
|
||||
currencies = Setting.plugin_redmine_contacts['major_currencies'].to_s.split(',').select { |c| !c.blank? }.map(&:strip)
|
||||
currencies = %w(USD EUR GBP RUB CHF) if currencies.blank?
|
||||
currencies.compact.uniq
|
||||
end
|
||||
|
||||
def self.default_tax
|
||||
Setting.plugin_redmine_contacts['default_tax'].to_f
|
||||
end
|
||||
|
||||
def self.tax_type
|
||||
((['1', '2'] & [Setting.plugin_redmine_contacts['tax_type'].to_s]).first || TAX_TYPE_EXCLUSIVE).to_i
|
||||
end
|
||||
|
||||
def self.tax_exclusive?
|
||||
ContactsSetting.tax_type == TAX_TYPE_EXCLUSIVE
|
||||
end
|
||||
|
||||
def self.thousands_delimiter
|
||||
([' ', ',', '.'] & [Setting.plugin_redmine_contacts['thousands_delimiter']]).first || ' '
|
||||
end
|
||||
|
||||
def self.decimal_separator
|
||||
([',', '.'] & [Setting.plugin_redmine_contacts['decimal_separator']]).first || '.'
|
||||
end
|
||||
|
||||
def self.disable_taxes?
|
||||
!!Setting.plugin_redmine_contacts['disable_taxes']
|
||||
end
|
||||
|
||||
def self.post_address_format
|
||||
unless Setting.plugin_redmine_contacts['post_address_format'].blank?
|
||||
Setting.plugin_redmine_contacts['post_address_format'].to_s.strip
|
||||
else
|
||||
"%street1%\n%street2%\n%city%, %postcode%\n%region%\n%country%"
|
||||
end
|
||||
end
|
||||
def self.deals_show_in_top_menu?
|
||||
!!Setting.plugin_redmine_contacts['deals_show_in_top_menu']
|
||||
end
|
||||
|
||||
def self.deals_show_in_app_menu?
|
||||
!!Setting.plugin_redmine_contacts['deals_show_in_app_menu']
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.hk(name, project_id)
|
||||
"#{name}-#{project_id.to_s}"
|
||||
end
|
||||
|
||||
# Returns the Setting instance for the setting named name
|
||||
# (record found in database or new record with default value)
|
||||
def self.find_or_default(name, project_id)
|
||||
name = name.to_s
|
||||
setting = find_by_name_and_project_id(name, project_id)
|
||||
setting ||= new(:name => name, :value => '', :project_id => project_id)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,244 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
module CrmQuery
|
||||
|
||||
def self.included(base)
|
||||
base.send :include, InstanceMethods
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def visible(*args)
|
||||
user = args.shift || User.current
|
||||
base = Project.allowed_to_condition(user, "view_#{queried_class.name.pluralize.downcase}".to_sym, *args)
|
||||
if Redmine::VERSION.to_s < '2.4'
|
||||
user_id = user.logged? ? user.id : 0
|
||||
return includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
|
||||
end
|
||||
|
||||
scope = eager_load(:project).where("#{table_name}.project_id IS NULL OR (#{base})")
|
||||
if user.admin?
|
||||
scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", Query::VISIBILITY_PRIVATE, user.id)
|
||||
elsif user.memberships.any?
|
||||
scope.where("#{table_name}.visibility = ?" +
|
||||
" OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
|
||||
"SELECT DISTINCT q.id FROM #{table_name} q" +
|
||||
" INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
|
||||
" INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
|
||||
" INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
|
||||
" WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
|
||||
" OR #{table_name}.user_id = ?",
|
||||
Query::VISIBILITY_PUBLIC, Query::VISIBILITY_ROLES, user.id, user.id)
|
||||
elsif user.logged?
|
||||
scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", Query::VISIBILITY_PUBLIC, user.id)
|
||||
else
|
||||
scope.where("#{table_name}.visibility = ?", Query::VISIBILITY_PUBLIC)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def visible?(user=User.current)
|
||||
return true if user.admin?
|
||||
return false unless project.nil? || user.allowed_to?("view_#{queried_class.name.pluralize.downcase}".to_sym, project)
|
||||
case visibility
|
||||
when Query::VISIBILITY_PUBLIC
|
||||
true
|
||||
when Query::VISIBILITY_ROLES
|
||||
if project
|
||||
(user.roles_for_project(project) & roles).any?
|
||||
else
|
||||
Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
|
||||
end
|
||||
else
|
||||
user == self.user
|
||||
end
|
||||
end
|
||||
|
||||
def is_private?
|
||||
visibility == Query::VISIBILITY_PRIVATE
|
||||
end
|
||||
|
||||
def is_public?
|
||||
!is_private?
|
||||
end
|
||||
|
||||
def initialize_project_filter(position=nil)
|
||||
if project.blank?
|
||||
project_values = []
|
||||
if User.current.logged? && User.current.memberships.any?
|
||||
project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
|
||||
end
|
||||
project_values += all_projects_values
|
||||
add_available_filter("project_id", :order => position,
|
||||
:type => :list, :values => project_values
|
||||
) unless project_values.empty?
|
||||
end
|
||||
end
|
||||
|
||||
def initialize_author_filter(position=nil)
|
||||
add_available_filter("author_id", :order => position,
|
||||
:type => :list_optional, :values => users_values
|
||||
) unless users_values.empty?
|
||||
end
|
||||
|
||||
def initialize_assignee_filter(position=nil)
|
||||
add_available_filter("assigned_to_id", :order => position,
|
||||
:type => :list_optional, :values => users_values
|
||||
) unless users_values.empty?
|
||||
end
|
||||
|
||||
def initialize_contact_country_filter(position=nil)
|
||||
contact_countries = l(:label_crm_countries).map{|k, v| [v, k]}
|
||||
add_available_filter("contact_country", :order => position,
|
||||
:type => :list_optional, :values => contact_countries, :label => :label_crm_contact_country
|
||||
) unless contact_countries.empty?
|
||||
end
|
||||
|
||||
def initialize_contact_city_filter(position=nil)
|
||||
add_available_filter("contact_city", :order => position,
|
||||
:type => :string, :label => :label_crm_contact_city
|
||||
)
|
||||
end
|
||||
|
||||
def sql_for_contact_country_field(field, operator, value)
|
||||
if operator == '*' # Any group
|
||||
contact_countries = l(:label_crm_countries).map{|k, v| k.to_s}
|
||||
operator = '=' # Override the operator since we want to find by assigned_to
|
||||
elsif operator == "!*"
|
||||
contact_countries = l(:label_crm_countries).map{|k, v| k.to_s}
|
||||
operator = '!' # Override the operator since we want to find by assigned_to
|
||||
else
|
||||
contact_countries = value
|
||||
end
|
||||
'(' + sql_for_field("address_id", operator, contact_countries, Address.table_name, "country_code", false) + ')'
|
||||
end
|
||||
|
||||
def sql_for_contact_city_field(field, operator, value)
|
||||
sql_for_field(field, operator, value, Address.table_name, "city")
|
||||
end
|
||||
|
||||
def sql_for_ids_field(field, operator, value)
|
||||
if operator == "*"
|
||||
"1=1"
|
||||
elsif operator == "="
|
||||
ids = value.first.to_s.scan(/\d+/).map(&:to_i).join(",")
|
||||
if ids.present?
|
||||
"#{self.queried_class.table_name}.id IN (#{ids})"
|
||||
else
|
||||
"1=0"
|
||||
end
|
||||
elsif operator == ">="
|
||||
id = value.first.to_s.scan(/\d+/).map(&:to_i).first
|
||||
if id.present?
|
||||
"#{self.queried_class.table_name}.id >= (#{id})"
|
||||
else
|
||||
"1=0"
|
||||
end
|
||||
elsif operator == "<="
|
||||
id = value.first.to_s.scan(/\d+/).map(&:to_i).first
|
||||
if id.present?
|
||||
"#{self.queried_class.table_name}.id <= (#{id})"
|
||||
else
|
||||
"1=0"
|
||||
end
|
||||
elsif operator == "><"
|
||||
if value.is_a? Array
|
||||
"#{self.queried_class.table_name}.id BETWEEN #{value.first} AND #{value.last}"
|
||||
else
|
||||
"1=0"
|
||||
end
|
||||
else
|
||||
"1=0"
|
||||
end
|
||||
end if Redmine::VERSION.to_s >= '3.3'
|
||||
|
||||
|
||||
def principals
|
||||
return @principals if @principals
|
||||
@principals = []
|
||||
if project
|
||||
@principals += project.principals.sort
|
||||
unless project.leaf?
|
||||
subprojects = project.descendants.visible.all
|
||||
@principals += Principal.member_of(subprojects)
|
||||
end
|
||||
else
|
||||
if all_projects.any?
|
||||
@principals += Principal.member_of(all_projects)
|
||||
end
|
||||
end
|
||||
@principals.uniq!
|
||||
@principals.sort!
|
||||
end
|
||||
|
||||
def users_values
|
||||
return @users_values if @users_values
|
||||
users = principals.select {|p| p.is_a?(User)}
|
||||
@users_values = []
|
||||
@users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
||||
@users_values += users.collect{|s| [s.name, s.id.to_s] }
|
||||
@users_values
|
||||
end
|
||||
|
||||
def object_count
|
||||
objects_scope.count
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
def object_count_by_group
|
||||
r = nil
|
||||
if grouped?
|
||||
begin
|
||||
# Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
|
||||
r = objects_scope.
|
||||
joins(joins_for_order_statement(group_by_statement)).
|
||||
group(group_by_statement).count
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
r = {nil => object_count}
|
||||
end
|
||||
c = group_by_column
|
||||
if c.is_a?(QueryCustomFieldColumn)
|
||||
r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
|
||||
end
|
||||
end
|
||||
r
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
def objects_scope(options={})
|
||||
raise NotImplementedError.new("You must implement #{name}.")
|
||||
end
|
||||
|
||||
def results_scope(options={})
|
||||
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
|
||||
|
||||
objects_scope(options).
|
||||
order(order_option).
|
||||
joins(joins_for_order_statement(order_option.join(','))).
|
||||
limit(options[:limit]).
|
||||
offset(options[:offset])
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,306 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class Deal < ActiveRecord::Base
|
||||
unloadable
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
|
||||
belongs_to :category, :class_name => 'DealCategory', :foreign_key => 'category_id'
|
||||
belongs_to :contact
|
||||
belongs_to :status, :class_name => 'DealStatus', :foreign_key => 'status_id'
|
||||
has_many :deals, :class_name => 'Deal', :foreign_key => 'reference_id'
|
||||
has_many :notes, :as => :source, :class_name => 'DealNote', :dependent => :delete_all
|
||||
has_many :deal_processes, :dependent => :delete_all
|
||||
has_many :deals_issues, :dependent => :destroy
|
||||
has_many :issues, :through => :deals_issues
|
||||
|
||||
if Redmine::Plugin.load && Redmine::Plugin.installed?(:redmine_products) && Redmine::Plugin.find(:redmine_products).version >= '2.0.2'
|
||||
has_many :lines, :class_name => 'ProductLine', :as => :container, :dependent => :delete_all
|
||||
has_many :products, :through => :lines, :uniq => true, :select => "#{Product.table_name}.*, #{ProductLine.table_name}.position"
|
||||
|
||||
accepts_nested_attributes_for :lines, :allow_destroy => true
|
||||
safe_attributes 'lines_attributes'
|
||||
acts_as_priceable :amount, :tax_amount, :subtotal, :total
|
||||
|
||||
before_validation :assign_lines
|
||||
before_save :calculate_price
|
||||
end
|
||||
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
has_and_belongs_to_many :related_contacts, lambda { order("#{Contact.table_name}.last_name, #{Contact.table_name}.first_name") }, :uniq => true, :class_name => 'Contact'
|
||||
else
|
||||
has_and_belongs_to_many :related_contacts, :order => "#{Contact.table_name}.last_name, #{Contact.table_name}.first_name", :uniq => true, :class_name => 'Contact'
|
||||
end
|
||||
|
||||
scope :visible, lambda {|*args|
|
||||
joins(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_deals, *args))
|
||||
}
|
||||
scope :by_project, lambda { |project_id| where(:project_id => project_id) unless project_id.blank? }
|
||||
scope :deletable, lambda { |*args| joins(:project).where(Project.allowed_to_condition(args.first || User.current, :delete_deals)) }
|
||||
|
||||
scope :live_search, lambda { |search| where("(#{Deal.table_name}.name LIKE ?)", "%#{search}%") }
|
||||
scope :live_search_with_contact, ->(search) do
|
||||
conditions = []
|
||||
values = {}
|
||||
search.split(' ').each_with_index { |word, index|
|
||||
key = :"v#{index}"
|
||||
conditions << "LOWER(#{Deal.table_name}.name) LIKE LOWER(:#{key})"
|
||||
conditions << "LOWER(#{Contact.table_name}.first_name) LIKE LOWER(:#{key})"
|
||||
conditions << "LOWER(#{Contact.table_name}.last_name) LIKE LOWER(:#{key})"
|
||||
conditions << "LOWER(#{Contact.table_name}.company) LIKE LOWER(:#{key})"
|
||||
conditions << "LOWER(#{Contact.table_name}.email) LIKE LOWER(:#{key})"
|
||||
values[key] = "%#{word}%"
|
||||
}
|
||||
sql = conditions.join(' OR ')
|
||||
joins(:contact).where(sql, values)
|
||||
end
|
||||
|
||||
scope :open, lambda { joins(:status).where("(#{DealStatus.table_name}.status_type = ? OR #{DealStatus.table_name}.status_type IS NULL)", DealStatus::OPEN_STATUS) }
|
||||
scope :closed, lambda { joins(:status).where("#{DealStatus.table_name}.status_type <> ?", DealStatus::OPEN_STATUS) }
|
||||
scope :won, lambda { joins(:status).where("#{DealStatus.table_name}.status_type = ?", DealStatus::WON_STATUS) }
|
||||
scope :lost, lambda { joins(:status).where("#{DealStatus.table_name}.status_type = ?", DealStatus::LOST_STATUS) }
|
||||
scope :was_in_status, lambda { |status_id| joins(:deal_processes).where(["#{DealProcess.table_name}.old_value = ? OR #{DealProcess.table_name}.value = ?", status_id, status_id]).uniq }
|
||||
scope :with_status, lambda { |status_id| where(:status_id => status_id) }
|
||||
|
||||
acts_as_priceable :price, :expected_revenue
|
||||
acts_as_customizable
|
||||
acts_as_viewable
|
||||
acts_as_watchable
|
||||
acts_as_attachable :view_permission => :view_deals,
|
||||
:delete_permission => :edit_deals
|
||||
|
||||
acts_as_event :datetime => :created_on,
|
||||
:url => Proc.new { |o| { :controller => 'deals', :action => 'show', :id => o } },
|
||||
:type => 'icon icon-add-deal',
|
||||
:title => Proc.new { |o| o.name },
|
||||
:description => Proc.new { |o| [o.price_to_s, o.contact ? o.contact.name : nil, o.background].join(' ').strip }
|
||||
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
acts_as_activity_provider :type => 'deals',
|
||||
:permission => :view_deals,
|
||||
:author_key => :author_id,
|
||||
:scope => joins(:project)
|
||||
|
||||
acts_as_searchable :columns => ["#{table_name}.name",
|
||||
"#{table_name}.background",
|
||||
"#{DealNote.table_name}.content"],
|
||||
:scope => includes([:project, :notes]),
|
||||
:date_column => :created_on
|
||||
else
|
||||
acts_as_activity_provider :type => 'deals',
|
||||
:permission => :view_deals,
|
||||
:author_key => :author_id,
|
||||
:find_options => { :include => :project }
|
||||
|
||||
acts_as_searchable :columns => ["#{table_name}.name",
|
||||
"#{table_name}.background",
|
||||
"#{DealNote.table_name}.content"],
|
||||
:include => [:project, :notes],
|
||||
:order_column => "#{table_name}.id"
|
||||
end
|
||||
|
||||
validates_presence_of :name, :project, :status
|
||||
validates_numericality_of :price, :allow_nil => true
|
||||
|
||||
after_update :create_deal_process
|
||||
after_create :send_notification
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'name',
|
||||
'background',
|
||||
'currency',
|
||||
'price',
|
||||
'price_type',
|
||||
'duration',
|
||||
'project_id',
|
||||
'author_id',
|
||||
'assigned_to_id',
|
||||
'status_id',
|
||||
'contact_id',
|
||||
'category_id',
|
||||
'probability',
|
||||
'due_date',
|
||||
'custom_field_values',
|
||||
'custom_fields',
|
||||
'watcher_user_ids',
|
||||
:if => lambda { |deal, user| deal.new_record? || user.allowed_to?(:edit_deals, deal.project) }
|
||||
|
||||
def initialize(attributes = nil, *args)
|
||||
super
|
||||
return unless new_record?
|
||||
# set default values for new records only
|
||||
self.status_id = DealStatus.default.try(:id)
|
||||
self.currency ||= ContactsSetting.default_currency
|
||||
end
|
||||
|
||||
def avatar
|
||||
end
|
||||
|
||||
def expected_revenue
|
||||
probability ? (probability.to_f / 100) * price.to_f : price
|
||||
end
|
||||
|
||||
def full_name
|
||||
result = ''
|
||||
result << contact.name + ': ' unless contact.blank?
|
||||
result << name
|
||||
end
|
||||
|
||||
def all_contacts
|
||||
@all_contacts ||= ([contact] + related_contacts).uniq
|
||||
end
|
||||
|
||||
def self.available_users(prj = nil)
|
||||
cond = '(1=1)'
|
||||
cond << " AND #{Deal.table_name}.project_id = #{prj.id}" if prj
|
||||
User.active.select("DISTINCT #{User.table_name}.*").
|
||||
joins("JOIN #{Deal.table_name} ON #{Deal.table_name}.assigned_to_id = #{User.table_name}.id").
|
||||
where(cond).
|
||||
order("#{User.table_name}.lastname, #{User.table_name}.firstname")
|
||||
end
|
||||
|
||||
def open?
|
||||
status.blank? || status.is_open?
|
||||
end
|
||||
|
||||
def init_deal_process(author)
|
||||
@current_deal_process ||= DealProcess.new(:deal => self, :author => (author || User.current))
|
||||
@deal_status_before_change = new_record? ? nil : status_id
|
||||
@current_deal_process
|
||||
end
|
||||
|
||||
def create_deal_process
|
||||
if @current_deal_process && @deal_status_before_change && !(@deal_status_before_change == status_id)
|
||||
@current_deal_process.old_value = @deal_status_before_change
|
||||
@current_deal_process.value = status_id
|
||||
@current_deal_process.save
|
||||
init_deal_process @current_deal_process.author
|
||||
end
|
||||
end
|
||||
|
||||
def visible?(usr = nil)
|
||||
(usr || User.current).allowed_to?(:view_deals, project)
|
||||
end
|
||||
|
||||
def editable?(usr = nil)
|
||||
(usr || User.current).allowed_to?(:edit_deals, project)
|
||||
end
|
||||
|
||||
def destroyable?(usr = nil)
|
||||
(usr || User.current).allowed_to?(:delete_deals, project)
|
||||
end
|
||||
|
||||
# Returns an array of projects that user can move deal to
|
||||
def self.allowed_target_projects(user = User.current)
|
||||
Project.where(Project.allowed_to_condition(user, :add_deals))
|
||||
end
|
||||
|
||||
# Returns the mail adresses of users that should be notified
|
||||
def recipients
|
||||
notified = []
|
||||
# Author and assignee are always notified unless they have been
|
||||
# locked or don't want to be notified
|
||||
notified << author if author
|
||||
if assigned_to
|
||||
notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
|
||||
end
|
||||
|
||||
notified += project.notified_users
|
||||
notified = notified.select { |u| u.active? }
|
||||
notified.uniq!
|
||||
# Remove users that can not view the contact
|
||||
notified.reject! { |user| !visible?(user) }
|
||||
notified.collect(&:mail)
|
||||
end
|
||||
|
||||
def status_was
|
||||
if status_id_changed? && status_id_was.present?
|
||||
@status_was ||= DealStatus.find_by_id(status_id_was)
|
||||
end
|
||||
end
|
||||
|
||||
def copy_from(arg)
|
||||
deal = arg.is_a?(Deal) ? arg : Deal.visible.find(arg)
|
||||
self.attributes = deal.attributes.dup.except('id', 'created_at', 'updated_at')
|
||||
self.custom_field_values = deal.custom_field_values.inject({}) { |h, v| h[v.custom_field_id] = v.value ; h }
|
||||
if Redmine::Plugin.load && Redmine::Plugin.installed?(:redmine_products) && Redmine::Plugin.find(:redmine_products).version >= '2.0.2'
|
||||
deal.lines.each do |line|
|
||||
lines.build(line.attributes)
|
||||
end
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def contact_country
|
||||
try(:contact).try(:address).try(:country)
|
||||
end
|
||||
|
||||
def contact_city
|
||||
try(:contact).try(:address).try(:city)
|
||||
end
|
||||
|
||||
if Redmine::Plugin.load && Redmine::Plugin.installed?(:redmine_products) && Redmine::Plugin.find(:redmine_products).version >= '2.0.2'
|
||||
def has_taxes?
|
||||
!lines.map(&:tax).all? { |t| t == 0 || t.blank? }
|
||||
end
|
||||
|
||||
def has_discounts?
|
||||
!lines.map(&:discount).all? { |t| t == 0 || t.blank? }
|
||||
end
|
||||
|
||||
def tax_amount
|
||||
lines.select { |l| !l.marked_for_destruction? }.inject(0) { |sum, l| sum + l.tax_amount }.to_f
|
||||
end
|
||||
|
||||
def subtotal
|
||||
lines.select { |l| !l.marked_for_destruction? }.inject(0) { |sum, l| sum + l.total }.to_f
|
||||
end
|
||||
|
||||
def total_units
|
||||
lines.inject(0) { |sum, l| sum + (l.product.blank? ? 0 : l.quantity) }
|
||||
end
|
||||
|
||||
def calculate_price
|
||||
return true if lines.select { |l| !l.marked_for_destruction? }.empty?
|
||||
self.price = subtotal + (ContactsSetting.tax_exclusive? ? tax_amount : 0)
|
||||
end
|
||||
end
|
||||
|
||||
def info
|
||||
result = ''
|
||||
result = status.name if status
|
||||
result = result + ' - ' + price_to_s unless price.blank?
|
||||
result.html_safe
|
||||
end
|
||||
private
|
||||
|
||||
def send_notification
|
||||
Mailer.crm_deal_add(self).deliver if Setting.notified_events.include?('crm_deal_added')
|
||||
end
|
||||
|
||||
if Redmine::Plugin.load && Redmine::Plugin.installed?(:redmine_products) && Redmine::Plugin.find(:redmine_products).version >= '2.0.2'
|
||||
def assign_lines
|
||||
return unless new_record?
|
||||
lines.each { |l| l.container = self }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class DealCategory < ActiveRecord::Base
|
||||
unloadable
|
||||
include Redmine::SafeAttributes
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'name'
|
||||
|
||||
belongs_to :project
|
||||
has_many :deals, :class_name => 'Deal', :foreign_key => 'category_id', :dependent => :nullify
|
||||
validates_presence_of :name, :project
|
||||
validates_uniqueness_of :name, :scope => [:project_id]
|
||||
validates_length_of :name, :maximum => 30
|
||||
|
||||
alias :destroy_without_reassign :destroy
|
||||
|
||||
# Destroy the category
|
||||
# If a category is specified, issues are reassigned to this category
|
||||
def destroy(reassign_to = nil)
|
||||
if reassign_to && reassign_to.is_a?(DealCategory) && reassign_to.project == self.project
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
Deal.where(:category_id => id).update_all(:category_id => reassign_to.id)
|
||||
else
|
||||
Deal.update_all("category_id = #{reassign_to.id}", "category_id = #{id}")
|
||||
end
|
||||
end
|
||||
destroy_without_reassign
|
||||
end
|
||||
|
||||
def <=>(category)
|
||||
name <=> category.name
|
||||
end
|
||||
|
||||
def to_s; name end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class DealCustomField < CustomField
|
||||
unloadable
|
||||
|
||||
def type_name
|
||||
:label_deal_plural
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class DealImport
|
||||
extend ActiveModel::Naming
|
||||
include ActiveModel::Conversion
|
||||
include ActiveModel::Validations
|
||||
include CSVImportable
|
||||
|
||||
attr_accessor :file, :project, :quotes_type
|
||||
|
||||
def klass
|
||||
Deal
|
||||
end
|
||||
|
||||
def build_from_fcsv_row(row)
|
||||
ret = Hash[row.to_hash.collect { |k, v| [k.underscore.tr(' ', '_'), force_utf8(v)] if k }].delete_if { |k, _v| !klass.column_names.include?(k) }
|
||||
ret[:due_date] = row['due date'].to_date if row['due date']
|
||||
ret[:status_id] = DealStatus.where(:name => row['status']).first.try(:id) if row['status']
|
||||
ret[:category_id] = DealCategory.where(:name => row['category']).first.try(:id) if row['category']
|
||||
ret[:assigned_to_id] = User.find_by_login(row['assignee']).try(:id) unless row['assignee'].blank?
|
||||
ret[:price] = row['sum'].to_f if row['sum']
|
||||
if row['contact'].to_s.match(/^\#(\d+):/)
|
||||
ret[:contact_id] = Contact.find_by_id($1).try(:id)
|
||||
end
|
||||
ret
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,101 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class DealKernelImport < Import
|
||||
|
||||
def klass
|
||||
Deal
|
||||
end
|
||||
|
||||
def saved_objects
|
||||
object_ids = saved_items.pluck(:obj_id)
|
||||
Deal.where(:id => object_ids).order(:id)
|
||||
end
|
||||
|
||||
def project=(project)
|
||||
settings['project'] = project.id
|
||||
end
|
||||
|
||||
def project
|
||||
settings['project']
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_object(row, _item = nil)
|
||||
deal = Deal.new
|
||||
deal.project = Project.find(settings['project'])
|
||||
deal.author = user
|
||||
|
||||
attributes = {}
|
||||
if name = row_value(row, 'name')
|
||||
attributes['name'] = name
|
||||
end
|
||||
if background = row_value(row, 'background')
|
||||
attributes['background'] = background
|
||||
end
|
||||
if currency = row_value(row, 'currency')
|
||||
attributes['currency'] = currency
|
||||
end
|
||||
if price = row_value(row, 'price')
|
||||
attributes['price'] = price.to_f
|
||||
end
|
||||
if probability = row_value(row, 'probability')
|
||||
attributes['probability'] = probability.to_i
|
||||
end
|
||||
if status = row_value(row, 'status')
|
||||
attributes['status_id'] = DealStatus.where('name = ?', status).first.try(:id)
|
||||
end
|
||||
if contact = row_value(row, 'contact')
|
||||
attributes['contact_id'] = Contact.by_full_name(contact).first.try(:id)
|
||||
end
|
||||
if assigned_to = row_value(row, 'assigned_to')
|
||||
attributes['assigned_to_id'] = User.where("LOWER(CONCAT(#{User.table_name}.firstname,' ',#{User.table_name}.lastname)) = ? ", assigned_to.mb_chars.downcase.to_s)
|
||||
.first
|
||||
.try(:id)
|
||||
end
|
||||
if category = row_value(row, 'category')
|
||||
attributes['category_id'] = DealCategory.where(:name => category).first.try(:id)
|
||||
end
|
||||
|
||||
attributes['custom_field_values'] = deal.custom_field_values.inject({}) do |h, v|
|
||||
value = case v.custom_field.field_format
|
||||
when 'date'
|
||||
row_date(row, "cf_#{v.custom_field.id}")
|
||||
when 'list'
|
||||
row_value(row, "cf_#{v.custom_field.id}").try(:split, ',')
|
||||
else
|
||||
row_value(row, "cf_#{v.custom_field.id}")
|
||||
end
|
||||
if value
|
||||
h[v.custom_field.id.to_s] =
|
||||
if value.is_a?(Array)
|
||||
value.map { |val| v.custom_field.value_from_keyword(val.strip, contact) }.compact.flatten
|
||||
else
|
||||
v.custom_field.value_from_keyword(value, contact)
|
||||
end
|
||||
end
|
||||
h
|
||||
end
|
||||
|
||||
deal.send :safe_attributes=, attributes, user
|
||||
deal
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class DealNote < Note
|
||||
unloadable
|
||||
include Redmine::SafeAttributes
|
||||
belongs_to :deal, :foreign_key => :source_id
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'subject', 'type_id', 'content'
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
if ActiveRecord::Base.connection.table_exists?('notes')
|
||||
acts_as_activity_provider :type => 'deals',
|
||||
:permission => :view_deals,
|
||||
:author_key => :author_id,
|
||||
:scope => joins(:deal => :project).where(:source_type => 'Deal')
|
||||
end
|
||||
else
|
||||
acts_as_activity_provider :type => 'deals',
|
||||
:permission => :view_deals,
|
||||
:author_key => :author_id,
|
||||
:find_options => { :joins => [:deal => :project],
|
||||
:conditions => { :source_type => 'Deal' } }
|
||||
end
|
||||
|
||||
scope :visible, lambda {|*args| joins(:deal => :project).
|
||||
where(Project.allowed_to_condition(args.first || User.current, :view_deals) +
|
||||
" AND (#{DealNote.table_name}.source_type = 'Deal')") }
|
||||
acts_as_attachable :view_permission => :view_deals,
|
||||
:delete_permission => :edit_deals
|
||||
|
||||
def custom_field_values
|
||||
Note.new.custom_field_values
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class DealProcess < ActiveRecord::Base
|
||||
unloadable
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'deal', 'author'
|
||||
|
||||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||
belongs_to :deal
|
||||
belongs_to :from, :class_name => 'DealStatus', :foreign_key => 'old_value'
|
||||
belongs_to :to, :class_name => 'DealStatus', :foreign_key => 'value'
|
||||
scope :visible, lambda { |*args| joins(:deal => :project).where(Project.allowed_to_condition(args.first || User.current, :view_deals)) }
|
||||
|
||||
after_create :send_notification
|
||||
|
||||
def recipients
|
||||
(deal.recipients + [author.mail]).uniq
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_notification
|
||||
Mailer.crm_deal_updated(self).deliver if Setting.notified_events.include?('crm_deal_updated')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,178 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class DealQuery < Query
|
||||
include CrmQuery
|
||||
include RedmineCrm::MoneyHelper
|
||||
|
||||
self.queried_class = Deal
|
||||
self.view_permission = :view_deals if Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch?
|
||||
|
||||
self.available_columns = [
|
||||
QueryColumn.new(:name, :sortable => "#{Deal.table_name}.name", :caption => :field_deal_name),
|
||||
QueryColumn.new(:price, :sortable => ["#{Deal.table_name}.currency", "#{Deal.table_name}.price"], :default_order => 'desc', :caption => :field_price),
|
||||
QueryColumn.new(:status, :sortable => "#{Deal.table_name}.status_id", :groupable => true, :caption => :field_contact_status),
|
||||
QueryColumn.new(:currency, :sortable => "#{Deal.table_name}.currency", :groupable => true, :caption => :field_currency),
|
||||
QueryColumn.new(:contact, :sortable => lambda { Contact.fields_for_order_statement }, :groupable => true, :caption => :label_contact),
|
||||
QueryColumn.new(:category, :sortable => "#{Deal.table_name}.category_id", :groupable => true),
|
||||
QueryColumn.new(:probability, :sortable => "#{Deal.table_name}.probability", :groupable => "#{Deal.table_name}.probability", :caption => :label_crm_probability),
|
||||
QueryColumn.new(:expected_revenue, :sortable => ["#{Deal.table_name}.currency", "#{Deal.table_name}.price * (#{Deal.table_name}.probability / 100)"], :caption => :label_crm_expected_revenue),
|
||||
QueryColumn.new(:contact_city, :caption => :label_crm_contact_city, :groupable => "#{Address.table_name}.city", :sortable => "#{Address.table_name}.city"),
|
||||
QueryColumn.new(:contact_country, :caption => :label_crm_contact_country, :groupable => "#{Address.table_name}.country_code", :sortable => "#{Address.table_name}.country_code"),
|
||||
QueryColumn.new(:due_date, :sortable => "#{Deal.table_name}.due_date"),
|
||||
QueryColumn.new(:due_date, :sortable => "#{Deal.table_name}.due_date"),
|
||||
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
|
||||
QueryColumn.new(:created_on, :sortable => "#{Deal.table_name}.created_on"),
|
||||
QueryColumn.new(:updated_on, :sortable => "#{Deal.table_name}.updated_on"),
|
||||
QueryColumn.new(:assigned_to, :sortable => lambda { User.fields_for_order_statement }, :groupable => true),
|
||||
QueryColumn.new(:author, :sortable => lambda { User.fields_for_order_statement('authors') }),
|
||||
QueryColumn.new(:background)
|
||||
]
|
||||
|
||||
def initialize(attributes = nil, *args)
|
||||
super attributes
|
||||
self.filters ||= { 'status_id' => { :operator => 'o', :values => [''] } }
|
||||
end
|
||||
|
||||
def initialize_available_filters
|
||||
add_available_filter 'ids', :type => :integer, :label => :label_deal if Redmine::VERSION.to_s >= '3.3'
|
||||
add_available_filter 'price', :type => :float, :label => :field_price
|
||||
add_available_filter 'currency', :type => :list,
|
||||
:label => :field_currency,
|
||||
:values => collection_for_currencies_select(ContactsSetting.default_currency, ContactsSetting.major_currencies)
|
||||
add_available_filter 'background', :type => :text, :label => :field_background
|
||||
add_available_filter 'due_date', :type => :date, :order => 20
|
||||
add_available_filter 'updated_on', :type => :date_past, :order => 20
|
||||
add_available_filter 'created_on', :type => :date, :order => 21
|
||||
add_available_filter 'probability', :type => :float, :label => :label_crm_probability
|
||||
|
||||
deal_statuses = (project.blank? ? DealStatus.order("#{DealStatus.table_name}.status_type, #{DealStatus.table_name}.position") : project.deal_statuses) || []
|
||||
add_available_filter('status_id',
|
||||
:type => :list_status, :values => deal_statuses.map { |a| [a.name, a.id.to_s] }, :label => :field_contact_status, :order => 1
|
||||
) unless deal_statuses.empty?
|
||||
|
||||
initialize_project_filter
|
||||
initialize_author_filter
|
||||
initialize_assignee_filter
|
||||
initialize_contact_country_filter
|
||||
initialize_contact_city_filter
|
||||
|
||||
add_custom_fields_filters(DealCustomField.where(:is_filter => true))
|
||||
add_associations_custom_fields_filters :contact, :notes, :author, :assigned_to
|
||||
if RedmineContacts.products_plugin_installed?
|
||||
products = Product.visible.all
|
||||
add_available_filter('products', :type => :list_optional,
|
||||
:values => products.map { |a| [a.name, a.id.to_s] }, :label => :label_product_plural
|
||||
) unless products.empty?
|
||||
|
||||
product_categories = []
|
||||
ProductCategory.category_tree(ProductCategory.order(:lft)) do |product_category, level|
|
||||
name_prefix = (level > 0 ? '-' * 2 * level + ' ' : '').html_safe
|
||||
product_categories << [(name_prefix + product_category.name).html_safe, product_category.id.to_s]
|
||||
end
|
||||
add_available_filter('product_category_id', :type => :list,
|
||||
:label => :label_products_category_filter,
|
||||
:values => product_categories
|
||||
) if product_categories.any?
|
||||
add_associations_custom_fields_filters :products, :lines
|
||||
end
|
||||
end
|
||||
|
||||
def available_columns
|
||||
return @available_columns if @available_columns
|
||||
@available_columns = self.class.available_columns.dup
|
||||
@available_columns += CustomField.where(:type => 'DealCustomField').all.map { |cf| QueryCustomFieldColumn.new(cf) }
|
||||
@available_columns += CustomField.where(:type => 'ContactCustomField').all.map { |cf| QueryAssociationCustomFieldColumn.new(:contact, cf) }
|
||||
@available_columns << QueryColumn.new(:products, :caption => :label_product_plural) if RedmineContacts.products_plugin_installed?
|
||||
@available_columns
|
||||
end
|
||||
|
||||
def default_columns_names
|
||||
@default_columns_names ||= [:id, :name, :contact, :price]
|
||||
end
|
||||
if RedmineContacts.products_plugin_installed?
|
||||
def sql_for_products_field(_field, operator, value)
|
||||
if operator == '*'
|
||||
products = Product.visible.all
|
||||
operator = '='
|
||||
elsif operator == '!*'
|
||||
products = Product.visible.all
|
||||
operator = '!'
|
||||
else
|
||||
products = Product.visible.where(:id => value)
|
||||
end
|
||||
products ||= []
|
||||
|
||||
order_products = products.map(&:id).uniq.compact.sort.collect(&:to_s)
|
||||
'(' + sql_for_field('product_id', operator, order_products, ProductLine.table_name, 'product_id', false) + ')'
|
||||
end
|
||||
|
||||
def sql_for_product_category_id_field(field, operator, value)
|
||||
category_ids = value
|
||||
category_ids += ProductCategory.where(:id => value).map(&:descendants).flatten.collect { |c| c.id.to_s }.uniq
|
||||
sql_for_field(field, operator, category_ids, Product.table_name, 'category_id')
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_status_id_field(field, operator, value)
|
||||
sql = ''
|
||||
case operator
|
||||
when "o"
|
||||
sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{DealStatus.table_name} WHERE status_type = #{DealStatus::OPEN_STATUS})" if field == "status_id"
|
||||
when "c"
|
||||
sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{DealStatus.table_name} WHERE status_type IN (#{DealStatus::WON_STATUS}, #{DealStatus::LOST_STATUS}))" if field == "status_id"
|
||||
else
|
||||
sql_for_field(field, operator, value, queried_table_name, field)
|
||||
end
|
||||
end
|
||||
|
||||
def deal_amount
|
||||
@deal_amount ||= objects_scope.group("#{Deal.table_name}.currency").sum(:price)
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
def weighted_amount
|
||||
@weighted_amount ||= objects_scope.open.group("#{Deal.table_name}.currency").sum("#{Deal.table_name}.price * #{Deal.table_name}.probability / 100")
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
def objects_scope(options={})
|
||||
scope = Deal.visible
|
||||
options[:search].split(' ').collect{ |search_string| scope = scope.live_search(search_string) } unless options[:search].blank?
|
||||
scope = scope.includes((query_includes + (options[:include] || [])).uniq).
|
||||
where(statement).
|
||||
where(options[:conditions])
|
||||
scope
|
||||
end
|
||||
|
||||
def query_includes
|
||||
includes = [:status, :project]
|
||||
includes << { :contact => :address } if self.filters['contact_country'] ||
|
||||
self.filters['contact_city'] ||
|
||||
[:contact_country, :contact_city].include?(group_by_column.try(:name))
|
||||
includes << :assigned_to if self.filters['assigned_to_id'] || (group_by_column && [:assigned_to].include?(group_by_column.name))
|
||||
if RedmineContacts.products_plugin_installed?
|
||||
includes << :products if filters['products']
|
||||
includes << :products if filters['product_category_id']
|
||||
end
|
||||
includes
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,116 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class DealStatus < ActiveRecord::Base
|
||||
unloadable
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
OPEN_STATUS = 0
|
||||
WON_STATUS = 1
|
||||
LOST_STATUS = 2
|
||||
|
||||
before_destroy :check_integrity
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'name', 'is_default', 'status_type', 'move_to', 'color_name', 'position'
|
||||
|
||||
has_and_belongs_to_many :projects
|
||||
has_many :deals, :foreign_key => 'status_id', :dependent => :nullify
|
||||
has_many :deal_processes_from, :class_name => 'DealProcess',:foreign_key => 'old_value', :dependent => :delete_all
|
||||
has_many :deal_processes_to, :class_name => 'DealProcess', :foreign_key => 'value', :dependent => :delete_all
|
||||
rcrm_acts_as_list :scope => 'status_type = #{status_type}'
|
||||
|
||||
scope :open, lambda { where(:status_type => DealStatus::OPEN_STATUS) }
|
||||
scope :won, lambda { where(:status_type => DealStatus::WON_STATUS) }
|
||||
scope :lost, lambda { where(:status_type => DealStatus::LOST_STATUS) }
|
||||
scope :closed, lambda { where("#{DealStatus.table_name}.status_type <> #{DealStatus::OPEN_STATUS}") }
|
||||
|
||||
after_save :update_default
|
||||
|
||||
validates_presence_of :name
|
||||
validates_uniqueness_of :name
|
||||
validates_length_of :name, :maximum => 30
|
||||
|
||||
def update_default
|
||||
DealStatus.where('id <> ?', id).update_all(:is_default => false) if is_default?
|
||||
end
|
||||
|
||||
# Returns the default status for new Deals
|
||||
def self.default
|
||||
where(:is_default => true).first
|
||||
end
|
||||
|
||||
def is_open?
|
||||
status_type == OPEN_STATUS
|
||||
end
|
||||
|
||||
def is_won?
|
||||
status_type == WON_STATUS
|
||||
end
|
||||
|
||||
def is_lost?
|
||||
status_type == LOST_STATUS
|
||||
end
|
||||
|
||||
def is_closed?
|
||||
!is_open?
|
||||
end
|
||||
|
||||
def status_type_name
|
||||
case status_type
|
||||
when OPEN_STATUS then l(:label_open_issues)
|
||||
when WON_STATUS then l(:label_crm_deal_status_won)
|
||||
when LOST_STATUS then l(:label_crm_deal_status_lost)
|
||||
else ''
|
||||
end
|
||||
end
|
||||
|
||||
def new_status_allowed_to?(status, roles, tracker)
|
||||
if status && roles && tracker
|
||||
!workflows.where(:new_status_id => status.id).where(:role_id => roles.collect(&:id)).where(:tracker_id => tracker.id).first.nil?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def color_name
|
||||
return '#' + "%06x" % color unless color.nil?
|
||||
end
|
||||
|
||||
def color_name=(clr)
|
||||
self.color = clr.from(1).hex
|
||||
end
|
||||
|
||||
def <=>(status)
|
||||
position <=> status.position
|
||||
end
|
||||
|
||||
def to_s; name end
|
||||
|
||||
private
|
||||
|
||||
def check_integrity
|
||||
raise "Can't delete status" if Deal.where(:status_id => id).any?
|
||||
end
|
||||
|
||||
# Deletes associated workflows
|
||||
def delete_workflows
|
||||
Workflow.delete_all(['old_status_id = :id OR new_status_id = :id', { :id => id }])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class DealsIssue < ActiveRecord::Base
|
||||
include Redmine::SafeAttributes
|
||||
belongs_to :issue
|
||||
belongs_to :deal
|
||||
|
||||
validate :validate_deals_issue
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'issue_id', 'deal_id', 'issue', 'deal'
|
||||
|
||||
def validate_deals_issue
|
||||
errors.add :deal_id, :invalid if deal_id && !deal
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class DealsPipelineProcessor
|
||||
attr_reader :scope
|
||||
|
||||
def initialize(scope)
|
||||
@scope = scope
|
||||
end
|
||||
|
||||
def count
|
||||
@scope.count
|
||||
end
|
||||
|
||||
def deals_for_status(status)
|
||||
if status.is_open?
|
||||
open_deals_for_status(status) + closed_deals_for_status(status)
|
||||
else
|
||||
@scope.where(:status_id => status.id)
|
||||
end
|
||||
end
|
||||
|
||||
def closed_deals_for_status(status)
|
||||
deal_status_ids = DealStatus.open.where('position >= ?', status.position).pluck(:id)
|
||||
first_condition = []
|
||||
second_condition = []
|
||||
if lost_status_ids.present?
|
||||
first_condition << "dp.value IN (#{lost_status_ids.join(',')})"
|
||||
second_condition << "dp2.value IN (#{lost_status_ids.join(',')})"
|
||||
end
|
||||
if won_status_ids.present?
|
||||
first_condition << "dp.old_value IN (#{won_status_ids.join(',')})"
|
||||
second_condition << "dp2.old_value IN (#{won_status_ids.join(',')})"
|
||||
end
|
||||
first_sql = first_condition.present? ? "NOT (#{first_condition.join(' AND ')})" : '1=1'
|
||||
second_sql = second_condition.present? ? "NOT (#{second_condition.join(' AND ')})" : '1=1'
|
||||
ret = @scope.closed.joins("LEFT OUTER JOIN #{DealProcess.table_name} dp on dp.deal_id = deals.id AND #{first_sql}").
|
||||
joins("LEFT OUTER JOIN #{DealProcess.table_name} dp2 ON (deals.id = dp2.deal_id AND (dp.created_at < dp2.created_at OR dp.created_at = dp2.created_at AND dp.id < dp2.id)) AND #{second_sql}").
|
||||
joins("LEFT OUTER JOIN #{DealStatus.table_name} ds ON (ds.id = deals.status_id)").
|
||||
where(['ds.status_type IN (?)', [DealStatus::WON_STATUS, DealStatus::LOST_STATUS] ]).
|
||||
where("dp2.id IS NULL")
|
||||
if status.is_open?
|
||||
ret.where(["(dp.old_value IN (?) OR (#{Deal.table_name}.status_id IN (?)))", deal_status_ids, won_status_ids])
|
||||
else
|
||||
ret.where(["dp.old_value IN (?)", deal_status_ids])
|
||||
end
|
||||
end
|
||||
|
||||
def open_deals_for_status(status)
|
||||
deal_status_ids = DealStatus.open.where('position >= ?', status.position).pluck(:id)
|
||||
@scope.open.joins("LEFT OUTER JOIN #{DealStatus.table_name} ds ON (ds.id = deals.status_id)").
|
||||
where(['ds.status_type NOT IN (?)', [DealStatus::WON_STATUS, DealStatus::LOST_STATUS] ]).
|
||||
where(["#{Deal.table_name}.status_id IN (?)", deal_status_ids])
|
||||
end
|
||||
|
||||
def won_status_ids
|
||||
@won_status_ids ||= DealStatus.won.pluck(:id)
|
||||
end
|
||||
|
||||
def lost_status_ids
|
||||
@lost_status_ids ||= DealStatus.lost.pluck(:id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,95 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class Note < ActiveRecord::Base
|
||||
unloadable
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||
belongs_to :source, :polymorphic => true, :touch => true
|
||||
|
||||
# added as a quick fix to allow eager loading of the polymorphic association for multiprojects
|
||||
|
||||
validates_presence_of :source, :author, :content
|
||||
|
||||
acts_as_customizable
|
||||
acts_as_attachable
|
||||
|
||||
acts_as_event :title => Proc.new {|o| "#{l(:label_crm_note_for)}: #{o.source.name}"},
|
||||
:type => "icon issue-note icon-issue-note",
|
||||
:group => :source,
|
||||
:url => Proc.new {|o| {:controller => 'notes', :action => 'show', :id => o.id }},
|
||||
:description => Proc.new {|o| o.content}
|
||||
|
||||
after_create :send_notification
|
||||
|
||||
cattr_accessor :note_types
|
||||
@@note_types = {:email => 0, :call => 1, :meeting => 2}
|
||||
cattr_accessor :cut_length
|
||||
@@cut_length = 1000
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'subject', 'type_id', 'author_id', 'note_time', 'content', 'created_on', 'custom_field_values'
|
||||
|
||||
def self.note_types
|
||||
@@note_types
|
||||
end
|
||||
|
||||
def note_time
|
||||
self.created_on.to_s(:time) unless self.created_on.blank?
|
||||
end
|
||||
|
||||
def note_time=(val)
|
||||
if !self.created_on.blank? && val.to_s.gsub(/\s/, "").match(/^(\d{1,2}):(\d{1,2})$/)
|
||||
self.created_on = self.created_on.change({:hour => $1.to_i % 24, :min => $2.to_i % 60})
|
||||
end
|
||||
end
|
||||
|
||||
def visible?(usr=nil)
|
||||
self.source.visible?(usr)
|
||||
end
|
||||
|
||||
def project
|
||||
self.source.respond_to?(:project) ? self.source.project : nil
|
||||
end
|
||||
|
||||
def editable_by?(usr, prj=nil)
|
||||
prj ||= @project || self.project
|
||||
usr && (usr.allowed_to?(:delete_notes, prj) || (self.author == usr && usr.allowed_to?(:delete_own_notes, prj)))
|
||||
# usr && usr.logged? && (usr.allowed_to?(:edit_notes, project) || (self.author == usr && usr.allowed_to?(:edit_own_notes, project)))
|
||||
end
|
||||
|
||||
def destroyable_by?(usr, prj=nil)
|
||||
prj ||= @project || self.project
|
||||
usr && (usr.allowed_to?(:delete_notes, prj) || (self.author == usr && usr.allowed_to?(:delete_own_notes, prj)))
|
||||
end
|
||||
|
||||
def created_on
|
||||
return nil if super.blank?
|
||||
zone = User.current.time_zone
|
||||
zone ? super.in_time_zone(zone) : (super.utc? ? super.localtime : super)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_notification
|
||||
Mailer.crm_note_add(self).deliver if Setting.notified_events.include?('crm_note_added')
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class NoteCustomField < CustomField
|
||||
unloadable
|
||||
|
||||
def type_name
|
||||
:label_crm_note_plural
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class RecentlyViewed < ActiveRecord::Base
|
||||
unloadable
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4
|
||||
safe_attributes 'viewer'
|
||||
|
||||
RECENTLY_VIEWED_LIMIT = 5
|
||||
|
||||
belongs_to :viewer, :class_name => 'User', :foreign_key => 'viewer_id'
|
||||
belongs_to :viewed, :polymorphic => true
|
||||
|
||||
validates_presence_of :viewed, :viewer
|
||||
|
||||
# after_save :increment_views_count
|
||||
def self.last(limit=RECENTLY_VIEWED_LIMIT, usr=nil)
|
||||
RecentlyViewed.where("#{RecentlyViewed.table_name}.viewer_id" => usr || User.current).order("#{RecentlyViewed.table_name}.updated_at DESC").limit(limit).collect{|v| v.viewed}.select(&:visible?).compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def increment_views_count
|
||||
self.increment!(:views_count)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# This file is a part of Redmine CRM (redmine_contacts) plugin,
|
||||
# customer relationship management plugin for Redmine
|
||||
#
|
||||
# Copyright (C) 2010-2018 RedmineUP
|
||||
# http://www.redmineup.com/
|
||||
#
|
||||
# redmine_contacts is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# redmine_contacts is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with redmine_contacts. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class Task < ActiveRecord::Base
|
||||
validates_presence_of :source_id, :issue_id, :source_type
|
||||
validates_uniqueness_of :source_id, :scope => [:issue_id, :source_type]
|
||||
|
||||
after_save :send_mails
|
||||
|
||||
private
|
||||
|
||||
def send_mails
|
||||
Mailer.deliver_contacts_issue_connected(Contact.find(contact_id), Issue.find(issue_id))
|
||||
true
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user