diff --git a/app/controllers/my_profile/manage_products_controller.rb b/app/controllers/my_profile/manage_products_controller.rb index d031db1..bbb6df9 100644 --- a/app/controllers/my_profile/manage_products_controller.rb +++ b/app/controllers/my_profile/manage_products_controller.rb @@ -111,6 +111,36 @@ class ManageProductsController < ApplicationController end end + def manage_product_details + @product = @profile.products.find(params[:id]) + if request.post? + @product.update_price_details(params[:price_details]) if params[:price_details] + render :partial => 'display_price_details' + else + render :partial => 'manage_product_details' + end + end + + def remove_price_detail + @product = @profile.products.find(params[:product]) + @price_detail = @product.price_details.find(params[:id]) + @product = @price_detail.product + if request.post? + @price_detail.destroy + render :nothing => true + end + end + + def display_price_composition_bar + @product = @profile.products.find(params[:id]) + render :partial => 'price_composition_bar' + end + + def display_inputs_cost + @product = @profile.products.find(params[:id]) + render :partial => 'inputs_cost' + end + def destroy @product = @profile.products.find(params[:id]) if @product.destroy @@ -167,4 +197,18 @@ class ManageProductsController < ApplicationController end end + def create_production_cost + cost = @profile.production_costs.create(:name => params[:id]) + if cost.valid? + cost.save + render :text => {:name => cost.name, + :id => cost.id, + :ok => true + }.to_json + else + render :text => {:ok => false, + :error_msg => _(cost.errors['name']) % {:fn => _('Name')} + }.to_json + end + end end diff --git a/app/helpers/manage_products_helper.rb b/app/helpers/manage_products_helper.rb index f9dce9d..3ba46f3 100644 --- a/app/helpers/manage_products_helper.rb +++ b/app/helpers/manage_products_helper.rb @@ -271,4 +271,23 @@ module ManageProductsHelper return input_amount_used if input.unit.blank? n_('1 %{singular_unit}', '%{num} %{plural_unit}', input.amount_used.to_f) % { :num => input_amount_used, :singular_unit => content_tag('span', input.unit.singular, :class => 'input-unit'), :plural_unit => content_tag('span', input.unit.plural, :class => 'input-unit') } end + + def select_production_cost(product,selected=nil) + url = url_for( :controller => 'manage_products', :action => 'create_production_cost' ) + prompt_msg = _('Insert the name of the new cost:') + error_msg = _('Something went wrong. Please, try again') + select_tag('price_details[][production_cost_id]', + options_for_select(product.available_production_costs.map {|item| [truncate(item.name, 10, '...'), item.id]} + [[_('Other cost'), '']], selected), + {:include_blank => _('Select the cost'), + :class => 'production-cost-selection', + :onchange => "productionCostTypeChange(this, '#{url}', '#{prompt_msg}', '#{error_msg}')"}) + end + + def price_composition_progressbar_text(product, args = {}) + currency = environment.currency_unit + production_cost = args[:production_cost_value] || product.formatted_value(:total_production_cost) + product_price = args[:product_price] || product.formatted_value(:price) + + _("%{currency} %{production_cost} of %{currency} %{product_price}") % {:currency => currency, :production_cost => content_tag('span', production_cost, :class => '.production_cost'), :product_price => content_tag('span', product_price, :class => 'product_price')} + end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index d7b9f96..3359e16 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -6,6 +6,7 @@ class Enterprise < Organization has_many :products, :dependent => :destroy, :order => 'name ASC' has_many :inputs, :through => :products + has_many :production_costs, :as => :owner has_and_belongs_to_many :fans, :class_name => 'Person', :join_table => 'favorite_enteprises_people' diff --git a/app/models/environment.rb b/app/models/environment.rb index a72ea35..0896682 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -174,6 +174,7 @@ class Environment < ActiveRecord::Base acts_as_accessible has_many :units, :order => 'position' + has_many :production_costs, :as => :owner def superior_intances [self, nil] diff --git a/app/models/input.rb b/app/models/input.rb index 97fef16..541fbc4 100644 --- a/app/models/input.rb +++ b/app/models/input.rb @@ -55,4 +55,8 @@ class Input < ActiveRecord::Base true end + def cost + return 0 if self.amount_used.blank? || self.price_per_unit.blank? + self.amount_used * self.price_per_unit + end end diff --git a/app/models/price_detail.rb b/app/models/price_detail.rb new file mode 100644 index 0000000..3cde3de --- /dev/null +++ b/app/models/price_detail.rb @@ -0,0 +1,27 @@ +class PriceDetail < ActiveRecord::Base + + belongs_to :product + validates_presence_of :product_id + + belongs_to :production_cost + validates_presence_of :production_cost_id + validates_uniqueness_of :production_cost_id, :scope => :product_id + + def price + self[:price] || 0 + end + + include FloatHelper + def price=(value) + if value.is_a?(String) + super(decimal_to_float(value)) + else + super(value) + end + end + + def formatted_value(value) + ("%.2f" % self[value]).to_s.gsub('.', product.enterprise.environment.currency_separator) if self[value] + end + +end diff --git a/app/models/product.rb b/app/models/product.rb index 5caecf9..546dd9b 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -5,6 +5,8 @@ class Product < ActiveRecord::Base has_many :product_qualifiers has_many :qualifiers, :through => :product_qualifiers has_many :inputs, :dependent => :destroy, :order => 'position' + has_many :price_details, :dependent => :destroy + has_many :production_costs, :through => :price_details validates_uniqueness_of :name, :scope => :enterprise_id, :allow_nil => true validates_presence_of :product_category_id @@ -101,8 +103,9 @@ class Product < ActiveRecord::Base enterprise.public_profile end - def formatted_value(value) - ("%.2f" % self[value]).to_s.gsub('.', enterprise.environment.currency_separator) if self[value] + def formatted_value(method) + value = self[method] || self.send(method) + ("%.2f" % value).to_s.gsub('.', enterprise.environment.currency_separator) if value end def price_with_discount @@ -175,4 +178,43 @@ class Product < ActiveRecord::Base true end + def inputs_cost + return 0 if inputs.empty? + inputs.map(&:cost).inject { |sum,price| sum + price } + end + + def total_production_cost + return inputs_cost if price_details.empty? + inputs_cost + price_details.map(&:price).inject { |sum,price| sum + price } + end + + def price_described? + return false if price.nil? + (price - total_production_cost).zero? + end + + def update_price_details(price_details) + self.price_details.destroy_all + price_details.each do |price_detail| + self.price_details.create(price_detail) + end + end + + def price_description_percentage + total_production_cost * 100 / price + end + + def available_production_costs + self.enterprise.environment.production_costs + self.enterprise.production_costs + end + + include ActionController::UrlWriter + def price_composition_bar_display_url + url_for({:host => enterprise.default_hostname, :controller => 'manage_products', :action => 'display_price_composition_bar', :profile => enterprise.identifier, :id => self.id }.merge(Noosfero.url_options)) + end + + def inputs_cost_update_url + url_for({:host => enterprise.default_hostname, :controller => 'manage_products', :action => 'display_inputs_cost', :profile => enterprise.identifier, :id => self.id }.merge(Noosfero.url_options)) + end + end diff --git a/app/models/production_cost.rb b/app/models/production_cost.rb new file mode 100644 index 0000000..f1e0025 --- /dev/null +++ b/app/models/production_cost.rb @@ -0,0 +1,8 @@ +class ProductionCost < ActiveRecord::Base + + belongs_to :owner, :polymorphic => true + validates_presence_of :owner + validates_presence_of :name + validates_length_of :name, :maximum => 30, :allow_blank => true + validates_uniqueness_of :name, :scope => [:owner_id, :owner_type] +end diff --git a/app/views/layouts/_javascript.rhtml b/app/views/layouts/_javascript.rhtml index 8640c18..65ee210 100644 --- a/app/views/layouts/_javascript.rhtml +++ b/app/views/layouts/_javascript.rhtml @@ -1,4 +1,3 @@ -<%= javascript_include_tag :defaults, 'jquery-latest.js', 'jquery.noconflict.js', 'jquery.cycle.all.min.js', 'thickbox.js', 'lightbox', 'jquery-ui-1.8.2.custom.min', 'jquery.scrollTo', 'jquery.form.js', 'jquery.cookie', 'reflection', 'add-and-join', 'jquery.tokeninput', 'report-abuse','colorbox', 'jquery-validation/jquery.validate', 'catalog', :cache => 'cache-general' %> - +<%= javascript_include_tag :defaults, 'jquery-latest.js', 'jquery.noconflict.js', 'jquery.cycle.all.min.js', 'thickbox.js', 'lightbox', 'jquery-ui-1.8.2.custom.min', 'jquery.scrollTo', 'jquery.form.js', 'jquery.cookie', 'reflection', 'add-and-join', 'jquery.tokeninput', 'report-abuse','colorbox', 'jquery-validation/jquery.validate', 'catalog', 'manage-products', :cache => 'cache-general' %> <% language = FastGettext.locale %> <%= javascript_include_tag 'jquery-validation/localization/messages_'+language, 'jquery-validation/localization/methods_'+language %> diff --git a/app/views/layouts/application.rhtml b/app/views/layouts/application.rhtml index 67c28f4..5159ac0 100644 --- a/app/views/layouts/application.rhtml +++ b/app/views/layouts/application.rhtml @@ -27,11 +27,8 @@ <%# Add custom tags/styles/etc via content_for %> <%= yield :head %> <%= javascript_tag('render_all_jquery_ui_widgets()') %> - + + <%= render :partial => 'shared/numbers_only_javascript' %> + +
+ +
diff --git a/app/views/manage_products/_edit_info.rhtml b/app/views/manage_products/_edit_info.rhtml index e4cff79..b904136 100644 --- a/app/views/manage_products/_edit_info.rhtml +++ b/app/views/manage_products/_edit_info.rhtml @@ -49,15 +49,12 @@ <%= hidden_field_tag "product[qualifiers_list]" %> <% end %> + <%= hidden_field_tag 'info-bar-update-url', @product.price_composition_bar_display_url, :class => 'bar-update-url' %> + <% button_bar do %> <%= submit_button :save, _('Save') %> <%= cancel_edit_product_link(@product, 'info') %> <% end %> <% end %> -<% javascript_tag do %> - jQuery(".numbers-only").keypress(function(event) { - var separator = "<%= environment.currency_separator %>" - return numbersonly(event, separator) - }); -<% end %> +<%= render :partial => 'shared/numbers_only_javascript' %> diff --git a/app/views/manage_products/_edit_input.rhtml b/app/views/manage_products/_edit_input.rhtml index 62c78cb..a349d43 100644 --- a/app/views/manage_products/_edit_input.rhtml +++ b/app/views/manage_products/_edit_input.rhtml @@ -1,5 +1,9 @@ <% form_for(@input, :url => {:controller => 'manage_products', :action => 'edit_input', :id => @input}, :html => {:method => 'post', :id => "edit-input-#{ @input.id }-form"}) do |f| %> + + <%= hidden_field_tag 'input-bar-update-url', @input.product.price_composition_bar_display_url, :class => 'bar-update-url' %> + <%= hidden_field_tag 'inputs-cost-update-url', @input.product.inputs_cost_update_url %> + diff --git a/app/views/manage_products/_edit_price_details.rhtml b/app/views/manage_products/_edit_price_details.rhtml new file mode 100644 index 0000000..d70d4be --- /dev/null +++ b/app/views/manage_products/_edit_price_details.rhtml @@ -0,0 +1,14 @@ +<% price_details.each do |price_detail| %> + + + + +<% end %> + +<%= render :partial => 'shared/numbers_only_javascript' %> diff --git a/app/views/manage_products/_inputs_cost.rhtml b/app/views/manage_products/_inputs_cost.rhtml new file mode 100644 index 0000000..9db5114 --- /dev/null +++ b/app/views/manage_products/_inputs_cost.rhtml @@ -0,0 +1,5 @@ +<%= float_to_currency(@product.inputs_cost) %> + + diff --git a/app/views/manage_products/_manage_product_details.rhtml b/app/views/manage_products/_manage_product_details.rhtml new file mode 100644 index 0000000..2347946 --- /dev/null +++ b/app/views/manage_products/_manage_product_details.rhtml @@ -0,0 +1,37 @@ +
+ <%= render :partial => 'price_composition_bar' %> +
+ +<% form_tag({:action => 'manage_product_details'}, :method => 'post', :id => 'manage-product-details-form') do %> +
+
<%= f.label :amount_used, label_amount_used(@input), :class => 'formlabel' %>
<%= select_production_cost(@product, price_detail.production_cost_id) %><%= labelled_form_field(environment.currency_unit, text_field_tag('price_details[][price]', price_detail.formatted_value(:price), :class => 'numbers-only price-details-price')) %> + <%= link_to_remote(_('Remove'), + :update => "price-detail-#{price_detail.id}", + :success => "jQuery('#manage-product-details-form input.submit').removeAttr('disabled').removeClass('disabled');", + :confirm => _('Are you sure that you want to remove this cost?'), + :url => { :action => 'remove_price_detail', :id => price_detail, :product => @product }) %> +
+ + + + + + <%= render :partial => 'edit_price_details', :locals => {:price_details => @product.price_details} %> +
<%= _('Inputs') %><%= float_to_currency(@product.inputs_cost) %><%= _('This value is composed by the total value of registered inputs') %>
+ + + <%= hidden_field(:product, :inputs_cost) %> + <%= hidden_field(:product, :price) %> + + <% button_bar do %> + <%= submit_button :save, _('Save'), :disabled => '', :class => 'disabled' %> + <%= button(:cancel, _('Cancel'), '#', :class => 'cancel-price-details', 'data-confirm' => _('If you leave, you will lose all unsaved information. Are you sure you want to quit?')) %> + <%= button(:add, _('New cost'), '#', :id => 'add-new-cost') %> + + <% end %> + +<% end %> + +
+ + + + + + +
<%= select_production_cost(@product) %><%= labelled_form_field(environment.currency_unit, text_field_tag('price_details[][price]', nil, :class => 'price-details-price')) %><%= link_to(_('Cancel'), '#', {:class => 'cancel-new-cost'}) %>
+
diff --git a/app/views/manage_products/_price_composition_bar.rhtml b/app/views/manage_products/_price_composition_bar.rhtml new file mode 100644 index 0000000..a9887d7 --- /dev/null +++ b/app/views/manage_products/_price_composition_bar.rhtml @@ -0,0 +1,22 @@ +<% javascript_tag do %> + var value = <%= @product.price_description_percentage %> + var total_cost = <%= @product.total_production_cost %> + var price = <%= @product.price %> + var described = false; + if (<%= @product.price_described? %>) { + var described = true; + } + priceCompositionBar(value,described,total_cost,price); +<% end %> + +
+
+
+
+ <%= price_composition_progressbar_text(@product) %> +
+
+
+ + +
diff --git a/app/views/manage_products/_price_details_button.rhtml b/app/views/manage_products/_price_details_button.rhtml new file mode 100644 index 0000000..8ee8eaf --- /dev/null +++ b/app/views/manage_products/_price_details_button.rhtml @@ -0,0 +1,10 @@ +<%= edit_ui_button( + _('Describe here the cost of production'), + {:action => 'manage_product_details', :id => @product.id}, + :id => 'manage-product-details-button', + 'data-primary-icon' => 'ui-icon-pencil', + 'data-secondary-icon' => 'ui-icon-triangle-1-s', + :title => _('Describe details about how the price was defined') +) %> +<%= javascript_tag("render_jquery_ui_buttons('manage-product-details-button')") %> + diff --git a/app/views/manage_products/show.rhtml b/app/views/manage_products/show.rhtml index ba5c3bf..fa118b7 100644 --- a/app/views/manage_products/show.rhtml +++ b/app/views/manage_products/show.rhtml @@ -23,7 +23,7 @@
- <% unless !@allowed_user && (@product.description.blank? && @product.inputs.empty?) %> + <% unless !@allowed_user && (@product.description.blank? && @product.inputs.empty? && !@product.price_described? ) %>
<%= render :partial => 'manage_products/display_description' %> @@ -39,6 +42,12 @@
<%= render :partial => 'manage_products/display_inputs' %>
+ <% if @product.price_described? || @allowed_user %> +
+ <%= render :partial => 'manage_products/display_price_details' %> + <%= render :partial => 'manage_products/price_details_button' %> +
+ <% end %>
<% end %> diff --git a/app/views/shared/_numbers_only_javascript.rhtml b/app/views/shared/_numbers_only_javascript.rhtml new file mode 100644 index 0000000..6d77641 --- /dev/null +++ b/app/views/shared/_numbers_only_javascript.rhtml @@ -0,0 +1,6 @@ +<% javascript_tag do %> + jQuery(".numbers-only").keypress(function(event) { + var separator = "<%= environment.currency_separator %>" + return numbersonly(event, separator) + }); +<% end %> diff --git a/db/migrate/20110403184315_create_production_cost.rb b/db/migrate/20110403184315_create_production_cost.rb new file mode 100644 index 0000000..027d3a4 --- /dev/null +++ b/db/migrate/20110403184315_create_production_cost.rb @@ -0,0 +1,13 @@ +class CreateProductionCost < ActiveRecord::Migration + def self.up + create_table :production_costs do |t| + t.string :name + t.references :owner, :polymorphic => true + t.timestamps + end + end + + def self.down + drop_table :production_costs + end +end diff --git a/db/migrate/20110403193953_create_price_details.rb b/db/migrate/20110403193953_create_price_details.rb new file mode 100644 index 0000000..41e55d2 --- /dev/null +++ b/db/migrate/20110403193953_create_price_details.rb @@ -0,0 +1,14 @@ +class CreatePriceDetails < ActiveRecord::Migration + def self.up + create_table :price_details do |t| + t.decimal :price, :default => 0 + t.references :product + t.references :production_cost + t.timestamps + end + end + + def self.down + drop_table :price_details + end +end diff --git a/db/schema.rb b/db/schema.rb index 1f48a0b..f8285a1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -323,6 +323,14 @@ ActiveRecord::Schema.define(:version => 20111004184104) do t.datetime "updated_at" end + create_table "price_details", :force => true do |t| + t.decimal "price", :default => 0.0 + t.integer "product_id" + t.integer "production_cost_id" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "product_categorizations", :force => true do |t| t.integer "category_id" t.integer "product_id" @@ -342,6 +350,14 @@ ActiveRecord::Schema.define(:version => 20111004184104) do t.datetime "updated_at" end + create_table "production_costs", :force => true do |t| + t.string "name" + t.integer "owner_id" + t.string "owner_type" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "products", :force => true do |t| t.integer "enterprise_id" t.integer "product_category_id" diff --git a/features/manage_product_price_details.feature b/features/manage_product_price_details.feature new file mode 100644 index 0000000..a08abf8 --- /dev/null +++ b/features/manage_product_price_details.feature @@ -0,0 +1,148 @@ + +Feature: manage product price details + As an enterprise owner + I want to manage the details of product's price + + Background: + Given the following users + | login | name | + | joaosilva | Joao Silva | + And the following enterprises + | identifier | owner | name | enabled | + | redemoinho | joaosilva | Rede Moinho | true | + Given the following product_category + | name | + | Music | + And the following product_categories + | name | parent | + | Rock | music | + | CD Player | music | + And the following product + | owner | category | name | price | + | redemoinho | rock | Abbey Road | 80.0 | + And feature "disable_products_for_enterprises" is disabled on environment + And the following inputs + | product | category | price_per_unit | amount_used | + | Abbey Road | Rock | 10.0 | 2 | + | Abbey Road | CD Player | 20.0 | 2 | + And the following production cost + | name | owner | + | Taxes | environment | + + @selenium + Scenario: list total value of inputs as price details + Given I am logged in as "joaosilva" + When I go to Rede Moinho's page of product Abbey Road + And I follow "Describe here the cost of production" + Then I should see "Inputs" + And I should see "60.0" within ".inputs-cost" + + @selenium + Scenario: cancel management of price details + Given I am logged in as "joaosilva" + When I go to Rede Moinho's page of product Abbey Road + And I follow "Describe here the cost of production" + When I follow "Cancel" + Then I should see "Describe here the cost of production" + + @selenium + Scenario: return to product after save + Given I am logged in as "joaosilva" + When I go to Rede Moinho's page of product Abbey Road + And I follow "Describe here the cost of production" + And I press "Save" + Then I should be on Rede Moinho's page of product Abbey Road + + @selenium + Scenario: add first item on price details + Given I am logged in as "joaosilva" + When I go to Rede Moinho's page of product Abbey Road + And I follow "Describe here the cost of production" + And I follow "New cost" + And I select "Taxes" + And I fill in "$" with "5.00" + And I press "Save" + Then I should not see "Save" + And I should see "Describe here the cost of production" + + @selenium + Scenario: edit a production cost + Given the following production cost + | name | owner | + | Energy | environment | + Given I am logged in as "joaosilva" + When I go to Rede Moinho's page of product Abbey Road + And I follow "Describe here the cost of production" + And I follow "New cost" + And I select "Taxes" + And I fill in "$" with "20.00" + And I press "Save" + Then I should not see "Save" + And I should see "Taxes" within "#display-price-details" + When I follow "Describe here the cost of production" + And I select "Energy" + And I press "Save" + And I should not see "Taxes" within "#display-price-details" + And I should see "Energy" within "#display-price-details" + + Scenario: not display product detail button if product does not have input + Given the following product + | owner | category | name | + | redemoinho | rock | Yellow Submarine | + And the following user + | login | name | + | mariasouza | Maria Souza | + And I am logged in as "mariasouza" + When I go to Rede Moinho's page of product Yellow Submarine + Then I should not see "Describe here the cost of production" + + Scenario: not display price details if price is not fully described + Given I go to Rede Moinho's page of product Abbey Road + Then I should not see "60.0" + + @selenium + Scenario: display price details if price is fully described + Given I am logged in as "joaosilva" + And I go to Rede Moinho's page of product Abbey Road + And I follow "Describe here the cost of production" + And I follow "New cost" + And I select "Taxes" + And I fill in "$" with "20.00" + And I press "Save" + Then I should see "Inputs" within ".price-detail-name" + And I should see "60.0" within ".price-detail-price" + + @selenium + Scenario: create a new cost clicking on select + Given I am logged in as "joaosilva" + And I go to Rede Moinho's page of product Abbey Road + And I follow "Describe here the cost of production" + And I want to add "Energy" as cost + And I select "Other cost" + And I press "Save" + When I follow "Describe here the cost of production" + Then I should see "Energy" within ".production-cost-selection" + + @selenium + Scenario: add created cost on new-cost-fields + Given I am logged in as "joaosilva" + And I go to Rede Moinho's page of product Abbey Road + And I follow "Describe here the cost of production" + And I want to add "Energy" as cost + And I select "Other cost" + Then I should see "Energy" within "#new-cost-fields" + + @selenium + Scenario: remove price detail + Given the following price detail + | product | production_cost | price | + | Abbey Road | Taxes | 20.0 | + And I am logged in as "joaosilva" + And I go to Rede Moinho's page of product Abbey Road + And I follow "Describe here the cost of production" + And I should see "Taxes" within "#manage-product-details-form" + When I follow "Remove" within "#manage-product-details-form" + And I confirm + And I press "Save" + And I follow "Describe here the cost of production" + Then I should not see "Taxes" within "#manage-product-details-form" diff --git a/features/step_definitions/noosfero_steps.rb b/features/step_definitions/noosfero_steps.rb index 0a075f7..61f2bd9 100644 --- a/features/step_definitions/noosfero_steps.rb +++ b/features/step_definitions/noosfero_steps.rb @@ -222,6 +222,22 @@ Given /^the following certifiers$/ do |table| end end +Given /^the following production costs?$/ do |table| + table.hashes.map{|item| item.dup}.each do |item| + owner_type = item.delete('owner') + owner = owner_type == 'environment' ? Environment.default : Profile[owner_type] + ProductionCost.create!(item.merge(:owner => owner)) + end +end + +Given /^the following price details?$/ do |table| + table.hashes.map{|item| item.dup}.each do |item| + product = Product.find_by_name item.delete('product') + production_cost = ProductionCost.find_by_name item.delete('production_cost') + product.price_details.create!(item.merge(:production_cost => production_cost)) + end +end + Given /^I am logged in as "(.+)"$/ do |username| visit('/account/logout') visit('/account/login') @@ -521,3 +537,7 @@ Given /^the following enterprise homepages?$/ do |table| ent.articles << home end end + +And /^I want to add "([^\"]*)" as cost$/ do |string| + selenium.answer_on_next_prompt(string) +end diff --git a/public/javascripts/manage-products.js b/public/javascripts/manage-products.js new file mode 100644 index 0000000..72034fa --- /dev/null +++ b/public/javascripts/manage-products.js @@ -0,0 +1,141 @@ +(function($) { + + $("#manage-product-details-button").live('click', function() { + $("#product-price-details").find('.loading-area').addClass('small-loading'); + url = $(this).attr('href'); + $.get(url, function(data){ + $("#manage-product-details-button").hide(); + $("#display-price-details").hide(); + $("#display-manage-price-details").html(data); + $("#product-price-details").find('.loading-area').removeClass('small-loading'); + }); + return false; + }); + + $(".cancel-price-details").live('click', function() { + if ( !$(this).hasClass('form-changed') ) { + cancelPriceDetailsEdition(); + } else { + if (confirm($(this).attr('data-confirm'))) { + cancelPriceDetailsEdition(); + } + } + return false; + }); + + $("#manage-product-details-form").live('submit', function(data) { + var form = this; + $(form).find('.loading-area').addClass('small-loading'); + $(form).css('cursor', 'progress'); + $.post(form.action, $(form).serialize(), function(data) { + $("#display-manage-price-details").html(data); + $("#manage-product-details-button").show(); + }); + if ($('#progressbar-icon').hasClass('ui-icon-check')) { + display_notice($('#price-described-notice').show()); + } + return false; + }); + + $("#add-new-cost").live('click', function() { + $('#display-product-price-details tbody').append($('#new-cost-fields tbody').html()); + return false; + }); + + $(".cancel-new-cost").live('click', function() { + $(this).parents('tr').remove(); + return false; + }); + + $("#product-info-form").live('submit', function(data) { + var form = this; + updatePriceCompositionBar(form); + }); + + $("form.edit_input").live('submit', function(data) { + var form = this; + updatePriceCompositionBar(form); + inputs_cost_update_url = $(form).find('#inputs-cost-update-url').val(); + $.get(inputs_cost_update_url, function(data){ + $(".inputs-cost").html(data); + }); + return false; + }); + + $("#manage-product-details-form .price-details-price").live('keydown', function(data) { + $('.cancel-price-details').addClass('form-changed'); + var product_price = parseFloat($('form #product_price').val()); + var total_cost = parseFloat($('#product_inputs_cost').val()); + + $('form .price-details-price').each(function() { + total_cost = total_cost + parseFloat($(this).val()); + }); + enablePriceDetailSubmit(); + + var described = (product_price - total_cost) == 0; + var percentage = total_cost * 100 / product_price; + priceCompositionBar(percentage, described, total_cost, product_price); + }); + + function cancelPriceDetailsEdition() { + $("#manage-product-details-button").show(); + $("#display-price-details").show(); + $("#display-manage-price-details").html(''); + }; + + function updatePriceCompositionBar(form) { + bar_url = $(form).find('.bar-update-url').val(); + $.get(bar_url, function(data){ + $("#price-composition-bar").html(data); + }); + }; + + function enablePriceDetailSubmit() { + $('#manage-product-details-form input.submit').removeAttr("disabled").removeClass('disabled'); + }; + +})(jQuery); + +function productionCostTypeChange(select, url, question, error_msg) { + if (select.value == '') { + var newType = prompt(question); + jQuery.ajax({ + url: url + "/" + newType, + dataType: 'json', + success: function(data, status, ajax){ + if (data.ok) { + var opt = jQuery(''); + opt.insertBefore(jQuery("option:last", select)); + select.selectedIndex = select.options.length - 2; + opt.clone().insertBefore('#new-cost-fields .production-cost-selection option:last'); + } else { + alert(data.error_msg); + } + }, + error: function(ajax, status, error){ + alert(error_msg); + } + }); + } +} + +function priceCompositionBar(value, described, total_cost, price) { + jQuery(function($) { + var bar_area = $('#price-composition-bar'); + $(bar_area).find('#progressbar').progressbar({ + value: value + }); + $(bar_area).find('.production-cost').html(total_cost.toFixed(2)); + $(bar_area).find('.product_price').html(price.toFixed(2)); + if (described) { + $(bar_area).find('#progressbar-icon').addClass('ui-icon-check'); + $(bar_area).find('#progressbar-icon').attr('title', $('#price-described-message').html()); + $(bar_area).find('div.ui-progressbar-value').addClass('price-described'); + } else { + $(bar_area).find('#progressbar-icon').removeClass('ui-icon-check'); + $(bar_area).find('#progressbar-icon').attr('title', $('#price-not-described-message').html()); + $(bar_area).find('div.ui-progressbar-value').removeClass('price-described'); + + } + }); +} diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 35e7e78..11ada2c 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -3383,6 +3383,86 @@ div#activation_enterprise div { font-weight: bold; } +/* * * * * * Price details * * * * * */ + +#display-price-details .price-details-list { + padding-left: 0px; +} + +#display-price-details .price-details-list li { + list-style: none; +} + +#display-price-details .price-details-list li .price-detail-name { + width: 200px; +} + +#display-price-details .price-details-list li .price-detail-name, +#display-price-details .price-details-list li .price-detail-price { + display: inline-block; +} + +#manage-product-details-form .formlabel, +#manage-product-details-form .formfield { + display: inline-block; +} + +#manage-product-details-form #add-new-cost { + float: right; +} + +/* * * Progress bar on price details edition * * */ + +#display-manage-price-details .ui-widget-content { + border: 1px solid #DDD; +} + +#display-manage-price-details .ui-progressbar { + height: 20px; +} + +#display-manage-price-details .ui-progressbar .ui-progressbar-value { + margin: 0px; + background-color: #A40000; + filter:alpha(opacity=70); + -moz-opacity: 0.7; + opacity: 0.7; +} + +#display-manage-price-details .ui-progressbar .ui-progressbar-value.price-described { + background-color: #4E9A06; +} + +#display-manage-price-details #price-details-info { + margin: 10px 0px; +} + +#display-manage-price-details #details-progressbar { + position: relative; + width: 410px; + display: inline-block; +} + +#display-manage-price-details #progressbar-text { + position: absolute; + top: 5px; + right: 7px; + font-size: 11px; +} + +#display-manage-price-details #progressbar-icon { + display: inline-block; + cursor: pointer; +} + +#display-manage-price-details #details-progressbar .ui-corner-left, +#display-manage-price-details #details-progressbar .ui-corner-right { + -moz-border-radius-bottomleft: 0px; + -moz-border-radius-bottomright: 0px; + -moz-border-radius-topleft: 0px; + -moz-border-radius-topright: 0px; +} + /* ==> public/stylesheets/controller_cms.css <== */ diff --git a/test/factories.rb b/test/factories.rb index 0df1bf7..06956d0 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -449,4 +449,12 @@ module Noosfero::Factory { :singular => 'Litre', :plural => 'Litres', :environment_id => 1 } end + ############################################### + # Production Cost + ############################################### + + def defaults_for_production_cost + { :name => 'Production cost ' + factory_num_seq.to_s } + end + end diff --git a/test/functional/manage_products_controller_test.rb b/test/functional/manage_products_controller_test.rb index 58442de..4db6d8f 100644 --- a/test/functional/manage_products_controller_test.rb +++ b/test/functional/manage_products_controller_test.rb @@ -468,4 +468,47 @@ class ManageProductsControllerTest < Test::Unit::TestCase assert_response 403 end + should 'remove price detail of a product' do + product = fast_create(Product, :enterprise_id => @enterprise.id, :product_category_id => @product_category.id) + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') + detail = product.price_details.create(:production_cost_id => cost.id, :price => 10) + + assert_equal [detail], product.price_details + + post :remove_price_detail, :id => detail.id, :product => product, :profile => @enterprise.identifier + product.reload + assert_equal [], product.price_details + end + + should 'create a production cost for enterprise' do + get :create_production_cost, :profile => @enterprise.identifier, :id => 'Taxes' + + assert_equal ['Taxes'], Enterprise.find(@enterprise.id).production_costs.map(&:name) + resp = ActiveSupport::JSON.decode(@response.body) + assert_equal 'Taxes', resp['name'] + assert resp['id'].kind_of?(Integer) + assert resp['ok'] + assert_nil resp['error_msg'] + end + + should 'display error if production cost has no name' do + get :create_production_cost, :profile => @enterprise.identifier + + resp = ActiveSupport::JSON.decode(@response.body) + assert_nil resp['name'] + assert_nil resp['id'] + assert !resp['ok'] + assert_match /blank/, resp['error_msg'] + end + + should 'display error if name of production cost is too long' do + get :create_production_cost, :profile => @enterprise.identifier, :id => 'a'*60 + + resp = ActiveSupport::JSON.decode(@response.body) + assert_nil resp['name'] + assert_nil resp['id'] + assert !resp['ok'] + assert_match /too long/, resp['error_msg'] + end + end diff --git a/test/unit/enterprise_test.rb b/test/unit/enterprise_test.rb index b454779..049324b 100644 --- a/test/unit/enterprise_test.rb +++ b/test/unit/enterprise_test.rb @@ -446,4 +446,8 @@ class EnterpriseTest < Test::Unit::TestCase assert_equal false, enterprise.receives_scrap_notification? end + should 'have production cost' do + e = fast_create(Enterprise) + assert_respond_to e, :production_costs + end end diff --git a/test/unit/environment_test.rb b/test/unit/environment_test.rb index 2437213..27901a8 100644 --- a/test/unit/environment_test.rb +++ b/test/unit/environment_test.rb @@ -1216,4 +1216,7 @@ class EnvironmentTest < Test::Unit::TestCase assert_not_includes environment.enabled_plugins, plugin end + should 'have production costs' do + assert_respond_to Environment.default, :production_costs + end end diff --git a/test/unit/input_test.rb b/test/unit/input_test.rb index 6f9ed41..300e401 100644 --- a/test/unit/input_test.rb +++ b/test/unit/input_test.rb @@ -162,4 +162,19 @@ class InputTest < Test::Unit::TestCase assert_kind_of Unit, input.build_unit end + should 'calculate cost of input' do + input = Input.new(:amount_used => 10, :price_per_unit => 2.00) + assert_equal 20.00, input.cost + end + + should 'cost 0 if amount not defined' do + input = Input.new(:price_per_unit => 2.00) + assert_equal 0.00, input.cost + end + + should 'cost 0 if price_per_unit is not defined' do + input = Input.new(:amount_used => 10) + assert_equal 0.00, input.cost + end + end diff --git a/test/unit/price_detail_test.rb b/test/unit/price_detail_test.rb new file mode 100644 index 0000000..bd70658 --- /dev/null +++ b/test/unit/price_detail_test.rb @@ -0,0 +1,81 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class PriceDetailTest < ActiveSupport::TestCase + + should 'have price 0 by default' do + p = PriceDetail.new + + assert p.price.zero? + end + + should 'return zero on price if it is blank' do + p = PriceDetail.new(:price => '') + + assert p.price.zero? + end + + should 'accept price in american\'s or brazilian\'s currency format' do + [ + [12.34, 12.34], + ["12.34", 12.34], + ["12,34", 12.34], + ["12.345.678,90", 12345678.90], + ["12,345,678.90", 12345678.90], + ["12.345.678", 12345678.00], + ["12,345,678", 12345678.00] + ].each do |input, output| + new_price_detail = PriceDetail.new(:price => input) + assert_equal output, new_price_detail.price + end + end + + should 'belongs to a product' do + p = PriceDetail.new + + assert_respond_to p, :product + end + + should 'product be mandatory' do + p = PriceDetail.new + p.valid? + + assert p.errors.invalid?(:product_id) + end + + should 'have production cost' do + product = fast_create(Product) + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') + detail = product.price_details.create(:production_cost_id => cost.id, :price => 10) + + assert_equal cost, PriceDetail.find(detail.id).production_cost + end + + should 'production cost be mandatory' do + p = PriceDetail.new + p.valid? + + assert p.errors.invalid?(:production_cost_id) + end + + should 'th production cost be unique on scope of product' do + product = fast_create(Product) + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'environment') + + detail1 = product.price_details.create(:production_cost_id => cost.id, :price => 10) + detail2 = product.price_details.build(:production_cost_id => cost.id, :price => 10) + + detail2.valid? + assert detail2.errors.invalid?(:production_cost_id) + end + + should 'format values to float with 2 decimals' do + enterprise = fast_create(Enterprise) + product = fast_create(Product, :enterprise_id => enterprise.id) + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'environment') + + price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10) + + assert_equal "10.00", price_detail.formatted_value(:price) + end + +end diff --git a/test/unit/product_test.rb b/test/unit/product_test.rb index 3e643c0..e7f6c5b 100644 --- a/test/unit/product_test.rb +++ b/test/unit/product_test.rb @@ -382,4 +382,132 @@ class ProductTest < Test::Unit::TestCase assert_includes Product.find_by_contents('thing'), p2 end + should 'respond to price details' do + product = Product.new + assert_respond_to product, :price_details + end + + should 'return total value of inputs' do + product = fast_create(Product) + first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2) + second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1) + + assert_equal 50.0, product.inputs_cost + end + + should 'return 0 on total value of inputs if has no input' do + product = fast_create(Product) + + assert product.inputs_cost.zero? + end + + should 'know if price is described' do + product = fast_create(Product, :price => 30.0) + + first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 1) + assert !Product.find(product.id).price_described? + + second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1) + assert Product.find(product.id).price_described? + end + + should 'return false on price_described if price of product is not defined' do + product = fast_create(Product) + + assert_equal false, product.price_described? + end + + should 'create price details' do + product = fast_create(Product) + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') + assert product.price_details.empty? + + product.update_price_details([{:production_cost_id => cost.id, :price => 10}]) + assert_equal 1, Product.find(product.id).price_details.size + end + + should 'update price of a cost on price details' do + product = fast_create(Product) + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') + cost2 = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') + price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10) + assert !product.price_details.empty? + + product.update_price_details([{:production_cost_id => cost.id, :price => 20}, {:production_cost_id => cost2.id, :price => 30}]) + assert_equal 20, product.price_details.find_by_production_cost_id(cost.id).price + assert_equal 2, Product.find(product.id).price_details.size + end + + should 'destroy price details if product is removed' do + product = fast_create(Product) + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') + price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10) + + assert_difference PriceDetail, :count, -1 do + product.destroy + end + end + + should 'have production costs' do + product = fast_create(Product) + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') + product.price_details.create(:production_cost_id => cost.id, :price => 10) + assert_equal [cost], Product.find(product.id).production_costs + end + + should 'return production costs from enterprise and environment' do + ent = fast_create(Enterprise) + product = fast_create(Product, :enterprise_id => ent.id) + ent_production_cost = fast_create(ProductionCost, :owner_id => ent.id, :owner_type => 'Profile') + env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment') + + assert_equal [env_production_cost, ent_production_cost], product.available_production_costs + end + + should 'return all production costs' do + ent = fast_create(Enterprise) + product = fast_create(Product, :enterprise_id => ent.id) + + env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment') + ent_production_cost = fast_create(ProductionCost, :owner_id => ent.id, :owner_type => 'Profile') + product.price_details.create(:production_cost => env_production_cost, :product => product) + assert_equal [env_production_cost, ent_production_cost], product.available_production_costs + end + + should 'return total value of production costs' do + ent = fast_create(Enterprise) + product = fast_create(Product, :enterprise_id => ent.id) + + env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment') + price_detail = product.price_details.create(:production_cost => env_production_cost, :price => 10) + + input = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2) + + assert_equal price_detail.price + input.cost, product.total_production_cost + end + + should 'return inputs cost as total value of production costs if has no price details' do + ent = fast_create(Enterprise) + product = fast_create(Product, :enterprise_id => ent.id) + + input = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2) + + assert_equal input.cost, product.total_production_cost + end + + should 'return 0 on total production cost if has no input and price details' do + product = fast_create(Product) + + assert product.total_production_cost.zero? + end + + should 'format inputs cost values to float with 2 decimals' do + ent = fast_create(Enterprise) + product = fast_create(Product, :enterprise_id => ent.id) + first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2) + second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1) + + assert_equal "50.00", product.formatted_value(:inputs_cost) + end + end diff --git a/test/unit/production_cost_test.rb b/test/unit/production_cost_test.rb new file mode 100644 index 0000000..f266b37 --- /dev/null +++ b/test/unit/production_cost_test.rb @@ -0,0 +1,102 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ProductionCostTest < ActiveSupport::TestCase + + should 'have name' do + p = ProductionCost.new + p.valid? + assert p.errors.invalid?(:name) + + p.name = 'Taxes' + p.valid? + assert !p.errors.invalid?(:name) + end + + should 'not validates name if it is blank' do + p = ProductionCost.new + + p.valid? + assert_equal 1, p.errors['name'].to_a.count + end + + should 'not have a too long name' do + p = ProductionCost.new + + p.name = 'a'*40 + p.valid? + assert p.errors.invalid?(:name) + + p.name = 'a'*30 + p.valid? + assert !p.errors.invalid?(:name) + end + + should 'not have duplicated name on same environment' do + cost = ProductionCost.create(:name => 'Taxes', :owner => Environment.default) + + invalid_cost = ProductionCost.new(:name => 'Taxes', :owner => Environment.default) + invalid_cost.valid? + + assert invalid_cost.errors.invalid?(:name) + end + + should 'not have duplicated name on same enterprise' do + enterprise = fast_create(Enterprise) + cost = ProductionCost.create(:name => 'Taxes', :owner => enterprise) + + invalid_cost = ProductionCost.new(:name => 'Taxes', :owner => enterprise) + invalid_cost.valid? + + assert invalid_cost.errors.invalid?(:name) + end + + should 'not allow same name on enterprise if already has on environment' do + enterprise = fast_create(Enterprise) + + cost1 = ProductionCost.create(:name => 'Taxes', :owner => Environment.default) + cost2 = ProductionCost.new(:name => 'Taxes', :owner => enterprise) + + cost2.valid? + + assert !cost2.errors.invalid?(:name) + end + + should 'allow duplicated name on different enterprises' do + enterprise = fast_create(Enterprise) + enterprise2 = fast_create(Enterprise) + + cost1 = ProductionCost.create(:name => 'Taxes', :owner => enterprise) + cost2 = ProductionCost.new(:name => 'Taxes', :owner => enterprise2) + + cost2.valid? + + assert !cost2.errors.invalid?(:name) + end + + should 'be associated to an environment as owner' do + p = ProductionCost.new + p.valid? + assert p.errors.invalid?(:owner) + + p.owner = Environment.default + p.valid? + assert !p.errors.invalid?(:owner) + end + + should 'be associated to an enterprise as owner' do + enterprise = fast_create(Enterprise) + p = ProductionCost.new + p.valid? + assert p.errors.invalid?(:owner) + + p.owner = enterprise + p.valid? + assert !p.errors.invalid?(:owner) + end + + should 'create a production cost on an enterprise' do + enterprise = fast_create(Enterprise) + enterprise.production_costs.create(:name => 'Energy') + assert_equal ['Energy'], enterprise.production_costs.map(&:name) + end +end -- libgit2 0.21.2