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