Commit 87ef45c503df22d1df1d83d5df2b6d5b0d865ef6

Authored by Daniela Feitosa
1 parent c803ee37

Added feature to describe the costs of a product

   * new table production_costs
     * environment and enterprises can have them
   * new table price_details
     * products have price details with a cost and a price
   * updated db/schema
   * included method to display the total value of inputs
   * included partials to display and edit price details
   * allowing users to create new cost from select
   * manage_products.js to separate js codes from application.js
   * numbers_only_javascript
     * created a partial with the js needed to allow only numbers on some fields
  * price composition bar is updated when an user edit some price

(ActionItem1413)
app/controllers/my_profile/manage_products_controller.rb
... ... @@ -111,6 +111,36 @@ class ManageProductsController < ApplicationController
111 111 end
112 112 end
113 113  
  114 + def manage_product_details
  115 + @product = @profile.products.find(params[:id])
  116 + if request.post?
  117 + @product.update_price_details(params[:price_details]) if params[:price_details]
  118 + render :partial => 'display_price_details'
  119 + else
  120 + render :partial => 'manage_product_details'
  121 + end
  122 + end
  123 +
  124 + def remove_price_detail
  125 + @product = @profile.products.find(params[:product])
  126 + @price_detail = @product.price_details.find(params[:id])
  127 + @product = @price_detail.product
  128 + if request.post?
  129 + @price_detail.destroy
  130 + render :nothing => true
  131 + end
  132 + end
  133 +
  134 + def display_price_composition_bar
  135 + @product = @profile.products.find(params[:id])
  136 + render :partial => 'price_composition_bar'
  137 + end
  138 +
  139 + def display_inputs_cost
  140 + @product = @profile.products.find(params[:id])
  141 + render :partial => 'inputs_cost'
  142 + end
  143 +
114 144 def destroy
115 145 @product = @profile.products.find(params[:id])
116 146 if @product.destroy
... ... @@ -167,4 +197,18 @@ class ManageProductsController < ApplicationController
167 197 end
168 198 end
169 199  
  200 + def create_production_cost
  201 + cost = @profile.production_costs.create(:name => params[:id])
  202 + if cost.valid?
  203 + cost.save
  204 + render :text => {:name => cost.name,
  205 + :id => cost.id,
  206 + :ok => true
  207 + }.to_json
  208 + else
  209 + render :text => {:ok => false,
  210 + :error_msg => _(cost.errors['name']) % {:fn => _('Name')}
  211 + }.to_json
  212 + end
  213 + end
