Commit 1ebe4f84dc0910b6ded065b60ed0e58612a93659
1 parent
3b213a94
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 (ActionItem1413)
Showing
32 changed files
with
968 additions
and
16 deletions
Show diff stats
app/controllers/my_profile/manage_products_controller.rb
... | ... | @@ -103,6 +103,26 @@ class ManageProductsController < ApplicationController |
103 | 103 | end |
104 | 104 | end |
105 | 105 | |
106 | + def manage_product_details | |
107 | + @product = @profile.products.find(params[:id]) | |
108 | + if request.post? | |
109 | + @product.update_price_details(params[:price_details]) if params[:price_details] | |
110 | + render :partial => 'display_price_details' | |
111 | + else | |
112 | + render :partial => 'manage_product_details' | |
113 | + end | |
114 | + end | |
115 | + | |
116 | + def remove_price_detail | |
117 | + @product = @profile.products.find(params[:product]) | |
118 | + @price_detail = @product.price_details.find(params[:id]) | |
119 | + @product = @price_detail.product | |
120 | + if request.post? | |
121 | + @price_detail.destroy | |
122 | + render :nothing => true | |
123 | + end | |
124 | + end | |
125 | + | |
106 | 126 | def destroy |
107 | 127 | @product = @profile.products.find(params[:id]) |
108 | 128 | if @product.destroy |
... | ... | @@ -159,4 +179,18 @@ class ManageProductsController < ApplicationController |
159 | 179 | end |
160 | 180 | end |
161 | 181 | |
182 | + def create_production_cost | |
183 | + cost = @profile.production_costs.create(:name => params[:id]) | |
184 | + if cost.valid? | |
185 | + cost.save | |
186 | + render :text => {:name => cost.name, | |
187 | + :id => cost.id, | |
188 | + :ok => true | |
189 | + }.to_json | |
190 | + else | |
191 | + render :text => {:ok => false, | |
192 | + :error_msg => _(cost.errors['name']) % {:fn => _('Name')} | |
193 | + }.to_json | |
194 | + end | |
195 | + end | |
162 | 196 | end | ... | ... |
app/helpers/manage_products_helper.rb
... | ... | @@ -271,4 +271,15 @@ 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 | |
274 | 285 | 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 | extra_data_for_index :product_categories |
11 | 12 | |
... | ... | @@ -164,5 +165,4 @@ class Enterprise < Organization |
164 | 165 | def enable_contact? |
165 | 166 | enable_contact_us |
166 | 167 | end |
167 | - | |
168 | 168 | end | ... | ... |
app/models/environment.rb
app/models/input.rb
... | ... | @@ -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 | 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 < 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 |
... | ... | @@ -149,4 +152,30 @@ class Product < ActiveRecord::Base |
149 | 152 | unit.blank? ? name : "#{name} - #{unit.name.downcase}" |
150 | 153 | end |
151 | 154 | |
155 | + def inputs_cost | |
156 | + return 0 if inputs.empty? | |
157 | + inputs.map(&:cost).inject { |sum,price| sum + price } | |
158 | + end | |
159 | + | |
160 | + def total_production_cost | |
161 | + return inputs_cost if price_details.empty? | |
162 | + inputs_cost + price_details.map(&:price).inject { |sum,price| sum + price } | |
163 | + end | |
164 | + | |
165 | + def price_described? | |
166 | + return false if price.nil? | |
167 | + (price - total_production_cost).zero? | |
168 | + end | |
169 | + | |
170 | + def update_price_details(price_details) | |
171 | + self.price_details.destroy_all | |
172 | + price_details.each do |price_detail| | |
173 | + self.price_details.create(price_detail) | |
174 | + end | |
175 | + end | |
176 | + | |
177 | + def available_production_costs | |
178 | + self.enterprise.environment.production_costs + self.enterprise.production_costs | |
179 | + end | |
180 | + | |
152 | 181 | end | ... | ... |
... | ... | @@ -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', :cache => 'cache-general' %> | |
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', 'manage-products', :cache => 'cache-general' %> | ... | ... |
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 %><%= | ... | ... |
... | ... | @@ -0,0 +1,19 @@ |
1 | +<div id='display-manage-price-details'></div> | |
2 | + | |
3 | +<% if @product.price_described? %> | |
4 | + <div id='display-price-details'> | |
5 | + <div id='price-details-title'><%= _('Price composition') %></div> | |
6 | + <ul class='price-details-list'> | |
7 | + <li> | |
8 | + <div class='price-detail-name'><%= _('Inputs:') %></div> | |
9 | + <div class='price-detail-price'><%= float_to_currency(@product.inputs_cost) %></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> | |
19 | +<% end %> | ... | ... |
app/views/manage_products/_edit_info.rhtml
... | ... | @@ -55,9 +55,4 @@ |
55 | 55 | <% end %> |
56 | 56 | <% end %> |
57 | 57 | |
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 %> | |
58 | +<%= render :partial => 'shared/numbers_only_javascript' %> | ... | ... |
... | ... | @@ -0,0 +1,13 @@ |
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 => 'price-details-price numbers-only')) %></td> | |
5 | + <td> | |
6 | + <%= button_to_remote(:remove, _('Remove'), | |
7 | + :update => "price-detail-#{price_detail.id}", | |
8 | + :confirm => _('Are you sure that you want to remove this cost?'), | |
9 | + :url => { :action => 'remove_price_detail', :id => price_detail, :product => @product }) %> | |
10 | + </tr> | |
11 | +<% end %> | |
12 | + | |
13 | +<%= render :partial => 'shared/numbers_only_javascript' %> | ... | ... |
... | ... | @@ -0,0 +1,53 @@ |
1 | +<%= button(:add, _('New cost'), '#', :id => 'add-new-cost') %> | |
2 | + | |
3 | +<% javascript_tag do %> | |
4 | + jQuery(function($) { | |
5 | + $('#progressbar').progressbar({ | |
6 | + value: <%= @product.total_production_cost %> | |
7 | + }); | |
8 | + if (<%= @product.price_described? %>) { | |
9 | + $('#progressbar-icon').addClass('ui-icon-check'); | |
10 | + $('#progressbar-icon').attr('title', $('#price-described-message').html()); | |
11 | + $('div.ui-progressbar-value').addClass('price-described'); | |
12 | + } | |
13 | + }); | |
14 | +<% end %> | |
15 | + | |
16 | +<div id="price-details-info"> | |
17 | + <div id="details-progressbar"> | |
18 | + <div id='progressbar'></div> | |
19 | + <div id='progressbar-text'><%= "%{currency}%{product_price}" % {:currency => environment.currency_unit, :production_cost_value => @product.total_production_cost, :product_price => @product.formatted_value(:price)} %></div> | |
20 | + </div> | |
21 | + <div id='progressbar-icon' class='ui-icon ui-icon-info' title='<%= _("The production cost of your product is not described yet. If you want to display the price composition, please add all the costs") %>'></div> | |
22 | + <div 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") %></div> | |
23 | +</div> | |
24 | + | |
25 | +<% form_tag({:action => 'manage_product_details'}, :method => 'post', :id => 'manage-product-details-form') do %> | |
26 | + <div> | |
27 | + <table id='display-product-price-details'> | |
28 | + <tr> | |
29 | + <td><%= _('Inputs') %></td> | |
30 | + <td colspan='2' class='inputs-cost'><%= float_to_currency(@product.inputs_cost) %></td> | |
31 | + </tr> | |
32 | + <%= render :partial => 'edit_price_details', :locals => {:price_details => @product.price_details} %> | |
33 | + </table> | |
34 | + </div> | |
35 | + | |
36 | + <% button_bar do %> | |
37 | + <%= submit_button :save, _('Save') %> | |
38 | + <%= button(:cancel, _('Cancel'), '#', :class => 'cancel-price-details') %> | |
39 | + <span class='loading-area'></span> | |
40 | + <% end %> | |
41 | + | |
42 | +<% end %> | |
43 | + | |
44 | +<div style='display:none'> | |
45 | + <table id='new-cost-fields'> | |
46 | + <tr> | |
47 | + <td><%= select_production_cost(@product) %></td> | |
48 | + <td><%= labelled_form_field(environment.currency_unit, text_field_tag('price_details[][price]', nil, :class => 'price-details-price')) %></td> | |
49 | + <td><%= link_to(_('Cancel'), '#', {:class => 'cancel-new-cost'}) %></td> | |
50 | + </tr> | |
51 | + </table> | |
52 | +</div> | |
53 | + | ... | ... |
... | ... | @@ -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
... | ... | @@ -38,6 +38,11 @@ |
38 | 38 | </div> |
39 | 39 | <% end %> |
40 | 40 | |
41 | + <div id='product-price-details'> | |
42 | + <%= render :partial => 'manage_products/display_price_details' %> | |
43 | + <%= render :partial => 'manage_products/price_details_button' %> | |
44 | + </div> | |
45 | + | |
41 | 46 | </div> |
42 | 47 | |
43 | 48 | <% button_bar do %> | ... | ... |
... | ... | @@ -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 @@ |
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
... | ... | @@ -9,7 +9,7 @@ |
9 | 9 | # |
10 | 10 | # It's strongly recommended to check this file into your version control system. |
11 | 11 | |
12 | -ActiveRecord::Schema.define(:version => 20110316171323) do | |
12 | +ActiveRecord::Schema.define(:version => 20110403193953) do | |
13 | 13 | |
14 | 14 | create_table "action_tracker", :force => true do |t| |
15 | 15 | t.integer "user_id" |
... | ... | @@ -315,6 +315,14 @@ ActiveRecord::Schema.define(:version => 20110316171323) do |
315 | 315 | t.datetime "updated_at" |
316 | 316 | end |
317 | 317 | |
318 | + create_table "price_details", :force => true do |t| | |
319 | + t.decimal "price", :default => 0.0 | |
320 | + t.integer "product_id" | |
321 | + t.integer "production_cost_id" | |
322 | + t.datetime "created_at" | |
323 | + t.datetime "updated_at" | |
324 | + end | |
325 | + | |
318 | 326 | create_table "product_categorizations", :force => true do |t| |
319 | 327 | t.integer "category_id" |
320 | 328 | t.integer "product_id" |
... | ... | @@ -334,6 +342,14 @@ ActiveRecord::Schema.define(:version => 20110316171323) do |
334 | 342 | t.datetime "updated_at" |
335 | 343 | end |
336 | 344 | |
345 | + create_table "production_costs", :force => true do |t| | |
346 | + t.string "name" | |
347 | + t.integer "owner_id" | |
348 | + t.string "owner_type" | |
349 | + t.datetime "created_at" | |
350 | + t.datetime "updated_at" | |
351 | + end | |
352 | + | |
337 | 353 | create_table "products", :force => true do |t| |
338 | 354 | t.integer "enterprise_id" |
339 | 355 | t.integer "product_category_id" | ... | ... |
... | ... | @@ -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
... | ... | @@ -175,6 +175,22 @@ Given /^the following certifiers$/ do |table| |
175 | 175 | end |
176 | 176 | end |
177 | 177 | |
178 | +Given /^the following production costs?$/ do |table| | |
179 | + table.hashes.map{|item| item.dup}.each do |item| | |
180 | + owner_type = item.delete('owner') | |
181 | + owner = owner_type == 'environment' ? Environment.default : Profile[owner_type] | |
182 | + ProductionCost.create!(item.merge(:owner => owner)) | |
183 | + end | |
184 | +end | |
185 | + | |
186 | +Given /^the following price details?$/ do |table| | |
187 | + table.hashes.map{|item| item.dup}.each do |item| | |
188 | + product = Product.find_by_name item.delete('product') | |
189 | + production_cost = ProductionCost.find_by_name item.delete('production_cost') | |
190 | + product.price_details.create!(item.merge(:production_cost => production_cost)) | |
191 | + end | |
192 | +end | |
193 | + | |
178 | 194 | Given /^I am logged in as "(.+)"$/ do |username| |
179 | 195 | visit('/account/logout') |
180 | 196 | visit('/account/login') |
... | ... | @@ -392,3 +408,6 @@ Given /^"([^\"]*)" asked to join "([^\"]*)"$/ do |person, organization| |
392 | 408 | AddMember.create!(:person => person, :organization => organization) |
393 | 409 | end |
394 | 410 | |
411 | +And /^I want to add "([^\"]*)" as cost$/ do |string| | |
412 | + selenium.answer_on_next_prompt(string) | |
413 | +end | ... | ... |
... | ... | @@ -0,0 +1,66 @@ |
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 | + $("#manage-product-details-button").show(); | |
17 | + $("#display-price-details").show(); | |
18 | + $("#display-manage-price-details").html(''); | |
19 | + return false; | |
20 | + }); | |
21 | + | |
22 | + $("#manage-product-details-form").live('submit', function(data) { | |
23 | + var form = this; | |
24 | + $(form).find('.loading-area').addClass('small-loading'); | |
25 | + $(form).css('cursor', 'progress'); | |
26 | + $.post(form.action, $(form).serialize(), function(data) { | |
27 | + $("#display-manage-price-details").html(data); | |
28 | + $("#manage-product-details-button").show(); | |
29 | + }); | |
30 | + return false; | |
31 | + }); | |
32 | + | |
33 | + $("#add-new-cost").live('click', function() { | |
34 | + $('#display-product-price-details tbody').append($('#new-cost-fields tbody').html()); | |
35 | + return false; | |
36 | + }); | |
37 | + | |
38 | + $(".cancel-new-cost").live('click', function() { | |
39 | + $(this).parents('tr').remove(); | |
40 | + return false; | |
41 | + }); | |
42 | + | |
43 | +})(jQuery); | |
44 | + | |
45 | +function productionCostTypeChange(select, url, question, error_msg) { | |
46 | + if (select.value == '') { | |
47 | + var newType = prompt(question); | |
48 | + jQuery.ajax({ | |
49 | + url: url + "/" + newType, | |
50 | + dataType: 'json', | |
51 | + success: function(data, status, ajax){ | |
52 | + if (data.ok) { | |
53 | + var opt = jQuery('<option value="' + data.id + '">' + newType + '</option>'); | |
54 | + opt.insertBefore(jQuery("option:last", select)); | |
55 | + select.selectedIndex = select.options.length - 2; | |
56 | + opt.clone().insertBefore('#new-cost-fields .production-cost-selection option:last'); | |
57 | + } else { | |
58 | + alert(data.error_msg); | |
59 | + } | |
60 | + }, | |
61 | + error: function(ajax, status, error){ | |
62 | + alert(error_msg); | |
63 | + } | |
64 | + }); | |
65 | + } | |
66 | +} | ... | ... |
public/stylesheets/application.css
... | ... | @@ -3216,6 +3216,85 @@ div#activation_enterprise div { |
3216 | 3216 | font-weight: bold; |
3217 | 3217 | } |
3218 | 3218 | |
3219 | +/* * * * * * Price details * * * * * */ | |
3220 | + | |
3221 | +#product-price-details { | |
3222 | + border: 1px solid #AAA; | |
3223 | + margin-top: 20px; | |
3224 | + padding: 15px 20px; | |
3225 | +} | |
3226 | + | |
3227 | +#display-price-details .price-details-list { | |
3228 | + padding-left: 0px; | |
3229 | +} | |
3230 | + | |
3231 | +#display-price-details .price-details-list li { | |
3232 | + list-style: none; | |
3233 | +} | |
3234 | + | |
3235 | +#display-price-details .price-details-list li .price-detail-name { | |
3236 | + width: 200px; | |
3237 | +} | |
3238 | + | |
3239 | +#display-price-details .price-details-list li .price-detail-name, | |
3240 | +#display-price-details .price-details-list li .price-detail-price { | |
3241 | + display: inline-block; | |
3242 | +} | |
3243 | + | |
3244 | +#manage-product-details-form .formlabel, | |
3245 | +#manage-product-details-form .formfield { | |
3246 | + display: inline-block; | |
3247 | +} | |
3248 | + | |
3249 | +/* * * Progress bar on price details edition * * */ | |
3250 | + | |
3251 | +#display-manage-price-details .ui-widget-content { | |
3252 | + border: 1px solid #DDD; | |
3253 | +} | |
3254 | + | |
3255 | +#display-manage-price-details .ui-progressbar { | |
3256 | + height: 20px; | |
3257 | +} | |
3258 | + | |
3259 | +#display-manage-price-details .ui-progressbar .ui-progressbar-value { | |
3260 | + margin: 0px; | |
3261 | + background-color: #A40000; | |
3262 | +} | |
3263 | + | |
3264 | +#display-manage-price-details .ui-progressbar .ui-progressbar-value.price-described { | |
3265 | + background-color: #4E9A06; | |
3266 | +} | |
3267 | + | |
3268 | +#display-manage-price-details #price-details-info { | |
3269 | + margin: 10px 0px; | |
3270 | +} | |
3271 | + | |
3272 | +#display-manage-price-details #details-progressbar { | |
3273 | + position: relative; | |
3274 | + width: 410px; | |
3275 | + display: inline-block; | |
3276 | +} | |
3277 | + | |
3278 | +#display-manage-price-details #progressbar-text { | |
3279 | + position: absolute; | |
3280 | + top: 5px; | |
3281 | + right: 7px; | |
3282 | + font-size: 11px; | |
3283 | +} | |
3284 | + | |
3285 | +#display-manage-price-details #progressbar-icon { | |
3286 | + display: inline-block; | |
3287 | + cursor: pointer; | |
3288 | +} | |
3289 | + | |
3290 | +#display-manage-price-details #details-progressbar .ui-corner-left, | |
3291 | +#display-manage-price-details #details-progressbar .ui-corner-right { | |
3292 | + -moz-border-radius-bottomleft: 0px; | |
3293 | + -moz-border-radius-bottomright: 0px; | |
3294 | + -moz-border-radius-topleft: 0px; | |
3295 | + -moz-border-radius-topright: 0px; | |
3296 | +} | |
3297 | + | |
3219 | 3298 | /* ==> public/stylesheets/controller_cms.css <== */ |
3220 | 3299 | |
3221 | 3300 | ... | ... |
test/factories.rb
... | ... | @@ -448,4 +448,12 @@ module Noosfero::Factory |
448 | 448 | { :singular => 'Litre', :plural => 'Litres', :environment_id => 1 } |
449 | 449 | end |
450 | 450 | |
451 | + ############################################### | |
452 | + # Production Cost | |
453 | + ############################################### | |
454 | + | |
455 | + def defaults_for_production_cost | |
456 | + { :name => 'Production cost ' + factory_num_seq.to_s } | |
457 | + end | |
458 | + | |
451 | 459 | end | ... | ... |
test/functional/manage_products_controller_test.rb
... | ... | @@ -421,4 +421,47 @@ class ManageProductsControllerTest < Test::Unit::TestCase |
421 | 421 | assert_tag :tag => 'div', :attributes => { :id => "product-#{product.id}-tabs" }, :descendant => {:tag => 'a', :attributes => {:href => '#product-inputs'}, :content => 'Inputs and raw material'} |
422 | 422 | end |
423 | 423 | |
424 | + should 'remove price detail of a product' do | |
425 | + product = fast_create(Product, :enterprise_id => @enterprise.id, :product_category_id => @product_category.id) | |
426 | + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') | |
427 | + detail = product.price_details.create(:production_cost_id => cost.id, :price => 10) | |
428 | + | |
429 | + assert_equal [detail], product.price_details | |
430 | + | |
431 | + post :remove_price_detail, :id => detail.id, :product => product, :profile => @enterprise.identifier | |
432 | + product.reload | |
433 | + assert_equal [], product.price_details | |
434 | + end | |
435 | + | |
436 | + should 'create a production cost for enterprise' do | |
437 | + get :create_production_cost, :profile => @enterprise.identifier, :id => 'Taxes' | |
438 | + | |
439 | + assert_equal ['Taxes'], Enterprise.find(@enterprise.id).production_costs.map(&:name) | |
440 | + resp = ActiveSupport::JSON.decode(@response.body) | |
441 | + assert_equal 'Taxes', resp['name'] | |
442 | + assert resp['id'].kind_of?(Integer) | |
443 | + assert resp['ok'] | |
444 | + assert_nil resp['error_msg'] | |
445 | + end | |
446 | + | |
447 | + should 'display error if production cost has no name' do | |
448 | + get :create_production_cost, :profile => @enterprise.identifier | |
449 | + | |
450 | + resp = ActiveSupport::JSON.decode(@response.body) | |
451 | + assert_nil resp['name'] | |
452 | + assert_nil resp['id'] | |
453 | + assert !resp['ok'] | |
454 | + assert_match /blank/, resp['error_msg'] | |
455 | + end | |
456 | + | |
457 | + should 'display error if name of production cost is too long' do | |
458 | + get :create_production_cost, :profile => @enterprise.identifier, :id => 'a'*60 | |
459 | + | |
460 | + resp = ActiveSupport::JSON.decode(@response.body) | |
461 | + assert_nil resp['name'] | |
462 | + assert_nil resp['id'] | |
463 | + assert !resp['ok'] | |
464 | + assert_match /too long/, resp['error_msg'] | |
465 | + end | |
466 | + | |
424 | 467 | end | ... | ... |
test/unit/enterprise_test.rb
... | ... | @@ -423,4 +423,8 @@ class EnterpriseTest < Test::Unit::TestCase |
423 | 423 | assert_equal false, enterprise.receives_scrap_notification? |
424 | 424 | end |
425 | 425 | |
426 | + should 'have production cost' do | |
427 | + e = fast_create(Enterprise) | |
428 | + assert_respond_to e, :production_costs | |
429 | + end | |
426 | 430 | end | ... | ... |
test/unit/environment_test.rb
... | ... | @@ -1116,4 +1116,7 @@ class EnvironmentTest < Test::Unit::TestCase |
1116 | 1116 | assert_equal ["Meter", "Kilo", "Litre"], Environment.default.units.map(&:singular) |
1117 | 1117 | end |
1118 | 1118 | |
1119 | + should 'have production costs' do | |
1120 | + assert_respond_to Environment.default, :production_costs | |
1121 | + end | |
1119 | 1122 | end | ... | ... |
test/unit/input_test.rb
... | ... | @@ -162,4 +162,19 @@ class InputTest < 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 | ... | ... |
... | ... | @@ -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
... | ... | @@ -375,4 +375,132 @@ class ProductTest < Test::Unit::TestCase |
375 | 375 | assert_includes Product.find_by_contents('thing'), p2 |
376 | 376 | end |
377 | 377 | |
378 | + should 'respond to price details' do | |
379 | + product = Product.new | |
380 | + assert_respond_to product, :price_details | |
381 | + end | |
382 | + | |
383 | + should 'return total value of inputs' do | |
384 | + product = fast_create(Product) | |
385 | + first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2) | |
386 | + second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1) | |
387 | + | |
388 | + assert_equal 50.0, product.inputs_cost | |
389 | + end | |
390 | + | |
391 | + should 'return 0 on total value of inputs if has no input' do | |
392 | + product = fast_create(Product) | |
393 | + | |
394 | + assert product.inputs_cost.zero? | |
395 | + end | |
396 | + | |
397 | + should 'know if price is described' do | |
398 | + product = fast_create(Product, :price => 30.0) | |
399 | + | |
400 | + first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 1) | |
401 | + assert !Product.find(product.id).price_described? | |
402 | + | |
403 | + second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1) | |
404 | + assert Product.find(product.id).price_described? | |
405 | + end | |
406 | + | |
407 | + should 'return false on price_described if price of product is not defined' do | |
408 | + product = fast_create(Product) | |
409 | + | |
410 | + assert_equal false, product.price_described? | |
411 | + end | |
412 | + | |
413 | + should 'create price details' do | |
414 | + product = fast_create(Product) | |
415 | + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') | |
416 | + assert product.price_details.empty? | |
417 | + | |
418 | + product.update_price_details([{:production_cost_id => cost.id, :price => 10}]) | |
419 | + assert_equal 1, Product.find(product.id).price_details.size | |
420 | + end | |
421 | + | |
422 | + should 'update price of a cost on price details' do | |
423 | + product = fast_create(Product) | |
424 | + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') | |
425 | + cost2 = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') | |
426 | + price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10) | |
427 | + assert !product.price_details.empty? | |
428 | + | |
429 | + product.update_price_details([{:production_cost_id => cost.id, :price => 20}, {:production_cost_id => cost2.id, :price => 30}]) | |
430 | + assert_equal 20, product.price_details.find_by_production_cost_id(cost.id).price | |
431 | + assert_equal 2, Product.find(product.id).price_details.size | |
432 | + end | |
433 | + | |
434 | + should 'destroy price details if product is removed' do | |
435 | + product = fast_create(Product) | |
436 | + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') | |
437 | + price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10) | |
438 | + | |
439 | + assert_difference PriceDetail, :count, -1 do | |
440 | + product.destroy | |
441 | + end | |
442 | + end | |
443 | + | |
444 | + should 'have production costs' do | |
445 | + product = fast_create(Product) | |
446 | + cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment') | |
447 | + product.price_details.create(:production_cost_id => cost.id, :price => 10) | |
448 | + assert_equal [cost], Product.find(product.id).production_costs | |
449 | + end | |
450 | + | |
451 | + should 'return production costs from enterprise and environment' do | |
452 | + ent = fast_create(Enterprise) | |
453 | + product = fast_create(Product, :enterprise_id => ent.id) | |
454 | + ent_production_cost = fast_create(ProductionCost, :owner_id => ent.id, :owner_type => 'Profile') | |
455 | + env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment') | |
456 | + | |
457 | + assert_equal [env_production_cost, ent_production_cost], product.available_production_costs | |
458 | + end | |
459 | + | |
460 | + should 'return all production costs' do | |
461 | + ent = fast_create(Enterprise) | |
462 | + product = fast_create(Product, :enterprise_id => ent.id) | |
463 | + | |
464 | + env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment') | |
465 | + ent_production_cost = fast_create(ProductionCost, :owner_id => ent.id, :owner_type => 'Profile') | |
466 | + product.price_details.create(:production_cost => env_production_cost, :product => product) | |
467 | + assert_equal [env_production_cost, ent_production_cost], product.available_production_costs | |
468 | + end | |
469 | + | |
470 | + should 'return total value of production costs' do | |
471 | + ent = fast_create(Enterprise) | |
472 | + product = fast_create(Product, :enterprise_id => ent.id) | |
473 | + | |
474 | + env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment') | |
475 | + price_detail = product.price_details.create(:production_cost => env_production_cost, :price => 10) | |
476 | + | |
477 | + input = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2) | |
478 | + | |
479 | + assert_equal price_detail.price + input.cost, product.total_production_cost | |
480 | + end | |
481 | + | |
482 | + should 'return inputs cost as total value of production costs if has no price details' do | |
483 | + ent = fast_create(Enterprise) | |
484 | + product = fast_create(Product, :enterprise_id => ent.id) | |
485 | + | |
486 | + input = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2) | |
487 | + | |
488 | + assert_equal input.cost, product.total_production_cost | |
489 | + end | |
490 | + | |
491 | + should 'return 0 on total production cost if has no input and price details' do | |
492 | + product = fast_create(Product) | |
493 | + | |
494 | + assert product.total_production_cost.zero? | |
495 | + end | |
496 | + | |
497 | + should 'format inputs cost values to float with 2 decimals' do | |
498 | + ent = fast_create(Enterprise) | |
499 | + product = fast_create(Product, :enterprise_id => ent.id) | |
500 | + first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2) | |
501 | + second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1) | |
502 | + | |
503 | + assert_equal "50.00", product.formatted_value(:inputs_cost) | |
504 | + end | |
505 | + | |
378 | 506 | end | ... | ... |
... | ... | @@ -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 | ... | ... |