Initial Redmine tooling and local plugin forks

This commit is contained in:
Jason Thistlethwaite
2026-04-24 22:01:18 +00:00
commit 9f682af0eb
683 changed files with 56878 additions and 0 deletions
@@ -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
View File
@@ -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('&gt; ','').gsub('&quot;', '"')
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
+306
View File
@@ -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