170 214 end
... ...
app/helpers/manage_products_helper.rb
... ... @@ -271,4 +271,23 @@ module ManageProductsHelper
271 271 return input_amount_used if input.unit.blank?
272 272 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') }
273 273 end
  274 +
  275 + def select_production_cost(product,selected=nil)
  276 + url = url_for( :controller => 'manage_products', :action => 'create_production_cost' )
  277 + prompt_msg = _('Insert the name of the new cost:')
  278 + error_msg = _('Something went wrong. Please, try again')
  279 + select_tag('price_details[][production_cost_id]',
  280 + options_for_select(product.available_production_costs.map {|item| [truncate(item.name, 10, '...'), item.id]} + [[_('Other cost'), '']], selected),
  281 + {:include_blank => _('Select the cost'),
  282 + :class => 'production-cost-selection',
  283 + :onchange => "productionCostTypeChange(this, '#{url}', '#{prompt_msg}', '#{error_msg}')"})
  284 + end
  285 +
  286 + def price_composition_progressbar_text(product, args = {})
  287 + currency = environment.currency_unit
  288 + production_cost = args[:production_cost_value] || product.formatted_value(:total_production_cost)
  289 + product_price = args[:product_price] || product.formatted_value(:price)
  290 +
  291 + _("%{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')}
  292 + end
274 293 end
... ...
app/models/enterprise.rb
... ... @@ -6,6 +6,7 @@ class Enterprise < Organization
6 6  
7 7 has_many :products, :dependent => :destroy, :order => 'name ASC'
8 8 has_many :inputs, :through => :products
  9 + has_many :production_costs, :as => :owner
9 10  
10 11 has_and_belongs_to_many :fans, :class_name => 'Person', :join_table => 'favorite_enteprises_people'
11 12  
... ...
app/models/environment.rb
... ... @@ -174,6 +174,7 @@ class Environment < ActiveRecord::Base
174 174 acts_as_accessible
175 175  
176 176 has_many :units, :order => 'position'
  177 + has_many :production_costs, :as => :owner
177 178  
178 179 def superior_intances
179 180 [self, nil]
... ...
app/models/input.rb
... ... @@ -55,4 +55,8 @@ class Input < ActiveRecord::Base
55 55 true
56 56 end
57 57  
  58 + def cost
  59 + return 0 if self.amount_used.blank? || self.price_per_unit.blank?
  60 + self.amount_used * self.price_per_unit
  61 + end
58 62 end
... ...
app/models/price_detail.rb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +class PriceDetail < ActiveRecord::Base
  2 +
  3 + belongs_to :product
  4 + validates_presence_of :product_id
  5 +
  6 + belongs_to :production_cost
  7 + validates_presence_of :production_cost_id
  8 + validates_uniqueness_of :production_cost_id, :scope => :product_id
  9 +
  10 + def price
  11 + self[:price] || 0
  12 + end
  13 +
  14 + include FloatHelper
  15 + def price=(value)
  16 + if value.is_a?(String)
  17 + super(decimal_to_float(value))
  18 + else
  19 + super(value)
  20 + end
  21 + end
  22 +
  23 + def formatted_value(value)
  24 + ("%.2f" % self[value]).to_s.gsub('.', product.enterprise.environment.currency_separator) if self[value]
  25 + end
  26 +
  27 +end
... ...
app/models/product.rb
... ... @@ -5,6 +5,8 @@ class Product &lt; ActiveRecord::Base
5 5 has_many :product_qualifiers
6 6 has_many :qualifiers, :through => :product_qualifiers
7 7 has_many :inputs, :dependent => :destroy, :order => 'position'
  8 + has_many :price_details, :dependent => :destroy
  9 + has_many :production_costs, :through => :price_details
8 10  
9 11 validates_uniqueness_of :name, :scope => :enterprise_id, :allow_nil => true
10 12 validates_presence_of :product_category_id
... ... @@ -101,8 +103,9 @@ class Product &lt; ActiveRecord::Base
101 103 enterprise.public_profile
102 104 end
103 105  
104   - def formatted_value(value)
105   - ("%.2f" % self[value]).to_s.gsub('.', enterprise.environment.currency_separator) if self[value]
  106 + def formatted_value(method)
  107 + value = self[method] || self.send(method)
  108 + ("%.2f" % value).to_s.gsub('.', enterprise.environment.currency_separator) if value
106 109 end
107 110  
108 111 def price_with_discount
... ... @@ -175,4 +178,43 @@ class Product &lt; ActiveRecord::Base
175 178 true
176 179 end
177 180  
  181 + def inputs_cost
  182 + return 0 if inputs.empty?
  183 + inputs.map(&:cost).inject { |sum,price| sum + price }
  184 + end
  185 +
  186 + def total_production_cost
  187 + return inputs_cost if price_details.empty?
  188 + inputs_cost + price_details.map(&:price).inject { |sum,price| sum + price }
  189 + end
  190 +
  191 + def price_described?
  192 + return false if price.nil?
  193 + (price - total_production_cost).zero?
  194 + end
  195 +
  196 + def update_price_details(price_details)
  197 + self.price_details.destroy_all
  198 + price_details.each do |price_detail|
  199 + self.price_details.create(price_detail)
  200 + end
  201 + end
  202 +
  203 + def price_description_percentage
  204 + total_production_cost * 100 / price
  205 + end
  206 +
  207 + def available_production_costs
  208 + self.enterprise.environment.production_costs + self.enterprise.production_costs
  209 + end
  210 +
  211 + include ActionController::UrlWriter
  212 + def price_composition_bar_display_url
  213 + url_for({:host => enterprise.default_hostname, :controller => 'manage_products', :action => 'display_price_composition_bar', :profile => enterprise.identifier, :id => self.id }.merge(Noosfero.url_options))
  214 + end
  215 +
  216 + def inputs_cost_update_url
  217 + url_for({:host => enterprise.default_hostname, :controller => 'manage_products', :action => 'display_inputs_cost', :profile => enterprise.identifier, :id => self.id }.merge(Noosfero.url_options))
  218 + end
  219 +
178 220 end
... ...
app/models/production_cost.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +class ProductionCost < ActiveRecord::Base
  2 +
  3 + belongs_to :owner, :polymorphic => true
  4 + validates_presence_of :owner
  5 + validates_presence_of :name
  6 + validates_length_of :name, :maximum => 30, :allow_blank => true
  7 + validates_uniqueness_of :name, :scope => [:owner_id, :owner_type]
  8 +end
... ...
app/views/layouts/_javascript.rhtml
1   -<%= 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' %>
2   -
  1 +<%= 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' %>
3 2 <% language = FastGettext.locale %>
4 3 <%= javascript_include_tag 'jquery-validation/localization/messages_'+language, 'jquery-validation/localization/methods_'+language %>
... ...
app/views/layouts/application.rhtml
... ... @@ -27,11 +27,8 @@
27 27 <%# Add custom tags/styles/etc via content_for %>
28 28 <%= yield :head %>
29 29 <%= javascript_tag('render_all_jquery_ui_widgets()') %>
30   - <script type="text/javascript">
31   - jQuery(".numbers-only").keypress(function(event) {
32   - return numbersonly(event, '<%= environment.currency_separator %>')
33   - });
34   - </script>
  30 +
  31 + <%= render :partial => 'shared/numbers_only_javascript' %>
35 32 </head>
36 33  
37 34 <body class='noosfero category<%= category_color %><%=
... ...
app/views/manage_products/_display_price_details.rhtml 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +<div id='display-manage-price-details'></div>
  2 +
  3 +<div id='display-price-details'>
  4 + <ul class='price-details-list'>
  5 + <li>
  6 + <div class='price-detail-name'><%= _('Inputs:') %></div>
  7 + <div class='price-detail-price inputs-cost'>
  8 + <%= render :partial => 'inputs_cost' %>
  9 + </div>
  10 + </li>
  11 + <% @product.price_details.each do |price_detail| %>
  12 + <li>
  13 + <div class='price-detail-name'><%= "%s:" % price_detail.production_cost.name %></div>
  14 + <div class='price-detail-price'><%= float_to_currency(price_detail.price) %></div>
  15 + </li>
  16 + <% end %>
  17 + </ul>
  18 +</div>
... ...
app/views/manage_products/_edit_info.rhtml
... ... @@ -49,15 +49,12 @@
49 49 <%= hidden_field_tag "product[qualifiers_list]" %>
50 50 <% end %>
51 51  
  52 + <%= hidden_field_tag 'info-bar-update-url', @product.price_composition_bar_display_url, :class => 'bar-update-url' %>
  53 +
52 54 <% button_bar do %>
53 55 <%= submit_button :save, _('Save') %>
54 56 <%= cancel_edit_product_link(@product, 'info') %>
55 57 <% end %>
56 58 <% end %>
57 59  
58   -<% javascript_tag do %>
59   - jQuery(".numbers-only").keypress(function(event) {
60   - var separator = "<%= environment.currency_separator %>"
61   - return numbersonly(event, separator)
62   - });
63   -<% end %>
  60 +<%= render :partial => 'shared/numbers_only_javascript' %>
... ...
app/views/manage_products/_edit_input.rhtml
1 1 <% form_for(@input, :url => {:controller => 'manage_products', :action => 'edit_input', :id => @input},
2 2 :html => {:method => 'post', :id => "edit-input-#{ @input.id }-form"}) do |f| %>
  3 +
  4 + <%= hidden_field_tag 'input-bar-update-url', @input.product.price_composition_bar_display_url, :class => 'bar-update-url' %>
  5 + <%= hidden_field_tag 'inputs-cost-update-url', @input.product.inputs_cost_update_url %>
  6 +
3 7 <table>
4 8 <tr>
5 9 <td><%= f.label :amount_used, label_amount_used(@input), :class => 'formlabel' %></td>
... ...
app/views/manage_products/_edit_price_details.rhtml 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +<% price_details.each do |price_detail| %>
  2 + <tr id='<%= "price-detail-#{price_detail.id}" %>'>
  3 + <td><%= select_production_cost(@product, price_detail.production_cost_id) %></td>
  4 + <td><%= labelled_form_field(environment.currency_unit, text_field_tag('price_details[][price]', price_detail.formatted_value(:price), :class => 'numbers-only price-details-price')) %></td>
  5 + <td>
  6 + <%= link_to_remote(_('Remove'),
  7 + :update => "price-detail-#{price_detail.id}",
  8 + :success => "jQuery('#manage-product-details-form input.submit').removeAttr('disabled').removeClass('disabled');",
  9 + :confirm => _('Are you sure that you want to remove this cost?'),
  10 + :url => { :action => 'remove_price_detail', :id => price_detail, :product => @product }) %>
  11 + </tr>
  12 +<% end %>
  13 +
  14 +<%= render :partial => 'shared/numbers_only_javascript' %>
... ...
app/views/manage_products/_inputs_cost.rhtml 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +<%= float_to_currency(@product.inputs_cost) %>
  2 +
  3 +<div id='price-described-notice' style='display:none;'>
  4 + <%= _("Congratulations! Now the product's price is open to the public") %>
  5 +</div>
... ...
app/views/manage_products/_manage_product_details.rhtml 0 → 100644
... ... @@ -0,0 +1,37 @@
  1 +<div id='price-composition-bar'>
  2 + <%= render :partial => 'price_composition_bar' %>
  3 +</div>
  4 +
  5 +<% form_tag({:action => 'manage_product_details'}, :method => 'post', :id => 'manage-product-details-form') do %>
  6 + <div>
  7 + <table id='display-product-price-details'>
  8 + <tr>
  9 + <td><%= _('Inputs') %></td>
  10 + <td class='inputs-cost'><%= float_to_currency(@product.inputs_cost) %></td>
  11 + <td><small><%= _('This value is composed by the total value of registered inputs') %></small></td>
  12 + </tr>
  13 + <%= render :partial => 'edit_price_details', :locals => {:price_details => @product.price_details} %>
  14 + </table>
  15 + </div>
  16 +
  17 + <%= hidden_field(:product, :inputs_cost) %>
  18 + <%= hidden_field(:product, :price) %>
  19 +
  20 + <% button_bar do %>
  21 + <%= submit_button :save, _('Save'), :disabled => '', :class => 'disabled' %>
  22 + <%= 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?')) %>
  23 + <%= button(:add, _('New cost'), '#', :id => 'add-new-cost') %>
  24 + <span class='loading-area'></span>
  25 + <% end %>
  26 +
  27 +<% end %>
  28 +
  29 +<div style='display:none'>
  30 + <table id='new-cost-fields'>
  31 + <tr>
  32 + <td><%= select_production_cost(@product) %></td>
  33 + <td><%= labelled_form_field(environment.currency_unit, text_field_tag('price_details[][price]', nil, :class => 'price-details-price')) %></td>
  34 + <td><%= link_to(_('Cancel'), '#', {:class => 'cancel-new-cost'}) %></td>
  35 + </tr>
  36 + </table>
  37 +</div>
... ...
app/views/manage_products/_price_composition_bar.rhtml 0 → 100644
... ... @@ -0,0 +1,22 @@
  1 +<% javascript_tag do %>
  2 + var value = <%= @product.price_description_percentage %>
  3 + var total_cost = <%= @product.total_production_cost %>
  4 + var price = <%= @product.price %>
  5 + var described = false;
  6 + if (<%= @product.price_described? %>) {
  7 + var described = true;
  8 + }
  9 + priceCompositionBar(value,described,total_cost,price);
  10 +<% end %>
  11 +
  12 +<div id="price-details-info">
  13 + <div id="details-progressbar">
  14 + <div id='progressbar'></div>
  15 + <div id='progressbar-text'>
  16 + <%= price_composition_progressbar_text(@product) %>
  17 + </div>
  18 + </div>
  19 + <div id='progressbar-icon' class='ui-icon ui-icon-info'></div>
  20 + <span id='price-not-described-message' style='display:none'><%= _("The production cost of your product is not described yet. If you want to display the price composition, please add all the costs") %></span>
  21 + <span id='price-described-message' style='display:none'><%= _("The production cost of your product is fully described and will be displayed on your product's page") %></span>
  22 +</div>
... ...
app/views/manage_products/_price_details_button.rhtml 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +<%= edit_ui_button(
  2 + _('Describe here the cost of production'),
  3 + {:action => 'manage_product_details', :id => @product.id},
  4 + :id => 'manage-product-details-button',
  5 + 'data-primary-icon' => 'ui-icon-pencil',
  6 + 'data-secondary-icon' => 'ui-icon-triangle-1-s',
  7 + :title => _('Describe details about how the price was defined')
  8 +) %>
  9 +<%= javascript_tag("render_jquery_ui_buttons('manage-product-details-button')") %>
  10 +<span class='loading-area'></span>
... ...
app/views/manage_products/show.rhtml
... ... @@ -23,7 +23,7 @@
23 23  
24 24 <br style='clear: both'/>
25 25  
26   - <% unless !@allowed_user && (@product.description.blank? && @product.inputs.empty?) %>
  26 + <% unless !@allowed_user && (@product.description.blank? && @product.inputs.empty? && !@product.price_described? ) %>
27 27 <div class='ui-tabs' id='product-<%= @product.id %>-tabs'>
28 28 <ul>
29 29 <% if !@product.description.blank? || @allowed_user %>
... ... @@ -32,6 +32,9 @@
32 32 <% if !@product.inputs.empty? || @allowed_user %>
33 33 <li class='tab'><a href='#product-inputs'><%= _('Inputs and raw material') %></a></li>
34 34 <% end %>
  35 + <% if @product.price_described? || @allowed_user %>
  36 + <li class='tab'><a href='#product-price-details'><%= _('Price composition') %></a></li>
  37 + <% end %>
35 38 </ul>
36 39 <div id='product-description'>
37 40 <%= render :partial => 'manage_products/display_description' %>
... ... @@ -39,6 +42,12 @@
39 42 <div id='product-inputs'>
40 43 <%= render :partial => 'manage_products/display_inputs' %>
41 44 </div>
  45 + <% if @product.price_described? || @allowed_user %>
  46 + <div id='product-price-details'>
  47 + <%= render :partial => 'manage_products/display_price_details' %>
  48 + <%= render :partial => 'manage_products/price_details_button' %>
  49 + </div>
  50 + <% end %>
42 51 </div>
43 52 <% end %>
44 53  
... ...
app/views/shared/_numbers_only_javascript.rhtml 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +<% javascript_tag do %>
  2 + jQuery(".numbers-only").keypress(function(event) {
  3 + var separator = "<%= environment.currency_separator %>"
  4 + return numbersonly(event, separator)
  5 + });
  6 +<% end %>
... ...
db/migrate/20110403184315_create_production_cost.rb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +class CreateProductionCost < ActiveRecord::Migration
  2 + def self.up
  3 + create_table :production_costs do |t|
  4 + t.string :name
  5 + t.references :owner, :polymorphic => true
  6 + t.timestamps
  7 + end
  8 + end
  9 +
  10 + def self.down
  11 + drop_table :production_costs
  12 + end
  13 +end
... ...
db/migrate/20110403193953_create_price_details.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +class CreatePriceDetails < ActiveRecord::Migration
  2 + def self.up
  3 + create_table :price_details do |t|
  4 + t.decimal :price, :default => 0
  5 + t.references :product
  6 + t.references :production_cost
  7 + t.timestamps
  8 + end
  9 + end
  10 +
  11 + def self.down
  12 + drop_table :price_details
  13 + end
  14 +end
... ...
db/schema.rb
... ... @@ -323,6 +323,14 @@ ActiveRecord::Schema.define(:version =&gt; 20111004184104) do
323 323 t.datetime "updated_at"
324 324 end
325 325  
  326 + create_table "price_details", :force => true do |t|
  327 + t.decimal "price", :default => 0.0
  328 + t.integer "product_id"
  329 + t.integer "production_cost_id"
  330 + t.datetime "created_at"
  331 + t.datetime "updated_at"
  332 + end
  333 +
326 334 create_table "product_categorizations", :force => true do |t|
327 335 t.integer "category_id"
328 336 t.integer "product_id"
... ... @@ -342,6 +350,14 @@ ActiveRecord::Schema.define(:version =&gt; 20111004184104) do
342 350 t.datetime "updated_at"
343 351 end
344 352  
  353 + create_table "production_costs", :force => true do |t|
  354 + t.string "name"
  355 + t.integer "owner_id"
  356 + t.string "owner_type"
  357 + t.datetime "created_at"
  358 + t.datetime "updated_at"
  359 + end
  360 +
345 361 create_table "products", :force => true do |t|
346 362 t.integer "enterprise_id"
347 363 t.integer "product_category_id"
... ...
features/manage_product_price_details.feature 0 → 100644
... ... @@ -0,0 +1,148 @@
  1 +
  2 +Feature: manage product price details
  3 + As an enterprise owner
  4 + I want to manage the details of product's price
  5 +
  6 + Background:
  7 + Given the following users
  8 + | login | name |
  9 + | joaosilva | Joao Silva |
  10 + And the following enterprises
  11 + | identifier | owner | name | enabled |
  12 + | redemoinho | joaosilva | Rede Moinho | true |
  13 + Given the following product_category
  14 + | name |
  15 + | Music |
  16 + And the following product_categories
  17 + | name | parent |
  18 + | Rock | music |
  19 + | CD Player | music |
  20 + And the following product
  21 + | owner | category | name | price |
  22 + | redemoinho | rock | Abbey Road | 80.0 |
  23 + And feature "disable_products_for_enterprises" is disabled on environment
  24 + And the following inputs
  25 + | product | category | price_per_unit | amount_used |
  26 + | Abbey Road | Rock | 10.0 | 2 |
  27 + | Abbey Road | CD Player | 20.0 | 2 |
  28 + And the following production cost
  29 + | name | owner |
  30 + | Taxes | environment |
  31 +
  32 + @selenium
  33 + Scenario: list total value of inputs as price details
  34 + Given I am logged in as "joaosilva"
  35 + When I go to Rede Moinho's page of product Abbey Road
  36 + And I follow "Describe here the cost of production"
  37 + Then I should see "Inputs"
  38 + And I should see "60.0" within ".inputs-cost"
  39 +
  40 + @selenium
  41 + Scenario: cancel management of price details
  42 + Given I am logged in as "joaosilva"
  43 + When I go to Rede Moinho's page of product Abbey Road
  44 + And I follow "Describe here the cost of production"
  45 + When I follow "Cancel"
  46 + Then I should see "Describe here the cost of production"
  47 +
  48 + @selenium
  49 + Scenario: return to product after save
  50 + Given I am logged in as "joaosilva"
  51 + When I go to Rede Moinho's page of product Abbey Road
  52 + And I follow "Describe here the cost of production"
  53 + And I press "Save"
  54 + Then I should be on Rede Moinho's page of product Abbey Road
  55 +
  56 + @selenium
  57 + Scenario: add first item on price details
  58 + Given I am logged in as "joaosilva"
  59 + When I go to Rede Moinho's page of product Abbey Road
  60 + And I follow "Describe here the cost of production"
  61 + And I follow "New cost"
  62 + And I select "Taxes"
  63 + And I fill in "$" with "5.00"
  64 + And I press "Save"
  65 + Then I should not see "Save"
  66 + And I should see "Describe here the cost of production"
  67 +
  68 + @selenium
  69 + Scenario: edit a production cost
  70 + Given the following production cost
  71 + | name | owner |
  72 + | Energy | environment |
  73 + Given I am logged in as "joaosilva"
  74 + When I go to Rede Moinho's page of product Abbey Road
  75 + And I follow "Describe here the cost of production"
  76 + And I follow "New cost"
  77 + And I select "Taxes"
  78 + And I fill in "$" with "20.00"
  79 + And I press "Save"
  80 + Then I should not see "Save"
  81 + And I should see "Taxes" within "#display-price-details"
  82 + When I follow "Describe here the cost of production"
  83 + And I select "Energy"
  84 + And I press "Save"
  85 + And I should not see "Taxes" within "#display-price-details"
  86 + And I should see "Energy" within "#display-price-details"
  87 +
  88 + Scenario: not display product detail button if product does not have input
  89 + Given the following product
  90 + | owner | category | name |
  91 + | redemoinho | rock | Yellow Submarine |
  92 + And the following user
  93 + | login | name |
  94 + | mariasouza | Maria Souza |
  95 + And I am logged in as "mariasouza"
  96 + When I go to Rede Moinho's page of product Yellow Submarine
  97 + Then I should not see "Describe here the cost of production"
  98 +
  99 + Scenario: not display price details if price is not fully described
  100 + Given I go to Rede Moinho's page of product Abbey Road
  101 + Then I should not see "60.0"
  102 +
  103 + @selenium
  104 + Scenario: display price details if price is fully described
  105 + Given I am logged in as "joaosilva"
  106 + And I go to Rede Moinho's page of product Abbey Road
  107 + And I follow "Describe here the cost of production"
  108 + And I follow "New cost"
  109 + And I select "Taxes"
  110 + And I fill in "$" with "20.00"
  111 + And I press "Save"
  112 + Then I should see "Inputs" within ".price-detail-name"
  113 + And I should see "60.0" within ".price-detail-price"
  114 +
  115 + @selenium
  116 + Scenario: create a new cost clicking on select
  117 + Given I am logged in as "joaosilva"
  118 + And I go to Rede Moinho's page of product Abbey Road
  119 + And I follow "Describe here the cost of production"
  120 + And I want to add "Energy" as cost
  121 + And I select "Other cost"
  122 + And I press "Save"
  123 + When I follow "Describe here the cost of production"
  124 + Then I should see "Energy" within ".production-cost-selection"
  125 +
  126 + @selenium
  127 + Scenario: add created cost on new-cost-fields
  128 + Given I am logged in as "joaosilva"
  129 + And I go to Rede Moinho's page of product Abbey Road
  130 + And I follow "Describe here the cost of production"
  131 + And I want to add "Energy" as cost
  132 + And I select "Other cost"
  133 + Then I should see "Energy" within "#new-cost-fields"
  134 +
  135 + @selenium
  136 + Scenario: remove price detail
  137 + Given the following price detail
  138 + | product | production_cost | price |
  139 + | Abbey Road | Taxes | 20.0 |
  140 + And I am logged in as "joaosilva"
  141 + And I go to Rede Moinho's page of product Abbey Road
  142 + And I follow "Describe here the cost of production"
  143 + And I should see "Taxes" within "#manage-product-details-form"
  144 + When I follow "Remove" within "#manage-product-details-form"
  145 + And I confirm
  146 + And I press "Save"
  147 + And I follow "Describe here the cost of production"
  148 + Then I should not see "Taxes" within "#manage-product-details-form"
... ...
features/step_definitions/noosfero_steps.rb
... ... @@ -222,6 +222,22 @@ Given /^the following certifiers$/ do |table|
222 222 end
223 223 end
224 224  
  225 +Given /^the following production costs?$/ do |table|
  226 + table.hashes.map{|item| item.dup}.each do |item|
  227 + owner_type = item.delete('owner')
  228 + owner = owner_type == 'environment' ? Environment.default : Profile[owner_type]
  229 + ProductionCost.create!(item.merge(:owner => owner))
  230 + end
  231 +end
  232 +
  233 +Given /^the following price details?$/ do |table|
  234 + table.hashes.map{|item| item.dup}.each do |item|
  235 + product = Product.find_by_name item.delete('product')
  236 + production_cost = ProductionCost.find_by_name item.delete('production_cost')
  237 + product.price_details.create!(item.merge(:production_cost => production_cost))
  238 + end
  239 +end
  240 +
225 241 Given /^I am logged in as "(.+)"$/ do |username|
226 242 visit('/account/logout')
227 243 visit('/account/login')
... ... @@ -521,3 +537,7 @@ Given /^the following enterprise homepages?$/ do |table|
521 537 ent.articles << home
522 538 end
523 539 end
  540 +
  541 +And /^I want to add "([^\"]*)" as cost$/ do |string|
  542 + selenium.answer_on_next_prompt(string)
  543 +end
... ...
public/javascripts/manage-products.js 0 → 100644
... ... @@ -0,0 +1,141 @@
  1 +(function($) {
  2 +
  3 + $("#manage-product-details-button").live('click', function() {
  4 + $("#product-price-details").find('.loading-area').addClass('small-loading');
  5 + url = $(this).attr('href');
  6 + $.get(url, function(data){
  7 + $("#manage-product-details-button").hide();
  8 + $("#display-price-details").hide();
  9 + $("#display-manage-price-details").html(data);
  10 + $("#product-price-details").find('.loading-area').removeClass('small-loading');
  11 + });
  12 + return false;
  13 + });
  14 +
  15 + $(".cancel-price-details").live('click', function() {
  16 + if ( !$(this).hasClass('form-changed') ) {
  17 + cancelPriceDetailsEdition();
  18 + } else {
  19 + if (confirm($(this).attr('data-confirm'))) {
  20 + cancelPriceDetailsEdition();
  21 + }
  22 + }
  23 + return false;
  24 + });
  25 +
  26 + $("#manage-product-details-form").live('submit', function(data) {
  27 + var form = this;
  28 + $(form).find('.loading-area').addClass('small-loading');
  29 + $(form).css('cursor', 'progress');
  30 + $.post(form.action, $(form).serialize(), function(data) {
  31 + $("#display-manage-price-details").html(data);
  32 + $("#manage-product-details-button").show();
  33 + });
  34 + if ($('#progressbar-icon').hasClass('ui-icon-check')) {
  35 + display_notice($('#price-described-notice').show());
  36 + }
  37 + return false;
  38 + });
  39 +
  40 + $("#add-new-cost").live('click', function() {
  41 + $('#display-product-price-details tbody').append($('#new-cost-fields tbody').html());
  42 + return false;
  43 + });
  44 +
  45 + $(".cancel-new-cost").live('click', function() {
  46 + $(this).parents('tr').remove();
  47 + return false;
  48 + });
  49 +
  50 + $("#product-info-form").live('submit', function(data) {
  51 + var form = this;
  52 + updatePriceCompositionBar(form);
  53 + });
  54 +
  55 + $("form.edit_input").live('submit', function(data) {
  56 + var form = this;
  57 + updatePriceCompositionBar(form);
  58 + inputs_cost_update_url = $(form).find('#inputs-cost-update-url').val();
  59 + $.get(inputs_cost_update_url, function(data){
  60 + $(".inputs-cost").html(data);
  61 + });
  62 + return false;
  63 + });
  64 +
  65 + $("#manage-product-details-form .price-details-price").live('keydown', function(data) {
  66 + $('.cancel-price-details').addClass('form-changed');
  67 + var product_price = parseFloat($('form #product_price').val());
  68 + var total_cost = parseFloat($('#product_inputs_cost').val());
  69 +
  70 + $('form .price-details-price').each(function() {
  71 + total_cost = total_cost + parseFloat($(this).val());
  72 + });
  73 + enablePriceDetailSubmit();
  74 +
  75 + var described = (product_price - total_cost) == 0;
  76 + var percentage = total_cost * 100 / product_price;
  77 + priceCompositionBar(percentage, described, total_cost, product_price);
  78 + });
  79 +
  80 + function cancelPriceDetailsEdition() {
  81 + $("#manage-product-details-button").show();
  82 + $("#display-price-details").show();
  83 + $("#display-manage-price-details").html('');
  84 + };
  85 +
  86 + function updatePriceCompositionBar(form) {
  87 + bar_url = $(form).find('.bar-update-url').val();
  88 + $.get(bar_url, function(data){
  89 + $("#price-composition-bar").html(data);
  90 + });
  91 + };
  92 +
  93 + function enablePriceDetailSubmit() {
  94 + $('#manage-product-details-form input.submit').removeAttr("disabled").removeClass('disabled');
  95 + };
  96 +
  97 +})(jQuery);
  98 +
  99 +function productionCostTypeChange(select, url, question, error_msg) {
  100 + if (select.value == '') {
  101 + var newType = prompt(question);
  102 + jQuery.ajax({
  103 + url: url + "/" + newType,
  104 + dataType: 'json',
  105 + success: function(data, status, ajax){
  106 + if (data.ok) {
  107 + var opt = jQuery('<option value="' + data.id + '">' + newType + '</option>');
  108 + opt.insertBefore(jQuery("option:last", select));
  109 + select.selectedIndex = select.options.length - 2;
  110 + opt.clone().insertBefore('#new-cost-fields .production-cost-selection option:last');
  111 + } else {
  112 + alert(data.error_msg);
  113 + }
  114 + },
  115 + error: function(ajax, status, error){
  116 + alert(error_msg);
  117 + }
  118 + });
  119 + }
  120 +}
  121 +
  122 +function priceCompositionBar(value, described, total_cost, price) {
  123 + jQuery(function($) {
  124 + var bar_area = $('#price-composition-bar');
  125 + $(bar_area).find('#progressbar').progressbar({
  126 + value: value
  127 + });
  128 + $(bar_area).find('.production-cost').html(total_cost.toFixed(2));
  129 + $(bar_area).find('.product_price').html(price.toFixed(2));
  130 + if (described) {
  131 + $(bar_area).find('#progressbar-icon').addClass('ui-icon-check');
  132 + $(bar_area).find('#progressbar-icon').attr('title', $('#price-described-message').html());
  133 + $(bar_area).find('div.ui-progressbar-value').addClass('price-described');
  134 + } else {
  135 + $(bar_area).find('#progressbar-icon').removeClass('ui-icon-check');
  136 + $(bar_area).find('#progressbar-icon').attr('title', $('#price-not-described-message').html());
  137 + $(bar_area).find('div.ui-progressbar-value').removeClass('price-described');
  138 +
  139 + }
  140 + });
  141 +}
... ...
public/stylesheets/application.css
... ... @@ -3383,6 +3383,86 @@ div#activation_enterprise div {
3383 3383 font-weight: bold;
3384 3384 }
3385 3385  
  3386 +/* * * * * * Price details * * * * * */
  3387 +
  3388 +#display-price-details .price-details-list {
  3389 + padding-left: 0px;
  3390 +}
  3391 +
  3392 +#display-price-details .price-details-list li {
  3393 + list-style: none;
  3394 +}
  3395 +
  3396 +#display-price-details .price-details-list li .price-detail-name {
  3397 + width: 200px;
  3398 +}
  3399 +
  3400 +#display-price-details .price-details-list li .price-detail-name,
  3401 +#display-price-details .price-details-list li .price-detail-price {
  3402 + display: inline-block;
  3403 +}
  3404 +
  3405 +#manage-product-details-form .formlabel,
  3406 +#manage-product-details-form .formfield {
  3407 + display: inline-block;
  3408 +}
  3409 +
  3410 +#manage-product-details-form #add-new-cost {
  3411 + float: right;
  3412 +}
  3413 +
  3414 +/* * * Progress bar on price details edition * * */
  3415 +
  3416 +#display-manage-price-details .ui-widget-content {
  3417 + border: 1px solid #DDD;
  3418 +}
  3419 +
  3420 +#display-manage-price-details .ui-progressbar {
  3421 + height: 20px;
  3422 +}
  3423 +
  3424 +#display-manage-price-details .ui-progressbar .ui-progressbar-value {
  3425 + margin: 0px;
  3426 + background-color: #A40000;
  3427 + filter:alpha(opacity=70);
  3428 + -moz-opacity: 0.7;
  3429 + opacity: 0.7;
  3430 +}
  3431 +
  3432 +#display-manage-price-details .ui-progressbar .ui-progressbar-value.price-described {
  3433 + background-color: #4E9A06;
  3434 +}
  3435 +
  3436 +#display-manage-price-details #price-details-info {
  3437 + margin: 10px 0px;
  3438 +}
  3439 +
  3440 +#display-manage-price-details #details-progressbar {
  3441 + position: relative;
  3442 + width: 410px;
  3443 + display: inline-block;
  3444 +}
  3445 +
  3446 +#display-manage-price-details #progressbar-text {
  3447 + position: absolute;
  3448 + top: 5px;
  3449 + right: 7px;
  3450 + font-size: 11px;
  3451 +}
  3452 +
  3453 +#display-manage-price-details #progressbar-icon {
  3454 + display: inline-block;
  3455 + cursor: pointer;
  3456 +}
  3457 +
  3458 +#display-manage-price-details #details-progressbar .ui-corner-left,
  3459 +#display-manage-price-details #details-progressbar .ui-corner-right {
  3460 + -moz-border-radius-bottomleft: 0px;
  3461 + -moz-border-radius-bottomright: 0px;
  3462 + -moz-border-radius-topleft: 0px;
  3463 + -moz-border-radius-topright: 0px;
  3464 +}
  3465 +
