diff --git a/app/helpers/contract_categories_helper.rb b/app/helpers/contract_categories_helper.rb new file mode 100644 index 0000000..746e913 --- /dev/null +++ b/app/helpers/contract_categories_helper.rb @@ -0,0 +1,2 @@ +module ContractCategoriesHelper +end diff --git a/app/models/contract.rb b/app/models/contract.rb new file mode 100644 index 0000000..fc4a8c2 --- /dev/null +++ b/app/models/contract.rb @@ -0,0 +1,120 @@ +class Contract < ActiveRecord::Base + unloadable + + 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 => 'ContractCategory', :foreign_key => 'category_id' + belongs_to :contact + belongs_to :status, :class_name => "ContractStatus", :foreign_key => "status_id" + has_many :contracts, :class_name => "contract", :foreign_key => "reference_id" + has_many :notes, :as => :source, :class_name => 'ContractNote', :dependent => :delete_all, :order => "created_on DESC" + has_many :contract_processes, :dependent => :delete_all + has_and_belongs_to_many :related_contacts, :class_name => 'Contact', :order => "#{Contact.table_name}.last_name, #{Contact.table_name}.first_name", :uniq => true + + + named_scope :visible, lambda {|*args| { :include => :project, + :conditions => Project.allowed_to_condition(args.first || User.current, :view_contracts)} } + + named_scope :deletable, lambda {|*args| { :include => :project, + :conditions => Project.allowed_to_condition(args.first || User.current, :delete_contracts) }} + + named_scope :live_search, lambda {|search| {:conditions => ["(#{Contract.table_name}.name LIKE ?)", "%#{search}%"] }} + + named_scope :open, :include => :status, :conditions => ["#{ContractStatus.table_name}.is_closed = ?", false] + + acts_as_customizable + acts_as_viewable + acts_as_watchable + acts_as_attachable :view_permission => :view_contracts, + :delete_permission => :edit_contracts + + acts_as_event :datetime => :created_on, + :url => Proc.new {|o| {:controller => 'contracts', :action => 'show', :id => o}}, + :type => 'icon-report', + :title => Proc.new {|o| o.name }, + :description => Proc.new {|o| [o.price, o.contact ? o.contact.name : nil, o.background].join(' ').strip } + + acts_as_searchable :columns => ["#{table_name}.name", + "#{table_name}.background"], + :include => [:project], + # sort by id so that limited eager loading doesn't break with postgresql + :order_column => "#{table_name}.id" + + + + validates_presence_of :name + validates_numericality_of :price, :allow_nil => true + + after_save :create_contract_process + + + include ActionView::Helpers::NumberHelper + include ::ContractsHelper + + def after_initialize + if new_record? + # set default values for new records only + self.status ||= ContractStatus.default + end + end + + def avatar + + end + + def full_name + result = '' + result << self.contact.name + ": " unless self.contact.blank? + result << self.name + end + + def all_contacts + @all_contacts ||= ([self.contact] + self.related_contacts ).uniq + end + + def self.available_users(prj=nil) + cond = "(1=1)" + cond << " AND #{Contract.table_name}.project_id = #{prj.id}" if prj + User.active.find(:all, :select => "DISTINCT #{User.table_name}.*", :joins => "JOIN #{Contract.table_name} ON #{Contract.table_name}.assigned_to_id = #{User.table_name}.id", :conditions => cond, :order => "#{User.table_name}.lastname, #{User.table_name}.firstname") + end + + + def init_contract_process(author) + @current_contract_process ||= ContractProcess.new(:contract => self, :author => (author || User.current)) + @contract_status_before_change = self.new_record? ? nil : self.status_id + updated_on_will_change! + @current_contract_process + end + + def create_contract_process + if @current_contract_process && !(@contract_status_before_change == self.status_id) + @current_contract_process.old_value = @contract_status_before_change + @current_contract_process.value = self.status_id + @current_contract_process.save + # reset current journal + init_contract_process @current_contract_process.author + end + end + + def visible?(usr=nil) + (usr || User.current).allowed_to?(:view_contracts, self.project) + end + + def editable?(usr=nil) + (usr || User.current).allowed_to?(:edit_contracts, self.project) + end + + def destroyable?(usr=nil) + (usr || User.current).allowed_to?(:delete_contracts, self.project) + end + + + def info + result = "" + result = self.status.name if self.status + result = result + " - " + contract_price(self) if !self.price.blank? + result + end + +end diff --git a/app/models/contract_category.rb b/app/models/contract_category.rb new file mode 100644 index 0000000..6789130 --- /dev/null +++ b/app/models/contract_category.rb @@ -0,0 +1,26 @@ +class ContractCategory < ActiveRecord::Base + unloadable + belongs_to :project + has_many :contracts, :foreign_key => 'category_id', :dependent => :nullify + + validates_presence_of :name + 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?(ContractCategory) && reassign_to.project == self.project + Contract.update_all("category_id = #{reassign_to.id}", "category_id = #{id}") + end + destroy_without_reassign + end + + def <=>(category) + name <=> category.name + end + + def to_s; name end +end diff --git a/app/models/contract_custom_field.rb b/app/models/contract_custom_field.rb new file mode 100644 index 0000000..7b4eae2 --- /dev/null +++ b/app/models/contract_custom_field.rb @@ -0,0 +1,7 @@ +class ContractCustomField < ActiveRecord::Base + unloadable + + def type_name + :label_contract_plural + end +end diff --git a/app/models/contract_note.rb b/app/models/contract_note.rb new file mode 100644 index 0000000..8ae74a4 --- /dev/null +++ b/app/models/contract_note.rb @@ -0,0 +1,23 @@ +class ContractNote < Note + unloadable + belongs_to :contract, :foreign_key => :source_id + + acts_as_searchable :columns => ["#{table_name}.content"], + :include => [:contract => :project], + :project_key => "#{Project.table_name}.id", + :permission => :view_contracts, + # sort by id so that limited eager loading doesn't break with postgresql + :order_column => "#{table_name}.id" + + acts_as_activity_provider :type => 'contacts', + :permission => :view_contracts, + :author_key => :author_id, + :find_options => {:include => [:contract => :project], + :conditions => {:source_type => 'Contract'}} + + named_scope :visible, lambda {|*args| { :include => [:contract => :project], + :conditions => Project.allowed_to_condition(args.first || User.current, :view_contracts) + + " AND (#{ContractNote.table_name}.source_type = 'Contract')"} } + + +end diff --git a/app/models/contract_process.rb b/app/models/contract_process.rb new file mode 100644 index 0000000..b87d80e --- /dev/null +++ b/app/models/contract_process.rb @@ -0,0 +1,7 @@ +class ContractProcess < ActiveRecord::Base + unloadable + + belongs_to :author, :class_name => "User", :foreign_key => "author_id" + belongs_to :contract + +end diff --git a/app/models/contract_status.rb b/app/models/contract_status.rb new file mode 100644 index 0000000..7d30831 --- /dev/null +++ b/app/models/contract_status.rb @@ -0,0 +1,78 @@ +class ContractStatus < ActiveRecord::Base + unloadable + has_and_belongs_to_many :projects + has_many :contracts, :foreign_key => 'status_id', :dependent => :nullify + + acts_as_list + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + + def after_save + ContractStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default? + end + + # Returns the default status for new Contracts + def self.default + find(:first, :conditions =>["is_default=?", true]) + end + + # Returns an array of all statuses the given role can switch to + # Uses association cache when called more than one time + def new_statuses_allowed_to(roles, tracker) + if roles && tracker + role_ids = roles.collect(&:id) + new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort + else + [] + end + end + + # Same thing as above but uses a database query + # More efficient than the previous method if called just once + def find_new_statuses_allowed_to(roles, tracker) + if roles && tracker + workflows.find(:all, + :include => :new_status, + :conditions => { :role_id => roles.collect(&:id), + :tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort + else + [] + end + end + + def new_status_allowed_to?(status, roles, tracker) + if status && roles && tracker + !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil? + else + false + end + end + + def color_name + return "#" + "%06x" % self.color unless self.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 Contract.find(:first, :conditions => ["status_id=?", self.id]) + end + + # Deletes associated workflows + def delete_workflows + Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}]) + end + end diff --git a/config/locales/es.yml b/config/locales/es.yml index ae795ac..0012f49 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -162,4 +162,7 @@ es: field_age: Age label_vcf_import: Import from vCard label_mail_from: From - permission_import_contacts: Import contacts \ No newline at end of file + permission_import_contacts: Import contacts + + #2.2.2-rodax + label_contract_plural: Categorías de contratos \ No newline at end of file diff --git a/db/migrate/026_create_contracts.rb b/db/migrate/026_create_contracts.rb new file mode 100644 index 0000000..fd1894b --- /dev/null +++ b/db/migrate/026_create_contracts.rb @@ -0,0 +1,31 @@ +class CreateContracts < ActiveRecord::Migration + def self.up + create_table :contracts do |t| + t.string :name + t.text :background + t.integer :currency + t.integer :duration + t.decimal :price + t.integer :price_type + t.integer :project_id + t.integer :author_id + t.integer :assigned_to_id + t.integer :status_id, :default => 0, :null => false + t.integer :contact_id + t.integer :category_id + t.datetime :created_on + t.datetime :updated_on + t.datetime :start_date + t.datetime :end_date + end + add_index :contracts, :contact_id + add_index :contracts, :status_id + add_index :contracts, :author_id + add_index :contracts, :category_id + + end + + def self.down + drop_table :contracts + end +end diff --git a/db/migrate/027_create_contract_notes.rb b/db/migrate/027_create_contract_notes.rb new file mode 100644 index 0000000..5d32588 --- /dev/null +++ b/db/migrate/027_create_contract_notes.rb @@ -0,0 +1,2 @@ +class CreateContractNotes < ActiveRecord::Migration +end diff --git a/db/migrate/028_create_contract_categories.rb b/db/migrate/028_create_contract_categories.rb new file mode 100644 index 0000000..5b5b037 --- /dev/null +++ b/db/migrate/028_create_contract_categories.rb @@ -0,0 +1,13 @@ +class CreateContractCategories < ActiveRecord::Migration + def self.up + create_table :contract_categories do |t| + t.string :name, :null => false + t.integer :project_id + end + add_index :contract_categories, :project_id + end + + def self.down + drop_table :contract_categories + end +end diff --git a/db/migrate/029_create_contract_custom_fields.rb b/db/migrate/029_create_contract_custom_fields.rb new file mode 100644 index 0000000..9c321c5 --- /dev/null +++ b/db/migrate/029_create_contract_custom_fields.rb @@ -0,0 +1,2 @@ +class CreateContractCustomFields < ActiveRecord::Migration +end diff --git a/db/migrate/031_create_contract_statuses.rb b/db/migrate/031_create_contract_statuses.rb new file mode 100644 index 0000000..ddf4946 --- /dev/null +++ b/db/migrate/031_create_contract_statuses.rb @@ -0,0 +1,19 @@ +class CreateContractStatuses < ActiveRecord::Migration + def self.up + create_table :contract_statuses do |t| + t.string :name, :null => false + t.integer :position + t.boolean :is_default, :default => false, :null => false + t.boolean :is_closed, :default => false, :null => false + t.integer :color, :default => 11184810, :null => false + end + add_index :contract_statuses, [:is_closed] + ContractStatus.create(:name => "Pending", :is_closed => false, :is_default => true, :color => "AAAAAA".hex) + ContractStatus.create(:name => "Won", :is_closed => true, :is_default => false, :color => "008000".hex) + ContractStatus.create(:name => "Lost", :is_closed =>true, :is_default => false, :color => "FF0000".hex) + end + + def self.down + drop_table :contract_statuses + end +end diff --git a/db/migrate/032_create_contract_processes.rb b/db/migrate/032_create_contract_processes.rb new file mode 100644 index 0000000..c722463 --- /dev/null +++ b/db/migrate/032_create_contract_processes.rb @@ -0,0 +1,17 @@ +class CreateContractProcesses < ActiveRecord::Migration + def self.up + create_table :contract_processes do |t| + t.integer :contract_id, :null => false + t.integer :author_id, :null => false + t.integer :old_value + t.integer :value, :null => false + t.datetime :created_at + end + add_index :contract_processes, [:author_id] + add_index :contract_processes, [:contract_id] + end + + def self.down + drop_table :contract_processes + end +end diff --git a/db/migrate/033_create_contract_statuses_projects.rb b/db/migrate/033_create_contract_statuses_projects.rb new file mode 100644 index 0000000..c7b8e3a --- /dev/null +++ b/db/migrate/033_create_contract_statuses_projects.rb @@ -0,0 +1,13 @@ +class CreateContractStatusesProjects < ActiveRecord::Migration + def self.up + create_table :contract_statuses_projects, :id => false do |t| + t.integer :project_id, :default => 0, :null => false + t.integer :contract_status_id, :default => 0, :null => false + end + add_index :contract_statuses_projects, [:project_id, :contract_status_id] + end + + def self.down + drop_table :contract_statuses_projects + end +end diff --git a/test/fixtures/contract_categories.yml b/test/fixtures/contract_categories.yml new file mode 100644 index 0000000..b49c4eb --- /dev/null +++ b/test/fixtures/contract_categories.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 +two: + id: 2 diff --git a/test/fixtures/contract_custom_fields.yml b/test/fixtures/contract_custom_fields.yml new file mode 100644 index 0000000..b49c4eb --- /dev/null +++ b/test/fixtures/contract_custom_fields.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 +two: + id: 2 diff --git a/test/fixtures/contract_notes.yml b/test/fixtures/contract_notes.yml new file mode 100644 index 0000000..b49c4eb --- /dev/null +++ b/test/fixtures/contract_notes.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 +two: + id: 2 diff --git a/test/fixtures/contract_processes.yml b/test/fixtures/contract_processes.yml new file mode 100644 index 0000000..b49c4eb --- /dev/null +++ b/test/fixtures/contract_processes.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 +two: + id: 2 diff --git a/test/fixtures/contract_statuses.yml b/test/fixtures/contract_statuses.yml new file mode 100644 index 0000000..b49c4eb --- /dev/null +++ b/test/fixtures/contract_statuses.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 +two: + id: 2 diff --git a/test/fixtures/contracts.yml b/test/fixtures/contracts.yml new file mode 100644 index 0000000..b49c4eb --- /dev/null +++ b/test/fixtures/contracts.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 +two: + id: 2 diff --git a/test/unit/contract_category_test.rb b/test/unit/contract_category_test.rb new file mode 100644 index 0000000..026eecb --- /dev/null +++ b/test/unit/contract_category_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ContractCategoryTest < ActiveSupport::TestCase + fixtures :contract_categories + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/contract_custom_field_test.rb b/test/unit/contract_custom_field_test.rb new file mode 100644 index 0000000..53e41b4 --- /dev/null +++ b/test/unit/contract_custom_field_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ContractCustomFieldTest < ActiveSupport::TestCase + fixtures :contract_custom_fields + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/contract_note_test.rb b/test/unit/contract_note_test.rb new file mode 100644 index 0000000..fb57a73 --- /dev/null +++ b/test/unit/contract_note_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ContractNoteTest < ActiveSupport::TestCase + fixtures :contract_notes + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/contract_process_test.rb b/test/unit/contract_process_test.rb new file mode 100644 index 0000000..67ddcb9 --- /dev/null +++ b/test/unit/contract_process_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ContractProcessTest < ActiveSupport::TestCase + fixtures :contract_processes + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/contract_status_test.rb b/test/unit/contract_status_test.rb new file mode 100644 index 0000000..9fe8dac --- /dev/null +++ b/test/unit/contract_status_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ContractStatusTest < ActiveSupport::TestCase + fixtures :contract_statuses + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/contracts_test.rb b/test/unit/contracts_test.rb new file mode 100644 index 0000000..ed242e3 --- /dev/null +++ b/test/unit/contracts_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ContractsTest < ActiveSupport::TestCase + fixtures :contracts + + # Replace this with your real tests. + def test_truth + assert true + end +end