Commit 87ef45c503df22d1df1d83d5df2b6d5b0d865ef6
1 parent
c803ee37
Exists in
master
and in
29 other branches
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)
Showing
35 changed files
with
1096 additions
and
16 deletions
Show diff stats
app/controllers/my_profile/manage_products_controller.rb
@@ -111,6 +111,36 @@ class ManageProductsController < ApplicationController | @@ -111,6 +111,36 @@ class ManageProductsController < ApplicationController | ||
111 | end | 111 | end |
112 | end | 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 | def destroy | 144 | def destroy |
115 | @product = @profile.products.find(params[:id]) | 145 | @product = @profile.products.find(params[:id]) |
116 | if @product.destroy | 146 | if @product.destroy |
@@ -167,4 +197,18 @@ class ManageProductsController < ApplicationController | @@ -167,4 +197,18 @@ class ManageProductsController < ApplicationController | ||
167 | end | 197 | end |
168 | end | 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 | end | 214 | end |
app/helpers/manage_products_helper.rb
@@ -271,4 +271,23 @@ module ManageProductsHelper | @@ -271,4 +271,23 @@ module ManageProductsHelper | ||
271 | return input_amount_used if input.unit.blank? | 271 | return input_amount_used if input.unit.blank? |
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') } | 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 | end | 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 | end | 293 | end |
app/models/enterprise.rb
@@ -6,6 +6,7 @@ class Enterprise < Organization | @@ -6,6 +6,7 @@ class Enterprise < Organization | ||
6 | 6 | ||
7 | has_many :products, :dependent => :destroy, :order => 'name ASC' | 7 | has_many :products, :dependent => :destroy, :order => 'name ASC' |
8 | has_many :inputs, :through => :products | 8 | has_many :inputs, :through => :products |
9 | + has_many :production_costs, :as => :owner | ||
9 | 10 | ||
10 | has_and_belongs_to_many :fans, :class_name => 'Person', :join_table => 'favorite_enteprises_people' | 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,6 +174,7 @@ class Environment < ActiveRecord::Base | ||
174 | acts_as_accessible | 174 | acts_as_accessible |
175 | 175 | ||
176 | has_many :units, :order => 'position' | 176 | has_many :units, :order => 'position' |
177 | + has_many :production_costs, :as => :owner | ||
177 | 178 | ||
178 | def superior_intances | 179 | def superior_intances |
179 | [self, nil] | 180 | [self, nil] |
app/models/input.rb
@@ -0,0 +1,27 @@ | @@ -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 < ActiveRecord::Base | @@ -5,6 +5,8 @@ class Product < ActiveRecord::Base | ||
5 | has_many :product_qualifiers | 5 | has_many :product_qualifiers |
6 | has_many :qualifiers, :through => :product_qualifiers | 6 | has_many :qualifiers, :through => :product_qualifiers |
7 | has_many :inputs, :dependent => :destroy, :order => 'position' | 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 | validates_uniqueness_of :name, :scope => :enterprise_id, :allow_nil => true | 11 | validates_uniqueness_of :name, :scope => :enterprise_id, :allow_nil => true |
10 | validates_presence_of :product_category_id | 12 | validates_presence_of :product_category_id |
@@ -101,8 +103,9 @@ class Product < ActiveRecord::Base | @@ -101,8 +103,9 @@ class Product < ActiveRecord::Base | ||
101 | enterprise.public_profile | 103 | enterprise.public_profile |
102 | end | 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 | end | 109 | end |
107 | 110 | ||
108 | def price_with_discount | 111 | def price_with_discount |
@@ -175,4 +178,43 @@ class Product < ActiveRecord::Base | @@ -175,4 +178,43 @@ class Product < ActiveRecord::Base | ||
175 | true | 178 | true |
176 | end | 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 | end | 220 | end |
@@ -0,0 +1,8 @@ | @@ -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 | <% language = FastGettext.locale %> | 2 | <% language = FastGettext.locale %> |
4 | <%= javascript_include_tag 'jquery-validation/localization/messages_'+language, 'jquery-validation/localization/methods_'+language %> | 3 | <%= javascript_include_tag 'jquery-validation/localization/messages_'+language, 'jquery-validation/localization/methods_'+language %> |
app/views/layouts/application.rhtml
@@ -27,11 +27,8 @@ | @@ -27,11 +27,8 @@ | ||
27 | <%# Add custom tags/styles/etc via content_for %> | 27 | <%# Add custom tags/styles/etc via content_for %> |
28 | <%= yield :head %> | 28 | <%= yield :head %> |
29 | <%= javascript_tag('render_all_jquery_ui_widgets()') %> | 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 | </head> | 32 | </head> |
36 | 33 | ||
37 | <body class='noosfero category<%= category_color %><%= | 34 | <body class='noosfero category<%= category_color %><%= |
@@ -0,0 +1,18 @@ | @@ -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,15 +49,12 @@ | ||
49 | <%= hidden_field_tag "product[qualifiers_list]" %> | 49 | <%= hidden_field_tag "product[qualifiers_list]" %> |
50 | <% end %> | 50 | <% end %> |
51 | 51 | ||
52 | + <%= hidden_field_tag 'info-bar-update-url', @product.price_composition_bar_display_url, :class => 'bar-update-url' %> | ||
53 | + | ||
52 | <% button_bar do %> | 54 | <% button_bar do %> |
53 | <%= submit_button :save, _('Save') %> | 55 | <%= submit_button :save, _('Save') %> |
54 | <%= cancel_edit_product_link(@product, 'info') %> | 56 | <%= cancel_edit_product_link(@product, 'info') %> |
55 | <% end %> | 57 | <% end %> |
56 | <% end %> | 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 | <% form_for(@input, :url => {:controller => 'manage_products', :action => 'edit_input', :id => @input}, | 1 | <% form_for(@input, :url => {:controller => 'manage_products', :action => 'edit_input', :id => @input}, |
2 | :html => {:method => 'post', :id => "edit-input-#{ @input.id }-form"}) do |f| %> | 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 | <table> | 7 | <table> |
4 | <tr> | 8 | <tr> |
5 | <td><%= f.label :amount_used, label_amount_used(@input), :class => 'formlabel' %></td> | 9 | <td><%= f.label :amount_used, label_amount_used(@input), :class => 'formlabel' %></td> |
@@ -0,0 +1,14 @@ | @@ -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' %> |
@@ -0,0 +1,37 @@ | @@ -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> |
@@ -0,0 +1,22 @@ | @@ -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> |
@@ -0,0 +1,10 @@ | @@ -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,7 +23,7 @@ | ||
23 | 23 | ||
24 | <br style='clear: both'/> | 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 | <div class='ui-tabs' id='product-<%= @product.id %>-tabs'> | 27 | <div class='ui-tabs' id='product-<%= @product.id %>-tabs'> |
28 | <ul> | 28 | <ul> |
29 | <% if !@product.description.blank? || @allowed_user %> | 29 | <% if !@product.description.blank? || @allowed_user %> |
@@ -32,6 +32,9 @@ | @@ -32,6 +32,9 @@ | ||
32 | <% if !@product.inputs.empty? || @allowed_user %> | 32 | <% if !@product.inputs.empty? || @allowed_user %> |
33 | <li class='tab'><a href='#product-inputs'><%= _('Inputs and raw material') %></a></li> | 33 | <li class='tab'><a href='#product-inputs'><%= _('Inputs and raw material') %></a></li> |
34 | <% end %> | 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 | </ul> | 38 | </ul> |
36 | <div id='product-description'> | 39 | <div id='product-description'> |
37 | <%= render :partial => 'manage_products/display_description' %> | 40 | <%= render :partial => 'manage_products/display_description' %> |
@@ -39,6 +42,12 @@ | @@ -39,6 +42,12 @@ | ||
39 | <div id='product-inputs'> | 42 | <div id='product-inputs'> |
40 | <%= render :partial => 'manage_products/display_inputs' %> | 43 | <%= render :partial => 'manage_products/display_inputs' %> |
41 | </div> | 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 | </div> | 51 | </div> |
43 | <% end %> | 52 | <% end %> |
44 | 53 |
@@ -0,0 +1,13 @@ | @@ -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 |
@@ -0,0 +1,14 @@ | @@ -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 => 20111004184104) do | @@ -323,6 +323,14 @@ ActiveRecord::Schema.define(:version => 20111004184104) do | ||
323 | t.datetime "updated_at" | 323 | t.datetime "updated_at" |
324 | end | 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 | create_table "product_categorizations", :force => true do |t| | 334 | create_table "product_categorizations", :force => true do |t| |
327 | t.integer "category_id" | 335 | t.integer "category_id" |
328 | t.integer "product_id" | 336 | t.integer "product_id" |
@@ -342,6 +350,14 @@ ActiveRecord::Schema.define(:version => 20111004184104) do | @@ -342,6 +350,14 @@ ActiveRecord::Schema.define(:version => 20111004184104) do | ||
342 | t.datetime "updated_at" | 350 | t.datetime "updated_at" |
343 | end | 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 | create_table "products", :force => true do |t| | 361 | create_table "products", :force => true do |t| |
346 | t.integer "enterprise_id" | 362 | t.integer "enterprise_id" |
347 | t.integer "product_category_id" | 363 | t.integer "product_category_id" |
@@ -0,0 +1,148 @@ | @@ -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,6 +222,22 @@ Given /^the following certifiers$/ do |table| | ||
222 | end | 222 | end |
223 | end | 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 | Given /^I am logged in as "(.+)"$/ do |username| | 241 | Given /^I am logged in as "(.+)"$/ do |username| |
226 | visit('/account/logout') | 242 | visit('/account/logout') |
227 | visit('/account/login') | 243 | visit('/account/login') |
@@ -521,3 +537,7 @@ Given /^the following enterprise homepages?$/ do |table| | @@ -521,3 +537,7 @@ Given /^the following enterprise homepages?$/ do |table| | ||
521 | ent.articles << home | 537 | ent.articles << home |
522 | end | 538 | end |
523 | end | 539 | end |
540 | + | ||
541 | +And /^I want to add "([^\"]*)" as cost$/ do |string| | ||
542 | + selenium.answer_on_next_prompt(string) | ||
543 | +end |
@@ -0,0 +1,141 @@ | @@ -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,6 +3383,86 @@ div#activation_enterprise div { | ||
3383 | font-weight: bold; | 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 | /* ==> public/stylesheets/controller_cms.css <== */ | 3466 | /* ==> public/stylesheets/controller_cms.css <== */ |
3387 | 3467 | ||
3388 | 3468 |
test/factories.rb
@@ -449,4 +449,12 @@ module Noosfero::Factory | @@ -449,4 +449,12 @@ module Noosfero::Factory | ||
449 | { :singular => 'Litre', :plural => 'Litres', :environment_id => 1 } | 449 | { :singular => 'Litre', :plural => 'Litres', :environment_id => 1 } |
450 | end | 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 | end | 460 | end |
test/functional/manage_products_controller_test.rb
@@ -468,4 +468,47 @@ class ManageProductsControllerTest < Test::Unit::TestCase | @@ -468,4 +468,47 @@ class ManageProductsControllerTest < Test::Unit::TestCase | ||
468 | assert_response 403 | 468 | assert_response 403 |
469 | end | 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 | end | 514 | end |
test/unit/enterprise_test.rb
@@ -446,4 +446,8 @@ class EnterpriseTest < Test::Unit::TestCase | @@ -446,4 +446,8 @@ class EnterpriseTest < Test::Unit::TestCase | ||
446 | assert_equal false, enterprise.receives_scrap_notification? | 446 | assert_equal false, enterprise.receives_scrap_notification? |
447 | end | 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 | end | 453 | end |
test/unit/environment_test.rb
@@ -1216,4 +1216,7 @@ class EnvironmentTest < Test::Unit::TestCase | @@ -1216,4 +1216,7 @@ class EnvironmentTest < Test::Unit::TestCase | ||
1216 | assert_not_includes environment.enabled_plugins, plugin | 1216 | assert_not_includes environment.enabled_plugins, plugin |
1217 | end | 1217 | end |
1218 | 1218 | ||
1219 | + should 'have production costs' do | ||
1220 | + assert_respond_to Environment.default, :production_costs | ||
1221 | + end | ||
1219 | end | 1222 | end |
test/unit/input_test.rb
@@ -162,4 +162,19 @@ class InputTest < Test::Unit::TestCase | @@ -162,4 +162,19 @@ class InputTest < Test::Unit::TestCase | ||
162 | assert_kind_of Unit, input.build_unit | 162 | assert_kind_of Unit, input.build_unit |
163 | end | 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 | end | 180 | end |
@@ -0,0 +1,81 @@ | @@ -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 < Test::Unit::TestCase | @@ -382,4 +382,132 @@ class ProductTest < Test::Unit::TestCase | ||
382 | assert_includes Product.find_by_contents('thing'), p2 | 382 | assert_includes Product.find_by_contents('thing'), p2 |
383 | end | 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 | end | 513 | end |
@@ -0,0 +1,102 @@ | @@ -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 |