3386 3466 /* ==> public/stylesheets/controller_cms.css <== */
3387 3467  
3388 3468  
... ...
test/factories.rb
... ... @@ -449,4 +449,12 @@ module Noosfero::Factory
449 449 { :singular => 'Litre', :plural => 'Litres', :environment_id => 1 }
450 450 end
451 451  
  452 + ###############################################
  453 + # Production Cost
  454 + ###############################################
  455 +
  456 + def defaults_for_production_cost
  457 + { :name => 'Production cost ' + factory_num_seq.to_s }
  458 + end
  459 +
452 460 end
... ...
test/functional/manage_products_controller_test.rb
... ... @@ -468,4 +468,47 @@ class ManageProductsControllerTest &lt; Test::Unit::TestCase
468 468 assert_response 403
469 469 end
470 470  
  471 + should 'remove price detail of a product' do
  472 + product = fast_create(Product, :enterprise_id => @enterprise.id, :product_category_id => @product_category.id)
  473 + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
  474 + detail = product.price_details.create(:production_cost_id => cost.id, :price => 10)
  475 +
  476 + assert_equal [detail], product.price_details
  477 +
  478 + post :remove_price_detail, :id => detail.id, :product => product, :profile => @enterprise.identifier
  479 + product.reload
  480 + assert_equal [], product.price_details
  481 + end
  482 +
  483 + should 'create a production cost for enterprise' do
  484 + get :create_production_cost, :profile => @enterprise.identifier, :id => 'Taxes'
  485 +
  486 + assert_equal ['Taxes'], Enterprise.find(@enterprise.id).production_costs.map(&:name)
  487 + resp = ActiveSupport::JSON.decode(@response.body)
  488 + assert_equal 'Taxes', resp['name']
  489 + assert resp['id'].kind_of?(Integer)
  490 + assert resp['ok']
  491 + assert_nil resp['error_msg']
  492 + end
  493 +
  494 + should 'display error if production cost has no name' do
  495 + get :create_production_cost, :profile => @enterprise.identifier
  496 +
  497 + resp = ActiveSupport::JSON.decode(@response.body)
  498 + assert_nil resp['name']
  499 + assert_nil resp['id']
  500 + assert !resp['ok']
  501 + assert_match /blank/, resp['error_msg']
  502 + end
  503 +
  504 + should 'display error if name of production cost is too long' do
  505 + get :create_production_cost, :profile => @enterprise.identifier, :id => 'a'*60
  506 +
  507 + resp = ActiveSupport::JSON.decode(@response.body)
  508 + assert_nil resp['name']
  509 + assert_nil resp['id']
  510 + assert !resp['ok']
  511 + assert_match /too long/, resp['error_msg']
  512 + end
  513 +
