+
+ <%= _("The production cost of your product is not described yet. If you want to display the price composition, please add all the costs") %>
+ <%= _("The production cost of your product is fully described and will be displayed on your product's page") %>
+
diff --git a/app/views/manage_products/_price_details_button.rhtml b/app/views/manage_products/_price_details_button.rhtml
new file mode 100644
index 0000000..8ee8eaf
--- /dev/null
+++ b/app/views/manage_products/_price_details_button.rhtml
@@ -0,0 +1,10 @@
+<%= edit_ui_button(
+ _('Describe here the cost of production'),
+ {:action => 'manage_product_details', :id => @product.id},
+ :id => 'manage-product-details-button',
+ 'data-primary-icon' => 'ui-icon-pencil',
+ 'data-secondary-icon' => 'ui-icon-triangle-1-s',
+ :title => _('Describe details about how the price was defined')
+) %>
+<%= javascript_tag("render_jquery_ui_buttons('manage-product-details-button')") %>
+
diff --git a/app/views/manage_products/show.rhtml b/app/views/manage_products/show.rhtml
index ba5c3bf..fa118b7 100644
--- a/app/views/manage_products/show.rhtml
+++ b/app/views/manage_products/show.rhtml
@@ -23,7 +23,7 @@
- <% unless !@allowed_user && (@product.description.blank? && @product.inputs.empty?) %>
+ <% unless !@allowed_user && (@product.description.blank? && @product.inputs.empty? && !@product.price_described? ) %>
<% if !@product.description.blank? || @allowed_user %>
@@ -32,6 +32,9 @@
<% if !@product.inputs.empty? || @allowed_user %>
<% end %>
diff --git a/app/views/shared/_numbers_only_javascript.rhtml b/app/views/shared/_numbers_only_javascript.rhtml
new file mode 100644
index 0000000..6d77641
--- /dev/null
+++ b/app/views/shared/_numbers_only_javascript.rhtml
@@ -0,0 +1,6 @@
+<% javascript_tag do %>
+ jQuery(".numbers-only").keypress(function(event) {
+ var separator = "<%= environment.currency_separator %>"
+ return numbersonly(event, separator)
+ });
+<% end %>
diff --git a/db/migrate/20110403184315_create_production_cost.rb b/db/migrate/20110403184315_create_production_cost.rb
new file mode 100644
index 0000000..027d3a4
--- /dev/null
+++ b/db/migrate/20110403184315_create_production_cost.rb
@@ -0,0 +1,13 @@
+class CreateProductionCost < ActiveRecord::Migration
+ def self.up
+ create_table :production_costs do |t|
+ t.string :name
+ t.references :owner, :polymorphic => true
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :production_costs
+ end
+end
diff --git a/db/migrate/20110403193953_create_price_details.rb b/db/migrate/20110403193953_create_price_details.rb
new file mode 100644
index 0000000..41e55d2
--- /dev/null
+++ b/db/migrate/20110403193953_create_price_details.rb
@@ -0,0 +1,14 @@
+class CreatePriceDetails < ActiveRecord::Migration
+ def self.up
+ create_table :price_details do |t|
+ t.decimal :price, :default => 0
+ t.references :product
+ t.references :production_cost
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :price_details
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1f48a0b..f8285a1 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -323,6 +323,14 @@ ActiveRecord::Schema.define(:version => 20111004184104) do
t.datetime "updated_at"
end
+ create_table "price_details", :force => true do |t|
+ t.decimal "price", :default => 0.0
+ t.integer "product_id"
+ t.integer "production_cost_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
create_table "product_categorizations", :force => true do |t|
t.integer "category_id"
t.integer "product_id"
@@ -342,6 +350,14 @@ ActiveRecord::Schema.define(:version => 20111004184104) do
t.datetime "updated_at"
end
+ create_table "production_costs", :force => true do |t|
+ t.string "name"
+ t.integer "owner_id"
+ t.string "owner_type"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
create_table "products", :force => true do |t|
t.integer "enterprise_id"
t.integer "product_category_id"
diff --git a/features/manage_product_price_details.feature b/features/manage_product_price_details.feature
new file mode 100644
index 0000000..a08abf8
--- /dev/null
+++ b/features/manage_product_price_details.feature
@@ -0,0 +1,148 @@
+
+Feature: manage product price details
+ As an enterprise owner
+ I want to manage the details of product's price
+
+ Background:
+ Given the following users
+ | login | name |
+ | joaosilva | Joao Silva |
+ And the following enterprises
+ | identifier | owner | name | enabled |
+ | redemoinho | joaosilva | Rede Moinho | true |
+ Given the following product_category
+ | name |
+ | Music |
+ And the following product_categories
+ | name | parent |
+ | Rock | music |
+ | CD Player | music |
+ And the following product
+ | owner | category | name | price |
+ | redemoinho | rock | Abbey Road | 80.0 |
+ And feature "disable_products_for_enterprises" is disabled on environment
+ And the following inputs
+ | product | category | price_per_unit | amount_used |
+ | Abbey Road | Rock | 10.0 | 2 |
+ | Abbey Road | CD Player | 20.0 | 2 |
+ And the following production cost
+ | name | owner |
+ | Taxes | environment |
+
+ @selenium
+ Scenario: list total value of inputs as price details
+ Given I am logged in as "joaosilva"
+ When I go to Rede Moinho's page of product Abbey Road
+ And I follow "Describe here the cost of production"
+ Then I should see "Inputs"
+ And I should see "60.0" within ".inputs-cost"
+
+ @selenium
+ Scenario: cancel management of price details
+ Given I am logged in as "joaosilva"
+ When I go to Rede Moinho's page of product Abbey Road
+ And I follow "Describe here the cost of production"
+ When I follow "Cancel"
+ Then I should see "Describe here the cost of production"
+
+ @selenium
+ Scenario: return to product after save
+ Given I am logged in as "joaosilva"
+ When I go to Rede Moinho's page of product Abbey Road
+ And I follow "Describe here the cost of production"
+ And I press "Save"
+ Then I should be on Rede Moinho's page of product Abbey Road
+
+ @selenium
+ Scenario: add first item on price details
+ Given I am logged in as "joaosilva"
+ When I go to Rede Moinho's page of product Abbey Road
+ And I follow "Describe here the cost of production"
+ And I follow "New cost"
+ And I select "Taxes"
+ And I fill in "$" with "5.00"
+ And I press "Save"
+ Then I should not see "Save"
+ And I should see "Describe here the cost of production"
+
+ @selenium
+ Scenario: edit a production cost
+ Given the following production cost
+ | name | owner |
+ | Energy | environment |
+ Given I am logged in as "joaosilva"
+ When I go to Rede Moinho's page of product Abbey Road
+ And I follow "Describe here the cost of production"
+ And I follow "New cost"
+ And I select "Taxes"
+ And I fill in "$" with "20.00"
+ And I press "Save"
+ Then I should not see "Save"
+ And I should see "Taxes" within "#display-price-details"
+ When I follow "Describe here the cost of production"
+ And I select "Energy"
+ And I press "Save"
+ And I should not see "Taxes" within "#display-price-details"
+ And I should see "Energy" within "#display-price-details"
+
+ Scenario: not display product detail button if product does not have input
+ Given the following product
+ | owner | category | name |
+ | redemoinho | rock | Yellow Submarine |
+ And the following user
+ | login | name |
+ | mariasouza | Maria Souza |
+ And I am logged in as "mariasouza"
+ When I go to Rede Moinho's page of product Yellow Submarine
+ Then I should not see "Describe here the cost of production"
+
+ Scenario: not display price details if price is not fully described
+ Given I go to Rede Moinho's page of product Abbey Road
+ Then I should not see "60.0"
+
+ @selenium
+ Scenario: display price details if price is fully described
+ Given I am logged in as "joaosilva"
+ And I go to Rede Moinho's page of product Abbey Road
+ And I follow "Describe here the cost of production"
+ And I follow "New cost"
+ And I select "Taxes"
+ And I fill in "$" with "20.00"
+ And I press "Save"
+ Then I should see "Inputs" within ".price-detail-name"
+ And I should see "60.0" within ".price-detail-price"
+
+ @selenium
+ Scenario: create a new cost clicking on select
+ Given I am logged in as "joaosilva"
+ And I go to Rede Moinho's page of product Abbey Road
+ And I follow "Describe here the cost of production"
+ And I want to add "Energy" as cost
+ And I select "Other cost"
+ And I press "Save"
+ When I follow "Describe here the cost of production"
+ Then I should see "Energy" within ".production-cost-selection"
+
+ @selenium
+ Scenario: add created cost on new-cost-fields
+ Given I am logged in as "joaosilva"
+ And I go to Rede Moinho's page of product Abbey Road
+ And I follow "Describe here the cost of production"
+ And I want to add "Energy" as cost
+ And I select "Other cost"
+ Then I should see "Energy" within "#new-cost-fields"
+
+ @selenium
+ Scenario: remove price detail
+ Given the following price detail
+ | product | production_cost | price |
+ | Abbey Road | Taxes | 20.0 |
+ And I am logged in as "joaosilva"
+ And I go to Rede Moinho's page of product Abbey Road
+ And I follow "Describe here the cost of production"
+ And I should see "Taxes" within "#manage-product-details-form"
+ When I follow "Remove" within "#manage-product-details-form"
+ And I confirm
+ And I press "Save"
+ And I follow "Describe here the cost of production"
+ Then I should not see "Taxes" within "#manage-product-details-form"
diff --git a/features/step_definitions/noosfero_steps.rb b/features/step_definitions/noosfero_steps.rb
index 0a075f7..61f2bd9 100644
--- a/features/step_definitions/noosfero_steps.rb
+++ b/features/step_definitions/noosfero_steps.rb
@@ -222,6 +222,22 @@ Given /^the following certifiers$/ do |table|
end
end
+Given /^the following production costs?$/ do |table|
+ table.hashes.map{|item| item.dup}.each do |item|
+ owner_type = item.delete('owner')
+ owner = owner_type == 'environment' ? Environment.default : Profile[owner_type]
+ ProductionCost.create!(item.merge(:owner => owner))
+ end
+end
+
+Given /^the following price details?$/ do |table|
+ table.hashes.map{|item| item.dup}.each do |item|
+ product = Product.find_by_name item.delete('product')
+ production_cost = ProductionCost.find_by_name item.delete('production_cost')
+ product.price_details.create!(item.merge(:production_cost => production_cost))
+ end
+end
+
Given /^I am logged in as "(.+)"$/ do |username|
visit('/account/logout')
visit('/account/login')
@@ -521,3 +537,7 @@ Given /^the following enterprise homepages?$/ do |table|
ent.articles << home
end
end
+
+And /^I want to add "([^\"]*)" as cost$/ do |string|
+ selenium.answer_on_next_prompt(string)
+end
diff --git a/public/javascripts/manage-products.js b/public/javascripts/manage-products.js
new file mode 100644
index 0000000..72034fa
--- /dev/null
+++ b/public/javascripts/manage-products.js
@@ -0,0 +1,141 @@
+(function($) {
+
+ $("#manage-product-details-button").live('click', function() {
+ $("#product-price-details").find('.loading-area').addClass('small-loading');
+ url = $(this).attr('href');
+ $.get(url, function(data){
+ $("#manage-product-details-button").hide();
+ $("#display-price-details").hide();
+ $("#display-manage-price-details").html(data);
+ $("#product-price-details").find('.loading-area').removeClass('small-loading');
+ });
+ return false;
+ });
+
+ $(".cancel-price-details").live('click', function() {
+ if ( !$(this).hasClass('form-changed') ) {
+ cancelPriceDetailsEdition();
+ } else {
+ if (confirm($(this).attr('data-confirm'))) {
+ cancelPriceDetailsEdition();
+ }
+ }
+ return false;
+ });
+
+ $("#manage-product-details-form").live('submit', function(data) {
+ var form = this;
+ $(form).find('.loading-area').addClass('small-loading');
+ $(form).css('cursor', 'progress');
+ $.post(form.action, $(form).serialize(), function(data) {
+ $("#display-manage-price-details").html(data);
+ $("#manage-product-details-button").show();
+ });
+ if ($('#progressbar-icon').hasClass('ui-icon-check')) {
+ display_notice($('#price-described-notice').show());
+ }
+ return false;
+ });
+
+ $("#add-new-cost").live('click', function() {
+ $('#display-product-price-details tbody').append($('#new-cost-fields tbody').html());
+ return false;
+ });
+
+ $(".cancel-new-cost").live('click', function() {
+ $(this).parents('tr').remove();
+ return false;
+ });
+
+ $("#product-info-form").live('submit', function(data) {
+ var form = this;
+ updatePriceCompositionBar(form);
+ });
+
+ $("form.edit_input").live('submit', function(data) {
+ var form = this;
+ updatePriceCompositionBar(form);
+ inputs_cost_update_url = $(form).find('#inputs-cost-update-url').val();
+ $.get(inputs_cost_update_url, function(data){
+ $(".inputs-cost").html(data);
+ });
+ return false;
+ });
+
+ $("#manage-product-details-form .price-details-price").live('keydown', function(data) {
+ $('.cancel-price-details').addClass('form-changed');
+ var product_price = parseFloat($('form #product_price').val());
+ var total_cost = parseFloat($('#product_inputs_cost').val());
+
+ $('form .price-details-price').each(function() {
+ total_cost = total_cost + parseFloat($(this).val());
+ });
+ enablePriceDetailSubmit();
+
+ var described = (product_price - total_cost) == 0;
+ var percentage = total_cost * 100 / product_price;
+ priceCompositionBar(percentage, described, total_cost, product_price);
+ });
+
+ function cancelPriceDetailsEdition() {
+ $("#manage-product-details-button").show();
+ $("#display-price-details").show();
+ $("#display-manage-price-details").html('');
+ };
+
+ function updatePriceCompositionBar(form) {
+ bar_url = $(form).find('.bar-update-url').val();
+ $.get(bar_url, function(data){
+ $("#price-composition-bar").html(data);
+ });
+ };
+
+ function enablePriceDetailSubmit() {
+ $('#manage-product-details-form input.submit').removeAttr("disabled").removeClass('disabled');
+ };
+
+})(jQuery);
+
+function productionCostTypeChange(select, url, question, error_msg) {
+ if (select.value == '') {
+ var newType = prompt(question);
+ jQuery.ajax({
+ url: url + "/" + newType,
+ dataType: 'json',
+ success: function(data, status, ajax){
+ if (data.ok) {
+ var opt = jQuery('');
+ opt.insertBefore(jQuery("option:last", select));
+ select.selectedIndex = select.options.length - 2;
+ opt.clone().insertBefore('#new-cost-fields .production-cost-selection option:last');
+ } else {
+ alert(data.error_msg);
+ }
+ },
+ error: function(ajax, status, error){
+ alert(error_msg);
+ }
+ });
+ }
+}
+
+function priceCompositionBar(value, described, total_cost, price) {
+ jQuery(function($) {
+ var bar_area = $('#price-composition-bar');
+ $(bar_area).find('#progressbar').progressbar({
+ value: value
+ });
+ $(bar_area).find('.production-cost').html(total_cost.toFixed(2));
+ $(bar_area).find('.product_price').html(price.toFixed(2));
+ if (described) {
+ $(bar_area).find('#progressbar-icon').addClass('ui-icon-check');
+ $(bar_area).find('#progressbar-icon').attr('title', $('#price-described-message').html());
+ $(bar_area).find('div.ui-progressbar-value').addClass('price-described');
+ } else {
+ $(bar_area).find('#progressbar-icon').removeClass('ui-icon-check');
+ $(bar_area).find('#progressbar-icon').attr('title', $('#price-not-described-message').html());
+ $(bar_area).find('div.ui-progressbar-value').removeClass('price-described');
+
+ }
+ });
+}
diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css
index 35e7e78..11ada2c 100644
--- a/public/stylesheets/application.css
+++ b/public/stylesheets/application.css
@@ -3383,6 +3383,86 @@ div#activation_enterprise div {
font-weight: bold;
}
+/* * * * * * Price details * * * * * */
+
+#display-price-details .price-details-list {
+ padding-left: 0px;
+}
+
+#display-price-details .price-details-list li {
+ list-style: none;
+}
+
+#display-price-details .price-details-list li .price-detail-name {
+ width: 200px;
+}
+
+#display-price-details .price-details-list li .price-detail-name,
+#display-price-details .price-details-list li .price-detail-price {
+ display: inline-block;
+}
+
+#manage-product-details-form .formlabel,
+#manage-product-details-form .formfield {
+ display: inline-block;
+}
+
+#manage-product-details-form #add-new-cost {
+ float: right;
+}
+
+/* * * Progress bar on price details edition * * */
+
+#display-manage-price-details .ui-widget-content {
+ border: 1px solid #DDD;
+}
+
+#display-manage-price-details .ui-progressbar {
+ height: 20px;
+}
+
+#display-manage-price-details .ui-progressbar .ui-progressbar-value {
+ margin: 0px;
+ background-color: #A40000;
+ filter:alpha(opacity=70);
+ -moz-opacity: 0.7;
+ opacity: 0.7;
+}
+
+#display-manage-price-details .ui-progressbar .ui-progressbar-value.price-described {
+ background-color: #4E9A06;
+}
+
+#display-manage-price-details #price-details-info {
+ margin: 10px 0px;
+}
+
+#display-manage-price-details #details-progressbar {
+ position: relative;
+ width: 410px;
+ display: inline-block;
+}
+
+#display-manage-price-details #progressbar-text {
+ position: absolute;
+ top: 5px;
+ right: 7px;
+ font-size: 11px;
+}
+
+#display-manage-price-details #progressbar-icon {
+ display: inline-block;
+ cursor: pointer;
+}
+
+#display-manage-price-details #details-progressbar .ui-corner-left,
+#display-manage-price-details #details-progressbar .ui-corner-right {
+ -moz-border-radius-bottomleft: 0px;
+ -moz-border-radius-bottomright: 0px;
+ -moz-border-radius-topleft: 0px;
+ -moz-border-radius-topright: 0px;
+}
+
/* ==> public/stylesheets/controller_cms.css <== */
diff --git a/test/factories.rb b/test/factories.rb
index 0df1bf7..06956d0 100644
--- a/test/factories.rb
+++ b/test/factories.rb
@@ -449,4 +449,12 @@ module Noosfero::Factory
{ :singular => 'Litre', :plural => 'Litres', :environment_id => 1 }
end
+ ###############################################
+ # Production Cost
+ ###############################################
+
+ def defaults_for_production_cost
+ { :name => 'Production cost ' + factory_num_seq.to_s }
+ end
+
end
diff --git a/test/functional/manage_products_controller_test.rb b/test/functional/manage_products_controller_test.rb
index 58442de..4db6d8f 100644
--- a/test/functional/manage_products_controller_test.rb
+++ b/test/functional/manage_products_controller_test.rb
@@ -468,4 +468,47 @@ class ManageProductsControllerTest < Test::Unit::TestCase
assert_response 403
end
+ should 'remove price detail of a product' do
+ product = fast_create(Product, :enterprise_id => @enterprise.id, :product_category_id => @product_category.id)
+ cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
+ detail = product.price_details.create(:production_cost_id => cost.id, :price => 10)
+
+ assert_equal [detail], product.price_details
+
+ post :remove_price_detail, :id => detail.id, :product => product, :profile => @enterprise.identifier
+ product.reload
+ assert_equal [], product.price_details
+ end
+
+ should 'create a production cost for enterprise' do
+ get :create_production_cost, :profile => @enterprise.identifier, :id => 'Taxes'
+
+ assert_equal ['Taxes'], Enterprise.find(@enterprise.id).production_costs.map(&:name)
+ resp = ActiveSupport::JSON.decode(@response.body)
+ assert_equal 'Taxes', resp['name']
+ assert resp['id'].kind_of?(Integer)
+ assert resp['ok']
+ assert_nil resp['error_msg']
+ end
+
+ should 'display error if production cost has no name' do
+ get :create_production_cost, :profile => @enterprise.identifier
+
+ resp = ActiveSupport::JSON.decode(@response.body)
+ assert_nil resp['name']
+ assert_nil resp['id']
+ assert !resp['ok']
+ assert_match /blank/, resp['error_msg']
+ end
+
+ should 'display error if name of production cost is too long' do
+ get :create_production_cost, :profile => @enterprise.identifier, :id => 'a'*60
+
+ resp = ActiveSupport::JSON.decode(@response.body)
+ assert_nil resp['name']
+ assert_nil resp['id']
+ assert !resp['ok']
+ assert_match /too long/, resp['error_msg']
+ end
+
end
diff --git a/test/unit/enterprise_test.rb b/test/unit/enterprise_test.rb
index b454779..049324b 100644
--- a/test/unit/enterprise_test.rb
+++ b/test/unit/enterprise_test.rb
@@ -446,4 +446,8 @@ class EnterpriseTest < Test::Unit::TestCase
assert_equal false, enterprise.receives_scrap_notification?
end
+ should 'have production cost' do
+ e = fast_create(Enterprise)
+ assert_respond_to e, :production_costs
+ end
end
diff --git a/test/unit/environment_test.rb b/test/unit/environment_test.rb
index 2437213..27901a8 100644
--- a/test/unit/environment_test.rb
+++ b/test/unit/environment_test.rb
@@ -1216,4 +1216,7 @@ class EnvironmentTest < Test::Unit::TestCase
assert_not_includes environment.enabled_plugins, plugin
end
+ should 'have production costs' do
+ assert_respond_to Environment.default, :production_costs
+ end
end
diff --git a/test/unit/input_test.rb b/test/unit/input_test.rb
index 6f9ed41..300e401 100644
--- a/test/unit/input_test.rb
+++ b/test/unit/input_test.rb
@@ -162,4 +162,19 @@ class InputTest < Test::Unit::TestCase
assert_kind_of Unit, input.build_unit
end
+ should 'calculate cost of input' do
+ input = Input.new(:amount_used => 10, :price_per_unit => 2.00)
+ assert_equal 20.00, input.cost
+ end
+
+ should 'cost 0 if amount not defined' do
+ input = Input.new(:price_per_unit => 2.00)
+ assert_equal 0.00, input.cost
+ end
+
+ should 'cost 0 if price_per_unit is not defined' do
+ input = Input.new(:amount_used => 10)
+ assert_equal 0.00, input.cost
+ end
+
end
diff --git a/test/unit/price_detail_test.rb b/test/unit/price_detail_test.rb
new file mode 100644
index 0000000..bd70658
--- /dev/null
+++ b/test/unit/price_detail_test.rb
@@ -0,0 +1,81 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class PriceDetailTest < ActiveSupport::TestCase
+
+ should 'have price 0 by default' do
+ p = PriceDetail.new
+
+ assert p.price.zero?
+ end
+
+ should 'return zero on price if it is blank' do
+ p = PriceDetail.new(:price => '')
+
+ assert p.price.zero?
+ end
+
+ should 'accept price in american\'s or brazilian\'s currency format' do
+ [
+ [12.34, 12.34],
+ ["12.34", 12.34],
+ ["12,34", 12.34],
+ ["12.345.678,90", 12345678.90],
+ ["12,345,678.90", 12345678.90],
+ ["12.345.678", 12345678.00],
+ ["12,345,678", 12345678.00]
+ ].each do |input, output|
+ new_price_detail = PriceDetail.new(:price => input)
+ assert_equal output, new_price_detail.price
+ end
+ end
+
+ should 'belongs to a product' do
+ p = PriceDetail.new
+
+ assert_respond_to p, :product
+ end
+
+ should 'product be mandatory' do
+ p = PriceDetail.new
+ p.valid?
+
+ assert p.errors.invalid?(:product_id)
+ end
+
+ should 'have production cost' do
+ product = fast_create(Product)
+ cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
+ detail = product.price_details.create(:production_cost_id => cost.id, :price => 10)
+
+ assert_equal cost, PriceDetail.find(detail.id).production_cost
+ end
+
+ should 'production cost be mandatory' do
+ p = PriceDetail.new
+ p.valid?
+
+ assert p.errors.invalid?(:production_cost_id)
+ end
+
+ should 'th production cost be unique on scope of product' do
+ product = fast_create(Product)
+ cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'environment')
+
+ detail1 = product.price_details.create(:production_cost_id => cost.id, :price => 10)
+ detail2 = product.price_details.build(:production_cost_id => cost.id, :price => 10)
+
+ detail2.valid?
+ assert detail2.errors.invalid?(:production_cost_id)
+ end
+
+ should 'format values to float with 2 decimals' do
+ enterprise = fast_create(Enterprise)
+ product = fast_create(Product, :enterprise_id => enterprise.id)
+ cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'environment')
+
+ price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10)
+
+ assert_equal "10.00", price_detail.formatted_value(:price)
+ end
+
+end
diff --git a/test/unit/product_test.rb b/test/unit/product_test.rb
index 3e643c0..e7f6c5b 100644
--- a/test/unit/product_test.rb
+++ b/test/unit/product_test.rb
@@ -382,4 +382,132 @@ class ProductTest < Test::Unit::TestCase
assert_includes Product.find_by_contents('thing'), p2
end
+ should 'respond to price details' do
+ product = Product.new
+ assert_respond_to product, :price_details
+ end
+
+ should 'return total value of inputs' do
+ product = fast_create(Product)
+ first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2)
+ second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1)
+
+ assert_equal 50.0, product.inputs_cost
+ end
+
+ should 'return 0 on total value of inputs if has no input' do
+ product = fast_create(Product)
+
+ assert product.inputs_cost.zero?
+ end
+
+ should 'know if price is described' do
+ product = fast_create(Product, :price => 30.0)
+
+ first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 1)
+ assert !Product.find(product.id).price_described?
+
+ second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1)
+ assert Product.find(product.id).price_described?
+ end
+
+ should 'return false on price_described if price of product is not defined' do
+ product = fast_create(Product)
+
+ assert_equal false, product.price_described?
+ end
+
+ should 'create price details' do
+ product = fast_create(Product)
+ cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
+ assert product.price_details.empty?
+
+ product.update_price_details([{:production_cost_id => cost.id, :price => 10}])
+ assert_equal 1, Product.find(product.id).price_details.size
+ end
+
+ should 'update price of a cost on price details' do
+ product = fast_create(Product)
+ cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
+ cost2 = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
+ price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10)
+ assert !product.price_details.empty?
+
+ product.update_price_details([{:production_cost_id => cost.id, :price => 20}, {:production_cost_id => cost2.id, :price => 30}])
+ assert_equal 20, product.price_details.find_by_production_cost_id(cost.id).price
+ assert_equal 2, Product.find(product.id).price_details.size
+ end
+
+ should 'destroy price details if product is removed' do
+ product = fast_create(Product)
+ cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
+ price_detail = product.price_details.create(:production_cost_id => cost.id, :price => 10)
+
+ assert_difference PriceDetail, :count, -1 do
+ product.destroy
+ end
+ end
+
+ should 'have production costs' do
+ product = fast_create(Product)
+ cost = fast_create(ProductionCost, :owner_id => Environment.default.id, :owner_type => 'Environment')
+ product.price_details.create(:production_cost_id => cost.id, :price => 10)
+ assert_equal [cost], Product.find(product.id).production_costs
+ end
+
+ should 'return production costs from enterprise and environment' do
+ ent = fast_create(Enterprise)
+ product = fast_create(Product, :enterprise_id => ent.id)
+ ent_production_cost = fast_create(ProductionCost, :owner_id => ent.id, :owner_type => 'Profile')
+ env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment')
+
+ assert_equal [env_production_cost, ent_production_cost], product.available_production_costs
+ end
+
+ should 'return all production costs' do
+ ent = fast_create(Enterprise)
+ product = fast_create(Product, :enterprise_id => ent.id)
+
+ env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment')
+ ent_production_cost = fast_create(ProductionCost, :owner_id => ent.id, :owner_type => 'Profile')
+ product.price_details.create(:production_cost => env_production_cost, :product => product)
+ assert_equal [env_production_cost, ent_production_cost], product.available_production_costs
+ end
+
+ should 'return total value of production costs' do
+ ent = fast_create(Enterprise)
+ product = fast_create(Product, :enterprise_id => ent.id)
+
+ env_production_cost = fast_create(ProductionCost, :owner_id => ent.environment.id, :owner_type => 'Environment')
+ price_detail = product.price_details.create(:production_cost => env_production_cost, :price => 10)
+
+ input = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2)
+
+ assert_equal price_detail.price + input.cost, product.total_production_cost
+ end
+
+ should 'return inputs cost as total value of production costs if has no price details' do
+ ent = fast_create(Enterprise)
+ product = fast_create(Product, :enterprise_id => ent.id)
+
+ input = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2)
+
+ assert_equal input.cost, product.total_production_cost
+ end
+
+ should 'return 0 on total production cost if has no input and price details' do
+ product = fast_create(Product)
+
+ assert product.total_production_cost.zero?
+ end
+
+ should 'format inputs cost values to float with 2 decimals' do
+ ent = fast_create(Enterprise)
+ product = fast_create(Product, :enterprise_id => ent.id)
+ first = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 20.0, :amount_used => 2)
+ second = fast_create(Input, :product_id => product.id, :product_category_id => fast_create(ProductCategory).id, :price_per_unit => 10.0, :amount_used => 1)
+
+ assert_equal "50.00", product.formatted_value(:inputs_cost)
+ end
+
end
diff --git a/test/unit/production_cost_test.rb b/test/unit/production_cost_test.rb
new file mode 100644
index 0000000..f266b37
--- /dev/null
+++ b/test/unit/production_cost_test.rb
@@ -0,0 +1,102 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ProductionCostTest < ActiveSupport::TestCase
+
+ should 'have name' do
+ p = ProductionCost.new
+ p.valid?
+ assert p.errors.invalid?(:name)
+
+ p.name = 'Taxes'
+ p.valid?
+ assert !p.errors.invalid?(:name)
+ end
+
+ should 'not validates name if it is blank' do
+ p = ProductionCost.new
+
+ p.valid?
+ assert_equal 1, p.errors['name'].to_a.count
+ end
+
+ should 'not have a too long name' do
+ p = ProductionCost.new
+
+ p.name = 'a'*40
+ p.valid?
+ assert p.errors.invalid?(:name)
+
+ p.name = 'a'*30
+ p.valid?
+ assert !p.errors.invalid?(:name)
+ end
+
+ should 'not have duplicated name on same environment' do
+ cost = ProductionCost.create(:name => 'Taxes', :owner => Environment.default)
+
+ invalid_cost = ProductionCost.new(:name => 'Taxes', :owner => Environment.default)
+ invalid_cost.valid?
+
+ assert invalid_cost.errors.invalid?(:name)
+ end
+
+ should 'not have duplicated name on same enterprise' do
+ enterprise = fast_create(Enterprise)
+ cost = ProductionCost.create(:name => 'Taxes', :owner => enterprise)
+
+ invalid_cost = ProductionCost.new(:name => 'Taxes', :owner => enterprise)
+ invalid_cost.valid?
+
+ assert invalid_cost.errors.invalid?(:name)
+ end
+
+ should 'not allow same name on enterprise if already has on environment' do
+ enterprise = fast_create(Enterprise)
+
+ cost1 = ProductionCost.create(:name => 'Taxes', :owner => Environment.default)
+ cost2 = ProductionCost.new(:name => 'Taxes', :owner => enterprise)
+
+ cost2.valid?
+
+ assert !cost2.errors.invalid?(:name)
+ end
+
+ should 'allow duplicated name on different enterprises' do
+ enterprise = fast_create(Enterprise)
+ enterprise2 = fast_create(Enterprise)
+
+ cost1 = ProductionCost.create(:name => 'Taxes', :owner => enterprise)
+ cost2 = ProductionCost.new(:name => 'Taxes', :owner => enterprise2)
+
+ cost2.valid?
+
+ assert !cost2.errors.invalid?(:name)
+ end
+
+ should 'be associated to an environment as owner' do
+ p = ProductionCost.new
+ p.valid?
+ assert p.errors.invalid?(:owner)
+
+ p.owner = Environment.default
+ p.valid?
+ assert !p.errors.invalid?(:owner)
+ end
+
+ should 'be associated to an enterprise as owner' do
+ enterprise = fast_create(Enterprise)
+ p = ProductionCost.new
+ p.valid?
+ assert p.errors.invalid?(:owner)
+
+ p.owner = enterprise
+ p.valid?
+ assert !p.errors.invalid?(:owner)
+ end
+
+ should 'create a production cost on an enterprise' do
+ enterprise = fast_create(Enterprise)
+ enterprise.production_costs.create(:name => 'Energy')
+ assert_equal ['Energy'], enterprise.production_costs.map(&:name)
+ end
+end
--
libgit2 0.21.2