Merge branch 'master' into checkout

This commit is contained in:
Will Marshall
2014-02-26 15:52:37 +11:00
21 changed files with 656 additions and 181 deletions

View File

@@ -20,30 +20,31 @@ productEditModule.directive "ofnDecimal", ->
viewValue
productEditModule.directive "ofnTrackProduct", ->
productEditModule.directive "ofnTrackProduct", ['$parse', ($parse) ->
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
property_name = attrs.ofnTrackProduct
ngModel.$parsers.push (viewValue) ->
if ngModel.$dirty
addDirtyProperty scope.dirtyProducts, scope.product.id, property_name, viewValue
parsedPropertyName = $parse(attrs.ofnTrackProduct)
addDirtyProperty scope.dirtyProducts, scope.product.id, parsedPropertyName, viewValue
scope.displayDirtyProducts()
viewValue
]
productEditModule.directive "ofnTrackVariant", ->
productEditModule.directive "ofnTrackVariant", ['$parse', ($parse) ->
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
property_name = attrs.ofnTrackVariant
ngModel.$parsers.push (viewValue) ->
dirtyVariants = {}
dirtyVariants = scope.dirtyProducts[scope.product.id].variants if scope.dirtyProducts.hasOwnProperty(scope.product.id) and scope.dirtyProducts[scope.product.id].hasOwnProperty("variants")
if ngModel.$dirty
addDirtyProperty dirtyVariants, scope.variant.id, property_name, viewValue
addDirtyProperty scope.dirtyProducts, scope.product.id, "variants", dirtyVariants
parsedPropertyName = $parse(attrs.ofnTrackVariant)
addDirtyProperty dirtyVariants, scope.variant.id, parsedPropertyName, viewValue
addDirtyProperty scope.dirtyProducts, scope.product.id, $parse("variants"), dirtyVariants
scope.displayDirtyProducts()
viewValue
]
productEditModule.directive "ofnToggleVariants", ->
link: (scope, element, attrs) ->
@@ -215,12 +216,18 @@ productEditModule.controller "AdminProductEditCtrl", [
if product.variants
for variant in product.variants
unit_value = $scope.variantUnitValue product, variant
variant.unit_value_with_description = "#{unit_value || ''} #{variant.unit_description || ''}".trim()
$scope.loadVariantVariantUnit product, variant
$scope.loadVariantVariantUnit product, product.master if product.master
$scope.loadVariantVariantUnit = (product, variant) ->
unit_value = $scope.variantUnitValue product, variant
unit_value = if unit_value? then unit_value else ''
variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim()
$scope.variantUnitValue = (product, variant) ->
if variant.unit_value
if variant.unit_value?
if product.variant_unit_scale
variant.unit_value / product.variant_unit_scale
else
@@ -279,6 +286,23 @@ productEditModule.controller "AdminProductEditCtrl", [
window.location = "/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit"
$scope.addVariant = (product) ->
product.variants.push
id: $scope.nextVariantId()
price: null
unit_value: null
unit_description: null
on_demand: false
on_hand: null
$scope.displayProperties[product.id].showVariants = true
$scope.nextVariantId = ->
$scope.variantIdCounter = 0 unless $scope.variantIdCounter?
$scope.variantIdCounter -= 1
$scope.variantIdCounter
$scope.deleteProduct = (product) ->
if confirm("Are you sure?")
$http(
@@ -291,14 +315,20 @@ productEditModule.controller "AdminProductEditCtrl", [
$scope.deleteVariant = (product, variant) ->
if confirm("Are you sure?")
$http(
method: "DELETE"
url: "/api/products/" + product.id + "/variants/" + variant.id
).success (data) ->
product.variants.splice product.variants.indexOf(variant), 1
delete $scope.dirtyProducts[product.id].variants[variant.id] if $scope.dirtyProducts.hasOwnProperty(product.id) and $scope.dirtyProducts[product.id].hasOwnProperty("variants") and $scope.dirtyProducts[product.id].variants.hasOwnProperty(variant.id)
$scope.displayDirtyProducts()
if !$scope.variantSaved(variant)
$scope.removeVariant(product, variant)
else
if confirm("Are you sure?")
$http(
method: "DELETE"
url: "/api/products/" + product.id + "/variants/" + variant.id
).success (data) ->
$scope.removeVariant(product, variant)
$scope.removeVariant = (product, variant) ->
product.variants.splice product.variants.indexOf(variant), 1
delete $scope.dirtyProducts[product.id].variants[variant.id] if $scope.dirtyProducts.hasOwnProperty(product.id) and $scope.dirtyProducts[product.id].hasOwnProperty("variants") and $scope.dirtyProducts[product.id].variants.hasOwnProperty(variant.id)
$scope.displayDirtyProducts()
$scope.cloneProduct = (product) ->
@@ -319,10 +349,31 @@ productEditModule.controller "AdminProductEditCtrl", [
Object.keys(product.variants).length > 0
$scope.hasUnit = (product) ->
product.variant_unit_with_scale?
$scope.variantSaved = (variant) ->
variant.hasOwnProperty('id') && variant.id > 0
$scope.hasOnDemandVariants = (product) ->
(variant for id, variant of product.variants when variant.on_demand).length > 0
$scope.submitProducts = ->
# Pack pack $scope.products, so they will match the list returned from the server,
# then pack $scope.dirtyProducts, ensuring that the correct product info is sent to the server.
$scope.packProduct product for id, product of $scope.products
$scope.packProduct product for id, product of $scope.dirtyProducts
productsToSubmit = filterSubmitProducts($scope.dirtyProducts)
if productsToSubmit.length > 0
$scope.updateProducts productsToSubmit # Don't submit an empty list
else
$scope.setMessage $scope.updateStatusMessage, "No changes to update.", color: "grey", 3000
$scope.updateProducts = (productsToSubmit) ->
$scope.displayUpdating()
$http(
@@ -338,27 +389,19 @@ productEditModule.controller "AdminProductEditCtrl", [
# doing things. TODO: Review together and decide on strategy here. -- Rohan, 14-1-2014
#if subset($scope.productsWithoutDerivedAttributes(), data)
if angular.toJson($scope.productsWithoutDerivedAttributes($scope.products)) == angular.toJson($scope.productsWithoutDerivedAttributes(data))
if $scope.productListsMatch $scope.products, data
$scope.resetProducts data
$timeout -> $scope.displaySuccess()
else
# console.log angular.toJson($scope.productsWithoutDerivedAttributes($scope.products))
# console.log "---"
# console.log angular.toJson($scope.productsWithoutDerivedAttributes(data))
# console.log "---"
$scope.displayFailure "Product lists do not match."
).error (data, status) ->
$scope.displayFailure "Server returned with error status: " + status
$scope.submitProducts = ->
# Pack pack $scope.products, so they will match the list returned from the server,
# then pack $scope.dirtyProducts, ensuring that the correct product info is sent to the server.
$scope.packProduct product for id, product of $scope.products
$scope.packProduct product for id, product of $scope.dirtyProducts
productsToSubmit = filterSubmitProducts($scope.dirtyProducts)
if productsToSubmit.length > 0
$scope.updateProducts productsToSubmit # Don't submit an empty list
else
$scope.setMessage $scope.updateStatusMessage, "No changes to update.", color: "grey", 3000
$scope.packProduct = (product) ->
if product.variant_unit_with_scale
match = product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
@@ -368,6 +411,7 @@ productEditModule.controller "AdminProductEditCtrl", [
else
product.variant_unit = product.variant_unit_with_scale
product.variant_unit_scale = null
$scope.packVariant product, product.master if product.master
if product.variants
for id, variant of product.variants
$scope.packVariant product, variant
@@ -375,14 +419,33 @@ productEditModule.controller "AdminProductEditCtrl", [
$scope.packVariant = (product, variant) ->
if variant.hasOwnProperty("unit_value_with_description")
match = variant.unit_value_with_description.match(/^([\d\.]+|)( |)(.*)$/)
match = variant.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/)
if match
product = $scope.findProduct(product.id)
variant.unit_value = parseFloat(match[1]) || null
variant.unit_value = parseFloat(match[1])
variant.unit_value = null if isNaN(variant.unit_value)
variant.unit_value *= product.variant_unit_scale if variant.unit_value && product.variant_unit_scale
variant.unit_description = match[3]
$scope.productListsMatch = (clientProducts, serverProducts) ->
$scope.copyNewVariantIds clientProducts, serverProducts
angular.toJson($scope.productsWithoutDerivedAttributes(clientProducts)) == angular.toJson($scope.productsWithoutDerivedAttributes(serverProducts))
# When variants are created clientside, they are given a negative id. The server
# responds with a real id, which would cause the productListsMatch() check to fail.
# To avoid that false negative, we copy the server variant id to the client for any
# negative ids.
$scope.copyNewVariantIds = (clientProducts, serverProducts) ->
if clientProducts?
for product, i in clientProducts
if product.variants?
for variant, j in product.variants
if variant.id < 0
variant.id = serverProducts[i].variants[j].id
$scope.productsWithoutDerivedAttributes = (products) ->
products_filtered = []
if products
@@ -394,6 +457,7 @@ productEditModule.controller "AdminProductEditCtrl", [
delete variant.unit_value_with_description
# If we end up live-updating this field, we might want to reinstate its verification here
delete variant.options_text
delete product.master
products_filtered
@@ -456,35 +520,30 @@ productEditModule.filter "rangeArray", ->
input.push(i) for i in [start..end]
input
filterSubmitProducts = (productsToFilter) ->
filteredProducts = []
if productsToFilter instanceof Object
angular.forEach productsToFilter, (product) ->
if product.hasOwnProperty("id")
filteredProduct = {}
filteredProduct = {id: product.id}
filteredVariants = []
hasUpdatableProperty = false
if product.hasOwnProperty("variants")
angular.forEach product.variants, (variant) ->
if not variant.deleted_at? and variant.hasOwnProperty("id")
hasUpdateableProperty = false
filteredVariant = {}
filteredVariant.id = variant.id
if variant.hasOwnProperty("on_hand")
filteredVariant.on_hand = variant.on_hand
hasUpdatableProperty = true
if variant.hasOwnProperty("price")
filteredVariant.price = variant.price
hasUpdatableProperty = true
if variant.hasOwnProperty("unit_value")
filteredVariant.unit_value = variant.unit_value
hasUpdatableProperty = true
if variant.hasOwnProperty("unit_description")
filteredVariant.unit_description = variant.unit_description
hasUpdatableProperty = true
filteredVariants.push filteredVariant if hasUpdatableProperty
result = filterSubmitVariant variant
filteredVariant = result.filteredVariant
variantHasUpdatableProperty = result.hasUpdatableProperty
filteredVariants.push filteredVariant if variantHasUpdatableProperty
if product.master?.hasOwnProperty("unit_value")
filteredProduct.unit_value = product.master.unit_value
hasUpdatableProperty = true
if product.master?.hasOwnProperty("unit_description")
filteredProduct.unit_description = product.master.unit_description
hasUpdatableProperty = true
hasUpdatableProperty = false
filteredProduct.id = product.id
if product.hasOwnProperty("name")
filteredProduct.name = product.name
hasUpdatableProperty = true
@@ -515,13 +574,31 @@ filterSubmitProducts = (productsToFilter) ->
filteredProducts
addDirtyProperty = (dirtyObjects, objectID, propertyName, propertyValue) ->
if dirtyObjects.hasOwnProperty(objectID)
dirtyObjects[objectID][propertyName] = propertyValue
else
filterSubmitVariant = (variant) ->
hasUpdatableProperty = false
filteredVariant = {}
if not variant.deleted_at? and variant.hasOwnProperty("id")
filteredVariant.id = variant.id unless variant.id <= 0
if variant.hasOwnProperty("on_hand")
filteredVariant.on_hand = variant.on_hand
hasUpdatableProperty = true
if variant.hasOwnProperty("price")
filteredVariant.price = variant.price
hasUpdatableProperty = true
if variant.hasOwnProperty("unit_value")
filteredVariant.unit_value = variant.unit_value
hasUpdatableProperty = true
if variant.hasOwnProperty("unit_description")
filteredVariant.unit_description = variant.unit_description
hasUpdatableProperty = true
{filteredVariant: filteredVariant, hasUpdatableProperty: hasUpdatableProperty}
addDirtyProperty = (dirtyObjects, objectID, parsedPropertyName, propertyValue) ->
if !dirtyObjects.hasOwnProperty(objectID)
dirtyObjects[objectID] = {}
dirtyObjects[objectID]["id"] = objectID
dirtyObjects[objectID][propertyName] = propertyValue
parsedPropertyName.assign(dirtyObjects[objectID], propertyValue)
removeCleanProperty = (dirtyObjects, objectID, propertyName) ->

View File

@@ -106,6 +106,8 @@ ul.column-list {
}
table#listing_products.bulk {
clear: both;
td.supplier {
select {
width: 125px;

View File

@@ -10,3 +10,7 @@
img
display: block
margin: 0px auto 8px
.contact
strong
padding-right: 1em

View File

@@ -122,7 +122,7 @@ product
//&.notes
//width: 140px
&.variant
width: 100px
width: 180px
&.quantity, &.bulk, &.price
width: 90px
.notes

View File

@@ -0,0 +1,3 @@
Spree::Admin::VariantsController.class_eval do
helper 'spree/products'
end

View File

@@ -4,5 +4,15 @@ module Spree
def variant_price_diff(variant)
"(#{number_to_currency variant.price})"
end
def product_has_variant_unit_option_type?(product)
product.option_types.any? { |option_type| variant_unit_option_type? option_type }
end
def variant_unit_option_type?(option_type)
Spree::Product.all_variant_unit_option_types.include? option_type
end
end
end

View File

@@ -11,8 +11,9 @@ Spree::Product.class_eval do
has_many :distributors, :through => :product_distributions
accepts_nested_attributes_for :product_distributions, :allow_destroy => true
delegate_belongs_to :master, :unit_value, :unit_description
attr_accessible :supplier_id, :distributor_ids, :product_distributions_attributes, :group_buy, :group_buy_unit_size, :variant_unit, :variant_unit_scale, :variant_unit_name, :notes
attr_accessible :supplier_id, :distributor_ids, :product_distributions_attributes, :group_buy, :group_buy_unit_size, :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value, :unit_description, :notes
validates_presence_of :supplier

View File

@@ -20,7 +20,11 @@ class Spree::ProductSet < ModelSet
def update_variants_attributes(product, variants_attributes)
variants_attributes.each do |attributes|
e = product.variants.detect { |e| e.id.to_s == attributes[:id].to_s && !e.id.nil? }
e.update_attributes(attributes.except(:id)) if e.present?
if e.present?
e.update_attributes(attributes.except(:id))
else
product.variants.create attributes
end
end
end

View File

@@ -16,14 +16,11 @@ Spree::Variant.class_eval do
price + fees_for(distributor, order_cycle)
end
# TODO: This method seems a little redundant. Though perhaps a useful interface.
# Consider removing.
def fees_for(distributor, order_cycle)
order_cycle.fees_for(self, distributor)
end
# Copied and modified from Spree::Variant
def options_text
values = self.option_values.joins(:option_type).order("#{Spree::OptionType.table_name}.position asc")

View File

@@ -0,0 +1,10 @@
/ insert_top "[data-hook='admin_variant_form_fields']"
- if product_has_variant_unit_option_type?(@product)
.field{"data-hook" => "unit_value"}
= f.label :unit_value, "Unit Value"
= f.text_field :unit_value, class: "fullwidth"
.field{"data-hook" => "unit_description"}
= f.label :unit_description, "Unit Description"
= f.text_field :unit_description, class: "fullwidth"

View File

@@ -0,0 +1,10 @@
/ replace "[data-hook='presentation']"
- unless variant_unit_option_type?(option.option_type)
.field{"data-hook" => "presentation"}
= label :new_variant, option.option_type.presentation
- if @variant.new_record?
= select(:new_variant, option.option_type.presentation, option.option_type.option_values.collect {|ov| [ ov.presentation, ov.id ] }, {}, {:class => 'select2 fullwidth'})
- else
- if opt = @variant.option_values.detect {|o| o.option_type == option.option_type }.try(:presentation)
= text_field(:new_variant, option.option_type.presentation, :value => opt, :disabled => 'disabled', :class => 'fullwidth')

View File

@@ -1,8 +1,18 @@
.contact.small-2.large-3.columns
%h3 Contact
%ul
%li= @distributor.email
%li= @distributor.website
= @distributor.address.address1
= @distributor.address.address2
= @distributor.address.city
%p
%strong E
%a{href: "mailto:#{@distributor.email}"}= @distributor.email
- unless @distributor.website.blank?
%p
%strong W
%a{href: @distributor.website}= @distributor.website
%p
= @distributor.address.address1
%br
= @distributor.address.address2
%br
= @distributor.address.city

View File

@@ -21,7 +21,7 @@
{{ product.supplier.name }}
%td.notes {{ product.notes | truncate:80 }}
%td
{{ product.master.options_text }}
%span{"ng-hide" => "product.variants.length > 0"} {{ product.master.options_text }}
%span{"ng-show" => "product.variants.length > 0"}
%img.collapse{src: "/assets/collapse.png",
"ng-show" => "product.show_variants",

View File

@@ -1,5 +1,6 @@
%td{colspan: 2}
%td
%td.notes
%td {{variant.options_text}}
%td
%input{type: :number,

View File

@@ -111,13 +111,16 @@
%tr.product
%td.left-actions
%a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' }
%a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" }
%td.supplier{ 'ng-show' => 'columns.supplier.visible' }
%select.select2{ 'ng-model' => 'product.supplier', :name => 'supplier', 'ofn-track-product' => 'supplier', 'ng-options' => 's.name for s in suppliers' }
%td{ 'ng-show' => 'columns.name.visible' }
%input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' }
%td.unit{ 'ng-show' => 'columns.unit.visible' }
%select.select2{ 'ng-model' => 'product.variant_unit_with_scale', :name => 'variant_unit_with_scale', 'ofn-track-product' => 'variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' }
%input{ 'ng-model' => 'product.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-product' => 'variant_unit_name', 'ng-show' => "product.variant_unit_with_scale == 'items'", :type => 'text' }
%option{'value' => '', 'ng-hide' => "hasVariants(product)"}
%input{ 'ng-model' => 'product.master.unit_value_with_description', :name => 'master_unit_value_with_description', 'ofn-track-product' => 'master.unit_value_with_description', :type => 'text', :placeholder => 'value', 'ng-show' => "!hasVariants(product) && hasUnit(product)" }
%input{ 'ng-model' => 'product.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-product' => 'variant_unit_name', :placeholder => 'unit', 'ng-show' => "product.variant_unit_with_scale == 'items'", :type => 'text' }
%td{ 'ng-show' => 'columns.price.visible' }
%input{ 'ng-model' => 'product.price', 'ofn-decimal' => :true, :name => 'price', 'ofn-track-product' => 'price', :type => 'text' }
%td{ 'ng-show' => 'columns.on_hand.visible' }
@@ -133,7 +136,8 @@
%a{ 'ng-click' => 'deleteProduct(product)', :class => "delete-product icon-trash no-text" }
%tr.variant{ 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'displayProperties[product.id].showVariants', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" }
%td.left-actions
%a{ :class => "variant-item icon-caret-right" }
%a{ :class => "variant-item icon-caret-right", 'ng-hide' => "$last" }
%a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "$last" }
%td{ 'ng-show' => 'columns.supplier.visible' }
%td{ 'ng-show' => 'columns.name.visible' }
{{ variant.options_text }}
@@ -146,7 +150,7 @@
%span{ 'ng-bind' => 'variant.on_hand', :name => 'variant_on_hand', 'ng-show' => 'variant.on_demand' }
%td{ 'ng-show' => 'columns.available_on.visible' }
%td.actions
%a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text" }
%a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text", 'ng-show' => "variantSaved(variant)" }
%td.actions
%td.actions
%a{ 'ng-click' => 'deleteVariant(product,variant)', :class => "delete-variant icon-trash no-text" }

View File

@@ -7,8 +7,11 @@ node( :on_hand ) { |p| p.on_hand.to_f.finite? ? p.on_hand : "On demand" }
node( :available_on ) { |p| p.available_on.blank? ? "" : p.available_on.strftime("%F %T") }
node( :permalink_live ) { |p| p.permalink }
node( :supplier ) do |p|
partial 'spree/api/enterprises/bulk_show', :object => p.supplier
partial 'spree/api/enterprises/bulk_show', :object => p.supplier
end
node( :variants ) do |p|
partial 'spree/api/variants/bulk_index', :object => p.variants.order('id ASC')
partial 'spree/api/variants/bulk_index', :object => p.variants.reorder('spree_variants.id ASC')
end
node( :master ) do |p|
partial 'spree/api/variants/bulk_show', :object => p.master
end

View File

@@ -229,7 +229,8 @@ feature %q{
end
end
scenario "create a new product" do
scenario "creating a new product" do
s = FactoryGirl.create(:supplier_enterprise)
d = FactoryGirl.create(:distributor_enterprise)
@@ -253,6 +254,57 @@ feature %q{
page.should have_field "product_name", with: 'Big Bag Of Apples'
end
scenario "creating new variants" do
# Given a product without variants or a unit
p = FactoryGirl.create(:product, variant_unit: nil, variant_unit_scale: nil)
login_to_admin_section
visit '/admin/products/bulk_edit'
# I should not see an add variant button
page.should_not have_selector 'a.add-variant', visible: true
# When I set the unit
select "Weight (kg)", from: "variant_unit_with_scale"
# I should see an add variant button
page.should have_selector 'a.add-variant', visible: true
# When I add three variants
page.find('a.add-variant', visible: true).click
page.find('a.add-variant', visible: true).click
page.find('a.add-variant', visible: true).click
# They should be added, and should see no edit buttons
page.all("tr.variant").count.should == 3
page.should_not have_selector "a.edit-variant", visible: true
# When I remove two, they should be removed
page.all('a.delete-variant').first.click
page.all('a.delete-variant').first.click
page.all("tr.variant").count.should == 1
# When I fill out variant details and hit update
fill_in "variant_unit_value_with_description", with: "4000 (12x250 mL bottles)"
fill_in "variant_price", with: "4.0"
fill_in "variant_on_hand", with: "10"
click_button 'Update'
page.find("span#update-status-message").should have_content "Update complete"
# Then I should see edit buttons for the new variant
page.should have_selector "a.edit-variant", visible: true
# And the variants should be saved
visit '/admin/products/bulk_edit'
page.should have_selector "a.view-variants"
first("a.view-variants").click
page.should have_field "variant_unit_value_with_description", with: "4000 (12x250 mL bottles)"
page.should have_field "variant_price", with: "4.0"
page.should have_field "variant_on_hand", with: "10"
end
scenario "updating a product with no variants (except master)" do
s1 = FactoryGirl.create(:supplier_enterprise)
s2 = FactoryGirl.create(:supplier_enterprise)
@@ -295,7 +347,7 @@ feature %q{
page.should have_field "on_hand", with: "18"
end
scenario "updating a product with an items variant unit" do
scenario "updating a product with a variant unit of 'items'" do
p = FactoryGirl.create(:product, variant_unit: 'weight', variant_unit_scale: 1000)
login_to_admin_section
@@ -341,6 +393,49 @@ feature %q{
end
describe "setting the master unit value for a product without variants" do
it "sets the master unit value" do
p = FactoryGirl.create(:product, variant_unit: nil, variant_unit_scale: nil)
login_to_admin_section
visit '/admin/products/bulk_edit'
page.should have_select "variant_unit_with_scale", selected: ''
page.should_not have_field "master_unit_value_with_description", visible: true
select "Weight (kg)", from: "variant_unit_with_scale"
fill_in "master_unit_value_with_description", with: '123 abc'
click_button 'Update'
page.find("span#update-status-message").should have_content "Update complete"
visit '/admin/products/bulk_edit'
page.should have_select "variant_unit_with_scale", selected: "Weight (kg)"
page.should have_field "master_unit_value_with_description", with: "123 abc"
p.reload
p.variant_unit.should == 'weight'
p.variant_unit_scale.should == 1000
p.master.unit_value.should == 123000
p.master.unit_description.should == 'abc'
end
it "does not show the field when the product has variants" do
p = FactoryGirl.create(:product, variant_unit: nil, variant_unit_scale: nil)
v = FactoryGirl.create(:variant, product: p, unit_value: nil, unit_description: nil)
login_to_admin_section
visit '/admin/products/bulk_edit'
select "Weight (kg)", from: "variant_unit_with_scale"
page.should_not have_field "master_unit_value_with_description", visible: true
end
end
scenario "updating a product with variants" do
s1 = FactoryGirl.create(:supplier_enterprise)
s2 = FactoryGirl.create(:supplier_enterprise)

View File

@@ -189,24 +189,20 @@ feature %q{
end
# And the suppliers should have fees
page.find('#order_cycle_incoming_exchange_0_enterprise_fees_0_enterprise_id option[selected=selected]').
text.should == oc.suppliers.first.name
page.find('#order_cycle_incoming_exchange_0_enterprise_fees_0_enterprise_fee_id option[selected=selected]').
text.should == oc.suppliers.first.enterprise_fees.first.name
page.should have_select 'order_cycle_incoming_exchange_0_enterprise_fees_0_enterprise_id', selected: oc.suppliers.first.name
page.should have_select 'order_cycle_incoming_exchange_0_enterprise_fees_0_enterprise_fee_id', selected: oc.suppliers.first.enterprise_fees.first.name
page.find('#order_cycle_incoming_exchange_1_enterprise_fees_0_enterprise_id option[selected=selected]').
text.should == oc.suppliers.last.name
page.find('#order_cycle_incoming_exchange_1_enterprise_fees_0_enterprise_fee_id option[selected=selected]').
text.should == oc.suppliers.last.enterprise_fees.first.name
page.should have_select 'order_cycle_incoming_exchange_1_enterprise_fees_0_enterprise_id', selected: oc.suppliers.last.name
page.should have_select 'order_cycle_incoming_exchange_1_enterprise_fees_0_enterprise_fee_id', selected: oc.suppliers.last.enterprise_fees.first.name
# And I should see the distributors
page.should have_selector 'td.distributor_name', :text => oc.distributors.first.name
page.should have_selector 'td.distributor_name', :text => oc.distributors.last.name
page.find('#order_cycle_outgoing_exchange_0_pickup_time').value.should == 'time 0'
page.find('#order_cycle_outgoing_exchange_0_pickup_instructions').value.should == 'instructions 0'
page.find('#order_cycle_outgoing_exchange_1_pickup_time').value.should == 'time 1'
page.find('#order_cycle_outgoing_exchange_1_pickup_instructions').value.should == 'instructions 1'
page.should have_field 'order_cycle_outgoing_exchange_0_pickup_time', with: 'time 0'
page.should have_field 'order_cycle_outgoing_exchange_0_pickup_instructions', with: 'instructions 0'
page.should have_field 'order_cycle_outgoing_exchange_1_pickup_time', with: 'time 1'
page.should have_field 'order_cycle_outgoing_exchange_1_pickup_instructions', with: 'instructions 1'
# And the distributors should have products
page.all('table.exchanges tbody tr.distributor').each do |row|
@@ -219,15 +215,11 @@ feature %q{
end
# And the distributors should have fees
page.find('#order_cycle_outgoing_exchange_0_enterprise_fees_0_enterprise_id option[selected=selected]').
text.should == oc.distributors.first.name
page.find('#order_cycle_outgoing_exchange_0_enterprise_fees_0_enterprise_fee_id option[selected=selected]').
text.should == oc.distributors.first.enterprise_fees.first.name
page.should have_select 'order_cycle_outgoing_exchange_0_enterprise_fees_0_enterprise_id', selected: oc.distributors.first.name
page.should have_select 'order_cycle_outgoing_exchange_0_enterprise_fees_0_enterprise_fee_id', selected: oc.distributors.first.enterprise_fees.first.name
page.find('#order_cycle_outgoing_exchange_1_enterprise_fees_0_enterprise_id option[selected=selected]').
text.should == oc.distributors.last.name
page.find('#order_cycle_outgoing_exchange_1_enterprise_fees_0_enterprise_fee_id option[selected=selected]').
text.should == oc.distributors.last.enterprise_fees.first.name
page.should have_select 'order_cycle_outgoing_exchange_1_enterprise_fees_0_enterprise_id', selected: oc.distributors.last.name
page.should have_select 'order_cycle_outgoing_exchange_1_enterprise_fees_0_enterprise_fee_id', selected: oc.distributors.last.enterprise_fees.first.name
end

View File

@@ -1,8 +1,8 @@
require "spec_helper"
feature %q{
As a supplier
I want set a supplier and distributor(s) for a product
As an admin
I want to set a supplier and distributor(s) for a product
} do
include AuthenticationWorkflow
include WebHelper

View File

@@ -0,0 +1,65 @@
require "spec_helper"
feature %q{
As an admin
I want to manage product variants
} do
include AuthenticationWorkflow
include WebHelper
scenario "editing unit value and description for a variant" do
# Given a product with unit-related option types, with a variant
p = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1")
v = create(:variant, product: p, unit_value: 1, unit_description: 'foo')
# And the product has option types for the unit-related and non-unit-related option values
p.option_types << v.option_values.first.option_type
# When I view the variant
login_to_admin_section
click_link 'Products'
within('#sub_nav') { click_link 'Products' }
click_link p.name
click_link 'Variants'
page.find('table.index .icon-edit').click
# Then I should not see a traditional option value field for the unit-related option value
page.all("div[data-hook='presentation'] input").count.should == 1
# And I should see unit value and description fields for the unit-related option value
page.should have_field "variant_unit_value", with: "1"
page.should have_field "variant_unit_description", with: "foo"
# When I update the fields and save the variant
fill_in "variant_unit_value", with: "123"
fill_in "variant_unit_description", with: "bar"
click_button 'Update'
page.should have_content %Q(Variant "#{p.name}" has been successfully updated!)
# Then the unit value and description should have been saved
v.reload
v.unit_value.should == 123
v.unit_description.should == 'bar'
end
it "does not show unit value or description fields when the product does not have a unit-related option type" do
# Given a product without unit-related option types, with a variant
p = create(:simple_product, variant_unit: nil, variant_unit_scale: nil)
v = create(:variant, product: p, unit_value: nil, unit_description: nil)
# And the product has option types for the variant's option values
p.option_types << v.option_values.first.option_type
# When I view the variant
login_to_admin_section
click_link 'Products'
within('#sub_nav') { click_link 'Products' }
click_link p.name
click_link 'Variants'
page.find('table.index .icon-edit').click
# Then I should not see unit value and description fields
page.should_not have_field "variant_unit_value"
page.should_not have_field "variant_unit_description"
end
end

View File

@@ -121,6 +121,27 @@ describe "filtering products for submission to database", ->
]
]
it "returns variants with a negative id without that id", ->
testProduct =
id: 1
variants: [
id: -1
on_hand: 5
price: 12.0
unit_value: 250
unit_description: "(bottle)"
]
expect(filterSubmitProducts([testProduct])).toEqual [
id: 1
variants_attributes: [
on_hand: 5
price: 12.0
unit_value: 250
unit_description: "(bottle)"
]
]
it "does not return variants_attributes property if variants is an empty array", ->
testProduct =
id: 1
@@ -171,6 +192,10 @@ describe "filtering products for submission to database", ->
group_buy: null
group_buy_unit_size: null
on_demand: false
master:
id: 2
unit_value: 250
unit_description: "foo"
variants: [
id: 1
on_hand: 2
@@ -190,6 +215,8 @@ describe "filtering products for submission to database", ->
variant_unit: 'volume'
variant_unit_scale: 1
variant_unit_name: 'loaf'
unit_value: 250
unit_description: "foo"
available_on: available_on
variants_attributes: [
id: 1
@@ -202,10 +229,17 @@ describe "filtering products for submission to database", ->
describe "Maintaining a live record of dirty products and properties", ->
parse = null
beforeEach ->
module "ofn.bulk_product_edit"
beforeEach inject(($parse) ->
parse = $parse
)
describe "adding product properties to the dirtyProducts object", -> # Applies to both products and variants
it "adds the product and the property to the list if property is dirty", ->
dirtyProducts = {}
addDirtyProperty dirtyProducts, 1, "name", "Product 1"
addDirtyProperty dirtyProducts, 1, parse("name"), "Product 1"
expect(dirtyProducts).toEqual 1:
id: 1
name: "Product 1"
@@ -216,7 +250,7 @@ describe "Maintaining a live record of dirty products and properties", ->
id: 1
notaname: "something"
addDirtyProperty dirtyProducts, 1, "name", "Product 3"
addDirtyProperty dirtyProducts, 1, parse("name"), "Product 3"
expect(dirtyProducts).toEqual 1:
id: 1
notaname: "something"
@@ -228,7 +262,7 @@ describe "Maintaining a live record of dirty products and properties", ->
id: 1
name: "Product 1"
addDirtyProperty dirtyProducts, 1, "name", "Product 2"
addDirtyProperty dirtyProducts, 1, parse("name"), "Product 2"
expect(dirtyProducts).toEqual 1:
id: 1
name: "Product 2"
@@ -420,12 +454,24 @@ describe "AdminProductEditCtrl", ->
scope.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "items"
it "loads data for variants (inc. master)", ->
spyOn scope, "loadVariantVariantUnit"
product =
variant_unit_scale: 1.0
master: {id: 1, unit_value: 1, unit_description: '(one)'}
variants: [{id: 2, unit_value: 2, unit_description: '(two)'}]
scope.loadVariantUnit product
expect(scope.loadVariantVariantUnit).toHaveBeenCalledWith product, product.variants[0]
expect(scope.loadVariantVariantUnit).toHaveBeenCalledWith product, product.master
describe "setting variant unit_value_with_description", ->
it "sets by combining unit_value and unit_description", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}]
scope.loadVariantUnit product
scope.loadVariantVariantUnit product, product.variants[0]
expect(product.variants[0]).toEqual
id: 1
unit_value: 1
@@ -436,23 +482,30 @@ describe "AdminProductEditCtrl", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 1}]
scope.loadVariantUnit product
scope.loadVariantVariantUnit product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '1'
it "uses unit_description when value is missing", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_description: 'Small'}]
scope.loadVariantUnit product
scope.loadVariantVariantUnit product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual 'Small'
it "converts values from base value to chosen unit", ->
product =
variant_unit_scale: 1000.0
variants: [{id: 1, unit_value: 2500}]
scope.loadVariantUnit product
scope.loadVariantVariantUnit product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '2.5'
it "displays a unit_value of zero", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 0}]
scope.loadVariantVariantUnit product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '0'
describe "calculating the scaled unit value for a variant", ->
it "returns the scaled value when variant has a unit_value", ->
@@ -465,6 +518,11 @@ describe "AdminProductEditCtrl", ->
variant = {unit_value: 5}
expect(scope.variantUnitValue(product, variant)).toEqual 5
it "returns zero when the value is zero", ->
product = {}
variant = {unit_value: 0}
expect(scope.variantUnitValue(product, variant)).toEqual 0
it "returns null when the variant has no unit_value", ->
product = {}
variant = {}
@@ -578,6 +636,43 @@ describe "AdminProductEditCtrl", ->
expect(scope.hasOnDemandVariants(product)).toBe(false)
describe "determining whether a product has variants", ->
it "returns true when it does", ->
product =
variants: [{id: 1}, {id: 2}]
expect(scope.hasVariants(product)).toBe(true)
it "returns false when it does not", ->
product =
variants: []
expect(scope.hasVariants(product)).toBe(false)
describe "determining whether a product has a unit", ->
it "returns true when it does", ->
product =
variant_unit_with_scale: 'weight_1000'
expect(scope.hasUnit(product)).toBe(true)
it "returns false when its unit is undefined", ->
product = {}
expect(scope.hasUnit(product)).toBe(false)
describe "determining whether a variant has been saved", ->
it "returns true when it has a positive id", ->
variant = {id: 1}
expect(scope.variantSaved(variant)).toBe(true)
it "returns false when it has no id", ->
variant = {}
expect(scope.variantSaved(variant)).toBe(false)
it "returns false when it has a negative id", ->
variant = {id: -1}
expect(scope.variantSaved(variant)).toBe(false)
describe "submitting products to be updated", ->
describe "packing products", ->
it "extracts variant_unit_with_scale into variant_unit and variant_unit_scale", ->
@@ -610,6 +705,17 @@ describe "AdminProductEditCtrl", ->
variant_unit_scale: null
variant_unit_with_scale: 'items'
it "packs the master variant", ->
spyOn scope, "packVariant"
testVariant = {id: 1}
testProduct =
id: 1
master: testVariant
scope.packProduct(testProduct)
expect(scope.packVariant).toHaveBeenCalledWith(testProduct, testVariant)
it "packs each variant", ->
spyOn scope, "packVariant"
testVariant = {id: 1}
@@ -654,6 +760,14 @@ describe "AdminProductEditCtrl", ->
unit_description: 'Medium'
unit_value_with_description: "Medium"
it "extracts into unit_description when a string starting with a number is provided", ->
testVariant = {unit_value_with_description: "1kg"}
scope.packVariant(testProduct, testVariant)
expect(testVariant).toEqual
unit_value: null
unit_description: '1kg'
unit_value_with_description: "1kg"
it "sets blank values when no value provided", ->
testVariant = {unit_value_with_description: ""}
scope.packVariant(testProduct, testVariant)
@@ -667,6 +781,15 @@ describe "AdminProductEditCtrl", ->
scope.packVariant(testProduct, testVariant)
expect(testVariant).toEqual {}
it "sets zero when the field is zero", ->
testProduct = {id: 123, variant_unit_scale: 1.0}
testVariant = {unit_value_with_description: "0"}
scope.packVariant(testProduct, testVariant)
expect(testVariant).toEqual
unit_value: 0
unit_description: ''
unit_value_with_description: "0"
it "converts value from chosen unit to base unit", ->
testProduct = {id: 123, variant_unit_scale: 1000}
testVariant = {unit_value_with_description: "250.5"}
@@ -789,6 +912,24 @@ describe "AdminProductEditCtrl", ->
expect(scope.displayFailure).toHaveBeenCalled()
describe "copying new variant ids from server to client", ->
it "copies server ids to the client where the client id is negative", ->
clientProducts = [
{
id: 123
variants: [{id: 1}, {id: -2}, {id: -3}]
}
]
serverProducts = [
{
id: 123
variants: [{id: 1}, {id: 4534}, {id: 3453}]
}
]
scope.copyNewVariantIds(clientProducts, serverProducts)
expect(clientProducts).toEqual(serverProducts)
describe "fetching products without derived attributes", ->
it "returns products without the variant_unit_with_scale field", ->
scope.products = [{id: 123, variant_unit_with_scale: 'weight_1000'}]
@@ -820,6 +961,14 @@ describe "AdminProductEditCtrl", ->
}
]
it "removes the master variant", ->
scope.products = [{id: 123, master: {id: 234, unit_value_with_description: 'foo'}}]
expect(scope.productsWithoutDerivedAttributes(scope.products)).toEqual [
{
id: 123
}
]
describe "deep copying products", ->
it "copies products", ->
@@ -845,6 +994,27 @@ describe "AdminProductEditCtrl", ->
expect(scope.findProduct(123)).toBeNull()
describe "adding variants", ->
beforeEach ->
scope.displayProperties ||= {123: {}}
it "adds first and subsequent variants", ->
product = {id: 123, variants: []}
scope.addVariant(product)
scope.addVariant(product)
expect(product).toEqual
id: 123
variants: [
{id: -1, price: null, unit_value: null, unit_description: null, on_demand: false, on_hand: null}
{id: -2, price: null, unit_value: null, unit_description: null, on_demand: false, on_hand: null}
]
it "shows the variant(s)", ->
product = {id: 123, variants: []}
scope.addVariant(product)
expect(scope.displayProperties[123].showVariants).toBe(true)
describe "deleting products", ->
it "deletes products with a http delete request to /api/products/id", ->
spyOn(window, "confirm").andReturn true
@@ -898,83 +1068,100 @@ describe "AdminProductEditCtrl", ->
describe "deleting variants", ->
it "deletes variants with a http delete request to /api/products/product_id/variants/(variant_id)", ->
spyOn(window, "confirm").andReturn true
scope.products = [
{
id: 9
permalink_live: "apples"
variants: [
id: 3
price: 12
]
}
{
id: 13
permalink_live: "oranges"
}
]
scope.dirtyProducts = {}
httpBackend.expectDELETE("/api/products/9/variants/3").respond 200, "data"
scope.deleteVariant scope.products[0], scope.products[0].variants[0]
httpBackend.flush()
describe "when the variant has not been saved", ->
it "removes the variant from products and dirtyProducts", ->
spyOn(window, "confirm").andReturn true
scope.products = [
{id: 1, variants: [{id: -1}]}
]
scope.dirtyProducts =
1: {id: 1, variants: {'-1': {id: -1}}}
scope.deleteVariant scope.products[0], scope.products[0].variants[0]
expect(scope.products).toEqual([
{id: 1, variants: []}
])
expect(scope.dirtyProducts).toEqual
1: {id: 1, variants: {}}
it "removes the specified variant from both the variants object and scope.dirtyProducts (if it exists there)", ->
spyOn(window, "confirm").andReturn true
scope.products = [
{
id: 9
permalink_live: "apples"
variants: [
{
describe "when the variant has been saved", ->
it "deletes variants with a http delete request to /api/products/product_id/variants/(variant_id)", ->
spyOn(window, "confirm").andReturn true
scope.products = [
{
id: 9
permalink_live: "apples"
variants: [
id: 3
price: 12.0
}
{
id: 4
price: 6.0
}
]
}
{
id: 13
permalink_live: "oranges"
}
]
scope.dirtyProducts =
9:
id: 9
variants:
3:
id: 3
price: 12.0
price: 12
]
}
{
id: 13
permalink_live: "oranges"
}
]
scope.dirtyProducts = {}
httpBackend.expectDELETE("/api/products/9/variants/3").respond 200, "data"
scope.deleteVariant scope.products[0], scope.products[0].variants[0]
httpBackend.flush()
4:
id: 4
price: 6.0
it "removes the specified variant from both the variants object and scope.dirtyProducts (if it exists there)", ->
spyOn(window, "confirm").andReturn true
scope.products = [
{
id: 9
permalink_live: "apples"
variants: [
{
id: 3
price: 12.0
}
{
id: 4
price: 6.0
}
]
}
{
id: 13
permalink_live: "oranges"
}
]
scope.dirtyProducts =
9:
id: 9
variants:
3:
id: 3
price: 12.0
4:
id: 4
price: 6.0
13:
id: 13
name: "P1"
13:
id: 13
name: "P1"
httpBackend.expectDELETE("/api/products/9/variants/3").respond 200, "data"
scope.deleteVariant scope.products[0], scope.products[0].variants[0]
httpBackend.flush()
expect(scope.products[0].variants).toEqual [
id: 4
price: 6.0
]
expect(scope.dirtyProducts).toEqual
9:
id: 9
variants:
4:
id: 4
price: 6.0
httpBackend.expectDELETE("/api/products/9/variants/3").respond 200, "data"
scope.deleteVariant scope.products[0], scope.products[0].variants[0]
httpBackend.flush()
expect(scope.products[0].variants).toEqual [
id: 4
price: 6.0
]
expect(scope.dirtyProducts).toEqual
9:
id: 9
variants:
4:
id: 4
price: 6.0
13:
id: 13
name: "P1"
13:
id: 13
name: "P1"