471 514 end
... ...
test/unit/enterprise_test.rb
... ... @@ -446,4 +446,8 @@ class EnterpriseTest &lt; Test::Unit::TestCase
446 446 assert_equal false, enterprise.receives_scrap_notification?
447 447 end
448 448  
  449 + should 'have production cost' do
  450 + e = fast_create(Enterprise)
  451 + assert_respond_to e, :production_costs
  452 + end
449 453 end
... ...
test/unit/environment_test.rb
... ... @@ -1216,4 +1216,7 @@ class EnvironmentTest &lt; Test::Unit::TestCase
1216 1216 assert_not_includes environment.enabled_plugins, plugin
1217 1217 end
1218 1218  
  1219 + should 'have production costs' do
  1220 + assert_respond_to Environment.default, :production_costs
  1221 + end
1219 1222 end
... ...
test/unit/input_test.rb
... ... @@ -162,4 +162,19 @@ class InputTest &lt; Test::Unit::TestCase
162 162 assert_kind_of Unit, input.build_unit
163 163 end
164 164  
  165 + should 'calculate cost of input' do
  166 + input = Input.new(:amount_used => 10, :price_per_unit => 2.00)
  167 + assert_equal 20.00, input.cost
  168 + end
  169 +
  170 + should 'cost 0 if amount not defined' do
  171 + input = Input.new(:price_per_unit => 2.00)
  172 + assert_equal 0.00, input.cost
  173 + end
  174 +
  175 + should 'cost 0 if price_per_unit is not defined' do
  176 + input = Input.new(:amount_used => 10)
  177 + assert_equal 0.00, input.cost
  178 + end
  179 +
