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
+14
View File
@@ -0,0 +1,14 @@
# Local Redmine Plugin Forks
This directory is the tracked source of truth for local Redmine plugin work.
The ignored `redmine-copy/` tree is a working/reference copy of the full legacy
Redmine install. Make plugin edits here first, then deploy or copy them into the
test Redmine instance or `redmine-copy/` as needed.
Tracked plugin folders:
- `redmine_event_outbox` - local event outbox plugin.
- `redmine_contacts` - RedmineUP contacts plugin with local compatibility fixes.
- `redmine_contacts_helpdesk` - RedmineUP helpdesk plugin with local API and
mail compatibility fixes.
+3
View File
@@ -0,0 +1,3 @@
gem "redmine_crm"
gem "vcard", "~> 0.2.8"
gem "spreadsheet", "~> 0.6.8"
+46
View File
@@ -0,0 +1,46 @@
= Contacts plugin
== Install
* Copy redmine_contacts plugin to {RAILS_APP}/plugins on your redmine path
* Run bundle install --without development test RAILS_ENV=production
* Run rake redmine:plugins NAME=redmine_contacts RAILS_ENV=production
== Uninstall
<pre>
rake redmine:plugins NAME=redmine_contacts VERSION=0 RAILS_ENV=production
rm -r plugins/redmine_contacts
</pre>
=== Tables created by CRM plugin
* contacts
* contacts_deals
* contacts_issues
* contacts_projects
* deals
* deal_categories
* deal_processes
* deal_statuses
* deal_statuses_projects
* notes
* tags
* taggings
* recently_vieweds
* contacts_settings
* contacts_queries
* addresses
* deals_issues
== Requirements
* Redmine 2.3+
== Test
bundle exec rake db:drop db:create db:migrate redmine:plugins RAILS_ENV=test_sqlite3
bundle exec rake test TEST="plugins/redmine_contacts/test/**/*_test.rb" RAILS_ENV=test_sqlite3
=== Test API
curl -v -H "Content-Type: application/xml" -X POST --data "@contact.xml" -u admin:admin http://localhost:3000/contacts.xml
@@ -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 ContactImportsController < ImporterBaseController
menu_item :contacts
helper :contacts
def klass
ContactImport
end
def importer_klass
ContactKernelImport
end
def instance_index
project_contacts_path(:project_id => @project.id)
end
end
@@ -0,0 +1,490 @@
# 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 ContactsController < ApplicationController
unloadable
Mime::Type.register 'text/x-vcard', :vcf
Mime::Type.register 'application/vnd.ms-excel', :xls
default_search_scope :contacts
before_action :find_contact, :only => [:show, :edit, :update, :destroy, :load_tab]
before_action :find_project, :only => [:new, :create]
before_action :authorize, :only => [:create, :new]
before_action :authorize_contacts, :only => [:edit, :update, :destroy]
before_action :find_optional_project, :only => [:index, :contacts_notes, :edit_mails, :send_mails, :bulk_update]
accept_rss_auth :index, :show
accept_api_auth :index, :show, :create, :update, :destroy
helper :attachments
helper :contacts
include ContactsHelper
helper :watchers
helper :deals
helper :notes
helper :custom_fields
include CustomFieldsHelper
helper :context_menus
include WatchersHelper
helper :sort
include SortHelper
helper :queries
include QueriesHelper
helper :crm_queries
include CrmQueriesHelper
include ApplicationHelper
include NotesHelper
def index
retrieve_crm_query('contact')
sort_init(@query.sort_criteria.empty? ? [['last_name', 'asc'], ['first_name', 'asc']] : @query.sort_criteria)
sort_update(@query.sortable_columns)
@query.sort_criteria = sort_criteria.to_a
if @query.valid?
case params[:format]
when 'csv', 'xls', 'vcf'
@limit = Setting.issues_export_limit.to_i
if Redmine::VERSION::STRING < '3.2' && params[:columns] == 'all'
@query.column_names = @query.available_columns.map(&:name)
end
when 'atom'
@limit = Setting.feeds_limit.to_i
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end
@contacts_count = @query.object_count
@contacts_pages = Paginator.new(@contacts_count, @limit, params['page'])
@offset ||= @contacts_pages.offset
@contact_count_by_group = @query.object_count_by_group
@contacts = @query.results_scope(
:include => [:avatar],
:search => params[:search],
:order => sort_clause,
:limit => @limit,
:offset => @offset
)
@filter_tags = @query.filters['tags'] && @query.filters['tags'][:values]
respond_to do |format|
format.html {
unless request.xhr?
last_notes
@tags = Contact.available_tags(:project => @project)
else
render :partial => contacts_list_style, :layout => false
end
}
format.api
format.atom { render_feed(@contacts, :title => "#{@project || Setting.app_title}: #{l(:label_contact_plural)}") }
format.csv {
send_data(query_to_csv(@contacts, @query, params[:csv] || {}),
:type => 'text/csv; header=present',
:filename => 'contacts.csv')
}
format.xls {
send_data(contacts_to_xls(@contacts),
:filename => 'contacts.xls',
:type => 'application/vnd.ms-excel',
:disposition => 'attachment')
}
format.vcf {
send_data(contacts_to_vcard(@contacts),
:filename => 'contacts.vcf',
:type => 'text/x-vcard',
:disposition => 'attachment')
}
end
else
respond_to do |format|
format.html {
last_notes
@tags = Contact.available_tags(:project => @project)
render(:template => 'contacts/index', :layout => !request.xhr?)
}
format.any(:atom, :csv, :pdf) { render(:nothing => true) }
format.api { render_validation_errors(@query) }
end
end
end
def show
find_contact_issues
respond_to do |format|
format.js if request.xhr?
format.html { @contact.viewed }
format.api
format.atom { render_feed(@notes, :title => "#{@contact.name || Setting.app_title}: #{l(:label_crm_note_plural)}") }
format.vcf { send_data(contact_to_vcard(@contact), :filename => "#{@contact.name}.vcf", :type => 'text/x-vcard;', :disposition => 'attachment') }
end
end
def edit
end
def update
@contact.safe_attributes = params[:contact]
@contact.save_attachments(params[:attachments] || (params[:contact] && params[:contact][:uploads]))
if @contact.save
flash[:notice] = l(:notice_successful_update)
remove_old_avatars
respond_to do |format|
format.html { redirect_to :action => 'show', :project_id => params[:project_id], :id => @contact }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { render 'edit', :project_id => params[:project_id], :id => @contact }
format.api { render_validation_errors(@contact) }
end
end
end
def destroy
if @contact.destroy
flash[:notice] = l(:notice_successful_delete)
else
flash[:error] = l(:notice_unsuccessful_save)
end
respond_to do |format|
format.html { redirect_back_or_default :action => 'index', :project_id => params[:project_id] }
format.api { render_api_ok }
end
end
def new
@duplicates = []
@contact = Contact.new
@contact.safe_attributes = params[:contact] if params[:contact] && params[:contact].is_a?(Hash)
end
def create
@contact = Contact.new(:project => @project, :author => User.current)
@contact.safe_attributes = params[:contact]
@contact.save_attachments(params[:attachments] || (params[:contact] && params[:contact][:uploads]))
if @contact.save
flash[:notice] = l(:notice_successful_create)
remove_old_avatars
respond_to do |format|
format.html { redirect_to (params[:continue] ? { :action => 'new', :project_id => @project } : { :action => 'show', :project_id => @project, :id => @contact }) }
format.js
format.api { redirect_on_create(params) }
end
else
respond_to do |format|
format.api { render_validation_errors(@contact) }
format.js { render :action => 'new' }
format.html { render :action => 'new' }
end
end
end
def contacts_notes
unless request.xhr?
@tags = Contact.available_tags(:project => @project)
end
contacts = find_contacts(false)
deals = find_deals
joins = " "
joins << " LEFT OUTER JOIN #{Contact.table_name} ON #{Note.table_name}.source_id = #{Contact.table_name}.id AND #{Note.table_name}.source_type = 'Contact' "
joins << " LEFT OUTER JOIN #{Deal.table_name} ON #{Note.table_name}.source_id = #{Deal.table_name}.id AND #{Note.table_name}.source_type = 'Deal' "
cond = "(1 = 1) "
cond << "and (#{Contact.table_name}.id in (#{contacts.any? ? contacts.map(&:id).join(', ') : 'NULL'})"
cond << " or #{Deal.table_name}.id in (#{deals.any? ? deals.map(&:id).join(', ') : 'NULL'}))"
cond << " and (LOWER(#{Note.table_name}.content) LIKE '%#{params[:search_note].downcase}%')" if params[:search_note] and request.xhr?
cond << " and (#{Note.table_name}.author_id = #{params[:note_author_id]})" if !params[:note_author_id].blank?
cond << " and (#{Note.table_name}.type_id = #{params[:type_id]})" if !params[:type_id].blank?
scope = Note.joins(joins).where(cond).order("#{Note.table_name}.created_on DESC")
@notes_pages = Paginator.new(scope.count, 20, params['page'])
@notes = scope.limit(20).offset(@notes_pages.offset)
respond_to do |format|
format.html { render :partial => "notes/notes_list", :layout => false, :locals => { :notes => @notes, :notes_pages => @notes_pages } if request.xhr? }
format.xml { render :xml => @notes }
format.csv { send_data(notes_to_csv(@notes), :type => 'text/csv; header=present', :filename => 'notes.csv') }
format.atom { render_feed(@notes, :title => "#{l(:label_crm_note_plural)}") }
end
end
def context_menu
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
@contacts = Contact.visible.where(:id => params[:selected_contacts])
@contact = @contacts.first if (@contacts.size == 1)
@can = { :edit => (@contact && @contact.editable?) || (@contacts && @contacts.collect { |c| c.editable? }.inject { |memo, d| memo && d }),
:create_deal => (@project && User.current.allowed_to?(:add_deals, @project)),
:create => (@project && User.current.allowed_to?(:add_contacts, @project)),
:delete => @contacts.collect { |c| c.deletable? }.inject { |memo, d| memo && d },
:send_mails => @contacts.collect { |c| c.send_mail_allowed? && !c.primary_email.blank? }.inject { |memo, d| memo && d }
}
render :layout => false
end
def bulk_destroy
@contacts = Contact.deletable.where(:id => params[:ids])
raise ActiveRecord::RecordNotFound if @contacts.empty?
@contacts.each(&:destroy)
redirect_back_or_default({ :action => 'index', :project_id => params[:project_id] })
end
def bulk_edit
@contacts = Contact.editable.where(:id => params[:ids])
@projects = @contacts.collect { |p| p.projects.to_a.compact }.compact.flatten.uniq
raise ActiveRecord::RecordNotFound if @contacts.empty?
@custom_fields = ContactCustomField.order(:name)
@tag_list = RedmineCrm::TagList.from(@contacts.map(&:tag_list).inject { |memo, t| memo | t })
@project = @projects.first
@assignables = @projects.map(&:assignable_users).inject { |memo, a| memo & a }
@add_projects = Project.allowed_to(:edit_contacts).order(:lft)
end
def bulk_update
@contacts = Contact.editable.where(:id => params[:ids])
raise ActiveRecord::RecordNotFound if @contacts.empty?
unsaved_contact_ids = []
@contacts.each do |contact|
contact.reload
params[:contact][:tag_list] = (contact.tag_list + RedmineCrm::TagList.from(params[:add_tag_list]) - RedmineCrm::TagList.from(params[:delete_tag_list])).uniq
add_project_ids = (!params[:add_projects_list].to_s.blank? && params[:add_projects_list].is_a?(Array)) ? Project.allowed_to(:edit_contacts).where(:id => params[:add_projects_list].collect{|p| p.to_i}).map(&:id) : []
delete_project_ids = (!params[:delete_projects_list].to_s.blank? && params[:delete_projects_list].is_a?(Array)) ? Project.allowed_to(:edit_contacts).where(:id => params[:delete_projects_list].collect{|p| p.to_i}).map(&:id) : []
project_ids = contact.project_ids + add_project_ids - delete_project_ids
params[:contact][:project_ids] = project_ids.uniq if project_ids.any?
contact.tags.clear
contact.safe_attributes = parse_params_for_bulk_contact_attributes(params)
unless contact.save
# Keep unsaved issue ids to display them in flash error
unsaved_contact_ids << contact.id
end
if !params[:note][:content].blank?
note = ContactNote.new
note.safe_attributes = params[:note]
note.author = User.current
contact.notes << note
end
end
set_flash_from_bulk_contact_save(@contacts, unsaved_contact_ids)
redirect_back_or_default({ :controller => 'contacts', :action => 'index', :project_id => @project })
end
def edit_mails
@contacts = Contact.visible.where(:id => params[:ids]).reject { |c| c.email.blank? }
raise ActiveRecord::RecordNotFound if @contacts.empty?
if !@contacts.collect { |c| c.send_mail_allowed? }.inject { |memo, d| memo && d }
deny_access
return
end
end
def send_mails
contacts = Contact.visible.where(:id => params[:ids])
raise ActiveRecord::RecordNotFound if contacts.empty?
if !contacts.collect { |c| c.send_mail_allowed? }.inject { |memo, d| memo && d }
deny_access
return
end
raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
# Force ActionMailer to raise delivery errors so we can catch it
ActionMailer::Base.raise_delivery_errors = true
delivered_contacts = []
error_contacts = []
contacts.each do |contact|
begin
params[:message] = mail_macro(contact, params[:"message-content"])
ContactsMailer.bulk_mail(contact, params).deliver
delivered_contacts << contact
note = ContactNote.new
note.subject = params[:subject]
note.content = params[:message]
note.author = User.current
note.type_id = Note.note_types[:email]
contact.notes << note
Attachment.attach_files(note, params[:attachments])
render_attachment_warning_if_needed(note)
rescue Exception => e
error_contacts << [contact, e.message]
end
flash[:notice] = l(:notice_email_sent, delivered_contacts.map { |c| "#{c.name} <span class='icon icon-email'>#{c.emails.first}</span>" }.join(', ')).chomp[0, 500] if delivered_contacts.any?
flash[:error] = l(:notice_email_error, error_contacts.map { |e| "#{e[0].name}: #{e[1]}"}.join(', ')).chomp[0, 500] if error_contacts.any?
end
ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
redirect_back_or_default({:controller => 'contacts', :action => 'index', :project_id => @project})
end
def preview_email
@text = mail_macro(Contact.visible.where(:id => params[:ids][0]).first, params[:"message-content"])
render :partial => 'common/preview'
end
def load_tab
end
private
def find_contact_issues
scope = @contact.issues
scope = scope.open unless RedmineContacts.settings[:show_closed_issues]
@contact_issues_count = scope.visible.count
@contact_issues = scope.visible.order("#{Issue.table_name}.status_id, #{Issue.table_name}.updated_on DESC").limit(10)
end
def remove_old_avatars
params_hash = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params
avatar_params = params_hash[:attachments].find { |_k, v| v['description'] == 'avatar' }.try(:last) if params_hash[:attachments].present?
return unless avatar_params
avatar_id = avatar_params['token'].split('.').first.to_i
@contact.attachments.where(:description => 'avatar').where('id != ?', avatar_id).destroy_all if @contact.avatar
end
def last_notes(count = 5)
scope = ContactNote.where({})
scope = scope.where("#{Project.table_name}.id = ?", @project.id) if @project
scope = scope.includes(:attachments)
@last_notes = scope.visible.
limit(count).
order("#{ContactNote.table_name}.created_on DESC").uniq
end
def find_contact
@contact = Contact.find(params[:id])
unless @contact.visible?
deny_access
return
end
project_id = (params[:contact] && params[:contact][:project_id]) || params[:project_id]
@project = Project.find_by_identifier(project_id)
@project ||= @contact.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_deals
scope = Deal.where({})
scope = scope.where("#{Deal.table_name}.project_id = ?", @project.id) if @project
scope = scope.where("#{Deal.table_name}.name LIKE ? ", '%' + params[:search] + '%') if params[:search]
scope = scope.where('1=0') if params[:tag]
@deals = scope.visible
end
def parse_params_for_bulk_contact_attributes(params)
attributes = (params[:contact] || {}).reject { |_k, v| v.blank? }
attributes.each { |k, v| attributes[k] = v.reject { |_key, val| val.blank? } if v.is_a?(Hash) }
attributes.keys.each { |k| attributes[k] = '' if attributes[k] == 'none' }
if custom = attributes[:custom_field_values]
custom.reject! { |_k, v| v.blank? }
custom.keys.each do |k|
if custom[k].is_a?(Array)
custom[k] << '' if custom[k].delete('__none__')
else
custom[k] = '' if custom[k] == '__none__'
end
end
attributes[:custom_field_values] = custom
end
attributes
end
def find_contacts(pages = true)
@tag = RedmineCrm::TagList.from(params[:tag]) unless params[:tag].blank?
scope = Contact.where({})
scope = scope.where("#{Contact.table_name}.job_title = ?", params[:job_title]) unless params[:job_title].blank?
scope = scope.where("#{Contact.table_name}.assigned_to_id = ?", params[:assigned_to_id]) unless params[:assigned_to_id].blank?
scope = scope.where("#{Contact.table_name}.is_company = ?", params[:query]) unless (params[:query].blank? || params[:query] == '2' || params[:query] == '3')
scope = scope.where("#{Contact.table_name}.author_id = ?", User.current) if params[:query] == '3'
case params[:query]
when '2' then scope = scope.order_by_creation
when '3' then scope = scope.order_by_creation
else scope = scope.order_by_name
end
scope = scope.by_project(@project)
params[:search].split(' ').collect{ |search_string| scope = scope.live_search(search_string) } if !params[:search].blank?
scope = scope.visible
scope = scope.tagged_with(params[:tag]) if !params[:tag].blank?
scope = scope.tagged_with(params[:notag], :exclude => true) if !params[:notag].blank?
@contacts_count = scope.count
@contacts = scope
if pages
page_size = params[:page_size].blank? ? 20 : params[:page_size].to_i
@contacts_pages = Paginator.new(self, @contacts_count, page_size, params[:page])
@offset = @contacts_pages.offset
@limit = @contacts_pages.items_per_page
@contacts = @contacts.eager_load([:tags, :avatar]).limit(@limit).offset(@offset)
fake_name = @contacts.first.name if @contacts.length > 0
end
@contacts
end
# Filter for bulk issue operations
def bulk_find_contacts
@contacts = Deal.find_all_by_id(params[:id] || params[:ids], :include => :project)
raise ActiveRecord::RecordNotFound if @contact.empty?
if @contacts.detect { |contact| !contact.visible? }
deny_access
return
end
@projects = @contacts.collect(&:projects).compact.uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project(project_id = nil)
project_id ||= (params[:contact] && params[:contact][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
def authorize_contacts(action = params[:action], _global = false)
case action.to_s
when 'edit', 'update'
@contact.editable? ? true : deny_access
when 'destroy'
@contact.deletable? ? true : deny_access
else
deny_access
end
end
def redirect_on_create(options)
if options[:redirect_on_success].to_s.match('^(http|https):\/\/')
redirect_to options[:redirect_on_success].to_s
else
render :action => 'show', :status => :created, :location => contact_url(@contact)
end
end
end
@@ -0,0 +1,102 @@
# 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 ContactsDuplicatesController < ApplicationController
unloadable
helper :contacts
before_action :find_project_by_project_id, :authorize, :except => :search
before_action :find_contact, :except => :duplicates
before_action :find_duplicate, :only => :merge
helper :contacts
def index
@contacts = @contact.duplicates
end
def duplicates
search_first_name = params[:contact][:first_name] if params[:contact] && !params[:contact][:first_name].blank?
search_last_name = params[:contact][:last_name] if params[:contact] && !params[:contact][:last_name].blank?
search_middle_name = params[:contact][:middle_name] if params[:contact] && !params[:contact][:middle_name].blank?
@contact = (Contact.find(params[:contact_id]) if !params[:contact_id].blank?) || Contact.new
@contact.first_name = search_first_name || ''
@contact.last_name = search_last_name || ''
@contact.middle_name = search_middle_name || ''
respond_to do |format|
format.html { render :partial => 'duplicates', :layout => false if request.xhr? }
end
end
def merge
@duplicate.notes << @contact.notes
@duplicate.deals << @contact.deals
@duplicate.related_deals << @contact.related_deals
@duplicate.issues << @contact.issues
@duplicate.projects << @contact.projects
@duplicate.email = (@duplicate.emails | @contact.emails).join(', ')
@duplicate.phone = (@duplicate.phones | @contact.phones).join(', ')
call_hook(:controller_contacts_duplicates_merge, { :params => params, :duplicate => @duplicate, :contact => @contact })
@duplicate.tag_list = @duplicate.tag_list | @contact.tag_list
begin
Contact.transaction do
@duplicate.save!
@duplicate.reload
@contact.reload
@contact.destroy
flash[:notice] = l(:notice_successful_merged)
redirect_to :controller => 'contacts', :action => 'show', :project_id => @project, :id => @duplicate
end
rescue
redirect_to :action => 'duplicates', :contact_id => @contact, :project_id => @project
end
end
def search
@contacts = []
q = (params[:q] || params[:term]).to_s.strip
if q.present?
scope = Contact.where({})
scope = scope.limit(params[:limit] || 10)
scope = scope.companies if params[:is_company]
scope = scope.where(["#{Contact.table_name}.id <> ?", params[:contact_id].to_i]) if params[:contact_id]
@contacts = scope.visible.by_project(@project).live_search(q).to_a.sort!{|x, y| x.name <=> y.name }
else
@contacts = @contact.duplicates
end
render :layout => false, :partial => 'list'
end
private
def find_duplicate
@duplicate = Contact.find(params[:duplicate_id])
render_403 unless @duplicate.editable?
rescue ActiveRecord::RecordNotFound
render_404
end
def find_contact
@contact = Contact.find(params[:contact_id])
rescue ActiveRecord::RecordNotFound
render_404 if !request.xhr?
end
end
@@ -0,0 +1,109 @@
# 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 ContactsIssuesController < ApplicationController
unloadable
before_action :find_contact, :only => [:create_issue, :delete]
before_action :find_issue, :except => [:create_issue]
before_action :find_project_by_project_id, :only => [:create_issue]
before_action :authorize_global, :only => [:close]
before_action :authorize
helper :contacts
def create_issue
deny_access unless User.current.allowed_to?(:manage_contact_issue_relations, @project) || User.current.allowed_to?(:add_issues, @project)
issue = Issue.new
issue.project = @project
issue.author = User.current
issue.status = IssueStatus.default if ActiveRecord::VERSION::MAJOR < 4
issue.start_date ||= Date.today
issue.contacts << @contact
issue.safe_attributes = params[:issue] if params[:issue]
if issue.save
flash[:notice] = l(:notice_successful_add)
else
flash[:error] = issue.errors.full_messages.join('<br>').html_safe
end
redirect_to :back
end
def create
contact_ids = []
if params[:contacts_issue].present?
contact_ids << (params[:contacts_issue][:contact_ids] || params[:contacts_issue][:contact_id])
else
contact_ids << params[:contact_id]
end
contact_ids.flatten.compact.uniq.each do |contact_id|
ContactsIssue.create(:issue_id => @issue.id, :contact_id => contact_id)
end
respond_to do |format|
format.html { redirect_to_referer_or { render :text => 'Added.', :layout => true } }
format.js
end
end
def new
end
def delete
@issue.contacts.delete(@contact)
respond_to do |format|
format.html { redirect_to :back }
format.js
end
end
def close
@issue.init_journal(User.current)
@issue.status = IssueStatus.where(:is_closed => true).first
@issue.save
respond_to do |format|
format.js
format.html { redirect_to :back }
end
end
def autocomplete_for_contact
q = params[:q].to_s
scope = Contact.where({})
q.split(' ').collect { |search_string| scope = scope.live_search(search_string) } unless q.blank?
@contacts = scope.visible.includes(:avatar).order(Contact.fields_for_order_statement).by_project(params[:cross_project_contacts] == '1' ? nil : @project).limit(100)
@contacts -= @issue.contacts if @issue
render :layout => false
end
private
def find_contact
@contact = Contact.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_issue
@issue = Issue.find(params[:issue_id])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,38 @@
# 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 ContactsMailerController < ActionController::Base
before_action :check_credential
helper :contacts
# Submits an incoming email to ContactsMailer
def index
options = params.dup
email = options.delete(:email)
head ContactsMailer.receive(email, options) ? :created : :unprocessable_entity
end
private
def check_credential
User.current = nil
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
end
end
end
@@ -0,0 +1,87 @@
# 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 ContactsProjectsController < ApplicationController
unloadable
before_action :find_optional_project, :find_contact
before_action :find_related_project, :only => [:destroy, :create]
before_action :check_count, :only => :destroy
accept_api_auth :create, :destroy
helper :contacts
def new
@show_form = "true"
respond_to do |format|
format.html { redirect_to :back }
format.js
end
rescue ::ActionController::RedirectBackError
render :text => 'Project added.', :layout => true
end
def create
@contact.projects << @related_project
if @contact.save
respond_to do |format|
format.html { redirect_to :back }
format.js { render :action => "new" }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { redirect_to :back }
format.js { render :action => "new" }
format.api { render_validation_errors(@contact) }
end
end
end
def destroy
@contact.projects.delete(@related_project)
respond_to do |format|
format.html { redirect_to :back }
format.js {render :action => "new"}
format.api { render_api_ok }
end
end
private
def find_related_project
@related_project = Project.find((params[:project] && params[:project][:id]) || params[:id])
raise Unauthorized unless User.current.allowed_to?(:edit_contacts, @related_project)
rescue ActiveRecord::RecordNotFound
render_404
end
def check_count
deny_access if @contact.projects.size <= 1
end
def find_contact
@contact = Contact.find(params[:contact_id])
raise Unauthorized unless @contact.editable?
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,34 @@
# 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 ContactsSettingsController < ApplicationController
unloadable
before_action :find_project_by_project_id, :authorize
def save
settings = params[:contacts_settings]
settings = settings.to_unsafe_hash if settings.class.to_s == 'ActionController::Parameters'
if settings && settings.is_a?(Hash)
settings.map do |k, v|
ContactsSetting[k, @project.id] = v
end
end
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => params[:tab]
end
end
@@ -0,0 +1,98 @@
# 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 ContactsTagsController < ApplicationController
unloadable
before_action :require_admin, :except => [:index]
before_action :find_tag, :only => [:edit, :update]
before_action :bulk_find_tags, :only => [:context_menu, :merge, :destroy]
accept_api_auth :index
def index
@tags = Contact.all_tag_counts(:order => :name)
respond_to do |format|
format.api
end
end
def edit
end
def destroy
@tags.each do |tag|
begin
tag.reload.destroy
Contact.where("#{Contact.table_name}.cached_tag_list LIKE ?", '%' + tag.name + '%').includes(:tags).each{|c| c.tag_list = c.all_tags_list; c.save}
rescue ::ActiveRecord::RecordNotFound # raised by #reload if tag no longer exists
# nothing to do, tag was already deleted (eg. by a parent)
end
end
redirect_back_or_default(:controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags")
end
def update
old_name = @tag.name
@tag.name = params[:tag][:name]
if @tag.save
Contact.where("#{Contact.table_name}.cached_tag_list LIKE ?", '%' + old_name + '%').includes(:tags).each{|c| c.tag_list = c.all_tags_list; c.save}
flash[:notice] = l(:notice_successful_update)
respond_to do |format|
format.html { redirect_to :controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags" }
end
else
respond_to do |format|
format.html { render :action => "edit"}
end
end
end
def context_menu
@tag = @tags.first if (@tags.size == 1)
@back = back_url
render :layout => false
end
def merge
if request.post? && params[:tag] && params[:tag][:name]
RedmineCrm::Tagging.transaction do
tag = RedmineCrm::Tag.where(:name => params[:tag][:name]).first || RedmineCrm::Tag.create(params[:tag])
RedmineCrm::Tagging.where(:tag_id => @tags.map(&:id)).update_all(:tag_id => tag.id)
@tags.select{|t| t.id != tag.id}.each do |t|
t.destroy
Contact.where("#{Contact.table_name}.cached_tag_list LIKE ?", '%' + t.name + '%').includes(:tags).each{|c| c.tag_list = c.all_tags_list; c.save}
end
redirect_to :controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags"
end
end
end
private
def bulk_find_tags
@tags = RedmineCrm::Tag.where(:id => params[:id] ? [params[:id]] : params[:ids])
raise ActiveRecord::RecordNotFound if @tags.empty?
end
def find_tag
@tag = RedmineCrm::Tag.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,97 @@
# 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 ContactsVcfController < ApplicationController
unloadable
before_action :find_project_by_project_id, :authorize
def load
begin
vcard = Vcard::Vcard.decode(params[:contact_vcf]).first
contact = {}
fill_name(vcard, contact)
contact[:phone] = vcard.telephones.join(', ')
contact[:email] = vcard.emails.join(', ')
contact[:website] = vcard.url.uri if vcard.url
contact[:birthday] = vcard.birthday
fill_background(vcard, contact)
fill_title(vcard, contact)
fill_address(vcard, contact) if vcard['ADR']
fill_company(vcard, contact) if vcard.org
respond_to do |format|
format.html { redirect_to :controller => 'contacts', :action => 'new', :project_id => @project, :contact => contact }
end
rescue Exception => e
flash[:error] = e.message
respond_to do |format|
format.html { redirect_to :back }
end
end
end
private
def fill_name(vcard, contact)
vcard_charset = get_field_encoding(vcard, 'N')
contact[:first_name] = encode(vcard_charset, vcard.name.given)
contact[:middle_name] = encode(vcard_charset, vcard.name.additional)
contact[:last_name] = encode(vcard_charset, vcard.name.family)
end
def fill_address(vcard, contact)
vcard_charset = get_field_encoding(vcard, 'ADR')
contact[:address_attributes] = {}
contact[:address_attributes][:street1] = encode(vcard_charset, vcard.address.street)
contact[:address_attributes][:city] = encode(vcard_charset, vcard.address.locality)
contact[:address_attributes][:postcode] = encode(vcard_charset, vcard.address.postalcode)
contact[:address_attributes][:region] = encode(vcard_charset, vcard.address.region)
end
def fill_background(vcard, contact)
vcard_charset = get_field_encoding(vcard, 'NOTE')
contact[:background] = encode(vcard_charset, vcard.note)
end
def fill_company(vcard, contact)
vcard_charset = get_field_encoding(vcard, 'ORG')
contact[:company] = encode(vcard_charset, vcard.org.first)
end
def fill_title(vcard, contact)
vcard_charset = get_field_encoding(vcard, 'TITLE')
contact[:job_title] = encode(vcard_charset, vcard.title)
end
def get_field_encoding(vcard, field_name)
vcard.fields.find { |field| field.name == field_name }.try(:pvalue, 'CHARSET')
end
def encode(vcard_charset, field)
return field if vcard_charset.nil?
if RUBY_VERSION < '1.9'
Iconv.conv('UTF-8', vcard_charset, field)
else
field.force_encoding(vcard_charset).encode('UTF-8')
end
end
end
@@ -0,0 +1,133 @@
# 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 CrmQueriesController < ApplicationController
before_action :find_query_class
before_action :find_query, :except => [:new, :create, :index]
before_action :find_optional_project, :only => [:new, :create]
before_action :set_menu_item
accept_api_auth :index
helper :queries
include QueriesHelper
def index
case params[:format]
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end
@query_count = @query_class.visible.count
@query_pages = Paginator.new @query_count, @limit, params['page']
@queries = @query_class.visible.
order("#{Query.table_name}.name").
limit(@limit).
offset(@offset).
all
respond_to do |format|
format.api
end
end
def new
@query = @query_class.new
@query.user = User.current
@query.project = @project
@query.visibility = @query_class::VISIBILITY_PRIVATE unless User.current.allowed_to?("manage_public_#{@object_type}s_queries".to_sym, @project) || User.current.admin?
@query.build_from_params(params)
end
def create
@query = @query_class.new(params_hash[:query])
@query.user = User.current
@query.project = params_hash[:query_is_for_all] ? nil : @project
@query.visibility = @query_class::VISIBILITY_PRIVATE unless User.current.allowed_to?("manage_public_#{@object_type}s_queries".to_sym, @project) || User.current.admin?
@query.build_from_params(params_hash)
@query.column_names = nil if params_hash[:default_columns]
if @query.save
flash[:notice] = l(:notice_successful_create)
redirect_to_list(:query_id => @query)
else
render :action => 'new', :layout => !request.xhr?
end
end
def edit
end
def update
@query.attributes = params_hash[:query]
@query.project = nil if params_hash[:query_is_for_all]
@query.visibility = @query_class::VISIBILITY_PRIVATE unless User.current.allowed_to?("manage_public_#{@object_type}s_queries".to_sym, @project) || User.current.admin?
@query.build_from_params(params_hash)
@query.column_names = nil if params_hash[:default_columns]
if @query.save
flash[:notice] = l(:notice_successful_update)
redirect_to_list(:query_id => @query)
else
render :action => 'edit'
end
end
def destroy
@query.destroy
redirect_to_list(:set_filter => 1)
end
private
def find_query_class
raise NameError if params[:object_type].blank?
@query_class = Object.const_get("#{params[:object_type].to_s.camelcase}Query")
@object_type = params[:object_type]
return false unless @query_class.is_a?(Query)
rescue NameError
render_404
end
def find_query
@query = @query_class.find(params[:id])
@project = @query.project
render_403 unless @query.editable_by?(User.current)
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
@project = Project.find(params[:project_id]) if params[:project_id]
render_403 unless User.current.allowed_to?("save_#{@object_type}s_queries".to_sym, @project, :global => true)
rescue ActiveRecord::RecordNotFound
render_404
end
def redirect_to_list(options)
redirect_to url_for({:controller => "#{@object_type}s", :action => "index", :project_id => @project}.merge(options))
end
def set_menu_item
menu_items[:project_tabs][:actions][action_name.to_sym] = "#{@object_type}s"
end
def params_hash
@params_hash ||= params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash.symbolize_keys : params
end
end
@@ -0,0 +1,109 @@
# 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 DealCategoriesController < ApplicationController
unloadable
menu_item :settings
model_object DealCategory
before_action :find_model_object, :except => [:new, :index, :create]
before_action :find_project_from_association, :except => [:new, :index, :create]
before_action :find_project_by_project_id, :only => [:new, :index, :create]
before_action :authorize
accept_api_auth :index, :update, :create, :destroy
def index
@categories = @project.deal_categories
respond_to do |format|
format.api
end
end
def create
@category = @project.deal_categories.build
@category.safe_attributes = params[:category]
if @category.save
flash[:notice] = l(:notice_successful_create)
respond_to do |format|
format.html { redirect_to_settings_in_projects }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@category) }
end
end
end
def new
@category = @project.deal_categories.build(params[:category])
end
def edit
end
def update
@category.safe_attributes = params[:category]
if @category.save
# @deal.contacts = [Contact.find(params[:contacts])] if params[:contacts]
flash[:notice] = l(:notice_successful_update)
respond_to do |format|
format.html { redirect_to_settings_in_projects }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@category) }
end
end
end
def destroy
@deal_count = @category.deals.size
if @deal_count == 0 || params[:todo] || api_request?
reassign_to = nil
if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?)
reassign_to = @project.deal_categories.find_by_id(params[:reassign_to_id])
end
@category.destroy(reassign_to)
respond_to do |format|
format.html { redirect_to_settings_in_projects }
format.api { render_api_ok }
end
return
end
@categories = @project.deal_categories - [@category]
end
private
def redirect_to_settings_in_projects
redirect_to settings_project_path(@project, :tab => 'deals')
end
# Wrap ApplicationController's find_model_object method to set
# @category instead of just @deal_category
def find_model_object
super
@category = @object
@project = @category.project
end
end
@@ -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 DealContactsController < ApplicationController
unloadable
before_action :find_project_by_project_id, :authorize
before_action :find_contact, :only => :delete
before_action :find_deal
helper :deals
helper :contacts
def search
@contacts = contacts.limit(10) - @deal.all_contacts
end
def autocomplete
@contacts = contacts.live_search(params[:q]).limit(100) - @deal.all_contacts
render :layout => false
end
def add
if params[:contact_id] && request.post?
find_contact
unless @deal.all_contacts.include?(@contact)
@deal.related_contacts << @contact
@deal.save
end
end
respond_to do |format|
format.html do
redirect_to :back
end
format.js
end
end
def delete
@deal.related_contacts.delete(@contact)
respond_to do |format|
format.html { redirect_to :back }
format.js
end
end
private
def contacts
Contact.visible.by_project(ContactsSetting.cross_project_contacts? ? nil : @project)
end
def find_contact
@contact = Contact.find(params[:contact_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_deal
@deal = Deal.find(params[:deal_id])
rescue ActiveRecord::RecordNotFound
render_404
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 DealImportsController < ImporterBaseController
menu_item :deals
helper :deals
def klass
DealImport
end
def importer_klass
DealKernelImport
end
def instance_index
project_deals_path(:project_id => @project.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 DealStatusesController < ApplicationController
unloadable
layout 'admin'
before_action :require_admin, :except => :assing_to_project
before_action :find_project_by_project_id, :authorize, :only => :assing_to_project
accept_api_auth :index
def index
@deal_statuses = DealStatus.order(:position)
respond_to do |format|
format.api
format.html { render :action => 'index', :layout => false if request.xhr? }
end
end
def new
@deal_status = DealStatus.new
end
def create
@deal_status = DealStatus.new
@deal_status.safe_attributes = params[:deal_status]
if @deal_status.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses'
else
render :action => 'new'
end
end
def edit
@deal_status = DealStatus.find(params[:id])
end
def update
@deal_status = DealStatus.find(params[:id])
@deal_status.safe_attributes = params[:deal_status]
@deal_status.insert_at(@deal_status.position) if @deal_status.position_changed?
if @deal_status.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses'
end
format.js { head 200 }
end
else
respond_to do |format|
format.html do
render :action => 'edit'
end
format.js { head 422 }
end
end
end
def destroy
DealStatus.find(params[:id]).destroy
redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses'
rescue
flash[:error] = l(:error_unable_delete_deal_status)
redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses'
end
def assing_to_project
if request.put?
@project.deal_statuses = !params[:deal_statuses].blank? ? DealStatus.find(params[:deal_statuses]) : []
@project.save
flash[:notice] = l(:notice_successful_update)
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'deals', :id => @project
end
end
@@ -0,0 +1,333 @@
# 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 DealsController < ApplicationController
unloadable
PRICE_TYPE_PULLDOWN = [l(:label_price_fixed_bid), l(:label_price_per_hour)]
before_action :find_deal, :only => [:show, :edit, :update, :destroy]
before_action :find_project, :only => [:new, :create, :update_form]
before_action :bulk_find_deals, :only => [:bulk_update, :bulk_edit, :bulk_destroy, :context_menu]
before_action :authorize, :except => [:index]
before_action :find_optional_project, :only => [:index]
before_action :update_deal_from_params, :only => [:edit, :update]
before_action :build_new_deal_from_params, :only => [:new, :update_form]
before_action :find_deal_attachments, :only => :show
skip_before_filter :authorize, :only => :add_product_line if RedmineContacts.products_plugin_installed?
accept_api_auth :index, :show, :create, :update, :destroy
helper :attachments
helper :timelog
helper :watchers
helper :custom_fields
helper :context_menus
helper :sort
helper :crm_queries
helper :notes
helper :queries
helper :calendars
include QueriesHelper
include CrmQueriesHelper
include WatchersHelper
include DealsHelper
include SortHelper
if RedmineContacts.products_plugin_installed?
include ProductsHelper
helper :products
end
def index
retrieve_crm_query('deal')
sort_init(@query.sort_criteria.empty? ? [['created_on', 'desc']] : @query.sort_criteria)
sort_update(@query.sortable_columns)
@query.sort_criteria = sort_criteria.to_a
if @query.valid?
case params[:format]
when 'csv', 'pdf'
@limit = Setting.issues_export_limit.to_i
when 'atom'
@limit = Setting.feeds_limit.to_i
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end
@deals_count = @query.object_count
@deals_scope = @query.objects_scope
@deal_amount = @query.deal_amount
@deal_weighted_amount = @query.weighted_amount
@deals_pages = Paginator.new @deals_count, @limit, params['page']
@offset ||= @deals_pages.offset
@deal_count_by_group = @query.object_count_by_group
@deals = @query.results_scope(
:include => [{ :contact => [:avatar, :projects, :address] }, :author],
:search => params[:search],
:order => sort_clause,
:limit => @limit,
:offset => @offset
)
if deals_list_style == 'crm_calendars/crm_calendar'
retrieve_crm_calendar(:start_date_field => 'due_date')
@calendar.events = @query.results_scope(
:include => [:contact],
:search => params[:search],
:conditions => ['due_date BETWEEN ? AND ?', @calendar.startdt, @calendar.enddt]
)
end
respond_to do |format|
format.html { request.xhr? ? render(:partial => deals_list_style, :layout => false) : last_notes }
format.api
format.atom { render_feed(@deals, :title => "#{@project || Setting.app_title}: #{l(:label_order_plural)}") }
format.csv { send_data(deals_to_csv(@deals), :type => 'text/csv; header=present', :filename => 'deals.csv') }
format.pdf { send_data(deals_to_pdf(@deals, @project, @query), :type => 'application/pdf', :filename => 'deals.pdf') }
end
else
respond_to do |format|
format.html { render(:template => 'deals/index', :layout => !request.xhr?) }
format.any(:atom, :csv, :pdf) { render(:nothing => true) }
format.api { render_validation_errors(@query) }
end
end
rescue ActiveRecord::RecordNotFound
render_404
end
def show
@note = DealNote.new(:created_on => Time.now)
respond_to do |format|
format.html do
@deal_issues = @deal.issues.visible
@deal.viewed
@deal_events = (@deal.deal_processes.where("#{DealProcess.table_name}.old_value IS NOT NULL").includes([:to, :from, :author]) | @deal.notes.includes([:attachments, :author])).map{|o| {:date => o.is_a?(DealProcess) ? o.created_at : o.created_on, :author => o.author, :object => o} }
@deal_events.sort! { |x, y| y[:date] <=> x[:date] }
end
format.api
end
end
def new
end
def create
@deal = Deal.new
@deal.safe_attributes = params[:deal]
@deal.project = @project
@deal.author ||= User.current
@deal.price = parsed_price(params[:deal][:price])
@deal.init_deal_process(User.current)
if @deal.save
flash[:notice] = l(:notice_successful_create)
respond_to do |format|
format.html { redirect_to(params[:continue] ? { :action => 'new' } : { :action => 'show', :id => @deal }) }
format.api { render :action => 'show', :status => :created, :location => deal_url(@deal) }
end
else
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@deal) }
end
end
end
def update
@deal.init_deal_process(User.current)
@deal.safe_attributes = params[:deal]
if @deal.save
# @deal.contacts = [Contact.find(params[:contacts])] if params[:contacts]
retrieve_crm_query('deal')
@deals_scope = @query.objects_scope
flash[:notice] = l(:notice_successful_update)
respond_to do |format|
format.html { redirect_back_or_default(:action => 'show', :id => @deal) }
format.api { render_api_ok }
format.js { render :update_total }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@deal) }
format.js { render "alert('Error!')" }
end
end
end
def edit
respond_to do |format|
format.html {}
format.xml {}
end
end
def destroy
if @deal.destroy
flash[:notice] = l(:notice_successful_delete)
respond_to do |format|
format.html { redirect_to :action => 'index', :project_id => params[:project_id] }
format.api { render_api_ok }
end
else
flash[:error] = l(:notice_unsuccessful_save)
end
end
def context_menu
@deal = @deals.first if @deals.size == 1
@can = { :edit => User.current.allowed_to?(:edit_deals, @projects),
:delete => User.current.allowed_to?(:delete_deals, @projects) }
@back = back_url
render :layout => false
end
def bulk_destroy
@deals.each do |deal|
begin
deal.reload.destroy
rescue ::ActiveRecord::RecordNotFound # raised by #reload if deal no longer exists
# nothing to do, deal was already deleted (eg. by a parent)
end
end
respond_to do |format|
format.html { redirect_back_or_default(:action => 'index', :project_id => params[:project_id]) }
format.api { head :ok }
end
end
def bulk_edit
@available_statuses = @projects.map(&:deal_statuses).inject { |memo, w| memo & w }
@custom_fields = DealCustomField.order(:name)
@available_categories = @projects.map(&:deal_categories).inject { |memo, w| memo & w }
@assignables = @projects.map(&:assignable_users).inject { |memo, a| memo & a }
end
def bulk_update
unsaved_deal_ids = []
@deals.each do |deal|
deal.reload
deal.init_deal_process(User.current)
deal.safe_attributes = parse_params_for_bulk_deal_attributes(params)
unless deal.save
# Keep unsaved deal ids to display them in flash error
unsaved_deal_ids << deal.id
end
if params[:note] && !params[:note][:content].blank?
note = DealNote.new
note.safe_attributes = params[:note]
note.author = User.current
deal.notes << note
end
end
set_flash_from_bulk_contact_save(@deals, unsaved_deal_ids)
redirect_back_or_default(:controller => 'deals', :action => 'index', :project_id => @project)
end
private
def last_notes(count = 5)
# TODO: Исправить говнокод этот и выделить все в плагин acts-as-noteble
scope = DealNote.where({})
scope = scope.where("#{Deal.table_name}.project_id = ?", @project.id) if @project
@last_notes = scope.visible.order("#{DealNote.table_name}.created_on DESC").limit(count)
end
def build_new_deal_from_params
if params[:id].blank?
@deal = Deal.new
@deal.assigned_to_id = User.current.id
@deal.name = params[:name] if params[:name]
@deal.contact = Contact.find(params[:contact_id]) if params[:contact_id]
if params[:copy_from]
begin
@copy_from = Deal.visible.find(params[:copy_from])
@deal.copy_from(@copy_from)
rescue ActiveRecord::RecordNotFound
render_404
return
end
end
else
@deal = Deal.visible.find(params[:id])
end
@deal.project = @project
@deal.author ||= User.current
@deal.safe_attributes = params[:deal]
@available_watchers = (@deal.project.users.sort + @deal.watcher_users).uniq
end
def update_deal_from_params
end
def update_form
end
def find_deal_attachments
@deal_attachments = Attachment.where(:container_type => 'Note', :container_id => @deal.notes.map(&:id)).order(:created_on)
end
def bulk_find_deals
@deals = Deal.where(:id => (params[:id] || params[:ids])).includes([:project, :contact])
raise ActiveRecord::RecordNotFound if @deals.empty?
if @deals.detect { |deal| !deal.visible? }
deny_access
return
end
@projects = @deals.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
def find_deal
@deal = Deal.where(:id => params[:id]).includes([:project, :status, :category]).first
@project = @deal.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project(project_id = nil)
project_id ||= (params[:deal] && params[:deal][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
def parse_params_for_bulk_deal_attributes(params)
attributes = (params[:deal] || {}).reject { |_k, v| v.blank? }
attributes.keys.each { |k| attributes[k] = '' if attributes[k] == 'none' }
attributes[:custom_field_values].reject! { |_k, v| v.blank? } if attributes[:custom_field_values]
attributes
end
def parsed_price(price)
return unless price
price.gsub!(ContactsSetting.thousands_delimiter, '')
price.gsub!(ContactsSetting.decimal_separator, '.')
price.to_f
end
end
@@ -0,0 +1,74 @@
# 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 DealsTasksController < ApplicationController
unloadable
before_action :find_project_by_project_id, :authorize
before_action :find_deal, :except => [:close]
before_action :find_issue, :except => [:new]
def new
issue = Issue.new
issue.subject = params[:task_subject]
issue.project = @project
issue.tracker_id = params[:task_tracker]
issue.author = User.current
issue.due_date = params[:due_date]
issue.assigned_to_id = params[:assigned_to]
issue.description = params[:task_description]
issue.status = IssueStatus.default
if issue.save
flash[:notice] = l(:notice_successful_add)
@deal.issues << issue
@deal.save
redirect_to :back
return
else
redirect_to :back
end
end
def close
@issue.status = IssueStatus.find(:first, :conditions => { :is_closed => true })
@issue.save
respond_to do |format|
format.js do
render :update do |page|
page["issue_#{params[:issue_id]}"].visual_effect :fade
end
end
format.html {redirect_to :back }
end
end
private
def find_deal
@deal = Deal.find(params[:deal_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_issue
@issue = Issue.find(params[:issue_id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,149 @@
# 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 ImporterBaseController < ApplicationController
unloadable
if Redmine::VERSION.to_s >= '3.2'
helper :imports
before_action :find_import, :only => [:show, :settings, :mapping, :run]
end
before_action :find_project_by_project_id, :authorize
def new
@importer = klass.new
if Redmine::VERSION.to_s >= '3.2'
render 'importers/kernel_new'
else
render 'importers/new'
end
end
def create
if Redmine::VERSION.to_s >= '3.2'
@import = importer_klass.new
@import.user = User.current
@import.project = @project
@import.file = params[:file]
@import.set_default_settings
if @import.save
redirect_to :controller => klass.name.tableize, :action => 'settings', :id => @import, :project_id => @project
else
render 'importers/kernel_new'
end
else
@importer = klass.new(params[klass.to_s.underscore.to_sym])
@importer.project = @project
if @importer.file && @importer.save
redirect_to instance_index
else
render 'importers/new'
end
end
end
def show
render 'importers/show'
end
def settings
if request.post? && @import.parse_file
return redirect_to :controller => klass.name.tableize, :action => 'mapping', :id => @import, :project_id => @project
end
render 'importers/settings'
rescue CSV::MalformedCSVError => e
flash.now[:error] = l(:error_invalid_csv_file_or_settings)
render 'importers/settings'
rescue ArgumentError, Encoding::InvalidByteSequenceError => e
flash.now[:error] = l(:error_invalid_file_encoding, :encoding => ERB::Util.h(@import.settings['encoding']))
render 'importers/settings'
rescue SystemCallError => e
flash.now[:error] = l(:error_can_not_read_import_file)
render 'importers/settings'
end
def mapping
mapping_object = klass.new.klass.new
@attributes = mapping_object.safe_attribute_names
@custom_fields = mapping_object.custom_field_values.map(&:custom_field)
if request.post?
respond_to do |format|
format.html do
if params[:previous]
redirect_to :controller => klass.name.tableize, :action => 'settings', :id => @import, :project_id => @project
else
redirect_to :controller => klass.name.tableize, :action => 'run', :id => @import, :project_id => @project
end
end
end
else
render 'importers/mapping'
end
end
def run
if request.post?
@current = @import.run(
:max_items => max_items_per_request,
:max_time => 10.seconds
)
respond_to do |format|
format.html do
if @import.finished?
redirect_to :controller => klass.name.tableize, :action => 'show', :id => @import, :project_id => @project
else
redirect_to :controller => klass.name.tableize, :action => 'run', :id => @import, :project_id => @project
end
end
format.js { render 'importers/run' }
end
else
render 'importers/run'
end
end
private
def find_import
@import = Import.where(:user_id => User.current.id, :filename => params[:id]).first
if @import.nil?
render_404
return
elsif @import.finished? && action_name != 'show'
redirect_to new_project_contact_import_path(@import)
return
end
update_from_params if request.post?
end
def update_from_params
if params[:import_settings].present?
@import.settings ||= {}
@import.settings.merge!(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash['import_settings'] : params['import_settings'])
@import.save!
end
end
def max_items_per_request
5
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 NotesController < ApplicationController
unloadable
default_search_scope :notes
# before_filter :find_model_object
before_action :find_note, :only => [:show, :edit, :update, :destroy]
before_action :find_project, :only => :create
before_action :find_note_source, :only => :create
before_action :find_optional_project, :only => :show
accept_api_auth :show, :create, :update, :destroy
helper :attachments
helper :custom_fields
def show
(render_403; return false) unless @note.visible?
respond_to do |format|
format.html
format.api
end
end
def new
find_note_source
@note = Note.new
@note.source = @note_source
end
def edit
(render_403; return false) unless @note.editable_by?(User.current, @project)
end
def update
@note.safe_attributes = params[:note]
if @note.save
@note.note_time = params[:note][:note_time] if params[:note] && params[:note][:note_time]
attachments = Attachment.attach_files(@note, (params[:attachments] || (params[:note] && params[:note][:uploads])))
render_attachment_warning_if_needed(@note)
flash[:notice] = l(:notice_successful_update)
respond_to do |format|
format.html { redirect_back_or_default({ :action => 'show', :project_id => @note.source.project, :id => @note }) }
format.api { render_api_ok }
end
else
respond_to do |format|
format.html { render :action => 'edit', :project_id => params[:project_id], :id => @note }
format.api { render_validation_errors(@note) }
end
end
end
def create
@note = Note.new
@note.safe_attributes = params[:note]
@note.source = @note_source
@note.note_time = params[:note][:note_time] if params[:note] && params[:note][:note_time]
@note.author = User.current
if @note.save
attachments = Attachment.attach_files(@note, (params[:attachments] || (params[:note] && params[:note][:uploads])))
render_attachment_warning_if_needed(@note)
flash[:notice] = l(:notice_successful_create)
respond_to do |format|
format.js
format.html { redirect_to :back }
format.api { render :action => 'show', :status => :created, :location => note_url(@note) }
end
else
respond_to do |format|
format.html { redirect_to :back }
format.api { render_validation_errors(@note) }
end
end
end
def destroy
(render_403; return false) unless @note.destroyable_by?(User.current, @project)
@note.destroy
respond_to do |format|
format.js
format.html { redirect_to :action => 'show', :project_id => @project, :id => @note.source }
format.api { render_api_ok }
end
# redirect_to :action => 'show', :project_id => @project, :id => @contact
end
private
def find_project(project_id = nil)
project_id ||= (params[:note] && params[:note][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
def find_note
@note = Note.find(params[:id])
@project ||= @note.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_note_source
note_source_type = (params[:note] && params[:note][:source_type]) || params[:source_type]
note_source_id = (params[:note] && params[:note][:source_id]) || params[:source_id]
klass = Object.const_get(note_source_type.camelcase)
@note_source = klass.find(note_source_id)
end
end
@@ -0,0 +1,110 @@
# 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 TasksController < ApplicationController
unloadable
before_action :find_project_by_project_id, :authorize, :except => [:index]
before_action :find_optional_project, :only => :index
before_action :find_taskable, :except => [:index, :add, :close]
before_action :find_issue, :except => [:index, :new]
def new
issue = Issue.new
issue.subject = params[:task_subject]
issue.project = @project
issue.tracker_id = params[:task_tracker]
issue.author = User.current
issue.due_date = params[:due_date]
issue.assigned_to_id = params[:assigned_to]
issue.description = params[:task_description]
issue.status = IssueStatus.default
if issue.save
flash[:notice] = l(:notice_successful_add)
@taskable.issues << issue
@taskable.save
redirect_to :back
return
else
redirect_to :back
end
end
def add
@show_form = 'true'
if params[:source_id] && params[:source_type] && request.post?
find_taskable
@taskable.issues << @issue
@taskable.save
end
taskable_name = @taskable.class.name.underscore
respond_to do |format|
format.html { redirect_to :back }
format.js do
render :update do |page|
page.replace_html "issue_#{taskable_name}s", :partial => "issues/#{taskable_name}s"
end
end
end
end
def delete
@issue.taskables.delete(@taskable)
taskable_name = @taskable.class.name.underscore
respond_to do |format|
format.html { redirect_to :back }
format.js do
render :update do |page|
page.replace_html "issue_#{taskable_name}s", :partial => "issues/#{taskable_name}s"
end
end
end
end
def close
@issue.status = IssueStatus.find(:first, :conditions => { :is_closed => true })
@issue.save
respond_to do |format|
format.js do
render :update do |page|
page["issue_#{params[:issue_id]}"].visual_effect :fade
end
end
format.html { redirect_to :back }
end
end
private
def find_taskable
klass = Object.const_get(params[:source_type].camelcase)
@taskable = klass.find(params[:source_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_issue
@issue = Issue.find(params[:issue_id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -0,0 +1,269 @@
# encoding: utf-8
#
# 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 ContactsHelper
def contact_tabs(contact)
contact_tabs = []
contact_tabs << {:name => 'notes', :partial => 'contacts/notes', :label => l(:label_crm_note_plural)} if contact.visible?
contact_tabs << {:name => 'contacts', :partial => 'company_contacts', :label => l(:label_contact_plural) + (contact.company_contacts.visible.count > 0 ? " (#{contact.company_contacts.count})" : "")} if contact.is_company?
contact_tabs << {:name => 'deals', :partial => 'deals/related_deals', :label => l(:label_deal_plural) + (contact.all_visible_deals.size > 0 ? " (#{contact.all_visible_deals.size})" : "") } if User.current.allowed_to?(:add_deals, @project)
contact_tabs
end
def settings_contacts_tabs
ret = [
{:name => 'general', :partial => 'settings/contacts/contacts_general', :label => :label_general},
{:name => 'money', :partial => 'settings/contacts/money', :label => :label_crm_money_settings},
{:name => 'tags', :partial => 'settings/contacts/contacts_tags', :label => :label_crm_tags_plural},
{:name => 'deal_statuses', :partial => 'settings/contacts/contacts_deal_statuses', :label => :label_crm_deal_status_plural},
]
ret.push({:name => 'hidden', :partial => 'settings/contacts/contacts_hidden', :label => :label_crm_contacts_hidden}) if params[:hidden]
ret
end
def collection_for_visibility_select
[[l(:label_crm_contacts_visibility_project), Contact::VISIBILITY_PROJECT],
[l(:label_crm_contacts_visibility_public), Contact::VISIBILITY_PUBLIC],
[l(:label_crm_contacts_visibility_private), Contact::VISIBILITY_PRIVATE]]
end
def contact_list_styles_for_select
list_styles = [[l(:label_crm_list_excerpt), "list_excerpt"]]
list_styles += [[l(:label_crm_list_list), "list"],
[l(:label_crm_list_cards), "list_cards"]]
end
def contacts_list_style
list_styles = contact_list_styles_for_select.map(&:last)
if params[:contacts_list_style].blank?
list_style = list_styles.include?(session[:contacts_list_style]) ? session[:contacts_list_style] : RedmineContacts.default_list_style
else
list_style = list_styles.include?(params[:contacts_list_style]) ? params[:contacts_list_style] : RedmineContacts.default_list_style
end
session[:contacts_list_style] = list_style
end
def authorized_for_permission?(permission, project, global = false)
User.current.allowed_to?(permission, project, :global => global)
end
def render_contact_projects_hierarchy(projects)
s = ''
project_tree(projects) do |project, level|
s << "<ul>"
name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
s << "<li id='project_#{project.id}'>" + name_prefix + link_to_project(project)
s += ' ' + link_to(image_tag('delete.png'),
contact_contacts_project_path(@contact, :id => project.id, :project_id => @project.id),
:remote => true,
:method => :delete,
:style => "vertical-align: middle",
:class => "delete",
:title => l(:button_delete)) if (projects.size > 1 && User.current.allowed_to?(:edit_contacts, project))
s << "</li>"
s << "</ul>"
end
s.html_safe
end
def contact_to_vcard(contact)
return false unless ContactsSetting.vcard?
card = Vcard::Vcard::Maker.make2 do |maker|
maker.add_name do |name|
name.prefix = ''
name.given = contact.first_name.to_s
name.family = contact.last_name.to_s
name.additional = contact.middle_name.to_s
end
maker.add_addr do |addr|
addr.preferred = true
addr.street = contact.street1.to_s.gsub("\r\n"," ").gsub("\n"," ")
addr.locality = contact.city.to_s
addr.region = contact.region.to_s
addr.postalcode = contact.postcode.to_s
addr.country = contact.country.to_s
addr.location = 'business'
end
maker.title = contact.job_title.to_s
maker.org = contact.company.to_s
maker.birthday = contact.birthday.to_date unless contact.birthday.blank?
maker.add_note(contact.background.to_s.gsub("\r\n"," ").gsub("\n", ' '))
maker.add_url(contact.website.to_s)
contact.phones.each { |phone| maker.add_tel(phone) }
contact.emails.each { |email| maker.add_email(email) }
end
avatar = contact.attachments.find_by_description('avatar')
card = card.encode.sub("END:VCARD", "PHOTO;BASE64:" + "\n " + [File.open(avatar.diskfile).read].pack('m').to_s.gsub(/[ \n]/, '').scan(/.{1,76}/).join("\n ") + "\nEND:VCARD") if avatar && avatar.readable?
card.to_s
end
def contacts_to_vcard(contacts)
return "" unless User.current.allowed_to?(:export_contacts, @project, :global => true)
contacts.map{|c| contact_to_vcard(c) }.join("\r\n")
end
def contacts_to_xls(contacts)
return "" unless User.current.allowed_to?(:export_contacts, @project, :global => true)
require 'spreadsheet'
Spreadsheet.client_encoding = 'UTF-8'
book = Spreadsheet::Workbook.new
sheet = book.create_worksheet
headers = [ "#",
l(:field_is_company),
l(:field_contact_first_name),
l(:field_contact_middle_name),
l(:field_contact_last_name),
l(:field_contact_job_title),
l(:field_contact_company),
l(:field_contact_phone),
l(:field_contact_email),
l(:label_crm_address),
l(:label_crm_city),
l(:label_crm_postcode),
l(:label_crm_region),
l(:label_crm_country),
l(:field_contact_skype),
l(:field_contact_website),
l(:field_birthday),
l(:field_contact_tag_names),
l(:label_crm_assigned_to),
l(:field_contact_background),
l(:field_created_on),
l(:field_updated_on)
]
custom_fields = ContactCustomField.order('LOWER(name)')
custom_fields.each { |f| headers << f.name }
idx = 0
row = sheet.row(idx)
row.replace headers
contacts.each do |contact|
idx += 1
row = sheet.row(idx)
fields = [contact.id,
contact.is_company ? 1 : 0,
contact.first_name,
contact.middle_name,
contact.last_name,
contact.job_title,
contact.company,
contact.phone,
contact.email,
contact.address.to_s.gsub("\r\n"," ").gsub("\n", ' '),
contact.city,
contact.postcode,
contact.region,
contact.country,
contact.skype_name,
contact.website,
format_date(contact.birthday),
contact.tag_list.to_s,
contact.assigned_to ? contact.assigned_to.name : "",
contact.background.to_s.gsub("\r\n"," ").gsub("\n", ' '),
format_date(contact.created_on),
format_date(contact.updated_on)
]
contact.custom_field_values.sort_by{|v| v.custom_field.name.downcase}.each {|custom_value| fields << RedmineContacts::CSVUtils.csv_custom_value(custom_value) }
row.replace fields
end
xls_stream = StringIO.new('')
book.write(xls_stream)
return xls_stream.string
end
def mail_macro(contact, message)
message = message.gsub(/%%NAME%%/, contact.first_name)
message = message.gsub(/%%FULL_NAME%%/, contact.name)
message = message.gsub(/%%COMPANY%%/, contact.company) if contact.company
message = message.gsub(/%%LAST_NAME%%/, contact.last_name) if contact.last_name
message = message.gsub(/%%MIDDLE_NAME%%/, contact.middle_name) if contact.middle_name
message = message.gsub(/%%DATE%%/, format_date(Date.today.to_s))
contact.custom_field_values.each do |value|
message = message.gsub(/%%#{value.custom_field.name}%%/, value.value.to_s)
end
message
end
def set_flash_from_bulk_contact_save(contacts, unsaved_contact_ids)
if unsaved_contact_ids.empty?
flash[:notice] = l(:notice_successful_update) unless contacts.empty?
else
flash[:error] = l(:notice_failed_to_save_contacts,
:count => unsaved_contact_ids.size,
:total => contacts.size,
:ids => '#' + unsaved_contact_ids.join(', #'))
end
end
def render_contact_tabs(tabs)
if tabs.any?
render :partial => 'common/contact_tabs', :locals => {:tabs => tabs}
else
content_tag 'p', l(:label_no_data), :class => "nodata"
end
end
def importer_link
project_contact_imports_path
end
def importer_show_link(importer, project)
project_contact_import_path(:id => importer, :project_id => project)
end
def importer_settings_link(importer, project)
settings_project_contact_import_path(:id => importer, :project => project)
end
def importer_run_link(importer, project)
run_project_contact_import_path(:id => importer, :project_id => project, :format => 'js')
end
def importer_link_to_object(contact)
link_to "#{contact.first_name} #{contact.last_name}", contact_path(contact)
end
def _project_contacts_path(project, *args)
if project
project_contacts_path(project, *args)
else
contacts_path(*args)
end
end
def deals_link_to_remove_fields(name, f, options={})
f.hidden_field(:_destroy) + link_to_function(name, "remove_order_fields(this); tooglePriceField()", options)
end
end
@@ -0,0 +1,24 @@
# encoding: utf-8
#
# 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 ContactsMoneyHelper
# Will be depricated
end
@@ -0,0 +1,94 @@
# encoding: utf-8
#
# 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 CrmQueriesHelper
def retrieve_crm_query(object_type)
query_class = Object.const_get("#{object_type.camelcase}Query")
if !params[:query_id].blank?
cond = "project_id IS NULL"
cond << " OR project_id = #{@project.id}" if @project
@query = query_class.where(cond).find(params[:query_id])
raise ::Unauthorized unless @query.visible?
@query.project = @project
session["#{object_type}_query".to_sym] = {:id => @query.id, :project_id => @query.project_id}
sort_clear
elsif api_request? || params[:set_filter] || session["#{object_type}_query".to_sym].nil? || session["#{object_type}_query".to_sym][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid
@query = query_class.new(:name => "_")
@query.project = @project
@query.build_from_params(params)
session["#{object_type}_query".to_sym] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
else
# retrieve from session
@query = query_class.find(session["#{object_type}_query".to_sym][:id]) if session["#{object_type}_query".to_sym][:id]
@query ||= query_class.new(:name => "_", :filters => session["#{object_type}_query".to_sym][:filters], :group_by => session["#{object_type}_query".to_sym][:group_by], :column_names => session["#{object_type}_query".to_sym][:column_names])
@query.project = @project
end
end
def retrieve_crm_calendar(options = {})
if params[:year] and params[:year].to_i > 1900
@year = params[:year].to_i
if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
@month = params[:month].to_i
end
end
@year ||= Date.today.year
@month ||= Date.today.month
@calendar = RedmineContacts::Helpers::CrmCalendar.new(Date.civil(@year, @month, 1), options)
end
def sidebar_crm_queries(query_class)
unless @sidebar_queries
@sidebar_queries = query_class.visible.
where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
order("#{query_class.table_name}.name ASC")
end
@sidebar_queries
end
def crm_query_links(title, queries, object_type)
# links to #index on contacts/show
return '' unless queries.any?
url_params = controller_name == "#{object_type}s" ? {:controller => "#{object_type}s", :action => 'index', :project_id => @project} : params
content_tag('h3', title) + "\n" +
content_tag('ul',
queries.collect {|query|
css = 'query'
css << ' selected' if query == @query
content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
}.join("\n").html_safe,
:class => 'queries'
) + "\n"
end
def render_sidebar_crm_queries(object_type)
query_class = Object.const_get("#{object_type.camelcase}Query")
out = ''.html_safe
out << crm_query_links(l(:label_my_queries), sidebar_crm_queries(query_class).select(&:is_private?), object_type)
out << crm_query_links(l(:label_query_plural), sidebar_crm_queries(query_class).reject(&:is_private?), object_type)
out
end
end
@@ -0,0 +1,185 @@
# encoding: utf-8
#
# 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 DealsHelper
include ContactsHelper
def collection_for_status_select
deal_statuses.collect{|s| [s.name, s.id.to_s]}
end
def deal_status_options_for_select(select="")
options_for_select(collection_for_status_select, select)
end
def deal_statuses
(!@project.blank? ? @project.deal_statuses : DealStatus.order("#{DealStatus.table_name}.status_type, #{DealStatus.table_name}.position")) || []
end
def deal_status_url(status_id, options={})
{:controller => 'deals',
:action => 'index',
:set_filter => 1,
:project_id => @project,
:fields => [:status_id],
:values => {:status_id => [status_id]},
:operators => {:status_id => '='}}.merge(options)
end
def pipeline_status_tag(deal_status, count, index)
total = @processor.scope.count
width ||= 20 if deal_status.is_won?
width ||= 40 if deal_status.is_lost?
width ||= (100 - 20) * (count.to_f / total.to_f) + 20
width_style = index == 0 ? "" : "width: #{width}%"
status_tag = content_tag(:span, deal_status.name)
content_tag(:span, status_tag, :class => "tag-label-color", :style => "background-color:#{deal_status.color_name};color:white; #{width_style}")
end
def remove_contractor_link(contact)
link_to(image_tag('delete.png'),
{:controller => "deal_contacts", :action => 'delete', :project_id => @project, :deal_id => @deal, :contact_id => contact},
:remote => true,
:method => :delete,
:data => {:confirm => l(:text_are_you_sure)},
:class => "delete", :title => l(:button_delete)) if User.current.allowed_to?(:edit_deals, @project)
end
def link_to_deal(deal)
link_to deal.name, deal_path(deal)
end
def deal_list_styles_for_select
[[l(:label_crm_list_excerpt), "list_excerpt"],
[l(:label_crm_list_list), "list"],
[l(:label_crm_list_board), "list_board"],
[l(:label_calendar), "crm_calendars/crm_calendar"],
[l(:label_crm_pipeline), "list_pipeline"]]
end
def deals_list_style
list_styles = deal_list_styles_for_select.map(&:last)
if params[:deals_list_style].blank?
list_style = list_styles.include?(session[:deals_list_style]) ? session[:deals_list_style] : RedmineContacts.default_list_style.gsub("list_cards", "list_board")
else
list_style = list_styles.include?(params[:deals_list_style]) ? params[:deals_list_style] : RedmineContacts.default_list_style.gsub("list_cards", "list_board")
end
session[:deals_list_style] = list_style
end
def retrieve_deals_query
if params[:status_id] || !params[:period].blank? || !params[:category_id].blank? || !params[:assigned_to_id].blank?
session[:deals_query] = {:project_id => (@project ? @project.id : nil),
:status_id => params[:status_id],
:category_id => params[:category_id],
:period => params[:period],
:assigned_to_id => params[:assigned_to_id]}
else
if api_request? || params[:set_filter] || session[:deals_query].nil? || session[:deals_query][:project_id] != (@project ? @project.id : nil)
session[:deals_query] = {}
else
params.merge!(session[:deals_query])
end
end
end
def pipeline_prices(scope)
prices_collection_by_currency(scope.group_by(&:currency).map{|k,v| [k, v.inject(0) { |sum, x| sum + x.price.to_f } ] }).join(' / ').html_safe
end
def deals_to_csv(deals)
return "" unless User.current.allowed_to?(:export_contacts, @project, :global => true)
decimal_separator = l(:general_csv_decimal_separator)
encoding = 'utf-8'
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
# csv header fields
headers = [ "#",
l(:field_name, :locale => :en),
l(:field_background, :locale => :en),
l(:field_currency, :locale => :en),
l(:field_price, :locale => :en),
l(:label_crm_probability, :locale => :en),
l(:label_crm_expected_revenue, :locale => :en),
l(:field_due_date, :locale => :en),
l(:field_author, :locale => :en),
l(:field_assigned_to, :locale => :en),
l(:field_status, :locale => :en),
l(:field_contact, :locale => :en),
l(:field_category, :locale => :en),
l(:field_created_on, :locale => :en),
l(:field_updated_on, :locale => :en)
]
custom_fields = DealCustomField.order(:name)
custom_fields.each {|f| headers << f.name}
csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
# csv lines
deals.each do |deal|
fields = [deal.id,
deal.name,
deal.background,
deal.currency,
deal.price,
deal.probability,
deal.expected_revenue,
format_date(deal.due_date),
deal.author,
deal.assigned_to,
deal.status,
deal.contact,
deal.category,
format_date(deal.created_on),
format_date(deal.updated_on)
]
deal.custom_field_values.sort_by{|v| v.custom_field.name}.each {|custom_value| fields << RedmineContacts::CSVUtils.csv_custom_value(custom_value) }
csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
end
end
export
end
def importer_link
project_deal_imports_path
end
def importer_show_link(importer, project)
project_deal_import_path(:id => importer, :project_id => project)
end
def importer_settings_link(importer, project)
settings_project_deal_import_path(:id => importer, :project => project)
end
def importer_run_link(importer, project)
run_project_deal_import_path(:id => importer, :project_id => project, :format => 'js')
end
def importer_link_to_object(deal)
link_to deal.name, deal_path(deal)
end
def _project_deals_path(project, *args)
if project
project_deals_path(project, *args)
else
deals_path(*args)
end
end
end
@@ -0,0 +1,132 @@
# encoding: utf-8
#
# 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 NotesHelper
include ContactsHelper
def collection_for_note_types_select
note_types = [[l(:label_crm_note), '']] + [:label_crm_note_type_email, :label_crm_note_type_call, :label_crm_note_type_meeting].each_with_index.collect{|type, i| [l(type), i]}
context = {:note_types => note_types}
call_hook(:helper_notes_note_type_label, context)
context[:note_types]
end
def authoring_note(created, author, options={})
return "<span class=\"author\">#{l(options[:label] || :label_crm_added_by)} #{link_to_user(author).to_s}</span>".html_safe if created.blank?
if RedmineContacts.settings[:note_authoring_time]
('<span class="author">' + l(options[:label] || :label_crm_added_by) + ' ' +
link_to_user(author).to_s + ', ' +
format_time(created).to_s + '</span>').html_safe
else
authoring(created, author, options={})
end
end
def add_note_url(note_source, project=nil)
{:controller => 'notes', :action => 'create', :source_id => note_source, :source_type => note_source.class.name, :project_id => project}
end
def contacts_thumbnails(obj, options={})
return false if !obj || !obj.respond_to?(:attachments)
options[:size] = options[:size].to_s || "100"
size = options[:size]
options[:size] = options[:size] + "x" + options[:size]
# options[:max_width] = size
# options[:max_heght] = size
max_file_size = options[:max_file_size] || 300.kilobytes
options[:class] = "thumbnail"
s = ""
# TODO: Regexp does not work
images = obj.attachments.select{|att| att.thumbnailable?}
images = images.select{|att| att.filename.match(options[:regexp])} if options[:regexp]
images.each do |att_file|
attachment_url = url_for :controller => 'attachments', :action => 'download', :id => att_file, :filename => att_file.filename
contacts_thumbnail_url = url_for(:controller => 'attachments',
:action => 'contacts_thumbnail',
:id => att_file,
:size => size)
image_url = Redmine::Thumbnail.convert_available? ? contacts_thumbnail_url : attachment_url
s << link_to(image_tag(image_url, options), attachment_url, {:title => att_file.filename}) if (att_file.filesize < max_file_size || Redmine::Thumbnail.convert_available?)
end
s.html_safe
end
def auto_contacts_thumbnails(obj)
s = ""
max_file_size = Setting.plugin_redmine_contacts[:max_contacts_thumbnail_file_size].to_i.kilobytes if !Setting.plugin_redmine_contacts[:max_contacts_thumbnail_file_size].blank?
s << contacts_thumbnails(obj, {:size => 100, :max_file_size => max_file_size}) if Setting.plugin_redmine_contacts[:auto_contacts_thumbnails]
s = content_tag(:p, s.html_safe, :class => "thumbnail") if !s.blank?
s.html_safe
end
def note_content(note)
s = ''
if note.content.length > Note.cut_length
if ActiveRecord::VERSION::MAJOR >= 4
s << truncate(note.content, :length => Note.cut_length) { link_to "#{l(:label_crm_note_read_more)}", note_path(:id => note, :project_id => @project) }
else
s << textilizable(truncate(note.content, :length => Note.cut_length,
:omission => "... \"#{l(:label_crm_note_read_more)}\":#{url_for(:controller => 'notes',
:action => 'show',
:project_id => @project,
:id => note)}"))
end
else
s << textilizable(note, :content)
end
s.html_safe
end
def notes_to_csv(notes)
decimal_separator = l(:general_csv_decimal_separator)
encoding = l(:general_csv_encoding)
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
# csv header fields
headers = [ "#",
l(:field_type, :locale => :en),
l(:label_date, :locale => :en),
l(:field_author, :locale => :en),
l(:field_content, :locale => :en)
]
# Export project custom fields if project is given
# otherwise export custom fields marked as "For all projects"
custom_fields = NoteCustomField.order(:name)
custom_fields.each {|f| headers << f.name}
# Description in the last column
csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
# csv lines
notes.each do |note|
fields = [note.id,
note.type_id,
format_time(note.created_on),
note.author.name,
note.content
]
custom_fields.each {|f| fields << RedmineContacts::CSVUtils.csv_custom_value(note.custom_value_for(f)) }
csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
end
end
export
end
end
@@ -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
@@ -0,0 +1,10 @@
<%= raw @companies.map {|company| {
'id' => company.id,
'name' => company.name,
'avatar' => avatar_to(company, :size => 16),
'email' => company.primary_email,
'label' => company.name,
'value' => company.name
}
}.to_json
%>
@@ -0,0 +1,11 @@
<%= raw @contacts.map {|contact| {
'id' => contact.id,
'text' => contact.name_with_company,
'name' => contact.name,
'avatar' => avatar_to(contact, :size => 16),
'company' => contact.is_company ? "" : contact.company.to_s,
'email' => contact.primary_email,
'value' => contact.id
}
}.to_json
%>
@@ -0,0 +1,4 @@
<%= raw @tags.collect {|tag|
tag.name
}.to_json
%>
@@ -0,0 +1,9 @@
<%= raw @deals.map {|deal| {
'id' => deal.id,
'label' => "#{deal.full_name} (#{deal.info})",
'text' => "#{deal.name} (#{deal.info})",
'avatar' => avatar_to(deal, :size => 16),
'value' => deal.id
}
}.to_json
%>
@@ -0,0 +1,8 @@
<p class="address">
<label><%= l(:label_crm_address) %></label>
<%= f.text_field :street1, :no_label => true, :placeholder => l(:label_crm_street1), :style => "width:90%;" -%></p>
<p class="address street2"><%= f.text_field :street2, :no_label => true, :placeholder => l(:label_crm_street2) -%></p>
<p class="address city"><%= f.text_field :city, :no_label => true, :placeholder => l(:label_crm_city) -%> </p>
<p class="address region"><%= f.text_field :region, :no_label => true, :placeholder => l(:label_crm_region) -%></p>
<p class="address postcode"><%= f.text_field :postcode, :no_label => true, :placeholder => l(:label_crm_postcode), :size => 12 -%></p>
<p class="address country"><%= f.select :country_code, countries_options_for_select(f.object.country_code), :no_label => true, :placeholder => l(:label_crm_country), :include_blank => true -%></p>
@@ -0,0 +1,17 @@
<% actions ||= "" %>
<table class="note_data" id="contact_data_<%= contact_data.id %>">
<tr>
<td class="avatar"><%= link_to avatar_to(contact_data, :size => "32"), note_source_url(contact_data), :id => "avatar" %></td>
<td class="name">
<h4 class="contacts_header">
<%= link_to contact_data.name, note_source_url(contact_data) %>
</h4>
<%= contact_data.info %>
</td>
<% if !actions.blank? %>
<td>
<%= actions %>
</td>
<% end %>
</tr>
</table>
@@ -0,0 +1,29 @@
<% selected_tab = params[:tab] ? params[:tab].to_s : tabs.first[:name] %>
<div class="tabs">
<ul>
<% tabs.each do |tab| -%>
<li><%= link_to tab[:label], tabs_contact_path(@contact, :tab => tab[:name]),
:id => "tab-#{tab[:name]}",
:class => (tab[:name] != selected_tab ? 'tab-header' : 'selected tab-header'),
:data => { :name => tab[:name], :partial => tab[:partial], :project_id => @project},
:onclick => "showContactTab('#{tab[:name]}'); this.blur(); return false;" %></li>
<% end -%>
</ul>
<div class="tabs-buttons" style="display:none;">
<button class="tab-left" onclick="moveTabLeft(this);"></button>
<button class="tab-right" onclick="moveTabRight(this);"></button>
</div>
</div>
<% tabs.each do |tab| %>
<% selected = tab[:name] == selected_tab %>
<div class='tab-placeholder tab-content <%= 'active loaded' if selected %>' id='tab-placeholder-<%= tab[:name] %>' style='<%= "display: block" if selected %>'>
<%= render(:partial => tab[:partial]) if selected %>
</div>
<% end %>
<script type='text/javascript'>
$(document).ready(displayTabsButtons);
$(window).resize(displayTabsButtons);
$(document).ready(function(){ setupDeferredTabs('<%= load_tab_contact_path(@contact) %>') });
</script>
@@ -0,0 +1,9 @@
<script type="text/javascript">
var field_formats = ['contact', 'company'];
var contact_filter_urls = {
'company': "<%= auto_complete_contacts_path(:project_id => (ContactsSetting.cross_project_contacts? ? nil : @project), :is_company => true) %>",
'contact': "<%= auto_complete_contacts_path(:project_id => (ContactsSetting.cross_project_contacts? ? nil : @project)) %>"
};
field_formats.push('deal');
contact_filter_urls['deal'] = '<%= auto_complete_deals_path(:project_id => (ContactsSetting.cross_project_contacts? ? nil : @project)) %>';
</script>
@@ -0,0 +1,4 @@
<% if notes_attachments.any? %>
<h3><%= l(:label_attachment_plural) %></h3>
<%= render :partial => 'attachments/links', :locals => {:attachments => notes_attachments, :options => {}} %>
<% end %>
@@ -0,0 +1,4 @@
<h3><%= l(:label_crm_recently_viewed) %></h3>
<div id="recently_viewed">
<%= render :partial => 'common/contact_data', :collection => RecentlyViewed.includes(:viewed).last(5).map(&:viewed).select{|v| !v.blank? && v.visible?} %>
</div>
@@ -0,0 +1,10 @@
<% if responsible_user.assigned_to %>
<h3><%= l(:label_crm_assigned_to) %></h3>
<div id="responsible_user">
<ul>
<li>
<%= (avatar(responsible_user.assigned_to, :size => "16").to_s + link_to_user(responsible_user.assigned_to, :class => 'user').to_s).html_safe %>
</li>
</ul>
</div>
<% end %>
@@ -0,0 +1,15 @@
<%= call_hook(:view_contacts_sidebar_top) %>
<h3><%= l(:label_crm_module_plural) %></h3>
<% if User.current.allowed_to?(:view_contacts, @project, :global => true) %>
<%= link_to l(:label_contact_plural), { :controller => 'contacts', :action => 'index', :project_id => @project, :set_filter => 1} %>
|
<% end %>
<% if User.current.allowed_to?(:view_deals, @project, :global => true) %>
<%= link_to l(:label_deal_plural), { :controller => 'deals', :action => 'index', :project_id => @project, :set_filter => 1} %>
|
<% end %>
<%= link_to l(:label_crm_note_plural), { :controller => 'contacts', :action => 'contacts_notes', :project_id => @project} %>
<%= call_hook(:view_contacts_sidebar_bottom) %>
@@ -0,0 +1,74 @@
<div id="attributes" class="attributes">
<div class="contextual">
<%- if ContactsSetting.vcard? -%>
<%= link_to 'vCard', contact_path(@contact, :format => :vcf) %>
<%- end -%>
</div>
<h3><%= if !@contact.is_company then l(:label_contact) else l(:label_crm_company) end %></h3>
<table class="contact sidebar attributes vcard">
<%= call_hook(:view_contacts_sidebar_attributes_top) %>
<tr>
<th class = "name"><%= l(:field_contact_name) %>:</th><td class="name fn <%= "org" if @contact.is_company %>"><%= h @contact.name(:firstname_middlename_lastname) %></td>
</tr>
<% if !@contact.job_title.blank? %>
<tr> <th class = "job_title"><%= !@contact.is_company ? l(:field_contact_job_title) : l(:field_company_field) %>:</th><td class="job_title title"><%= h @contact.job_title %></td></tr>
<% end %>
<% if !@contact.is_company %>
<tr><th class = "company"><%=l(:field_contact_company)%>:</th><td class="company org"><%= h @contact.company %></td></tr>
<% end %>
<tr>
<th class = "address"><%= l(:field_contact_address) %>:</th>
<% unless @contact.address.blank? %>
<td class="address adr">
<%= @contact.post_address.gsub("\n", "<br/>").html_safe %>
<br>
<%= link_to l(:label_crm_show_on_map), "http://maps.google.com/maps?f=q&q=#{@contact.address}+(#{@contact.name})&ie=UTF8&om=1"%>
</td>
<% end %>
</tr>
<tr class = "tel">
<th class = "phone"><%= l(:field_contact_phone) %>:</th>
<td class = "phones">
<% @contact.phones.each do |phone| %>
<span class="value"><%= h phone %> <br></span>
<% end %>
</td>
</tr>
<tr class = "emails">
<th><%= l(:field_contact_email) %>:</th>
<td>
<% @contact.emails.each do |email| %>
<span class="email"><%= mail_to email %> <br></span>
<% end %>
</td>
</tr>
<tr>
<th class = "website"><%= l(:field_contact_website) %>:</th>
<td class="website url"><%= link_to @contact.website, @contact.website_address, :class => 'external' %></td>
</tr>
<% if !@contact.skype_name.blank? %>
<tr>
<th class = "skype"><%= l(:field_contact_skype) %>:</th>
<td class="skype"><%= skype_to @contact.skype_name %></td>
</tr>
<% end %>
<% if !@contact.birthday.blank? %>
<tr> <th class = "birthday"><%= l(:field_birthday) %>:</th><td class="birthday bday" title=<%= "#{format_date(@contact.birthday)}" %>><%= "#{@contact.birthday.day} #{t('date.month_names')[@contact.birthday.month]}"%></td> </tr>
<tr> <th class = "age"><%= l(:field_age) %>:</th><td class="ega"><%= @contact.age %></td> </tr>
<% end %>
<% @contact.custom_field_values.compact.each do |custom_value| %>
<% if !custom_value.value.blank? %>
<tr> <th class = "custom_field"><%= custom_value.custom_field.name%>:</th><td> <%= simple_format_without_paragraph(h(show_value(custom_value))) %></td> </tr>
<% end %>
<% end %>
<% if @contact.assigned_to %>
<tr><th class="author"><%=l(:label_crm_assigned_to)%>:</th><td class="author"><%= avatar(@contact.assigned_to, :size => "14") %><%= link_to_user(@contact.assigned_to) %></td></tr>
<% end %>
<%= call_hook(:view_contacts_sidebar_attributes_bottom) %>
</table>
</div>
@@ -0,0 +1,13 @@
<% @company_contacts = @contact.company_contacts.visible.uniq %>
<% if @contact.is_company %>
<div id="company_contacts">
<div class="contextual">
<%= link_to_if_authorized l(:label_crm_add_contact), {:controller => 'contacts', :action => 'new', :project_id => @project, :contact => {:company => @contact.name}} %>
</div>
<h3><%= l(:label_contact_plural) %></h3>
<%= render :partial => 'common/contact_data', :collection => @company_contacts %>
<div style="clear:both;"> </div>
</div>
<% end %>
@@ -0,0 +1,36 @@
<div class="contact card">
<table class="subject_header">
<tr>
<td class="avatar"><%= contact_tag(contact_card, :type => "avatar", :size => 64) %></td>
<td class="name" style="vertical-align: top;">
<h2><%= contact_tag(contact_card, :type => "plain") %> </h2>
<p>
<%= h contact_card.job_title %>
<% if !contact_card.is_company %>
<%= " #{l(:label_crm_at_company)} " unless (contact_card.job_title.blank? or contact_card.company.blank?) %>
<% if contact_card.contact_company %>
<%= link_to contact_card.contact_company.name, {:controller => 'contacts', :action => 'show', :project_id => contact_card.contact_company.project(@project), :id => contact_card.contact_company.id } %>
<% else %>
<%= h contact_card.company %>
<% end %>
<% end %>
</p>
</td>
</tr>
<tr>
<td colspan="2" style="padding-left: 10px;">
<% if contact_card.phones.any? %>
<p class="phone icon icon-phone"><%= contact_card.phones.first %></p>
<% end %>
<% if contact_card.emails.any? %>
<p class="email icon icon-email"><%= mail_to contact_card.emails.first %></p>
<% end %>
<%= tag_links(contact_card.tag_list) %>
</td>
</tr>
</table>
</div>
@@ -0,0 +1,4 @@
<p><%= form.check_box :is_filter %></p>
<% if (@custom_field.respond_to?(:format) && @custom_field.format.searchable_supported) || !@custom_field.respond_to?(:format) %>
<p><%= form.check_box :searchable %></p>
<% end %>
+105
View File
@@ -0,0 +1,105 @@
<%= back_url_hidden_field_tag %>
<%= error_messages_for 'contact' %>
<div class = "box tabular" id="contact_data">
<script type="text/javascript" charset="utf-8">
function togglePerson(element) {
if (element.checked) {
$('#person_data').hide();
$('#job_title label').get(0).innerHTML ='<%= l(:field_company_field) %>';
$('#first_name label').get(0).innerHTML='<%= l(:field_company_name) %>' + '<span class="required"> *</span>';
} else {
$('#person_data').show();
$('#job_title label').get(0).innerHTML ='<%= l(:field_contact_job_title) %>';
$('#first_name label').get(0).innerHTML='<%= l(:field_contact_first_name) %>' + '<span class="required"> *</span>';
}
}
</script>
<p class="avatar extended" id="watchers">
<%= avatar_to(@contact, :size => "64", :style => "vertical-align: middle;") %>
<%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => @contact.avatar},
:data => {:confirm => l(:text_are_you_sure)},
:method => :delete,
:class => 'delete',
:style => "vertical-align: middle;",
:title => l(:button_delete) unless @contact.avatar.blank? %>
</p>
<p class="extended">
<%= label_tag l(:field_contact_avatar) %>
<span id="attachments_fields"></span>
<span class="add_attachment">
<%= file_field_tag 'dummy_file',
:size => 30,
:id => nil,
:class => 'file_selector',
:multiple => true,
:onchange => 'uploadAvatar(this);',
:data => {
:max_file_size => Setting.attachment_max_size.to_i.kilobytes,
:max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)),
:max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i,
:upload_path => uploads_path(:format => 'js'),
:description_placeholder => l(:label_optional_description)
} %>
</span>
</p>
<p><%= f.check_box(:is_company, :label => l(:field_contact_is_company), :onclick => "togglePerson(this)" ) %></p>
<p id="first_name"><%= f.text_field :first_name, :label => !@contact.is_company ? l(:field_contact_first_name) : l(:field_company_name), :required => true, :style => "width:90%;" %></p>
<div id="person_data" style="<%= 'display: none;' if @contact.is_company %>">
<p><%= f.text_field :middle_name, :label=>l(:field_contact_middle_name) %></p>
<p><%= f.text_field :last_name, :label=>l(:field_contact_last_name), :id => 'contact_last_name' %></p>
<p><%= f.text_field :company, :label=>l(:field_contact_company) -%></p>
<%= javascript_tag "observeAutocompleteField('contact_company', '#{escape_javascript auto_complete_companies_path}')" %>
<p class="extended"><%= f.text_field :birthday, :size => 12 %><%= calendar_for('contact_birthday') %> </p>
</div>
<p id="job_title"><%= f.text_field :job_title, :label => !@contact.is_company ? l(:field_contact_job_title) : l(:field_company_field) %></p>
<% @contact.build_address if @contact.address.blank? %>
<%= f.fields_for(:address) do |a| %>
<span class="extended">
<%= render :partial => 'common/address_form', :locals => {:f => a} %>
</span>
<% end %>
<div id="phones_fields">
<p>
<%= f.text_field :phone, :label=>l(:field_contact_phone), :style => "width:90%;" -%>
<br>
<em class="info"><%= l(:text_comma_separated) %></em>
</p>
</div>
<p>
<%= f.text_field 'email', :label=>l(:field_contact_email), :style => "width:90%;" -%>
<br>
<em class="info"><%= l(:text_comma_separated) %></em>
</p>
<p class="extended"><%= f.text_field 'website', :label=>l(:field_contact_website) -%></p>
<p class="extended"><%= f.text_field 'skype_name', :label=>l(:field_contact_skype) -%></p>
<% @contact.custom_field_values.each do |value| %>
<p class="<%= "extended" unless value.custom_field.is_required? %>">
<%= custom_field_tag_with_label :contact, value %>
</p>
<% end -%>
<p class="extended notes"><%= f.text_area :background , :cols => 80, :rows => 8, :class => 'wiki-edit', :label=>l(:field_contact_background) %></p>
<%= wikitoolbar_for 'contact_background' %>
<p class="extended">
<%= label_tag l(:label_crm_tags_plural) %>
<%= render :partial => "contacts_tags/tags_form" %>
</p>
<% if @project %>
<p class="extended"><%= f.select :assigned_to_id, (@project.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true, :label => l(:label_crm_assigned_to) %></p>
<% end %>
<p class="extended"><%= f.select :visibility, collection_for_visibility_select, :include_blank => false, :label => l(:label_crm_contacts_visibility) %></p>
<p id="show_details_link" style="display: none;"><%= link_to l(:label_crm_show_details), "javascript:void(0);", :onclick => "$('#contact_data .extended').show();" %></p>
</div>
@@ -0,0 +1,24 @@
<div id="tags_data">
<%= tag_links(@contact.tag_list) %>
<% if authorize_for('contacts', 'update') %>
<span class="contextual">
<%= link_to l(:label_crm_edit_tags), {}, :onclick => "$('#edit_tags_form').show(); $('#tags_data').hide(); return false;", :id => 'edit_tags_link' %>
</span>
<% end %>
</div>
<div id="edit_tags_form" style="display:none;">
<%= form_tag( {:controller => 'contacts',
:action => 'update',
:project_id => @project,
:id => @contact },
:method => :put,
:multipart => true ) do %>
<%= render :partial => "contacts_tags/tags_form" %>
<%= submit_tag l(:button_save), :class => "button-small" %>
<%= link_to l(:button_cancel), {}, :onclick => "$('#edit_tags_form').hide(); $('#tags_data').show(); return false;" %>
<% end %>
</div>
@@ -0,0 +1,44 @@
<%= form_tag({}, :data => {:cm_url => context_menu_contacts_path}) do %>
<%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params) %>
<%= hidden_field_tag 'project_id', @project.id if @project %>
<div class="autoscroll">
<table class="list contacts">
<thead>
<tr>
<th class="checkbox hide-when-print">
<%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleCRMIssuesSelection(this); return false;',
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
</th>
<% @query.columns.each do |column| %>
<%= Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? ? column_header(@query, column) : column_header(column) %>
<% end %>
</tr>
</thead>
<% previous_group = false %>
<tbody>
<% @contacts.each do |contact| -%>
<% if @query.grouped? && (group = @query.group_by_column.value(contact)) != previous_group %>
<% reset_cycle %>
<tr class="group open">
<td colspan="<%= @query.columns.size + 2 %>">
<span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
<%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, contact) %> <span class="count">(<%= @contact_count_by_group[group] %>)</span>
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
</td>
</tr>
<% previous_group = group %>
<% end %>
<tr id="contact-<%= h(contact.id) %>" class="contact <%= 'company' if contact.is_company %> hascontextmenu <%= cycle('odd', 'even') %>">
<td class="checkbox hide-when-print"><%= check_box_tag("selected_contacts[]", contact.id, false, :id => nil) %></td>
<% @query.columns.each do |column| %><%= content_tag 'td', column_content(column, contact), :class => column.css_classes %><% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
@@ -0,0 +1,51 @@
<%= form_tag({}, :data => {:cm_url => context_menu_contacts_path}) do %>
<%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params) %>
<%= hidden_field_tag 'project_id', @project.id if @project %>
<div class="splitcontentleft">
<% i = 0 %>
<% split_on = (@contacts.size / 2.0).ceil - 1 %>
<% @contacts.each do |contact| %>
<% @contact = contact %>
<div class="contact card">
<table class="subject_header">
<tr>
<td class="avatar"><%= contact_tag(contact, :type => "avatar", :size => 64) %></td>
<td class="name" style="vertical-align: top;">
<h2><%= contact_tag(contact, :type => "plain") %> </h2>
<p>
<%= h contact.job_title %>
<% if !contact.is_company %>
<%= " #{l(:label_crm_at_company)} " unless (contact.job_title.blank? or contact.company.blank?) %>
<% if contact.contact_company %>
<%= link_to contact.contact_company.name, {:controller => 'contacts', :action => 'show', :project_id => contact.contact_company.project(@project), :id => contact.contact_company.id } %>
<% else %>
<%= h contact.company %>
<% end %>
<% end %>
</p>
<% if contact.phones.any? %>
<p class="phone icon icon-phone"><%= contact.phones.first %></p>
<% end %>
<% if contact.emails.any? %>
<p class="email icon icon-email"><%= mail_to contact.emails.first %></p>
<% end %>
<%= tag_links(contact.tag_list) %>
</td>
</tr>
</table>
</div>
<% if i == split_on -%>
</div><div class="splitcontentright">
<% end -%>
<% i += 1 -%>
<% end -%>
</div>
<div style="clear:both;"> </div>
<% end %>
@@ -0,0 +1,54 @@
<%= form_tag({}, :data => {:cm_url => context_menu_contacts_path}) do %>
<%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params) %>
<%= hidden_field_tag 'project_id', @project.id if @project %>
<div class="autoscroll">
<table class="contacts index">
<tbody>
<% previous_group = false %>
<% @contacts.each do |contact| %>
<% if @query.grouped? && (group = @query.group_by_column.value(contact)) != previous_group %>
<% reset_cycle %>
<tr class="group open">
<td colspan="<%= @query.columns.size + 2 %>">
<span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
<%= group.blank? ? 'None' : column_content(@query.group_by_column, contact) %> <span class="count">(<%= @contact_count_by_group[group] %>)</span>
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
</td>
</tr>
<% previous_group = group %>
<% end %>
<tr class="hascontextmenu <%= cycle('odd', 'even') %> ">
<td class="checkbox">
<%= check_box_tag "selected_contacts[]", contact.id, false, :onclick => "toggleContact(event, this);" %>
</td>
<td class="avatar">
<%= link_to avatar_to(contact, :size => "32"), contact_path(contact, :project_id => @project), :id => "avatar" %>
</td>
<td class="name">
<h1><%= link_to contact.name, contact_path(contact, :project_id => @project) %></h1>
<p>
<%= link_to contact.website, contact.website_address, :class => 'external', :only_path => true unless !contact.is_company %>
<%= mail_to contact.emails.first unless contact.is_company%>
<div><%= contact.phones.first %></div>
</p>
</td>
<td class="info">
<div class="title_and_company" >
<%= contact.job_title %>
<% if !contact.is_company %>
<%= " #{l(:label_crm_at_company)} " unless (contact.job_title.blank? or contact.company.blank?) %>
<%= contact.company %>
<% end %>
</div>
<div class="tags">
<%= tag_links(contact.tag_list) %>
<%# tag_links(RedmineCrm::TagList.from(contact.cached_tag_list)) %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
@@ -0,0 +1,37 @@
<script type="text/javascript">
jQuery(function($) {
// when the #search field changes
var duplicates = function() {
var form = $("#contact_form"); // grab the form wrapping the search bar.
var url = '<%= escape_javascript(url_for({:controller => "contacts_duplicates", :action => "duplicates", :project_id => @project, :contact_id => @contact})) %>';
var formData = form.serialize();
$.get(url, formData, function(data) { // perform an AJAX get, the trailing function is what happens on successful get.
$("#duplicates").html(data); // replace the "results" div with the result of action taken
});
}
$("#contact_first_name").observe_field(2, duplicates);
$("#contact_middle_name").observe_field(2, duplicates);
$("#contact_last_name").observe_field(2, duplicates);
});
</script>
<%# observe_field("contact_first_name",
:frequency => 1,
:update => 'duplicates',
:url => {:controller => 'contacts_duplicates', :action => 'duplicates', :project_id => @project, :contact_id => @contact},
:with => "$('contact_form').serialize()") %>
<%# observe_field("contact_middle_name",
:frequency => 1,
:update => 'duplicates',
:url => {:controller => 'contacts_duplicates', :action => 'duplicates', :project_id => @project, :contact_id => @contact},
:with => "$('contact_form').serialize()") %>
<%# observe_field("contact_last_name",
:frequency => 1,
:update => 'duplicates',
:url => {:controller => 'contacts_duplicates', :action => 'duplicates', :project_id => @project, :contact_id => @contact},
:with => "$('contact_form').serialize()") %>
@@ -0,0 +1,15 @@
<h3 class="title"><%=l(:label_crm_contact_new)%></h3>
<%= labelled_form_for @contact, :url => project_contacts_path(@project), :remote => true do |f| %>
<%= hidden_field_tag :contact_field_name, params[:contact_field_name] %>
<%= hidden_field_tag :contacts_is_company, params[:contacts_is_company] %>
<%= render :partial => 'contacts/form', :locals => { :f => f } %>
<p class="buttons">
<%= submit_tag l(:button_create), :name => nil %>
<%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
</p>
<% end %>
<% content_for :header_tags do %>
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
<% end %>
@@ -0,0 +1,29 @@
<%
extend Redmine::Pagination
source_id_cond = @contact.is_company ? Contact.visible.where(:company => @contact.first_name).map(&:id) << @contact.id : @contact.id
@note = Note.new(:created_on => Time.now)
scope = Note.where({:source_id => source_id_cond, :source_type => 'Contact'}).includes(:attachments).order("#{Note.table_name}.created_on DESC")
@notes_pages = Redmine::Pagination::Paginator.new(scope.count, 20, params['page'])
@notes = scope.limit(20).offset(@notes_pages.offset)
%>
<% if authorize_for(:notes, :create) %>
<div class="add-note hide-when-print">
<%= render :partial => 'notes/add', :locals => {:note_source => @contact} %>
</div>
<% end %>
<% if @contact.is_public? || authorize_for(:notes, :show) %>
<div id="comments">
<div id="notes">
<%= render :partial => 'notes/note_item', :collection => @notes, :locals => {:show_info => @contact.is_company, :note_source => @contact} %>
<span class="pagination"> <%= pagination_links_full @notes_pages %> </span>
</div>
</div>
<% other_formats_links do |f| %>
<% filtered_params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params %>
<%= f.link_to 'Atom', :url => filtered_params.merge(:key => User.current.rss_key) %>
<% end %>
<% end %>
@@ -0,0 +1,10 @@
<div id="tags_data">
<span class="tags">
<%= render :partial => 'contacts/tags_item', :collection => tag_list, :locals => {:is_note => false} %>
</span>
<% if editable && authorize_for('contacts', 'update') %>
<span class="contextual">
<%= link_to l(:label_crm_edit_tags), {}, :onclick => "$('#edit_tags_form').show(); $('#tags_data').hide(); return false;", :id => 'edit_tags_link' %>
</span>
<% end %>
</div>
@@ -0,0 +1,7 @@
<div id="tags">
<span id="single_tags">
<h3><%= l(:label_crm_tags_plural) %></h3>
<span class="tag_list"><%= safe_join(tags_cloud.map{|tag| tag_link(tag.name, :count => tag.count)}, ContactsSetting.monochrome_tags? ? ', ' : ' ').html_safe %></span>
</span>
</div>
@@ -0,0 +1,19 @@
<%
html_options = {:id => "tag_#{tags_item.id}",
:style => "background-color: #{tags_item.color_name}"}
taggable_type ||= 'contacts'
tag_url = {:controller => taggable_type,
:action => 'index',
:set_filter => 1,
:fields => [:tags],
:values => {:tags => [tags_item.name]},
:operators => {:tags => '='}}
%>
<span class="tag" >
<%- if !is_note -%>
<%= link_to tags_item.name + "#{"(" + tags_item.count.to_s + ")" if tags_item.count > 0}", {:project_id => @project}.merge!(tag_url), html_options %>
<%- else -%>
<%= link_to tags_item.name, {:controller => "contacts", :action => "contacts_notes", :project_id => @project, :tag => tags_item.name}, html_options %>
<%- end -%>
</span>
@@ -0,0 +1,130 @@
<h2><%= l(:label_crm_bulk_edit_selected_contacts) %></h2>
<div class="box" id="duplicates">
<ul>
<% @contacts.each do |contact| %>
<li>
<%= avatar_to contact, :size => "16" %>
<%= link_to_source contact %>,
<%= h contact.job_title %>
<%= " #{l(:label_crm_at_company)} " unless (contact.job_title.blank? or contact.company.blank?) %>
<% if contact.contact_company %>
<%= link_to contact.contact_company.name, {:controller => 'contacts', :action => 'show', :id => contact.contact_company.id } %>
<% else %>
<%= h contact.company %>
<% end %>
<%= "(#{l(:field_contact_tag_names)}: #{contact.tag_list})" if contact.tags.any? %>
</li>
<% end %>
</ul>
</div>
<%= form_tag(:action => 'bulk_update', :project_id => @project) do %>
<%= @contacts.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %>
<div class="box tabular">
<fieldset class="attributes">
<legend><%= l(:label_change_properties) %></legend>
<div class="splitcontentleft">
<p>
<label><%= l(:field_company) %></label>
<%= text_field_tag('contact[company]', '') %>
<%= javascript_tag "observeAutocompleteField('contact_company', '#{escape_javascript auto_complete_companies_path(:project_id => @project)}')" %>
</p>
<% @custom_fields.each do |custom_field| %>
<p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('contact', custom_field, @projects) %></p>
<% end %>
<p>
<label><%= l(:label_crm_assigned_to) %></label>
<%= select_tag('contact[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') +
content_tag('option', l(:label_nobody), :value => 'none') +
options_from_collection_for_select(@assignables, :id, :name)) %>
</p>
<p>
<label><%= l(:label_crm_contacts_visibility) %></label>
<%= select_tag 'contact[visibility]', options_for_select(collection_for_visibility_select) %>
</p>
</div>
<div class="splitcontentright">
<p>
<label><%= l(:field_contact_job_title) %>/<%= l(:field_company_field) %></label>
<%= text_field_tag('contact[job_title]', '') %>
</p>
<p class="address city">
<label><%= l(:label_crm_city) %></label>
<%= text_field_tag 'contact[address_attributes][city]' -%>
</p>
<p class="address region">
<label><%= l(:label_crm_region) %></label>
<%= text_field_tag 'contact[address_attributes][region]' -%>
</p>
<p class="address country">
<label><%= l(:label_crm_country) %></label>
<%= select_tag 'contact[address_attributes][country_code]', options_for_select(l(:label_crm_countries).map{|k, v| [v, k]}), :include_blank => true -%>
</p>
</div>
</fieldset>
<fieldset class="attributes">
<legend><%= l(:label_crm_tags_plural) %></legend>
<div class="splitcontentleft">
<p id="add_tags" class="contacts-tags-edit">
<label><%= l(:field_add_tags) %></label>
<%= text_field_tag 'add_tag_list', '', :size => 10, :class => "hol" %><%= tagsedit_with_source_for("#add_tag_list", auto_complete_contact_tags_path(:project_id => @project)) %>
</p>
</div>
<div class="splitcontentright">
<p id="delete_tags" class="contacts-tags-edit">
<label><%= l(:field_delete_tags) %></label>
<%= text_field_tag 'delete_tag_list', '', :label => :field_contact_tag_names, :size => 10, :class => "hol" %><%= tagsedit_with_source_for("#delete_tag_list", auto_complete_contact_tags_path(:project_id => @project)) %>
</p>
</div>
</fieldset>
<% if @add_projects.any? %>
<fieldset class="attributes">
<legend><%= l(:label_project_plural) %></legend>
<div class="splitcontentleft">
<p>
<label><%= l(:label_crm_add_into) %></label>
<%= select_tag 'add_projects_list[]', content_tag('option', l(:label_no_change_option), :value => '', :selected => 'selected') + project_tree_options_for_select(@add_projects), :multiple => false %>
</p>
</div>
<div class="splitcontentright">
<p>
<label><%= l(:label_crm_delete_from) %></label>
<%= select_tag 'delete_projects_list[]', content_tag('option', l(:label_no_change_option), :value => '', :selected => 'selected') + project_tree_options_for_select(@add_projects), :multiple => false %>
</p>
</div>
</fieldset>
<% end %>
<fieldset><legend><%= l(:field_notes) %></legend>
<%= text_area_tag 'note[content]', '', :cols => 60, :rows => 10, :class => 'wiki-edit' %>
<%= wikitoolbar_for 'note_content' %>
</fieldset>
</div>
<p><%= submit_tag l(:button_submit) %></p>
<% end %>
@@ -0,0 +1,64 @@
<% filtered_params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params %>
<div class="filters">
<% if !@tag %>
<%= form_tag(filtered_params, :id => "query_form", :method => :get) do %>
<%= hidden_field_tag('project_id', @project.to_param) if @project %>
<h2>
<span class="scope_title">
<%= l(:label_crm_contact_all_note_plural) %>
</span>
<span class="live_search">
<%= text_field_tag(:search_note, params[:search_note], :autocomplete => "off", :class => "live_search_field", :placeholder => l(:label_crm_contact_search) ) %>
<script type="text/javascript">
jQuery(function($) {
// when the #search field changes
$("#search_note").observe_field(2, function() {
var form = $("#query_form"); // grab the form wrapping the search bar.
var url = form.attr("action");
var formData = form.serialize();
$.get(url, formData, function(data) { // perform an AJAX get, the trailing function is what happens on successful get.
$("#contacts_notes").html(data); // replace the "results" div with the result of action taken
});
});
});
</script>
</span>
</h2>
<% end %>
<% else %>
<h2 class="scope_title"><%= "#{l(:label_crm_contact_tag)}(#{@notes_pages.item_count}): #{tag_links(@tag)}".html_safe %> </h2>
<% end %>
</div>
<div id="contacts_notes">
<%= render :partial => 'notes/notes_list' %>
</div>
<% other_formats_links do |f| %>
<%= f.link_to 'Atom', :url => filtered_params.merge(:key => User.current.rss_key) %>
<%= f.link_to 'CSV', :url => filtered_params %>
<% end %>
<% content_for :sidebar do %>
<%= render :partial => 'common/sidebar' %>
<h3><%= l(:label_crm_note_plural) %></h3>
<div id="note_types">
<% collection_for_note_types_select.each do |note_type| %>
<%= radio_button_tag "note_type", note_type[1], filtered_params[:type_id].to_s == note_type[1].to_s, {:onchange => "document.location='#{url_for(filtered_params.merge(:type_id => note_type[1]))}';", :id => "note_type_#{note_type[1]}" }%>
<%= label_tag "note_type_#{note_type[1]}", note_type[0] %>
<br>
<% end %>
</div>
<h3><%= l(:label_crm_tags_plural) %></h3>
<div id="tags">
<span class="tag_list"><%= @tags.map{|tag| content_tag(:span, link_to(tag.name, {:controller => "contacts", :action => "contacts_notes", :project_id => @project, :tag => tag.name}), {}.merge(ContactsSetting.monochrome_tags? ? {:class => "tag-label"} : {:class => "tag-label-color", :style => "background-color: #{tag_color(tag.name)}"}))}.join(' ').html_safe %></span>
</div>
<%= render :partial => 'common/recently_viewed' %>
<% end %>
<% content_for(:header_tags) do %>
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
<%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %>
<% end %>
@@ -0,0 +1,35 @@
<ul>
<%= call_hook(:view_contacts_context_menu_start, {:contacts => @contacts, :can => @can, :back => @back }) %>
<% unless @contact.nil? %>
<li><%= context_menu_link l(:button_edit), {:controller => 'contacts', :action => 'edit', :id => @contact, :project_id => @project}, :class => 'icon-edit', :disabled => !@can[:edit] %></li>
<% if User.current.logged? %>
<li><%= watcher_link(@contact, User.current) %></li>
<% end %>
<% if !@project.nil? %>
<li><%= context_menu_link l(:label_crm_deal_new), {:controller => 'deals', :action => 'new', :project_id => @project, :contact_id => @contact},
:class => 'icon-add-deal', :disabled => !@can[:create_deal] %></li>
<% if @contact.is_company? %>
<li><%= context_menu_link l(:label_crm_add_contact), {:controller => 'contacts', :action => 'new', :project_id => @project, :contact => {:company => @contact.name}},
:class => 'icon-company-contact', :disabled => !@can[:create] %></li>
<% end %>
<% end %>
<% else %>
<li><%= context_menu_link l(:button_edit), {:controller => 'contacts', :action => 'bulk_edit', :ids => @contacts.collect(&:id)},
:class => 'icon-edit', :disabled => !@can[:edit] %></li>
<% end %>
<li><%= context_menu_link l(:label_crm_send_mail), {:controller => 'contacts', :action => 'edit_mails', :ids => @contacts.collect(&:id), :project_id => @project}, :class => 'icon-email', :disabled => !@can[:send_mails] %></li>
<%= call_hook(:view_contacts_context_menu_before_delete, {:contacts => @contacts, :can => @can, :back => @back }) %>
<li><%= context_menu_link l(:button_delete), {:controller => 'contacts', :action => 'bulk_destroy', :ids => @contacts.collect(&:id), :project_id => @project},
:method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon-del', :disabled => !@can[:delete] %></li>
<% if !@contact && Redmine::VERSION.to_s >= '3.3' %>
<li>
<%= context_menu_link l(:button_filter), _project_contacts_path(@project, :set_filter => 1, :ids => @contacts.map(&:id).join(',')),
:class => 'icon-list' %>
</li>
<% end %>
<%= call_hook(:view_contacts_context_menu_end, {:contacts => @contacts, :can => @can, :back => @back }) %>
</ul>
@@ -0,0 +1,13 @@
hideModal();
<% field_id = params[:contact_field_name].to_s.gsub("[", "_").gsub("]", "") -%>
$('select#<%= field_id %>')
.append($("<option></option>")
.attr("value",'<%= @contact.id %>')
.attr("selected",'selected')
.text('<%= @contact.name %>'));
$('input#<%= field_id %>').val('<%= @contact.id %>');
$('#<%= field_id %>_selected_contact').text('<%= @contact.name %>');
$('#<%= field_id %>_selected_contact').show();
$('#<%= field_id %>_selected_contact').scrollTop( 0 );
$('input#<%= field_id %>').hide();
$('#<%= field_id %>_edit_link').show();
+28
View File
@@ -0,0 +1,28 @@
<div class="contextual">
<%= link_to_if_authorized l(:label_crm_merge_duplicate_plural), {:controller => 'contacts_duplicates', :action => 'index', :project_id => @project, :contact_id => @contact}, :class => 'icon icon-merge' unless @contact.new_record? %>
</div>
<h2><%= l(:label_crm_contact_edit_information) %></h2>
<%= labelled_form_for :contact, @contact,
:url => {:action => 'update', :project_id => @project, :id => @contact},
:html => { :multipart => true, :method => :put, :id => "contact_form" } do |f| %>
<%= render :partial => 'form', :locals => {:f => f} %>
<%= render :partial => 'name_observer' %>
<%= submit_tag l(:button_save) -%>
<% end -%>
<% content_for :sidebar do %>
<%= render :partial => 'common/sidebar' %>
<%= render :partial => 'contacts_duplicates/duplicates' %>
<div id="contact_projects">
<%= render :partial => 'contacts_projects/related' %>
</div>
<% end %>
<% content_for :header_tags do %>
<%= javascript_include_tag 'attachments' %>
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
<%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %>
<%= robot_exclusion_tag %>
<% end %>
@@ -0,0 +1,64 @@
<h2><%= l(:label_crm_bulk_send_mail_selected_contacts) %></h2>
<div class="box" id="duplicates">
<ul>
<% @contacts.each do |contact| %>
<li>
<%= avatar_to contact, :size => "16" %>
<%= link_to_source contact %>
<%= "(#{contact.job_title}) " unless contact.job_title.blank? %>
- <%= contact.emails.first %>
</li>
<% end %>
</ul>
</div>
<%= form_for(:email_message, :url => {:action => 'send_mails', :project_id => @project}, :html => {:multipart => true, :id => 'message-form'}) do %>
<%= @contacts.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %>
<div class="box tabular">
<p>
<label><%= l(:field_mail_from) %></label>
<%= text_field_tag('from', "#{User.current.name} <#{User.current.mail}>", :style => "width: 98%;") %>
<%= link_to "#{l(:label_crm_contacts_cc)}/#{l(:label_crm_contacts_bcc)}", '#' , :onclick => "$('#mail_cc').show();$(this).hide();" %>
</p>
<span id="mail_cc" style="display:none;">
<p>
<label><%= l(:label_crm_contacts_cc) %></label>
<%= text_field_tag('cc', '', :style => "width: 98%;") %>
</p>
<p>
<label><%= l(:label_crm_contacts_bcc) %></label>
<%= text_field_tag('bcc', '', :style => "width: 98%;") %>
</p>
</span>
<p>
<label><%= l(:field_subject) %></label>
<%= text_field_tag('subject', '', :id => "subject", :style => "width: 98%;") %>
</p>
<p>
<label><%= l(:field_message) %></label>
<%= text_area_tag 'message-content', '', :cols => 60, :rows => 10, :class => 'wiki-edit' %>
<em class="info"><%= l(:text_email_macros, :macro => "%%NAME%%, %%LAST_NAME%%, %%MIDDLE_NAME%%, %%FULL_NAME%%, %%COMPANY%%, %%DATE%%, %%[Custom field]%%") %></em>
</p>
<%= wikitoolbar_for 'message-content' %>
<p id="attachments_form"><%= label_tag('attachments[1][file]', l(:label_attachment_plural))%><%= render :partial => 'attachments/form' %></p>
</div>
<p>
<%= submit_tag l(:button_submit) %>
<%= preview_link({ :controller => 'contacts', :action => 'preview_email' }, 'message-form') %>
</p>
<% end %>
<div id="preview" class="wiki"></div>
<% content_for :header_tags do %>
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
<% end %>
@@ -0,0 +1,53 @@
api.array :contacts, api_meta(:total_count => @contacts_count, :offset => @offset, :limit => @limit) do
@contacts.each do |contact|
api.contact do
api.id contact.id
api.avatar(:attachment_id => contact.avatar.id) if contact.avatar
api.is_company contact.is_company
api.first_name contact.first_name
api.last_name contact.last_name
api.middle_name contact.middle_name
api.company contact.company
api.website contact.website
api.skype_name contact.skype_name
api.birthday contact.birthday
api.job_title contact.job_title
api.background contact.background
api.author(:id => contact.author_id, :name => contact.author.name) unless contact.author.nil?
api.assigned_to(:id => contact.assigned_to_id, :name => contact.assigned_to.name) unless contact.assigned_to.nil?
api.address do
api.full_address contact.address
api.street contact.street1
api.city contact.city
api.region contact.region
api.country contact.country
api.country_code contact.address.country_code unless contact.address.blank?
api.postcode contact.postcode
end
api.array :phones do
contact.phones.each do |phone|
api.phone do
api.number phone
end
end
end if contact.phones.any?
api.array :emails do
contact.emails.each do |email|
api.email do
api.address email
end
end
end if contact.emails.any?
api.tag_list contact.tag_list
render_api_custom_values contact.custom_field_values, api
api.created_on contact.created_on
api.updated_on contact.updated_on
end
end
end
+203
View File
@@ -0,0 +1,203 @@
<% filtered_params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params %>
<div class="contextual">
<% if !@query.new_record? && @query.editable_by?(User.current) %>
<%= link_to l(:button_contacts_edit_query), edit_crm_query_path(@query, :object_type => "contact"), :class => 'icon icon-edit' %>
<%= link_to l(:button_contacts_delete_query), crm_query_path(@query, :object_type => "contact"), :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, :class => 'icon icon-del' %>
<% end %>
<%= link_to_if_authorized l(:label_crm_contact_new), {:controller => 'contacts', :action => 'new', :project_id => @project}, :class => 'icon icon-add' %>
<%= link_to_if_authorized l(:label_crm_import), {:controller => 'contact_imports', :action => 'new', :project_id => @project}, :class => 'icon icon-import', :id => 'import_from_csv' %>
<%= call_hook(:view_contacts_action_menu) %>
</div>
<% html_title(@query.new_record? ? l(:label_contact_plural) : @query.name) %>
<%= form_tag({ :controller => 'contacts', :action => 'index', :project_id => @project }, :method => :get, :id => 'query_form') do %>
<script type="text/javascript">
jQuery(function($) {
// when the #search field changes
$("#search").observe_field(2, function() {
var form = $("#query_form"); // grab the form wrapping the search bar.
var url = form.attr("action");
form.find('[name="c[]"] option').each(function(i, elem){
$(elem).attr('selected', true)
})
var formData = form.serialize();
form.find('[name="c[]"] option').each(function(i, elem){
$(elem).attr('selected', false)
})
$.get(url, formData, function(data) { // perform an AJAX get, the trailing function is what happens on successful get.
$("#contact_list").html(data); // replace the "results" div with the result of action taken
});
});
});
</script>
<h2 class="contacts_header">
<span id='scope_header' class="scope_title">
<%= @query.new_record? ? l(:label_contact_plural) : h(@query.name) %>
</span>
<span class="live_search">
<%= text_field_tag(:search, params[:search], :autocomplete => "off", :class => "live_search_field", :placeholder => l(:label_crm_contact_search) ) %>
</span>
<span class="tags">
<%= tag_links(@filter_tags) %>
</span>
</h2>
<%= hidden_field_tag 'set_filter', '1' %>
<%= hidden_field_tag 'object_type', 'contact' %>
<div id="query_form_content" class="hide-when-print">
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
</div>
</fieldset>
<fieldset class="collapsible collapsed">
<legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
<div style="display: none;">
<table>
<tr style="<%= 'display: none;' if contacts_list_style != 'list' %>">
<td><%= l(:field_column_names) %></td>
<td><%= render_query_columns_selection(@query) %></td>
</tr>
<tr>
<% if contacts_list_style != 'list_cards' %>
<td><label for='group_by'><%= l(:field_group_by) %></label></td>
<td><%= select_tag('group_by', options_for_select([[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, @query.group_by)) %></td>
<% end -%>
<% if contacts_list_style != 'list' %>
<td><label for='sort'><%= l(:label_sort) %></label></td>
<td><%= select_tag('sort',
options_for_select(
[[]] +
[[l(:field_contact_job_title), "job_title"],
[l(:field_contact_company), "company"],
[l(:field_created_on), "created_on:desc"],
[l(:field_updated_on), "updated_on:desc"]],
params[:sort])
) %></td>
<% end %>
</tr>
<tr>
<td><label for='contacts_list_style'><%= l(:label_crm_list_partial_style) %></label></td>
<td><%= select_tag('contacts_list_style', options_for_select(contact_list_styles_for_select, contacts_list_style)) %></td>
</tr>
</table>
</div>
</fieldset>
</div>
<p class="buttons hide-when-print">
<%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %>
<%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %>
<% if @query.new_record? && User.current.allowed_to?(:save_contacts_queries, @project, :global => true) %>
<%= link_to_function l(:button_save),
"$('#query_form').attr('action', '#{ @project ? new_project_crm_query_path(@project) : new_crm_query_path }'); submit_query_form('query_form')",
:class => 'icon icon-save' %>
<% end %>
</p>
<% end %>
<%= error_messages_for 'query' %>
<% if @query.valid? %>
<div id="contact_list">
<% if @contacts.empty? %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% else %>
<%= render :partial => contacts_list_style %>
<span class="pagination"><%= pagination_links_full @contacts_pages, @contacts_count %></span>
<% end %>
</div>
<% if User.current.allowed_to?(:export_contacts, @project, :global => true) %>
<% other_formats_links do |f| %>
<%= f.link_to 'Atom', :url => filtered_params.merge(:key => User.current.rss_key) %>
<% if contacts_list_style == 'list' %>
<%= f.link_to 'CSV', :url => filtered_params, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
<% else %>
<%= f.link_to 'CSV', :url => filtered_params %>
<% end %>
<%- if ContactsSetting.vcard? -%>
<%= f.link_to 'VCF', :url => filtered_params %>
<%- end -%>
<%- if ContactsSetting.spreadsheet? -%>
<%= f.link_to 'XLS', :url => filtered_params %>
<%- end -%>
<% end %>
<div id="csv-export-options" style="display:none;">
<h3 class="title">
<%= l(:label_export_options, :export_format => 'CSV') %>
</h3>
<%= form_tag({:controller => 'contacts',
:action => 'index',
:project_id => @project,
:format => 'csv'},
:method => :get,
:id => 'csv-export-form') do %>
<% columns_name = 'c[]' %>
<% if Redmine::VERSION::STRING > '3.2.1' %>
<%= query_as_hidden_field_tags(@query) %>
<% if Redmine::VERSION::STRING < '3.4' %>
<%= hidden_field_tag 'sort', @sort_criteria.to_param, :id => nil %>
<% columns_name = 'csv[columns]' %>
<% end %>
<% elsif Redmine::VERSION::STRING < '3.2' %>
<% columns_name = 'columns' %>
<% end %>
<p>
<label>
<%= radio_button_tag columns_name, '', true %>
<%= l(:description_selected_columns) %>
</label>
<br />
<label>
<%= radio_button_tag columns_name, 'all_inline' %>
<%= l(:description_all_columns) %>
</label>
</p>
<p class="buttons">
<%= submit_tag l(:button_export),
:name => nil,
:onclick => "hideModal(this);" %>
<%= submit_tag l(:button_cancel),
:name => nil,
:onclick => "hideModal(this);",
:type => 'button' %>
</p>
<% end %>
</div>
<% end %>
<% end %>
<% content_for :sidebar do %>
<%= render :partial => 'common/sidebar' %>
<%= render :partial => 'tags_cloud', :object => @tags %>
<%= render_sidebar_crm_queries('contact') %>
<%= render :partial => 'notes/last_notes', :object => @last_notes %>
<%= render :partial => 'common/recently_viewed' %>
<%= call_hook(:view_contacts_sidebar_contacts_list_bottom) %>
<% end %>
<% content_for(:header_tags) do %>
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
<%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %>
<%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => l(:label_contact_plural)) %>
<% end %>
<% if Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? %>
<%= context_menu %>
<% else %>
<%= context_menu url_for( {:controller => "contacts", :action => "context_menu"} ) %>
<% end %>
@@ -0,0 +1,5 @@
<%
@tab = params[:tab_name]
@partial = params[:partial]
%>
$('#tab-placeholder-<%= @tab %>').html("<%= j(render :partial => @partial) %>")
+23
View File
@@ -0,0 +1,23 @@
<h2><%= l(:label_crm_contact_new) %></h2>
<%= labelled_form_for :contact, @contact, :url => {:action => 'create', :project_id => @project}, :html => { :multipart => true, :id => 'contact_form'} do |f| %>
<%= render :partial => 'form', :locals => {:f => f} %>
<%= render :partial => 'name_observer' %>
<%= submit_tag l(:button_save) -%>
<%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
<% end -%>
<% content_for :sidebar do %>
<%= render :partial => 'common/sidebar' %>
<%= render :partial => 'contacts_duplicates/duplicates' %>
<%= render :partial => 'contacts_vcf/load' %>
<% end %>
<% content_for :header_tags do %>
<%= javascript_include_tag 'attachments' %>
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
<%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %>
<%= robot_exclusion_tag %>
<% end %>
@@ -0,0 +1,9 @@
$('#ajax-modal').html('<%= escape_javascript(render :partial => 'contacts/new_modal') %>');
$('#ajax-modal #contact_data .extended').hide();
$('#ajax-modal #contact_data #show_details_link').show();
showModal('ajax-modal', '800px');
$('#new_contact').submit( function(event) {
$('.file_selector').val('');
event.preventDefault();
});
@@ -0,0 +1,110 @@
api.contact do
api.id @contact.id
api.avatar(:attachment_id => @contact.avatar.id) if @contact.avatar
api.is_company @contact.is_company
api.first_name @contact.first_name
api.last_name @contact.last_name
api.middle_name @contact.middle_name
api.company @contact.company
api.website @contact.website
api.skype_name @contact.skype_name
api.birthday @contact.birthday
api.job_title @contact.job_title
api.background @contact.background
api.author(:id => @contact.author_id, :name => @contact.author.name) unless @contact.author.nil?
api.assigned_to(:id => @contact.assigned_to_id, :name => @contact.assigned_to.name) unless @contact.assigned_to.nil?
api.address do
api.full_address @contact.address
api.street @contact.street1
api.city @contact.city
api.region @contact.region
api.country @contact.country
api.country_code @contact.address.country_code unless @contact.address.blank?
api.postcode @contact.postcode
end
api.array :phones do
@contact.phones.each do |phone|
api.phone do
api.number phone
end
end
end if @contact.phones.any?
api.array :emails do
@contact.emails.each do |email|
api.email do
api.address email
end
end
end if @contact.emails.any?
api.tag_list @contact.tag_list
render_api_custom_values @contact.custom_field_values, api
api.created_on @contact.created_on
api.updated_on @contact.updated_on
api.array :projects do
@contact.projects.each do |project|
api.project(:id => project.id, :name => project.name)
end
end if @contact.projects.present?
if authorize_for(:notes, :show)
api.array :notes do
@contact.notes.each do |note|
api.note do
api.id note.id
api.content note.content
api.type_id note.type_id
api.author(:id => note.author_id, :name => note.author.name) unless note.author.nil?
api.created_on note.created_on
api.updated_on note.updated_on
end
end
end if include_in_api_response?('notes') && @contact.notes.present? && User.current.allowed_to?(:view_contacts, @project)
end
api.array :contacts do
@contact.company_contacts.each do |contact|
api.contact(:id => contact.id, :name => contact.name )
end
end if include_in_api_response?('contacts') && @contact.company_contacts.present?
api.array :deals do
(@contact.related_deals + @contact.deals).each do |deal|
api.deal do
api.id deal.id
api.price deal.price
api.currency deal.currency
api.price_type deal.price_type
api.name deal.name
api.project(:id => deal.project.id, :name => deal.project.name)
api.status(:id => deal.status.id, :name => deal.status.name)
api.background deal.background
api.created_on deal.created_on
api.updated_on deal.updated_on
end
end
end if include_in_api_response?('deals') && (@contact.related_deals + @contact.deals).present? && User.current.allowed_to?(:view_deals, @project)
if authorize_for(:issues, :show)
api.array :issues do
@contact.issues.each do |issue|
api.issue do
api.id issue.id
api.subject issue.subject
api.status(:id => issue.status_id, :name => issue.status.name) unless issue.status.nil?
api.due_date issue.due_date
api.created_on issue.created_on
api.updated_on issue.updated_on
end
end
end if include_in_api_response?('issues') && @contact.issues.present? && User.current.allowed_to?(:view_issues, @project)
end
call_hook(:api_contacts_show)
end
+86
View File
@@ -0,0 +1,86 @@
<%= error_messages_for 'contact', 'note' %>
<% html_title "#{l(:label_contact)} ##{@contact.id}: #{@contact.name}" %>
<div class="contextual">
<%= call_hook(:view_contacts_before_actions, :contact => @contact, :project => @project) %>
<%= link_to l(:label_profile), user_path(@contact.redmine_user), :class => 'icon icon-user' unless @contact.redmine_user.blank? %>
<%= link_to(l(:button_create), {:controller => 'users', :action => 'new_from_contact', :contact_id => @contact.id, :id => 'current'}, :class => 'icon icon-user') if (User.current.admin? && @contact.redmine_user.blank? && !@contact.email.blank?) %>
<%= link_to_if_authorized l(:label_crm_send_mail), {:controller => 'contacts', :action => 'edit_mails', :ids => [@contact.id], :project_id => @project}, :class => 'icon icon-email' unless @contact.primary_email.blank? %>
<%= watcher_link(@contact, User.current) %>
<%= link_to l(:button_edit), {:controller => 'contacts', :action => 'edit', :project_id => @project, :id => @contact}, :class => 'icon icon-edit' if @contact.editable? %>
<%= link_to l(:button_delete), {:controller => 'contacts', :action => 'destroy', :project_id => @project, :id => @contact}, :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, :class => 'icon icon-del' if @contact.deletable? %>
<%= call_hook(:view_contacts_after_actions, :contact => @contact, :project => @project) %>
</div>
<h2><%= !@contact.is_company ? l(:label_contact) : l(:label_crm_company) %> #<%= @contact.id %></h2>
<div class="contact details<%= ' private' if @contact.is_private? %>">
<table class="subject_header">
<tr>
<td class="avatar"><%= avatar_to(@contact, :size => "64", :full_size => true) %></td>
<td class="name" style="vertical-align: top;">
<h1><%= h @contact.name %></h1>
<% if !@contact.is_company %>
<p>
<%= h @contact.job_title %>
<%= " #{l(:label_crm_at_company)} " unless (@contact.job_title.blank? or @contact.company.blank?) %>
<% if @contact.contact_company %>
<%= link_to @contact.contact_company.name, {:controller => 'contacts', :action => 'show', :project_id => @contact.contact_company.project(@project), :id => @contact.contact_company.id } %>
<% else %>
<%= h @contact.company %>
<% end %>
</p>
<% end %>
<%= render :partial => 'form_tags', :object => @contact.tags, :locals => {:editable => true} %>
</td>
<% if @contact.phones.any? || @contact.emails.any? %>
<td class="subject_info">
<ul>
<% if @contact.phones.any? %>
<li class="phone icon icon-phone"><%= @contact.phones.first %></li>
<% end %>
<% if @contact.emails.any? %>
<li class="email icon icon-email"><%= mail_to @contact.emails.first %></li>
<% end %>
</ul>
</td>
<% end %>
</tr>
</table>
<%= call_hook(:view_contacts_show_details_bottom, :contact => @contact) %>
</div>
<%= render_contact_tabs contact_tabs(@contact) %>
<% content_for :sidebar do %>
<%= render :partial => 'common/sidebar' %>
<%= render :partial => 'attributes' %>
<%= call_hook(:view_contacts_sidebar_after_attributes, :contact => @contact) %>
<%= render :partial => 'contacts_issues/issues', :locals => {:contact => @contact, :issues => @contact_issues} %>
<%= call_hook(:view_contacts_sidebar_after_tasks, :contact => @contact) %>
<%= render :partial => 'common/notes_attachments', :object => @contact.notes_attachments %>
<%= call_hook(:view_contacts_sidebar_after_notes_attachments, :contact => @contact) %>
<% if !@contact.background.blank? %>
<h3><%= l(:label_crm_background_info) %></h3>
<div class="wiki"><%= textilizable(@contact, :background) %></div>
<% end %>
<div id="contact_projects">
<%= render :partial => 'contacts_projects/related' %>
</div>
<%= render :partial => 'common/recently_viewed' %>
<% end %>
<% content_for :header_tags do %>
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
<%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %>
<%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@contact.name} - ##{@contact.id}") %>
<% end %>
@@ -0,0 +1,23 @@
<div id="duplicates">
<% if @contact.duplicates.any? %>
<% if !@contact.new_record? %>
<div class="contextual">
<%= link_to_if_authorized l(:label_crm_merge_duplicate_plural), {:controller => 'contacts_duplicates', :action => 'index', :project_id => @project, :contact_id => @contact} %>
</div>
<% end %>
<h3><%= l(:label_crm_duplicate_plural) %></h3>
<ul class="box">
<% @contact.duplicates.each do |contact| %>
<li>
<%= avatar_to contact, :size => "16" %>
<%= link_to_source contact %>
<%= "(#{contact.job_title}) " unless contact.job_title.blank? %>
</li>
<% end %>
</ul>
<% end %>
</div>
@@ -0,0 +1,8 @@
<% @contacts.each do |contact| %>
<li>
<%= radio_button_tag "duplicate_id", contact.id %>
<%= avatar_to contact, :size => "16" %>
<%= link_to_source contact %>
<%= "(#{contact.job_title}) " unless contact.job_title.blank? %>
</li>
<% end %>
@@ -0,0 +1,54 @@
<%= breadcrumb link_to(@contact.name, note_source_url(@contact)) %>
<div class="contact_data_header">
<table class="note_data">
<tr>
<td class="avatar"><%= link_to avatar_to(@contact, :size => "32"), note_source_url(@contact), :id => "avatar" %> </td>
<td class="name">
<h2 class="note_title">
<%= l(:label_crm_duplicate_for_plural) %>: <%= @contact.name %>
</h2>
<p>
<%= h @contact.job_title %>
<%= " #{l(:label_crm_at_company)} " unless (@contact.job_title.blank? or @contact.company.blank?) %>
<% if @contact.is_company && @contact.contact_company %>
<%= link_to @contact.contact_company.name, note_source_url(@contact.contact_company) %>
<% else %>
<%= h @contact.company %>
<% end %>
</p>
</td>
</tr>
</table>
</div>
<%= form_tag({:controller => 'contacts_duplicates', :action => 'merge', :project_id => @project, :contact_id => @contact}) do %>
<div class="box" id="duplicates">
<%= text_field_tag(:principal_search, params[:topic_search] , :autocomplete => "off", :placeholder => l(:label_crm_contact_search) ) %>
<%= javascript_tag "observeSearchfield('principal_search', 'contact_duplicates', '#{escape_javascript contacts_duplicates_search_path(:contact_id => @contact, :by_email => true)}')" %>
<%= content_tag('div', l(:notice_merged_warning), :class => "flash warning") %>
<ul id="contact_duplicates">
<%= render :partial => 'list' %>
</ul>
</div>
<%= submit_tag l(:label_crm_merge_duplicate_plural) %>
<% end %>
<% html_title "#{l(:label_crm_duplicate_plural)} #{@contact.name}" %>
<% content_for :sidebar do %>
<%= render :partial => 'common/sidebar' %>
<%= render :partial => 'common/recently_viewed' %>
<% end %>
<% content_for :header_tags do %>
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
<%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %>
<%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %>
<% end %>
@@ -0,0 +1,6 @@
<% content_for :header_tags do %>
<%= select2_assets %>
<%= stylesheet_link_tag(:contacts, :plugin => 'redmine_contacts') %>
<%= javascript_include_tag(:contacts_select2, :plugin => 'redmine_contacts') %>
<%= javascript_include_tag(:contacts, :plugin => 'redmine_contacts') %>
<% end %>
@@ -0,0 +1,35 @@
<%= fields_for "issue" do |ff| %>
<%= label_tag 'task_subject', l(:field_subject)%> <br>
<%= ff.text_field :subject %>
<p>
<%= label_tag :assigned_to_id, l(:field_assigned_to)%> <br>
<%= ff.select :assigned_to_id, @project.assignable_users.collect {|m| [m.name, m.id]}, :selected => User.current.id, :include_blank => true %>
</p>
<%= label_tag 'due_date', l(:field_due_date)%> <br>
<%= ff.text_field :due_date, :value => Date.today, :size => 12 %><%= calendar_for('issue_due_date') %><br>
<p>
<%= label_tag :description, l(:field_description)%> <br>
<%= ff.text_area :description, :value => "", :rows => 6, :class => 'wiki-edit' , :style => "width: 98%;" %><br>
</p>
<% if @project.issue_categories.any? %>
<p>
<%= label_tag :category_id, l(:field_category)%> <br>
<%= ff.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
</p>
<% end %>
<p>
<%= label_tag :tracker_id, l(:field_tracker)%> <br>
<%= ff.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]} %>
</p>
<% end %>
<br>
<hr>
<br>
<%= submit_tag l(:button_add), :class => "button-small" %>
@@ -0,0 +1,45 @@
<% if !@issue.blank? && User.current.allowed_to?(:view_contacts, @project) %>
<div id="issue_contacts">
<% if User.current.allowed_to?(:manage_contact_issue_relations, @project) %>
<div class="contextual">
<%= link_to l(:button_add),
{:controller => 'contacts_issues', :action => 'new', :issue_id => @issue},
:remote => true,
:method => 'get' %>
</div>
<% end %>
<h3><%= l(:label_contact_plural) %> </h3>
<ul>
<% @issue.contacts.order_by_name.visible.each do |contact| %>
<li id="contact_<%= contact.id%>">
<%= contact_tag(contact) %>
<%= "(#{contact.job_title}) " unless contact.job_title.blank? %>
<% if User.current.allowed_to?(:delete_contacts, @project) %>
<%= link_to(image_tag('delete.png'),
{ :controller => 'contacts_issues',
:action => 'delete',
:issue_id => @issue,
:project_id => @project,
:id => contact.id},
:remote => true,
:method => :delete,
:data => {:confirm => l(:text_are_you_sure)},
:class => "delete",
:title => l(:button_delete) ) %>
<% end %>
</li>
<% end %>
</ul>
</div>
<% end %>
@@ -0,0 +1,11 @@
<tr id=<%="contact_issue_#{issue_item.id}"%>>
<td class="done_checkbox">
<%= check_box_tag :close, '', issue_item.closed?, :disabled => (issue_item.assigned_to != User.current) || issue_item.closed?, :onclick => "$(this).attr('disabled', 'disabled'); $('#contact_issue_#{issue_item.id}').css('text-decoration', 'line-through');$.post('#{url_for({:controller => "contacts_issues", :action => "close", :issue_id => issue_item.id})}');".html_safe unless Setting.plugin_redmine_contacts[:one_click_close] %>
</td>
<td class="issue_item_subject">
<%= link_to(issue_item.subject, {:controller => :issues, :action => :show, :id => issue_item}, :class => "issue status-#{issue_item.status_id} priority-#{issue_item.priority_id} #{'closed' if issue_item.closed?}") %>
<%= avatar(issue_item.assigned_to, :size => "14", :title => issue_item.assigned_to.name).to_s.html_safe if issue_item.assigned_to %>
</td>
<td style="vertical-align: top; text-align: right;"><%= format_date(issue_item.due_date) %></td>
</tr>

Some files were not shown because too many files have changed in this diff Show More