165 180 end
... ...
test/unit/price_detail_test.rb 0 → 100644
... ... @@ -0,0 +1,81 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class PriceDetailTest < ActiveSupport::TestCase
  4 +
  5 + should 'have price 0 by default' do
  6 + p = PriceDetail.new
  7 +
  8 + assert p.price.zero?
  9 + end
  10 +
  11 + should 'return zero on price if it is blank' do
  12 + p = PriceDetail.new(:price => '')
  13 +
  14 + assert p.price.zero?
  15 + end
  16 +
  17 + should 'accept price in american\'s or brazilian\'s currency format' do
  18 + [
  19 + [12.34, 12.34],
  20 + ["12.34", 12.34],
  21 + ["12,34", 12.34],
  22 + ["12.345.678,90", 12345678.90],
  23 + ["12,345,678.90", 12345678.90],
  24 + ["12.345.678", 12345678.00],
  25 + ["12,345,678", 12345678.00]
  26 + ].each do |input, output|
  27 + new_price_detail = PriceDetail.new(:price => input)
  28 + assert_equal output, new_price_detail.price
  29 + end
  30 + end
  31 +
  32 + should 'belongs to a product' do
  33 + p = PriceDetail.new
  34 +
  35 + assert_respond_to p, :product
  36 + end
  37 +
  38 + should 'product be mandatory' do
  39 + p = PriceDetail.new
  40 + p.valid?
  41 +
  42 + assert p.errors.invalid?(:product_id)
  43 + end
  44 +
  45 + should 'have production cost' do
  46 + product = fast_create(Product)
  47 + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
  48 + detail = product.price_details.create(:production_cost_id => cost.id, :price => 10)
  49 +
  50 + assert_equal cost, PriceDetail.find(detail.id).production_cost
  51 + end
  52 +
  53 + should 'production cost be mandatory' do
  54 + p = PriceDetail.new
  55 + p.valid?
  56 +
  57 + assert p.errors.invalid?(:production_cost_id)
  58 + end
  59 +
  60 + should 'th production cost be unique on scope of product' do
  61 + product = fast_create(Product)
  62 + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'environment')
  63 +
  64 + detail1 = product.price_details.create(:production_cost_id => cost.id, :price => 10)
  65 + detail2 = product.price_details.build(:production_cost_id => cost.id, :price => 10)
  66 +
  67 + detail2.valid?
  68 + assert detail2.errors.invalid?(:production_cost_id)
  69 + end
  70 +
  71 + should 'format values to float with 2 decimals' do
  72 + enterprise = fast_create(Enterprise)
  73 + product = fast_create(Product, :enterprise_id => enterprise.id)
  74 + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'environment')
  75 +
  76 + price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10)
  77 +
  78 + assert_equal "10.00", price_detail.formatted_value(:price)
  79 + end
  80 +
  81 +end
... ...
test/unit/product_test.rb
... ... @@ -382,4 +382,132 @@ class ProductTest &lt; Test::Unit::TestCase
382 382 assert_includes Product.find_by_contents('thing'), p2
383 383 end
384 384  
  385 + should 'respond to price details' do
  386 + product = Product.new
  387 + assert_respond_to product, :price_details
  388 + end
  389 +
  390 + should 'return total value of inputs' do
  391 + product = fast_create(Product)
  392 + first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2)
  393 + second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1)
  394 +
  395 + assert_equal 50.0, product.inputs_cost
  396 + end
  397 +
  398 + should 'return 0 on total value of inputs if has no input' do
  399 + product = fast_create(Product)
  400 +
  401 + assert product.inputs_cost.zero?
  402 + end
  403 +
  404 + should 'know if price is described' do
  405 + product = fast_create(Product, :price => 30.0)
  406 +
  407 + first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 1)
  408 + assert !Product.find(product.id).price_described?
  409 +
  410 + second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1)
  411 + assert Product.find(product.id).price_described?
  412 + end
  413 +
  414 + should 'return false on price_described if price of product is not defined' do
  415 + product = fast_create(Product)
  416 +
  417 + assert_equal false, product.price_described?
  418 + end
  419 +
  420 + should 'create price details' do
  421 + product = fast_create(Product)
  422 + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
  423 + assert product.price_details.empty?
  424 +
  425 + product.update_price_details([{:production_cost_id => cost.id, :price => 10}])
  426 + assert_equal 1, Product.find(product.id).price_details.size
  427 + end
  428 +
  429 + should 'update price of a cost on price details' do
  430 + product = fast_create(Product)
  431 + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
  432 + cost2 = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
  433 + price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10)
  434 + assert !product.price_details.empty?
  435 +
  436 + product.update_price_details([{:production_cost_id => cost.id, :price => 20}, {:production_cost_id => cost2.id, :price => 30}])
  437 + assert_equal 20, product.price_details.find_by_production_cost_id(cost.id).price
  438 + assert_equal 2, Product.find(product.id).price_details.size
  439 + end
  440 +
  441 + should 'destroy price details if product is removed' do
  442 + product = fast_create(Product)
  443 + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
  444 + price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10)
  445 +
  446 + assert_difference PriceDetail, :count, -1 do
  447 + product.destroy
  448 + end
  449 + end
  450 +
  451 + should 'have production costs' do
  452 + product = fast_create(Product)
  453 + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
  454 + product.price_details.create(:production_cost_id => cost.id, :price => 10)
  455 + assert_equal [cost], Product.find(product.id).production_costs
  456 + end
  457 +
  458 + should 'return production costs from enterprise and environment' do
  459 + ent = fast_create(Enterprise)
  460 + product = fast_create(Product, :enterprise_id => ent.id)
  461 + ent_production_cost = fast_create(ProductionCost, :owner_id => ent.id, :owner_type => 'Profile')
  462 + env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment')
  463 +
  464 + assert_equal [env_production_cost, ent_production_cost], product.available_production_costs
  465 + end
  466 +
  467 + should 'return all production costs' do
  468 + ent = fast_create(Enterprise)
  469 + product = fast_create(Product, :enterprise_id => ent.id)
  470 +
  471 + env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment')
  472 + ent_production_cost = fast_create(ProductionCost, :owner_id => ent.id, :owner_type => 'Profile')
  473 + product.price_details.create(:production_cost => env_production_cost, :product => product)
  474 + assert_equal [env_production_cost, ent_production_cost], product.available_production_costs
  475 + end
  476 +
  477 + should 'return total value of production costs' do
  478 + ent = fast_create(Enterprise)
  479 + product = fast_create(Product, :enterprise_id => ent.id)
  480 +
  481 + env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment')
  482 + price_detail = product.price_details.create(:production_cost => env_production_cost, :price => 10)
  483 +
  484 + input = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2)
  485 +
  486 + assert_equal price_detail.price + input.cost, product.total_production_cost
  487 + end
  488 +
  489 + should 'return inputs cost as total value of production costs if has no price details' do
  490 + ent = fast_create(Enterprise)
  491 + product = fast_create(Product, :enterprise_id => ent.id)
  492 +
  493 + input = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2)
  494 +
  495 + assert_equal input.cost, product.total_production_cost
  496 + end
  497 +
  498 + should 'return 0 on total production cost if has no input and price details' do
  499 + product = fast_create(Product)
  500 +
  501 + assert product.total_production_cost.zero?
  502 + end
  503 +
  504 + should 'format inputs cost values to float with 2 decimals' do
  505 + ent = fast_create(Enterprise)
  506 + product = fast_create(Product, :enterprise_id => ent.id)
  507 + first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2)
  508 + second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1)
  509 +
  510 + assert_equal "50.00", product.formatted_value(:inputs_cost)
  511 + end
  512 +
385 513 end
... ...
test/unit/production_cost_test.rb 0 → 100644
... ... @@ -0,0 +1,102 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class ProductionCostTest < ActiveSupport::TestCase
  4 +
  5 + should 'have name' do
  6 + p = ProductionCost.new
  7 + p.valid?
  8 + assert p.errors.invalid?(:name)
  9 +
  10 + p.name = 'Taxes'
  11 + p.valid?
  12 + assert !p.errors.invalid?(:name)
  13 + end
  14 +
  15 + should 'not validates name if it is blank' do
  16 + p = ProductionCost.new
  17 +
  18 + p.valid?
  19 + assert_equal 1, p.errors['name'].to_a.count
  20 + end
  21 +
  22 + should 'not have a too long name' do
  23 + p = ProductionCost.new
  24 +
  25 + p.name = 'a'*40
  26 + p.valid?
  27 + assert p.errors.invalid?(:name)
  28 +
  29 + p.name = 'a'*30
  30 + p.valid?
  31 + assert !p.errors.invalid?(:name)
  32 + end
  33 +
  34 + should 'not have duplicated name on same environment' do
  35 + cost = ProductionCost.create(:name => 'Taxes', :owner => Environment.default)
  36 +
  37 + invalid_cost = ProductionCost.new(:name => 'Taxes', :owner => Environment.default)
  38 + invalid_cost.valid?
  39 +
  40 + assert invalid_cost.errors.invalid?(:name)
  41 + end
  42 +
  43 + should 'not have duplicated name on same enterprise' do
  44 + enterprise = fast_create(Enterprise)
  45 + cost = ProductionCost.create(:name => 'Taxes', :owner => enterprise)
  46 +
  47 + invalid_cost = ProductionCost.new(:name => 'Taxes', :owner => enterprise)
  48 + invalid_cost.valid?
  49 +
  50 + assert invalid_cost.errors.invalid?(:name)
  51 + end
  52 +
  53 + should 'not allow same name on enterprise if already has on environment' do
  54 + enterprise = fast_create(Enterprise)
  55 +
  56 + cost1 = ProductionCost.create(:name => 'Taxes', :owner => Environment.default)
  57 + cost2 = ProductionCost.new(:name => 'Taxes', :owner => enterprise)
  58 +
  59 + cost2.valid?
  60 +
  61 + assert !cost2.errors.invalid?(:name)
  62 + end
  63 +
  64 + should 'allow duplicated name on different enterprises' do
  65 + enterprise = fast_create(Enterprise)
  66 + enterprise2 = fast_create(Enterprise)
  67 +
  68 + cost1 = ProductionCost.create(:name => 'Taxes', :owner => enterprise)
  69 + cost2 = ProductionCost.new(:name => 'Taxes', :owner => enterprise2)
  70 +
  71 + cost2.valid?
  72 +
  73 + assert !cost2.errors.invalid?(:name)
  74 + end
  75 +
  76 + should 'be associated to an environment as owner' do
  77 + p = ProductionCost.new
  78 + p.valid?
  79 + assert p.errors.invalid?(:owner)
  80 +
  81 + p.owner = Environment.default
  82 + p.valid?
  83 + assert !p.errors.invalid?(:owner)
  84 + end
  85 +
  86 + should 'be associated to an enterprise as owner' do
  87 + enterprise = fast_create(Enterprise)
  88 + p = ProductionCost.new
  89 + p.valid?
  90 + assert p.errors.invalid?(:owner)
  91 +
  92 + p.owner = enterprise
  93 + p.valid?
  94 + assert !p.errors.invalid?(:owner)
  95 + end
  96 +
  97 + should 'create a production cost on an enterprise' do
  98 + enterprise = fast_create(Enterprise)
  99 + enterprise.production_costs.create(:name => 'Energy')
  100 + assert_equal ['Energy'], enterprise.production_costs.map(&:name)
  101 + end
  102 +end
... ...