mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-15 23:57:48 +00:00
Merge branch 'master' into bom
This commit is contained in:
4
Gemfile
4
Gemfile
@@ -1,7 +1,7 @@
|
||||
source 'https://rubygems.org'
|
||||
ruby "1.9.3"
|
||||
|
||||
gem 'rails', '3.2.14'
|
||||
gem 'rails', '3.2.17'
|
||||
|
||||
gem 'pg'
|
||||
gem 'spree', :github => 'openfoodfoundation/spree', :branch => '1-3-stable'
|
||||
@@ -16,6 +16,7 @@ gem 'comfortable_mexican_sofa'
|
||||
gem 'simple_form', :github => 'RohanM/simple_form'
|
||||
|
||||
gem 'unicorn'
|
||||
gem 'angularjs-rails'
|
||||
gem 'bugsnag'
|
||||
gem 'newrelic_rpm'
|
||||
gem 'haml'
|
||||
@@ -50,6 +51,7 @@ group :assets do
|
||||
gem 'turbo-sprockets-rails3'
|
||||
gem 'zurb-foundation', :github => 'zurb/foundation'
|
||||
end
|
||||
gem 'foundation_rails_helper', github: 'willrjmarshall/foundation_rails_helper', branch: "rails3"
|
||||
|
||||
gem 'jquery-rails'
|
||||
|
||||
|
||||
75
Gemfile.lock
75
Gemfile.lock
@@ -93,6 +93,16 @@ GIT
|
||||
i18n (~> 0.5)
|
||||
spree (~> 1.1)
|
||||
|
||||
GIT
|
||||
remote: git://github.com/willrjmarshall/foundation_rails_helper.git
|
||||
revision: 4d5d53fdc4b1fb71e66524d298c5c635de82cfbb
|
||||
branch: rails3
|
||||
specs:
|
||||
foundation_rails_helper (0.4)
|
||||
actionpack (>= 3.0)
|
||||
activemodel (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
|
||||
GIT
|
||||
remote: git://github.com/zurb/foundation.git
|
||||
revision: a81d639847ba5fc2e324b749b8e409e31e8d83c9
|
||||
@@ -117,12 +127,12 @@ PATH
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionmailer (3.2.14)
|
||||
actionpack (= 3.2.14)
|
||||
actionmailer (3.2.17)
|
||||
actionpack (= 3.2.17)
|
||||
mail (~> 2.5.4)
|
||||
actionpack (3.2.14)
|
||||
activemodel (= 3.2.14)
|
||||
activesupport (= 3.2.14)
|
||||
actionpack (3.2.17)
|
||||
activemodel (= 3.2.17)
|
||||
activesupport (= 3.2.17)
|
||||
builder (~> 3.0.0)
|
||||
erubis (~> 2.7.0)
|
||||
journey (~> 1.0.4)
|
||||
@@ -142,25 +152,26 @@ GEM
|
||||
json (~> 1.7)
|
||||
money (< 6.0.0)
|
||||
nokogiri (~> 1.4)
|
||||
activemodel (3.2.14)
|
||||
activesupport (= 3.2.14)
|
||||
activemodel (3.2.17)
|
||||
activesupport (= 3.2.17)
|
||||
builder (~> 3.0.0)
|
||||
activerecord (3.2.14)
|
||||
activemodel (= 3.2.14)
|
||||
activesupport (= 3.2.14)
|
||||
activerecord (3.2.17)
|
||||
activemodel (= 3.2.17)
|
||||
activesupport (= 3.2.17)
|
||||
arel (~> 3.0.2)
|
||||
tzinfo (~> 0.3.29)
|
||||
activeresource (3.2.14)
|
||||
activemodel (= 3.2.14)
|
||||
activesupport (= 3.2.14)
|
||||
activesupport (3.2.14)
|
||||
activeresource (3.2.17)
|
||||
activemodel (= 3.2.17)
|
||||
activesupport (= 3.2.17)
|
||||
activesupport (3.2.17)
|
||||
i18n (~> 0.6, >= 0.6.4)
|
||||
multi_json (~> 1.0)
|
||||
acts_as_list (0.1.4)
|
||||
addressable (2.3.3)
|
||||
andand (1.3.3)
|
||||
angularjs-rails (1.2.13)
|
||||
ansi (1.4.2)
|
||||
arel (3.0.2)
|
||||
arel (3.0.3)
|
||||
awesome_nested_set (2.1.5)
|
||||
activerecord (>= 3.0.0)
|
||||
awesome_print (1.0.2)
|
||||
@@ -291,7 +302,7 @@ GEM
|
||||
httparty (0.11.0)
|
||||
multi_json (~> 1.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (0.6.5)
|
||||
i18n (0.6.9)
|
||||
journey (1.0.4)
|
||||
jquery-rails (2.2.2)
|
||||
railties (>= 3.0, < 5.0)
|
||||
@@ -319,12 +330,12 @@ GEM
|
||||
mime-types (~> 1.16)
|
||||
treetop (~> 1.4.8)
|
||||
method_source (0.8.1)
|
||||
mime-types (1.25)
|
||||
mime-types (1.25.1)
|
||||
mini_portile (0.5.2)
|
||||
money (5.0.0)
|
||||
i18n (~> 0.4)
|
||||
json
|
||||
multi_json (1.8.2)
|
||||
multi_json (1.8.4)
|
||||
multi_xml (0.5.5)
|
||||
net-scp (1.1.2)
|
||||
net-ssh (>= 2.6.5)
|
||||
@@ -346,7 +357,7 @@ GEM
|
||||
http_parser.rb (~> 0.5.3)
|
||||
polyamorous (0.5.0)
|
||||
activerecord (~> 3.0)
|
||||
polyglot (0.3.3)
|
||||
polyglot (0.3.4)
|
||||
pry (0.9.12.2)
|
||||
coderay (~> 1.0.5)
|
||||
method_source (~> 0.8)
|
||||
@@ -366,23 +377,23 @@ GEM
|
||||
rack
|
||||
rack-test (0.6.2)
|
||||
rack (>= 1.0)
|
||||
rails (3.2.14)
|
||||
actionmailer (= 3.2.14)
|
||||
actionpack (= 3.2.14)
|
||||
activerecord (= 3.2.14)
|
||||
activeresource (= 3.2.14)
|
||||
activesupport (= 3.2.14)
|
||||
rails (3.2.17)
|
||||
actionmailer (= 3.2.17)
|
||||
actionpack (= 3.2.17)
|
||||
activerecord (= 3.2.17)
|
||||
activeresource (= 3.2.17)
|
||||
activesupport (= 3.2.17)
|
||||
bundler (~> 1.0)
|
||||
railties (= 3.2.14)
|
||||
railties (3.2.14)
|
||||
actionpack (= 3.2.14)
|
||||
activesupport (= 3.2.14)
|
||||
railties (= 3.2.17)
|
||||
railties (3.2.17)
|
||||
actionpack (= 3.2.17)
|
||||
activesupport (= 3.2.17)
|
||||
rack-ssl (~> 1.3.2)
|
||||
rake (>= 0.8.7)
|
||||
rdoc (~> 3.4)
|
||||
thor (>= 0.14.6, < 2.0)
|
||||
raindrops (0.9.0)
|
||||
rake (10.1.0)
|
||||
rake (10.1.1)
|
||||
ransack (0.7.2)
|
||||
actionpack (~> 3.0)
|
||||
activerecord (~> 3.0)
|
||||
@@ -495,6 +506,7 @@ PLATFORMS
|
||||
|
||||
DEPENDENCIES
|
||||
andand
|
||||
angularjs-rails
|
||||
awesome_print
|
||||
aws-sdk
|
||||
bugsnag
|
||||
@@ -510,6 +522,7 @@ DEPENDENCIES
|
||||
eaterprises_feature!
|
||||
factory_girl_rails
|
||||
faker
|
||||
foundation_rails_helper!
|
||||
geocoder
|
||||
gmaps4rails
|
||||
guard
|
||||
@@ -531,7 +544,7 @@ DEPENDENCIES
|
||||
rabl
|
||||
rack-livereload
|
||||
rack-ssl
|
||||
rails (= 3.2.14)
|
||||
rails (= 3.2.17)
|
||||
representative_view
|
||||
rspec-rails
|
||||
sass
|
||||
|
||||
@@ -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) ->
|
||||
@@ -198,13 +199,22 @@ 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
|
||||
variant.unit_value / product.variant_unit_scale
|
||||
if variant.unit_value?
|
||||
if product.variant_unit_scale
|
||||
variant.unit_value / product.variant_unit_scale
|
||||
else
|
||||
variant.unit_value
|
||||
else
|
||||
null
|
||||
|
||||
@@ -259,6 +269,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(
|
||||
@@ -271,14 +298,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) ->
|
||||
@@ -299,10 +332,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(
|
||||
@@ -318,27 +372,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\.]+)$/)
|
||||
@@ -348,6 +394,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
|
||||
@@ -355,14 +402,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 *= product.variant_unit_scale if variant.unit_value
|
||||
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
|
||||
@@ -374,6 +440,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
|
||||
|
||||
|
||||
@@ -436,35 +503,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
|
||||
@@ -495,13 +557,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) ->
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
#= require jquery_ujs
|
||||
#= require jquery-ui
|
||||
#= require spin
|
||||
#= require ../shared/angular
|
||||
#= require ../shared/angular-resource
|
||||
#
|
||||
#= require angular
|
||||
#= require angular-resource
|
||||
#
|
||||
#= require ../shared/jquery.timeago
|
||||
#= require foundation
|
||||
#= require ./shop
|
||||
@@ -11,5 +13,3 @@
|
||||
|
||||
$ ->
|
||||
$(document).foundation()
|
||||
|
||||
|
||||
|
||||
2
app/assets/javascripts/darkswarm/checkout.js.coffee
Normal file
2
app/assets/javascripts/darkswarm/checkout.js.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
window.Checkout = angular.module("Checkout", ["ngResource", "filters"]).config ($httpProvider) ->
|
||||
$httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content')
|
||||
@@ -0,0 +1,11 @@
|
||||
angular.module("Checkout").controller "CheckoutCtrl", ($scope, $rootScope) ->
|
||||
$scope.require_ship_address = false
|
||||
$scope.shipping_method = -1
|
||||
$scope.payment_method = -1
|
||||
|
||||
$scope.shippingMethodChanged = ->
|
||||
$scope.require_ship_address = $("#order_shipping_method_id_" + $scope.shipping_method).attr("data-require-ship-address")
|
||||
|
||||
$scope.purchase = (event)->
|
||||
event.preventDefault()
|
||||
checkout.submit()
|
||||
498
app/assets/javascripts/jquery-migrate-1.0.0.js
Normal file
498
app/assets/javascripts/jquery-migrate-1.0.0.js
Normal file
@@ -0,0 +1,498 @@
|
||||
/*!
|
||||
* jQuery Migrate - v1.0.0 - 2013-01-14
|
||||
* https://github.com/jquery/jquery-migrate
|
||||
* Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors; Licensed MIT
|
||||
*/
|
||||
(function( jQuery, window, undefined ) {
|
||||
"use strict";
|
||||
|
||||
|
||||
var warnedAbout = {};
|
||||
|
||||
// List of warnings already given; public read only
|
||||
jQuery.migrateWarnings = [];
|
||||
|
||||
// Set to true to prevent console output; migrateWarnings still maintained
|
||||
jQuery.migrateMute = true;
|
||||
|
||||
// Forget any warnings we've already given; public
|
||||
jQuery.migrateReset = function() {
|
||||
warnedAbout = {};
|
||||
jQuery.migrateWarnings.length = 0;
|
||||
};
|
||||
|
||||
function migrateWarn( msg) {
|
||||
if ( !warnedAbout[ msg ] ) {
|
||||
warnedAbout[ msg ] = true;
|
||||
jQuery.migrateWarnings.push( msg );
|
||||
if ( window.console && console.warn && !jQuery.migrateMute ) {
|
||||
console.warn( "JQMIGRATE: " + msg );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function migrateWarnProp( obj, prop, value, msg ) {
|
||||
if ( Object.defineProperty ) {
|
||||
// On ES5 browsers (non-oldIE), warn if the code tries to get prop;
|
||||
// allow property to be overwritten in case some other plugin wants it
|
||||
try {
|
||||
Object.defineProperty( obj, prop, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
migrateWarn( msg );
|
||||
return value;
|
||||
},
|
||||
set: function( newValue ) {
|
||||
migrateWarn( msg );
|
||||
value = newValue;
|
||||
}
|
||||
});
|
||||
return;
|
||||
} catch( err ) {
|
||||
// IE8 is a dope about Object.defineProperty, can't warn there
|
||||
}
|
||||
}
|
||||
|
||||
// Non-ES5 (or broken) browser; just set the property
|
||||
jQuery._definePropertyBroken = true;
|
||||
obj[ prop ] = value;
|
||||
}
|
||||
|
||||
if ( document.compatMode === "BackCompat" ) {
|
||||
// jQuery has never supported or tested Quirks Mode
|
||||
migrateWarn( "jQuery is not compatible with Quirks Mode" );
|
||||
}
|
||||
|
||||
|
||||
var attrFn = {},
|
||||
attr = jQuery.attr,
|
||||
valueAttrGet = jQuery.attrHooks.value && jQuery.attrHooks.value.get ||
|
||||
function() { return null; },
|
||||
valueAttrSet = jQuery.attrHooks.value && jQuery.attrHooks.value.set ||
|
||||
function() { return undefined; },
|
||||
rnoType = /^(?:input|button)$/i,
|
||||
rnoAttrNodeType = /^[238]$/,
|
||||
rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
|
||||
ruseDefault = /^(?:checked|selected)$/i;
|
||||
|
||||
// jQuery.attrFn
|
||||
migrateWarnProp( jQuery, "attrFn", attrFn, "jQuery.attrFn is deprecated" );
|
||||
|
||||
jQuery.attr = function( elem, name, value, pass ) {
|
||||
var lowerName = name.toLowerCase(),
|
||||
nType = elem && elem.nodeType;
|
||||
|
||||
if ( pass ) {
|
||||
migrateWarn("jQuery.fn.attr( props, pass ) is deprecated");
|
||||
if ( elem && !rnoAttrNodeType.test( nType ) && jQuery.isFunction( jQuery.fn[ name ] ) ) {
|
||||
return jQuery( elem )[ name ]( value );
|
||||
}
|
||||
}
|
||||
|
||||
// Warn if user tries to set `type` since it breaks on IE 6/7/8
|
||||
if ( name === "type" && value !== undefined && rnoType.test( elem.nodeName ) ) {
|
||||
migrateWarn("Can't change the 'type' of an input or button in IE 6/7/8");
|
||||
}
|
||||
|
||||
// Restore boolHook for boolean property/attribute synchronization
|
||||
if ( !jQuery.attrHooks[ lowerName ] && rboolean.test( lowerName ) ) {
|
||||
jQuery.attrHooks[ lowerName ] = {
|
||||
get: function( elem, name ) {
|
||||
// Align boolean attributes with corresponding properties
|
||||
// Fall back to attribute presence where some booleans are not supported
|
||||
var attrNode,
|
||||
property = jQuery.prop( elem, name );
|
||||
return property === true || typeof property !== "boolean" &&
|
||||
( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ?
|
||||
|
||||
name.toLowerCase() :
|
||||
undefined;
|
||||
},
|
||||
set: function( elem, value, name ) {
|
||||
var propName;
|
||||
if ( value === false ) {
|
||||
// Remove boolean attributes when set to false
|
||||
jQuery.removeAttr( elem, name );
|
||||
} else {
|
||||
// value is true since we know at this point it's type boolean and not false
|
||||
// Set boolean attributes to the same name and set the DOM property
|
||||
propName = jQuery.propFix[ name ] || name;
|
||||
if ( propName in elem ) {
|
||||
// Only set the IDL specifically if it already exists on the element
|
||||
elem[ propName ] = true;
|
||||
}
|
||||
|
||||
elem.setAttribute( name, name.toLowerCase() );
|
||||
}
|
||||
return name;
|
||||
}
|
||||
};
|
||||
|
||||
// Warn only for attributes that can remain distinct from their properties post-1.9
|
||||
if ( ruseDefault.test( lowerName ) ) {
|
||||
migrateWarn( "jQuery.fn.attr(" + lowerName + ") may use property instead of attribute" );
|
||||
}
|
||||
}
|
||||
|
||||
return attr.call( jQuery, elem, name, value );
|
||||
};
|
||||
|
||||
// attrHooks: value
|
||||
jQuery.attrHooks.value = {
|
||||
get: function( elem, name ) {
|
||||
var nodeName = ( elem.nodeName || "" ).toLowerCase();
|
||||
if ( nodeName === "button" ) {
|
||||
return valueAttrGet.apply( this, arguments );
|
||||
}
|
||||
if ( nodeName !== "input" && nodeName !== "option" ) {
|
||||
migrateWarn("property-based jQuery.fn.attr('value') is deprecated");
|
||||
}
|
||||
return name in elem ?
|
||||
elem.value :
|
||||
null;
|
||||
},
|
||||
set: function( elem, value ) {
|
||||
var nodeName = ( elem.nodeName || "" ).toLowerCase();
|
||||
if ( nodeName === "button" ) {
|
||||
return valueAttrSet.apply( this, arguments );
|
||||
}
|
||||
if ( nodeName !== "input" && nodeName !== "option" ) {
|
||||
migrateWarn("property-based jQuery.fn.attr('value', val) is deprecated");
|
||||
}
|
||||
// Does not return so that setAttribute is also used
|
||||
elem.value = value;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var matched, browser,
|
||||
oldInit = jQuery.fn.init,
|
||||
// Note this does NOT include the # XSS fix from 1.7!
|
||||
rquickExpr = /^(?:.*(<[\w\W]+>)[^>]*|#([\w\-]*))$/;
|
||||
|
||||
// $(html) "looks like html" rule change
|
||||
jQuery.fn.init = function( selector, context, rootjQuery ) {
|
||||
var match;
|
||||
|
||||
if ( selector && typeof selector === "string" && !jQuery.isPlainObject( context ) &&
|
||||
(match = rquickExpr.exec( selector )) && match[1] ) {
|
||||
// This is an HTML string according to the "old" rules; is it still?
|
||||
if ( selector.charAt( 0 ) !== "<" ) {
|
||||
migrateWarn("$(html) HTML strings must start with '<' character");
|
||||
}
|
||||
// Now process using loose rules; let pre-1.8 play too
|
||||
if ( context && context.context ) {
|
||||
// jQuery object as context; parseHTML expects a DOM object
|
||||
context = context.context;
|
||||
}
|
||||
if ( jQuery.parseHTML ) {
|
||||
return oldInit.call( this, jQuery.parseHTML( jQuery.trim(selector), context, true ),
|
||||
context, rootjQuery );
|
||||
}
|
||||
}
|
||||
return oldInit.apply( this, arguments );
|
||||
};
|
||||
jQuery.fn.init.prototype = jQuery.fn;
|
||||
|
||||
jQuery.uaMatch = function( ua ) {
|
||||
ua = ua.toLowerCase();
|
||||
|
||||
var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
|
||||
/(webkit)[ \/]([\w.]+)/.exec( ua ) ||
|
||||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
|
||||
/(msie) ([\w.]+)/.exec( ua ) ||
|
||||
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
|
||||
[];
|
||||
|
||||
return {
|
||||
browser: match[ 1 ] || "",
|
||||
version: match[ 2 ] || "0"
|
||||
};
|
||||
};
|
||||
|
||||
matched = jQuery.uaMatch( navigator.userAgent );
|
||||
browser = {};
|
||||
|
||||
if ( matched.browser ) {
|
||||
browser[ matched.browser ] = true;
|
||||
browser.version = matched.version;
|
||||
}
|
||||
|
||||
// Chrome is Webkit, but Webkit is also Safari.
|
||||
if ( browser.chrome ) {
|
||||
browser.webkit = true;
|
||||
} else if ( browser.webkit ) {
|
||||
browser.safari = true;
|
||||
}
|
||||
|
||||
jQuery.browser = browser;
|
||||
|
||||
// Warn if the code tries to get jQuery.browser
|
||||
migrateWarnProp( jQuery, "browser", browser, "jQuery.browser is deprecated" );
|
||||
|
||||
jQuery.sub = function() {
|
||||
function jQuerySub( selector, context ) {
|
||||
return new jQuerySub.fn.init( selector, context );
|
||||
}
|
||||
jQuery.extend( true, jQuerySub, this );
|
||||
jQuerySub.superclass = this;
|
||||
jQuerySub.fn = jQuerySub.prototype = this();
|
||||
jQuerySub.fn.constructor = jQuerySub;
|
||||
jQuerySub.sub = this.sub;
|
||||
jQuerySub.fn.init = function init( selector, context ) {
|
||||
if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {
|
||||
context = jQuerySub( context );
|
||||
}
|
||||
|
||||
return jQuery.fn.init.call( this, selector, context, rootjQuerySub );
|
||||
};
|
||||
jQuerySub.fn.init.prototype = jQuerySub.fn;
|
||||
var rootjQuerySub = jQuerySub(document);
|
||||
migrateWarn( "jQuery.sub() is deprecated" );
|
||||
return jQuerySub;
|
||||
};
|
||||
|
||||
|
||||
var oldFnData = jQuery.fn.data;
|
||||
|
||||
jQuery.fn.data = function( name ) {
|
||||
var ret, evt,
|
||||
elem = this[0];
|
||||
|
||||
// Handles 1.7 which has this behavior and 1.8 which doesn't
|
||||
if ( elem && name === "events" && arguments.length === 1 ) {
|
||||
ret = jQuery.data( elem, name );
|
||||
evt = jQuery._data( elem, name );
|
||||
if ( ( ret === undefined || ret === evt ) && evt !== undefined ) {
|
||||
migrateWarn("Use of jQuery.fn.data('events') is deprecated");
|
||||
return evt;
|
||||
}
|
||||
}
|
||||
return oldFnData.apply( this, arguments );
|
||||
};
|
||||
|
||||
|
||||
var rscriptType = /\/(java|ecma)script/i,
|
||||
oldSelf = jQuery.fn.andSelf || jQuery.fn.addBack,
|
||||
oldFragment = jQuery.buildFragment;
|
||||
|
||||
jQuery.fn.andSelf = function() {
|
||||
migrateWarn("jQuery.fn.andSelf() replaced by jQuery.fn.addBack()");
|
||||
return oldSelf.apply( this, arguments );
|
||||
};
|
||||
|
||||
// Since jQuery.clean is used internally on older versions, we only shim if it's missing
|
||||
if ( !jQuery.clean ) {
|
||||
jQuery.clean = function( elems, context, fragment, scripts ) {
|
||||
// Set context per 1.8 logic
|
||||
context = context || document;
|
||||
context = !context.nodeType && context[0] || context;
|
||||
context = context.ownerDocument || context;
|
||||
|
||||
migrateWarn("jQuery.clean() is deprecated");
|
||||
|
||||
var i, elem, handleScript, jsTags,
|
||||
ret = [];
|
||||
|
||||
jQuery.merge( ret, jQuery.buildFragment( elems, context ).childNodes );
|
||||
|
||||
// Complex logic lifted directly from jQuery 1.8
|
||||
if ( fragment ) {
|
||||
// Special handling of each script element
|
||||
handleScript = function( elem ) {
|
||||
// Check if we consider it executable
|
||||
if ( !elem.type || rscriptType.test( elem.type ) ) {
|
||||
// Detach the script and store it in the scripts array (if provided) or the fragment
|
||||
// Return truthy to indicate that it has been handled
|
||||
return scripts ?
|
||||
scripts.push( elem.parentNode ? elem.parentNode.removeChild( elem ) : elem ) :
|
||||
fragment.appendChild( elem );
|
||||
}
|
||||
};
|
||||
|
||||
for ( i = 0; (elem = ret[i]) != null; i++ ) {
|
||||
// Check if we're done after handling an executable script
|
||||
if ( !( jQuery.nodeName( elem, "script" ) && handleScript( elem ) ) ) {
|
||||
// Append to fragment and handle embedded scripts
|
||||
fragment.appendChild( elem );
|
||||
if ( typeof elem.getElementsByTagName !== "undefined" ) {
|
||||
// handleScript alters the DOM, so use jQuery.merge to ensure snapshot iteration
|
||||
jsTags = jQuery.grep( jQuery.merge( [], elem.getElementsByTagName("script") ), handleScript );
|
||||
|
||||
// Splice the scripts into ret after their former ancestor and advance our index beyond them
|
||||
ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
|
||||
i += jsTags.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
|
||||
jQuery.buildFragment = function( elems, context, scripts, selection ) {
|
||||
var ret,
|
||||
warning = "jQuery.buildFragment() is deprecated";
|
||||
|
||||
// Set context per 1.8 logic
|
||||
context = context || document;
|
||||
context = !context.nodeType && context[0] || context;
|
||||
context = context.ownerDocument || context;
|
||||
|
||||
try {
|
||||
ret = oldFragment.call( jQuery, elems, context, scripts, selection );
|
||||
|
||||
// jQuery < 1.8 required arrayish context; jQuery 1.9 fails on it
|
||||
} catch( x ) {
|
||||
ret = oldFragment.call( jQuery, elems, context.nodeType ? [ context ] : context[ 0 ], scripts, selection );
|
||||
|
||||
// Success from tweaking context means buildFragment was called by the user
|
||||
migrateWarn( warning );
|
||||
}
|
||||
|
||||
// jQuery < 1.9 returned an object instead of the fragment itself
|
||||
if ( !ret.fragment ) {
|
||||
migrateWarnProp( ret, "fragment", ret, warning );
|
||||
migrateWarnProp( ret, "cacheable", false, warning );
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
var eventAdd = jQuery.event.add,
|
||||
eventRemove = jQuery.event.remove,
|
||||
eventTrigger = jQuery.event.trigger,
|
||||
oldToggle = jQuery.fn.toggle,
|
||||
oldLive = jQuery.fn.live,
|
||||
oldDie = jQuery.fn.die,
|
||||
ajaxEvents = "ajaxStart|ajaxStop|ajaxSend|ajaxComplete|ajaxError|ajaxSuccess",
|
||||
rajaxEvent = new RegExp( "\\b(?:" + ajaxEvents + ")\\b" ),
|
||||
rhoverHack = /(?:^|\s)hover(\.\S+|)\b/,
|
||||
hoverHack = function( events ) {
|
||||
if ( typeof( events ) != "string" || jQuery.event.special.hover ) {
|
||||
return events;
|
||||
}
|
||||
if ( rhoverHack.test( events ) ) {
|
||||
migrateWarn("'hover' pseudo-event is deprecated, use 'mouseenter mouseleave'");
|
||||
}
|
||||
return events && events.replace( rhoverHack, "mouseenter$1 mouseleave$1" );
|
||||
};
|
||||
|
||||
// Event props removed in 1.9, put them back if needed; no practical way to warn them
|
||||
if ( jQuery.event.props && jQuery.event.props[ 0 ] !== "attrChange" ) {
|
||||
jQuery.event.props.unshift( "attrChange", "attrName", "relatedNode", "srcElement" );
|
||||
}
|
||||
|
||||
// Undocumented jQuery.event.handle was "deprecated" in jQuery 1.7
|
||||
migrateWarnProp( jQuery.event, "handle", jQuery.event.dispatch, "jQuery.event.handle is undocumented and deprecated" );
|
||||
|
||||
// Support for 'hover' pseudo-event and ajax event warnings
|
||||
jQuery.event.add = function( elem, types, handler, data, selector ){
|
||||
if ( elem !== document && rajaxEvent.test( types ) ) {
|
||||
migrateWarn( "AJAX events should be attached to document: " + types );
|
||||
}
|
||||
eventAdd.call( this, elem, hoverHack( types || "" ), handler, data, selector );
|
||||
};
|
||||
jQuery.event.remove = function( elem, types, handler, selector, mappedTypes ){
|
||||
eventRemove.call( this, elem, hoverHack( types ) || "", handler, selector, mappedTypes );
|
||||
};
|
||||
|
||||
jQuery.fn.error = function() {
|
||||
var args = Array.prototype.slice.call( arguments, 0);
|
||||
migrateWarn("jQuery.fn.error() is deprecated");
|
||||
args.splice( 0, 0, "error" );
|
||||
if ( arguments.length ) {
|
||||
return this.bind.apply( this, args );
|
||||
}
|
||||
// error event should not bubble to window, although it does pre-1.7
|
||||
this.triggerHandler.apply( this, args );
|
||||
return this;
|
||||
};
|
||||
|
||||
jQuery.fn.toggle = function( fn, fn2 ) {
|
||||
|
||||
// Don't mess with animation or css toggles
|
||||
if ( !jQuery.isFunction( fn ) || !jQuery.isFunction( fn2 ) ) {
|
||||
return oldToggle.apply( this, arguments );
|
||||
}
|
||||
migrateWarn("jQuery.fn.toggle(handler, handler...) is deprecated");
|
||||
|
||||
// Save reference to arguments for access in closure
|
||||
var args = arguments,
|
||||
guid = fn.guid || jQuery.guid++,
|
||||
i = 0,
|
||||
toggler = function( event ) {
|
||||
// Figure out which function to execute
|
||||
var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i;
|
||||
jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 );
|
||||
|
||||
// Make sure that clicks stop
|
||||
event.preventDefault();
|
||||
|
||||
// and execute the function
|
||||
return args[ lastToggle ].apply( this, arguments ) || false;
|
||||
};
|
||||
|
||||
// link all the functions, so any of them can unbind this click handler
|
||||
toggler.guid = guid;
|
||||
while ( i < args.length ) {
|
||||
args[ i++ ].guid = guid;
|
||||
}
|
||||
|
||||
return this.click( toggler );
|
||||
};
|
||||
|
||||
jQuery.fn.live = function( types, data, fn ) {
|
||||
migrateWarn("jQuery.fn.live() is deprecated");
|
||||
if ( oldLive ) {
|
||||
return oldLive.apply( this, arguments );
|
||||
}
|
||||
jQuery( this.context ).on( types, this.selector, data, fn );
|
||||
return this;
|
||||
};
|
||||
|
||||
jQuery.fn.die = function( types, fn ) {
|
||||
migrateWarn("jQuery.fn.die() is deprecated");
|
||||
if ( oldDie ) {
|
||||
return oldDie.apply( this, arguments );
|
||||
}
|
||||
jQuery( this.context ).off( types, this.selector || "**", fn );
|
||||
return this;
|
||||
};
|
||||
|
||||
// Turn global events into document-triggered events
|
||||
jQuery.event.trigger = function( event, data, elem, onlyHandlers ){
|
||||
if ( !elem & !rajaxEvent.test( event ) ) {
|
||||
migrateWarn( "Global events are undocumented and deprecated" );
|
||||
}
|
||||
return eventTrigger.call( this, event, data, elem || document, onlyHandlers );
|
||||
};
|
||||
jQuery.each( ajaxEvents.split("|"),
|
||||
function( _, name ) {
|
||||
jQuery.event.special[ name ] = {
|
||||
setup: function() {
|
||||
var elem = this;
|
||||
|
||||
// The document needs no shimming; must be !== for oldIE
|
||||
if ( elem !== document ) {
|
||||
jQuery.event.add( document, name + "." + jQuery.guid, function() {
|
||||
jQuery.event.trigger( name, null, elem, true );
|
||||
});
|
||||
jQuery._data( this, name, jQuery.guid++ );
|
||||
}
|
||||
return false;
|
||||
},
|
||||
teardown: function() {
|
||||
if ( this !== document ) {
|
||||
jQuery.event.remove( document, name + "." + jQuery._data( this, name ) );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
})( jQuery, window );
|
||||
3
app/assets/javascripts/shop/checkout.js.coffee
Normal file
3
app/assets/javascripts/shop/checkout.js.coffee
Normal file
@@ -0,0 +1,3 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
|
||||
@@ -106,6 +106,8 @@ ul.column-list {
|
||||
}
|
||||
|
||||
table#listing_products.bulk {
|
||||
clear: both;
|
||||
|
||||
td.supplier {
|
||||
select {
|
||||
width: 125px;
|
||||
|
||||
2
app/assets/stylesheets/darkswarm/checkout.css.sass
Normal file
2
app/assets/stylesheets/darkswarm/checkout.css.sass
Normal file
@@ -0,0 +1,2 @@
|
||||
checkout
|
||||
display: block
|
||||
@@ -10,3 +10,7 @@
|
||||
img
|
||||
display: block
|
||||
margin: 0px auto 8px
|
||||
|
||||
.contact
|
||||
strong
|
||||
padding-right: 1em
|
||||
|
||||
16
app/assets/stylesheets/darkswarm/forms.css.sass
Normal file
16
app/assets/stylesheets/darkswarm/forms.css.sass
Normal file
@@ -0,0 +1,16 @@
|
||||
@import variables
|
||||
|
||||
form
|
||||
fieldset
|
||||
padding: 0px
|
||||
border: none
|
||||
legend
|
||||
border: 1px solid $dark-grey
|
||||
border-left: 0px
|
||||
border-right: 0px
|
||||
padding: 16px 24px
|
||||
display: block
|
||||
width: 100%
|
||||
margin-bottom: 1em
|
||||
text-transform: uppercase
|
||||
color: #999999
|
||||
@@ -4,7 +4,7 @@
|
||||
product
|
||||
display: block
|
||||
|
||||
shop
|
||||
.darkswarm
|
||||
#search
|
||||
font-size: 2em
|
||||
@include big-input
|
||||
@@ -29,6 +29,9 @@ shop
|
||||
location
|
||||
font-family: "AvenirBla_IE", "AvenirBla"
|
||||
padding-right: 16px
|
||||
@media all and (max-width: 768px)
|
||||
location, location + small
|
||||
display: block
|
||||
|
||||
#distributor_title
|
||||
float: left
|
||||
@@ -38,28 +41,37 @@ shop
|
||||
ordercycle
|
||||
@media all and (max-width: 768px)
|
||||
float: left
|
||||
clear: left
|
||||
padding-bottom: 12px
|
||||
display: block
|
||||
float: right
|
||||
form.custom
|
||||
width: 400px
|
||||
//width: 400px
|
||||
text-align: right
|
||||
margin-right: 1em
|
||||
@media all and (max-width: 768px)
|
||||
padding-left: 1em
|
||||
padding-top: 1em
|
||||
& > strong
|
||||
line-height: 2.5
|
||||
font-size: 1.29em
|
||||
padding-right: 14px
|
||||
@media all and (max-width: 768px)
|
||||
font-size: 1.2em
|
||||
.custom.dropdown
|
||||
width: 280px
|
||||
display: inline-block
|
||||
background: transparent
|
||||
border-width: 2px
|
||||
border-color: #666666
|
||||
font-size: 1.28em
|
||||
font-size: 1em
|
||||
margin-bottom: 0
|
||||
@media all and (max-width: 768px)
|
||||
font-size: 1.2em
|
||||
width: 180px
|
||||
closing
|
||||
font-size: 0.875em
|
||||
display: block
|
||||
float: right
|
||||
padding-top: 14px
|
||||
|
||||
tabs
|
||||
@@ -76,18 +88,28 @@ shop
|
||||
margin: 0px 0px 0px 40px
|
||||
p
|
||||
max-width: 555px
|
||||
@media all and (max-width: 768px)
|
||||
height: auto !important
|
||||
& > .title, &.active > .title
|
||||
text-transform: uppercase
|
||||
line-height: 50px
|
||||
@media all and (max-width: 768px)
|
||||
line-height: 30px !important
|
||||
border: none
|
||||
&, &:hover
|
||||
background: none
|
||||
a
|
||||
padding: 0px 2.2em
|
||||
@media all and (max-width: 768px)
|
||||
line-height: inherit !important
|
||||
|
||||
products
|
||||
display: block
|
||||
padding-top: 36px
|
||||
padding-top: 2.3em
|
||||
@media all and (max-width: 768px)
|
||||
padding-top: 1em
|
||||
input.button.right
|
||||
float: left
|
||||
table
|
||||
table-layout: fixed
|
||||
width: 100%
|
||||
@@ -96,11 +118,11 @@ shop
|
||||
th
|
||||
line-height: 50px
|
||||
&.name
|
||||
//width: 300px
|
||||
&.notes
|
||||
width: 140px
|
||||
width: 330px
|
||||
//&.notes
|
||||
//width: 140px
|
||||
&.variant
|
||||
width: 100px
|
||||
width: 180px
|
||||
&.quantity, &.bulk, &.price
|
||||
width: 90px
|
||||
.notes
|
||||
@@ -116,9 +138,14 @@ shop
|
||||
border-right: 0px
|
||||
td
|
||||
padding: 20px 0px
|
||||
&.name img
|
||||
float: left
|
||||
margin-right: 30px
|
||||
&.name
|
||||
img
|
||||
float: left
|
||||
margin-right: 30px
|
||||
@media all and (max-width: 768px)
|
||||
margin-right: 1em
|
||||
div
|
||||
min-width: 150px
|
||||
tr.product-description
|
||||
display: none
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
$fawn: #f6efe5
|
||||
$dark-grey: #c7c7c7
|
||||
|
||||
3
app/assets/stylesheets/shop/checkout.css.scss
Normal file
3
app/assets/stylesheets/shop/checkout.css.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
// Place all the styles related to the Shop::Checkout controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
@@ -8,6 +8,7 @@ module Admin
|
||||
def index
|
||||
@include_calculators = params[:include_calculators].present?
|
||||
@enterprise = current_enterprise
|
||||
@enterprises = Enterprise.managed_by(spree_current_user).by_name
|
||||
|
||||
blank_enterprise_fee = EnterpriseFee.new
|
||||
blank_enterprise_fee.enterprise = current_enterprise
|
||||
|
||||
@@ -5,6 +5,15 @@ class ApplicationController < ActionController::Base
|
||||
before_filter :load_data_for_sidebar
|
||||
before_filter :require_certified_hostname
|
||||
|
||||
|
||||
def after_sign_in_path_for(resource)
|
||||
if request.referer and referer_path = URI(request.referer).path
|
||||
[main_app.shop_checkout_path].include?(referer_path) ? referer_path : root_path
|
||||
else
|
||||
root_path
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def load_data_for_menu
|
||||
@cms_site = Cms::Site.where(:identifier => 'open-food-network').first
|
||||
|
||||
71
app/controllers/shop/checkout_controller.rb
Normal file
71
app/controllers/shop/checkout_controller.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
class Shop::CheckoutController < Spree::CheckoutController
|
||||
layout 'darkswarm'
|
||||
|
||||
prepend_before_filter :require_order_cycle
|
||||
prepend_before_filter :require_distributor_chosen
|
||||
skip_before_filter :check_registration
|
||||
|
||||
include EnterprisesHelper
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @order.update_attributes(params[:order])
|
||||
fire_event('spree.checkout.update')
|
||||
|
||||
while @order.state != "complete"
|
||||
if @order.next
|
||||
state_callback(:after)
|
||||
else
|
||||
flash[:error] = t(:payment_processing_failed)
|
||||
respond_with @order, location: main_app.shop_checkout_path
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if @order.state == "complete" || @order.completed?
|
||||
flash.notice = t(:order_processed_successfully)
|
||||
flash[:commerce_tracking] = "nothing special"
|
||||
respond_with(@order, :location => order_path(@order))
|
||||
else
|
||||
respond_with @order, location: main_app.shop_checkout_path
|
||||
end
|
||||
else
|
||||
respond_with @order, location: main_app.shop_checkout_path
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def skip_state_validation?
|
||||
true
|
||||
end
|
||||
|
||||
def set_distributor
|
||||
unless @distributor = current_distributor
|
||||
redirect_to main_app.root_path
|
||||
end
|
||||
end
|
||||
|
||||
def require_order_cycle
|
||||
unless current_order_cycle
|
||||
redirect_to main_app.shop_path
|
||||
end
|
||||
end
|
||||
|
||||
def load_order
|
||||
@order = current_order
|
||||
redirect_to main_app.shop_path and return unless @order and @order.checkout_allowed?
|
||||
raise_insufficient_quantity and return if @order.insufficient_stock_lines.present?
|
||||
redirect_to main_app.shop_path and return if @order.completed?
|
||||
before_address
|
||||
state_callback(:before)
|
||||
end
|
||||
|
||||
# Overriding Spree's methods
|
||||
def raise_insufficient_quantity
|
||||
flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity)
|
||||
redirect_to main_app.shop_path
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
class ShopController < BaseController
|
||||
class Shop::ShopController < BaseController
|
||||
layout "darkswarm"
|
||||
|
||||
before_filter :set_distributor
|
||||
@@ -22,19 +22,18 @@ class ShopController < BaseController
|
||||
if request.post?
|
||||
if oc = OrderCycle.with_distributor(@distributor).active.find_by_id(params[:order_cycle_id])
|
||||
current_order(true).set_order_cycle! oc
|
||||
render partial: "shop/order_cycle"
|
||||
render partial: "shop/shop/order_cycle"
|
||||
else
|
||||
render status: 404, json: ""
|
||||
end
|
||||
else
|
||||
render partial: "shop/order_cycle"
|
||||
render partial: "shop/shop/order_cycle"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_distributor
|
||||
|
||||
unless @distributor = current_distributor
|
||||
redirect_to root_path
|
||||
end
|
||||
13
app/controllers/spree/admin/variants_controller_decorator.rb
Normal file
13
app/controllers/spree/admin/variants_controller_decorator.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
Spree::Admin::VariantsController.class_eval do
|
||||
helper 'spree/products'
|
||||
|
||||
|
||||
protected
|
||||
|
||||
def create_before
|
||||
option_values = params[:new_variant]
|
||||
option_values.andand.each_value {|id| @object.option_values << OptionValue.find(id)}
|
||||
@object.save
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,5 +1,9 @@
|
||||
Spree::CheckoutController.class_eval do
|
||||
|
||||
#def update
|
||||
#binding.pry
|
||||
#end
|
||||
|
||||
private
|
||||
|
||||
def before_payment
|
||||
@@ -14,8 +18,9 @@ Spree::CheckoutController.class_eval do
|
||||
|
||||
last_used_bill_address, last_used_ship_address = find_last_used_addresses(@order.email)
|
||||
preferred_bill_address, preferred_ship_address = spree_current_user.bill_address, spree_current_user.ship_address if spree_current_user.respond_to?(:bill_address) && spree_current_user.respond_to?(:ship_address)
|
||||
|
||||
@order.bill_address ||= preferred_bill_address || last_used_bill_address || Spree::Address.default
|
||||
@order.ship_address ||= preferred_ship_address || last_used_ship_address || Spree::Address.default
|
||||
@order.ship_address ||= preferred_ship_address || last_used_ship_address || nil
|
||||
end
|
||||
|
||||
def after_complete
|
||||
|
||||
@@ -24,4 +24,4 @@ Spree::UserSessionsController.class_eval do
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
module ApplicationHelper
|
||||
include FoundationRailsHelper::FlashHelper
|
||||
|
||||
def home_page_cms_content
|
||||
if controller.controller_name == 'home' && controller.action_name == 'index'
|
||||
cms_page_content(:content, Cms::Page.find_by_full_path('/'))
|
||||
|
||||
2
app/helpers/shop/checkout_helper.rb
Normal file
2
app/helpers/shop/checkout_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module Shop::CheckoutHelper
|
||||
end
|
||||
@@ -4,5 +4,22 @@ 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
|
||||
|
||||
|
||||
def product_variant_unit_options
|
||||
[['Weight', 'weight'],
|
||||
['Volume', 'volume'],
|
||||
['Items', 'items']]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,8 @@ class EnterpriseFee < ActiveRecord::Base
|
||||
attr_accessible :enterprise_id, :fee_type, :name, :calculator_type
|
||||
|
||||
FEE_TYPES = %w(packing transport admin sales)
|
||||
PER_ORDER_CALCULATORS = ['Spree::Calculator::FlatRate', 'Spree::Calculator::FlexiRate']
|
||||
|
||||
|
||||
validates_inclusion_of :fee_type, :in => FEE_TYPES
|
||||
validates_presence_of :name
|
||||
@@ -21,12 +23,19 @@ class EnterpriseFee < ActiveRecord::Base
|
||||
end
|
||||
}
|
||||
|
||||
scope :per_item, lambda {
|
||||
joins(:calculator).where('spree_calculators.type NOT IN (?)', PER_ORDER_CALCULATORS)
|
||||
}
|
||||
scope :per_order, lambda {
|
||||
joins(:calculator).where('spree_calculators.type IN (?)', PER_ORDER_CALCULATORS)
|
||||
}
|
||||
|
||||
def self.clear_all_adjustments_for(line_item)
|
||||
line_item.order.adjustments.where(originator_type: 'EnterpriseFee', source_id: line_item, source_type: 'Spree::LineItem').destroy_all
|
||||
end
|
||||
|
||||
def self.clear_all_adjustments_on_order(order)
|
||||
order.adjustments.where(originator_type: 'EnterpriseFee', source_type: 'Spree::LineItem').destroy_all
|
||||
order.adjustments.where(originator_type: 'EnterpriseFee').destroy_all
|
||||
end
|
||||
|
||||
# Create an adjustment that starts as locked. Preferable to making an adjustment and locking it since
|
||||
|
||||
@@ -20,6 +20,7 @@ class Exchange < ActiveRecord::Base
|
||||
scope :from_enterprises, lambda { |enterprises| where('exchanges.sender_id IN (?)', enterprises) }
|
||||
scope :to_enterprises, lambda { |enterprises| where('exchanges.receiver_id IN (?)', enterprises) }
|
||||
scope :with_variant, lambda { |variant| joins(:exchange_variants).where('exchange_variants.variant_id = ?', variant) }
|
||||
scope :any_variant, lambda { |variants| joins(:exchange_variants).where('exchange_variants.variant_id IN (?)', variants) }
|
||||
scope :with_product, lambda { |product| joins(:exchange_variants).where('exchange_variants.variant_id IN (?)', product.variants_including_master) }
|
||||
|
||||
def clone!(new_order_cycle)
|
||||
@@ -35,6 +36,10 @@ class Exchange < ActiveRecord::Base
|
||||
receiver == order_cycle.coordinator
|
||||
end
|
||||
|
||||
def role
|
||||
incoming? ? 'supplier' : 'distributor'
|
||||
end
|
||||
|
||||
def to_h(core=false)
|
||||
h = attributes.merge({ 'variant_ids' => variant_ids.sort, 'enterprise_fee_ids' => enterprise_fee_ids.sort })
|
||||
h.reject! { |k| %w(id order_cycle_id created_at updated_at).include? k } if core
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'open_food_network/enterprise_fee_applicator'
|
||||
|
||||
class OrderCycle < ActiveRecord::Base
|
||||
belongs_to :coordinator, :class_name => 'Enterprise'
|
||||
has_and_belongs_to_many :coordinator_fees, :class_name => 'EnterpriseFee', :join_table => 'coordinator_fees'
|
||||
@@ -144,56 +146,72 @@ class OrderCycle < ActiveRecord::Base
|
||||
|
||||
# -- Fees
|
||||
def fees_for(variant, distributor)
|
||||
enterprise_fees_for(variant, distributor).sum do |fee|
|
||||
per_item_enterprise_fee_applicators_for(variant, distributor).sum do |applicator|
|
||||
# Spree's Calculator interface accepts Orders or LineItems,
|
||||
# so we meet that interface with a struct.
|
||||
line_item = OpenStruct.new variant: variant, quantity: 1
|
||||
fee[:enterprise_fee].compute_amount(line_item)
|
||||
# Amount is faked, this is a method on LineItem
|
||||
line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price
|
||||
applicator.enterprise_fee.compute_amount(line_item)
|
||||
end
|
||||
end
|
||||
|
||||
def create_adjustments_for(line_item)
|
||||
def create_line_item_adjustments_for(line_item)
|
||||
variant = line_item.variant
|
||||
distributor = line_item.order.distributor
|
||||
|
||||
enterprise_fees_for(variant, distributor).each { |fee| create_adjustment_for_fee line_item, fee[:enterprise_fee], fee[:label], fee[:role] }
|
||||
per_item_enterprise_fee_applicators_for(variant, distributor).each do |applicator|
|
||||
applicator.create_line_item_adjustment(line_item)
|
||||
end
|
||||
end
|
||||
|
||||
def create_order_adjustments_for(order)
|
||||
per_order_enterprise_fee_applicators_for(order).each do |applicator|
|
||||
applicator.create_order_adjustment(order)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
# -- Fees
|
||||
def enterprise_fees_for(variant, distributor)
|
||||
def per_item_enterprise_fee_applicators_for(variant, distributor)
|
||||
fees = []
|
||||
|
||||
exchanges_carrying(variant, distributor).each do |exchange|
|
||||
exchange.enterprise_fees.each do |enterprise_fee|
|
||||
role = exchange.incoming? ? 'supplier' : 'distributor'
|
||||
fees << {enterprise_fee: enterprise_fee,
|
||||
label: adjustment_label_for(variant, enterprise_fee, role),
|
||||
role: role}
|
||||
exchange.enterprise_fees.per_item.each do |enterprise_fee|
|
||||
fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role)
|
||||
end
|
||||
end
|
||||
|
||||
coordinator_fees.each do |enterprise_fee|
|
||||
fees << {enterprise_fee: enterprise_fee,
|
||||
label: adjustment_label_for(variant, enterprise_fee, 'coordinator'),
|
||||
role: 'coordinator'}
|
||||
coordinator_fees.per_item.each do |enterprise_fee|
|
||||
fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, 'coordinator')
|
||||
end
|
||||
|
||||
fees
|
||||
end
|
||||
|
||||
def create_adjustment_for_fee(line_item, enterprise_fee, label, role)
|
||||
a = enterprise_fee.create_locked_adjustment(label, line_item.order, line_item, true)
|
||||
AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: role
|
||||
end
|
||||
def per_order_enterprise_fee_applicators_for(order)
|
||||
fees = []
|
||||
|
||||
def adjustment_label_for(variant, enterprise_fee, role)
|
||||
"#{variant.product.name} - #{enterprise_fee.fee_type} fee by #{role} #{enterprise_fee.enterprise.name}"
|
||||
exchanges_supplying(order).each do |exchange|
|
||||
exchange.enterprise_fees.per_order.each do |enterprise_fee|
|
||||
fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role)
|
||||
end
|
||||
end
|
||||
|
||||
coordinator_fees.per_order.each do |enterprise_fee|
|
||||
fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, 'coordinator')
|
||||
end
|
||||
|
||||
fees
|
||||
end
|
||||
|
||||
def exchanges_carrying(variant, distributor)
|
||||
exchanges.to_enterprises([coordinator, distributor]).with_variant(variant)
|
||||
end
|
||||
|
||||
def exchanges_supplying(order)
|
||||
variants = order.line_items.map(&:variant)
|
||||
exchanges.to_enterprises([coordinator, order.distributor]).any_variant(variants)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
Spree::Address.class_eval do
|
||||
has_one :enterprise
|
||||
belongs_to :country, class_name: "Spree::Country"
|
||||
|
||||
geocoded_by :full_address
|
||||
|
||||
|
||||
@@ -14,6 +14,26 @@ Spree::Order.class_eval do
|
||||
|
||||
before_validation :shipping_address_from_distributor
|
||||
|
||||
checkout_flow do
|
||||
go_to_state :address
|
||||
go_to_state :delivery
|
||||
go_to_state :payment, :if => lambda { |order|
|
||||
if order.ship_address.andand.valid?
|
||||
# Fix for #2191
|
||||
if order.shipping_method
|
||||
order.create_shipment!
|
||||
order.update_totals
|
||||
end
|
||||
order.payment_required?
|
||||
else
|
||||
false
|
||||
end
|
||||
}
|
||||
go_to_state :confirm, :if => lambda { |order| order.confirmation_required? }
|
||||
go_to_state :complete, :if => lambda { |order| (order.payment_required? && order.has_unprocessed_payments?) || !order.payment_required? }
|
||||
remove_transition :from => :delivery, :to => :confirm
|
||||
end
|
||||
|
||||
|
||||
# -- Scopes
|
||||
scope :managed_by, lambda { |user|
|
||||
@@ -80,13 +100,15 @@ Spree::Order.class_eval do
|
||||
|
||||
line_items.each do |line_item|
|
||||
if provided_by_order_cycle? line_item
|
||||
order_cycle.create_adjustments_for line_item
|
||||
order_cycle.create_line_item_adjustments_for line_item
|
||||
|
||||
else
|
||||
pd = product_distribution_for line_item
|
||||
pd.create_adjustment_for line_item if pd
|
||||
end
|
||||
end
|
||||
|
||||
order_cycle.create_order_adjustments_for self if order_cycle
|
||||
end
|
||||
|
||||
def set_variant_attributes(variant, attributes)
|
||||
@@ -117,12 +139,17 @@ Spree::Order.class_eval do
|
||||
|
||||
def shipping_address_from_distributor
|
||||
if distributor
|
||||
self.ship_address = distributor.address.clone
|
||||
# This method is confusing to conform to the vagaries of the multi-step checkout
|
||||
# We copy over the shipping address when we have no shipping method selected
|
||||
# We can refactor this when we drop the multi-step checkout option
|
||||
if shipping_method.nil? or shipping_method.andand.require_ship_address == false
|
||||
self.ship_address = distributor.address.clone
|
||||
|
||||
if bill_address
|
||||
self.ship_address.firstname = bill_address.firstname
|
||||
self.ship_address.lastname = bill_address.lastname
|
||||
self.ship_address.phone = bill_address.phone
|
||||
if bill_address
|
||||
self.ship_address.firstname = bill_address.firstname
|
||||
self.ship_address.lastname = bill_address.lastname
|
||||
self.ship_address.phone = bill_address.phone
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -135,5 +162,4 @@ Spree::Order.class_eval do
|
||||
def product_distribution_for(line_item)
|
||||
line_item.variant.product.product_distribution_for self.distributor
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
Spree::Product.class_eval do
|
||||
# We have an after_destroy callback on Spree::ProductOptionType. However, if we
|
||||
# don't specify dependent => destroy on this association, it is not called. See:
|
||||
# https://github.com/rails/rails/issues/7618
|
||||
has_many :option_types, :through => :product_option_types, :dependent => :destroy
|
||||
|
||||
|
||||
belongs_to :supplier, :class_name => 'Enterprise'
|
||||
|
||||
has_many :product_distributions, :dependent => :destroy
|
||||
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
|
||||
|
||||
|
||||
10
app/models/spree/product_option_type_decorator.rb
Normal file
10
app/models/spree/product_option_type_decorator.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
Spree::ProductOptionType.class_eval do
|
||||
after_destroy :remove_option_values
|
||||
|
||||
def remove_option_values
|
||||
self.product.variants_including_master.each do |variant|
|
||||
option_values = variant.option_values.where(option_type_id: self.option_type)
|
||||
variant.option_values.destroy *option_values
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -54,13 +51,18 @@ Spree::Variant.class_eval do
|
||||
|
||||
def option_value_name
|
||||
value, unit = option_value_value_unit
|
||||
separator = value_scaled? ? '' : ' '
|
||||
|
||||
name_fields = []
|
||||
name_fields << "#{value} #{unit}" if value.present? && unit.present?
|
||||
name_fields << "#{value}#{separator}#{unit}" if value.present? && unit.present?
|
||||
name_fields << unit_description if unit_description.present?
|
||||
name_fields.join ' '
|
||||
end
|
||||
|
||||
def value_scaled?
|
||||
self.product.variant_unit_scale.present?
|
||||
end
|
||||
|
||||
def option_value_value_unit
|
||||
if unit_value.present?
|
||||
if %w(weight volume).include? self.product.variant_unit
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/ insert_top "[data-hook='admin_product_form_right']"
|
||||
|
||||
= f.field_container :variant_unit do
|
||||
= f.label :variant_unit, 'Variant unit'
|
||||
= f.select :variant_unit, product_variant_unit_options, {:include_blank => true}, {:class => "select2 fullwidth"}
|
||||
= f.error_message_on :variant_unit
|
||||
|
||||
= f.field_container :variant_unit_scale do
|
||||
= f.label :variant_unit_scale, 'Variant unit scale'
|
||||
= f.text_field :variant_unit_scale
|
||||
= f.error_message_on :variant_unit_scale
|
||||
|
||||
= f.field_container :variant_unit_name do
|
||||
= f.label :variant_unit_name, 'Variant unit name'
|
||||
= f.text_field :variant_unit_name
|
||||
= f.error_message_on :variant_unit_name
|
||||
@@ -0,0 +1,17 @@
|
||||
/ insert_before "[data-hook='new_product_attrs']"
|
||||
|
||||
.row
|
||||
.alpha.six.columns
|
||||
= f.label :variant_unit, 'Variant unit'
|
||||
= f.select :variant_unit, product_variant_unit_options, {:include_blank => true}, {:class => "select2 fullwidth"}
|
||||
= f.error_message_on :variant_unit
|
||||
|
||||
.four.columns
|
||||
= f.label :variant_unit_scale, 'Variant unit scale'
|
||||
= f.text_field :variant_unit_scale, {class: "fullwidth"}
|
||||
= f.error_message_on :variant_unit_scale
|
||||
|
||||
.omega.six.columns
|
||||
= f.label :variant_unit_name, 'Variant unit name'
|
||||
= f.text_field :variant_unit_name, {class: "fullwidth"}
|
||||
= f.error_message_on :variant_unit_name
|
||||
@@ -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"
|
||||
@@ -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')
|
||||
@@ -25,7 +25,7 @@
|
||||
%tr{'ng-repeat' => 'enterprise_fee in enterprise_fees | filter:query'}
|
||||
%td
|
||||
= f.ng_hidden_field :id
|
||||
= f.ng_collection_select :enterprise_id, Enterprise.managed_by(spree_current_user), :id, :name, 'enterprise_fee.enterprise_id', :include_blank => true
|
||||
= f.ng_collection_select :enterprise_id, @enterprises, :id, :name, 'enterprise_fee.enterprise_id', :include_blank => true
|
||||
%td= f.ng_select :fee_type, enterprise_fee_type_options, 'enterprise_fee.fee_type'
|
||||
%td= f.ng_text_field :name
|
||||
%td= f.ng_collection_select :calculator_type, @calculators, :name, :description, 'enterprise_fee.calculator_type', {'class' => 'calculator_type', 'ng-model' => 'calculatorType', 'spree-ensure-calculator-preferences-match-type' => "1"}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
%body.off-canvas
|
||||
= render partial: "shared/menu"
|
||||
= display_flash_messages
|
||||
|
||||
%section{ role: "main" }
|
||||
= yield
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.contact.small-2.large-3.columns
|
||||
%h3 Contact
|
||||
%ul
|
||||
%li= @distributor.email
|
||||
%li= @distributor.website
|
||||
= @distributor.address.address1
|
||||
= @distributor.address.address2
|
||||
= @distributor.address.city
|
||||
11
app/views/shop/_details.html.haml
Normal file
11
app/views/shop/_details.html.haml
Normal file
@@ -0,0 +1,11 @@
|
||||
%navigation
|
||||
%distributor.details.row
|
||||
#distributor_title
|
||||
%img.left{src: current_distributor.logo.url(:thumb)}
|
||||
%h4
|
||||
= current_distributor.name
|
||||
%location= current_distributor.address.city
|
||||
%small
|
||||
%a{href: "/"} Change location
|
||||
|
||||
= render partial: "shop/shop/order_cycles"
|
||||
@@ -1,27 +0,0 @@
|
||||
%ordercycle{"ng-controller" => "OrderCycleCtrl"}
|
||||
|
||||
:javascript
|
||||
angular.module('Shop').value('orderCycleData', #{render "shop/order_cycle"})
|
||||
|
||||
|
||||
- if @order_cycles.empty?
|
||||
Orders are currently closed for this hub
|
||||
%p
|
||||
Please contact your hub directly to see if they accept late orders,
|
||||
or wait until the next cycle opens.
|
||||
|
||||
= render partial: "shop/next_order_cycle"
|
||||
= render partial: "shop/last_order_cycle"
|
||||
|
||||
- else
|
||||
%form.custom
|
||||
%strong.avenir Ready for
|
||||
%select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id",
|
||||
"ng-change" => "changeOrderCycle()",
|
||||
"ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}"}
|
||||
|
||||
%closing
|
||||
-#%img{src: "/icon/goes/here"}
|
||||
Orders close
|
||||
%strong {{ order_cycle.orders_close_at | date_in_words }}
|
||||
|
||||
86
app/views/shop/checkout/_form.html.haml
Normal file
86
app/views/shop/checkout/_form.html.haml
Normal file
@@ -0,0 +1,86 @@
|
||||
%checkout{"ng-controller" => "CheckoutCtrl"}
|
||||
= f_form_for current_order, url: main_app.shop_update_checkout_path, html: {name: "checkout", id: "checkout_form"} do |f|
|
||||
.large-12.columns
|
||||
%fieldset#details
|
||||
%legend Customer Details
|
||||
.row
|
||||
.large-6.columns
|
||||
= f.text_field :email
|
||||
= f.fields_for :bill_address do |ba|
|
||||
.large-6.columns
|
||||
= ba.text_field :phone
|
||||
= f.fields_for :bill_address do |ba|
|
||||
.row
|
||||
.large-6.columns
|
||||
= ba.text_field :firstname
|
||||
.large-6.columns
|
||||
= ba.text_field :lastname
|
||||
|
||||
%fieldset
|
||||
%legend Billing Address
|
||||
= f.fields_for :bill_address do |ba|
|
||||
.row
|
||||
.large-12.columns
|
||||
= ba.text_field :address1, label: "Billing Address"
|
||||
.row
|
||||
.large-12.columns
|
||||
= ba.text_field :address2
|
||||
.row
|
||||
.large-6.columns
|
||||
= ba.text_field :city
|
||||
.large-6.columns
|
||||
= ba.select :country_id, Spree::Country.order(:name).select([:id, :name]).map{|c| [c.name, c.id]}
|
||||
.row
|
||||
.large-6.columns
|
||||
= ba.select :state_id, Spree::State.order(:name).select([:id, :name]).map{|c| [c.name, c.id]}
|
||||
.large-6.columns.right
|
||||
= ba.text_field :zipcode
|
||||
|
||||
%fieldset#shipping
|
||||
|
||||
%legend Shipping
|
||||
- checked_id = @order.shipping_method_id || current_distributor.shipping_methods.first.andand.id
|
||||
- for ship_method, i in current_distributor.shipping_methods.uniq
|
||||
.row
|
||||
.large-12.columns
|
||||
= f.radio_button :shipping_method_id, ship_method.id,
|
||||
text: ship_method.name,
|
||||
"ng-init" => "shipping_method = #{checked_id}; shippingMethodChanged()",
|
||||
"ng-model" => "shipping_method",
|
||||
"ng-change" => "shippingMethodChanged()",
|
||||
"data-require-ship-address" => ship_method.require_ship_address
|
||||
|
||||
= f.fields_for :ship_address do |sa|
|
||||
#ship_address{"ng-show" => "require_ship_address"}
|
||||
.row
|
||||
.large-12.columns
|
||||
= sa.text_field :address1
|
||||
.row
|
||||
.large-12.columns
|
||||
= sa.text_field :address2
|
||||
|
||||
.row
|
||||
.large-6.columns
|
||||
= sa.text_field :city
|
||||
.large-6.columns
|
||||
= sa.select :country_id, Spree::Country.order(:name).select([:id, :name]).map{|c| [c.name, c.id]}
|
||||
.row
|
||||
.large-6.columns.right
|
||||
= sa.text_field :zipcode
|
||||
.row
|
||||
.large-6.columns
|
||||
= sa.text_field :firstname
|
||||
.large-6.columns
|
||||
= sa.text_field :lastname
|
||||
%fieldset#payment
|
||||
%legend Payment Details
|
||||
- current_order.available_payment_methods.each do |method|
|
||||
.row
|
||||
.large-12.columns
|
||||
%label
|
||||
= radio_button_tag "order[payments_attributes][][payment_method_id]", method.id, false,
|
||||
"ng-model" => "payment_method"
|
||||
= method.name
|
||||
.row{"ng-show" => "payment_method == #{method.id}"}
|
||||
.large-12.columns
|
||||
= render partial: "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method }
|
||||
14
app/views/shop/checkout/_login.html.haml
Normal file
14
app/views/shop/checkout/_login.html.haml
Normal file
@@ -0,0 +1,14 @@
|
||||
= form_for Spree::User.new, :html => {'data-type' => :json}, :as => :spree_user, :url => spree.spree_user_session_path do |f|
|
||||
%fieldset
|
||||
%legend I have an OFN Account
|
||||
%p
|
||||
= f.label :email, t(:email)
|
||||
= f.email_field :email, :class => 'title', :tabindex => 1, :id => "login_spree_user_email"
|
||||
%p
|
||||
= f.label :password, t(:password)
|
||||
= f.password_field :password, :class => 'title', :tabindex => 2, :id => "login_spree_user_password"
|
||||
%p
|
||||
%label
|
||||
= f.check_box :remember_me
|
||||
= f.label :remember_me, t(:remember_me)
|
||||
%p= f.submit t(:login), :class => 'button primary', :tabindex => 3, :id => "login_spree_user_remember_me"
|
||||
14
app/views/shop/checkout/_signup.html.haml
Normal file
14
app/views/shop/checkout/_signup.html.haml
Normal file
@@ -0,0 +1,14 @@
|
||||
= form_for Spree::User.new, :as => :spree_user, :url => spree.spree_user_registration_path(@spree_user) do |f|
|
||||
%fieldset
|
||||
%legend New to OFN?
|
||||
%p
|
||||
= f.label :email, t(:email)
|
||||
= f.email_field :email, :class => 'title', :id => "signup_spree_user_email"
|
||||
%p
|
||||
= f.label :password, t(:password)
|
||||
= f.password_field :password, :class => 'title', :id => "signup_spree_user_password"
|
||||
%p
|
||||
= f.label :password_confirmation, t(:confirm_password)
|
||||
= f.password_field :password_confirmation, :class => 'title', :id => "signup_spree_user_password_confirmation"
|
||||
|
||||
= f.submit "Sign Up", :class => 'button'
|
||||
23
app/views/shop/checkout/_summary.html.haml
Normal file
23
app/views/shop/checkout/_summary.html.haml
Normal file
@@ -0,0 +1,23 @@
|
||||
%orderdetails{"ng-controller" => "CheckoutCtrl"}
|
||||
= form_for current_order, url: "#", html: {"ng-submit" => "purchase($event)"} do |f|
|
||||
%fieldset
|
||||
%legend Your Order
|
||||
%table
|
||||
%tr
|
||||
%th Cart subtotal
|
||||
%td= current_order.display_item_total
|
||||
|
||||
- checkout_adjustments_for_summary(current_order).each do |adjustment|
|
||||
%tr
|
||||
%th= adjustment.label
|
||||
%td= adjustment.display_amount.to_html
|
||||
%tr
|
||||
%th Cart total
|
||||
%td= current_order.display_total.to_html
|
||||
- if current_order.price_adjustment_totals.present?
|
||||
- current_order.price_adjustment_totals.each do |label, total|
|
||||
%tr
|
||||
%th= label
|
||||
%td= total
|
||||
|
||||
= f.submit "Purchase", class: "button"
|
||||
25
app/views/shop/checkout/edit.html.haml
Normal file
25
app/views/shop/checkout/edit.html.haml
Normal file
@@ -0,0 +1,25 @@
|
||||
.darkswarm
|
||||
- content_for :order_cycle_form do
|
||||
%strong.avenir
|
||||
Order ready on
|
||||
= pickup_time current_order_cycle
|
||||
|
||||
= render partial: "shop/details"
|
||||
|
||||
%checkout{"ng-app" => "Checkout"}
|
||||
.row
|
||||
.large-9.columns
|
||||
- unless spree_current_user
|
||||
.row
|
||||
%section#checkout_login
|
||||
.large-6.columns
|
||||
= render partial: "shop/checkout/login"
|
||||
%section#checkout_signup
|
||||
.large-6.columns
|
||||
= render partial: "shop/checkout/signup"
|
||||
.row
|
||||
= render partial: "shop/checkout/form"
|
||||
|
||||
.large-3.columns
|
||||
.row
|
||||
= render partial: "shop/checkout/summary"
|
||||
18
app/views/shop/shop/_contact_us.html.haml
Normal file
18
app/views/shop/shop/_contact_us.html.haml
Normal file
@@ -0,0 +1,18 @@
|
||||
.contact.small-2.large-3.columns
|
||||
%h3 Contact
|
||||
|
||||
%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
|
||||
16
app/views/shop/shop/_order_cycles.html.haml
Normal file
16
app/views/shop/shop/_order_cycles.html.haml
Normal file
@@ -0,0 +1,16 @@
|
||||
%ordercycle{"ng-controller" => "OrderCycleCtrl"}
|
||||
:javascript
|
||||
angular.module('Shop').value('orderCycleData', #{render "shop/shop/order_cycle"})
|
||||
|
||||
- if @order_cycles.empty?
|
||||
Orders are currently closed for this hub
|
||||
%p
|
||||
Please contact your hub directly to see if they accept late orders,
|
||||
or wait until the next cycle opens.
|
||||
|
||||
= render partial: "shop/shop/next_order_cycle"
|
||||
= render partial: "shop/shop/last_order_cycle"
|
||||
|
||||
- else
|
||||
%form.custom
|
||||
= yield :order_cycle_form
|
||||
@@ -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",
|
||||
@@ -48,7 +48,7 @@
|
||||
%small{"ng-show" => "(product.variants.length > 0)"} from
|
||||
{{ product.price | currency }}
|
||||
%tr.product-description
|
||||
%td{colspan: 6}{{ product.notes | truncate:80 }}
|
||||
%td{colspan: 2}{{ product.notes | truncate:80 }}
|
||||
%tr{"ng-repeat" => "variant in product.variants", "ng-show" => "product.show_variants"}
|
||||
= render partial: "shop/variant"
|
||||
= render partial: "shop/shop/variant"
|
||||
%input.button.right{type: :submit, value: "Check Out"}
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
%td{colspan: 2}
|
||||
%td
|
||||
%td.notes
|
||||
%td {{variant.options_text}}
|
||||
%td
|
||||
%input{type: :number,
|
||||
@@ -1,17 +1,17 @@
|
||||
%shop{"ng-app" => "Shop"}
|
||||
%navigation
|
||||
%distributor.details.row
|
||||
#distributor_title
|
||||
%img.left{src: @distributor.logo.url(:thumb)}
|
||||
%h4
|
||||
= @distributor.name
|
||||
%location= @distributor.address.city
|
||||
%small
|
||||
%a{href: "/"} Change location
|
||||
%shop.darkswarm{"ng-app" => "Shop"}
|
||||
|
||||
= render partial: "shop/order_cycles"
|
||||
- content_for :order_cycle_form do
|
||||
%strong.avenir Ready for
|
||||
%select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id",
|
||||
"ng-change" => "changeOrderCycle()",
|
||||
"ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}"}
|
||||
|
||||
%closing
|
||||
-#%img{src: "/icon/goes/here"}
|
||||
Orders close
|
||||
%strong {{ order_cycle.orders_close_at | date_in_words }}
|
||||
= render partial: "shop/details"
|
||||
|
||||
-#%description
|
||||
|
||||
%tabs
|
||||
.row
|
||||
@@ -44,9 +44,9 @@
|
||||
%p Contact
|
||||
|
||||
%products.row
|
||||
= render partial: "shop/products"
|
||||
= render partial: "shop/shop/products"
|
||||
#footer
|
||||
%section.row
|
||||
= render partial: "shop/contact_us"
|
||||
= render partial: "shop/about_us"
|
||||
= render partial: "shop/shop/contact_us"
|
||||
= render partial: "shop/shop/about_us"
|
||||
= render partial: "shared/copyright"
|
||||
@@ -2,7 +2,7 @@
|
||||
.alpha.six.columns
|
||||
= f.field_container :supplier do
|
||||
= f.label :supplier
|
||||
= f.collection_select(:supplier_id, Enterprise.is_primary_producer.managed_by(spree_current_user), :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
|
||||
= f.collection_select(:supplier_id, Enterprise.is_primary_producer.managed_by(spree_current_user).by_name, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
|
||||
= f.error_message_on :supplier
|
||||
.four.columns
|
||||
= f.field_container :group_buy do
|
||||
@@ -17,4 +17,4 @@
|
||||
.omega.six.columns
|
||||
= f.field_container :group_buy_unit_size do
|
||||
= f.label :group_buy_unit_size
|
||||
= f.text_field :group_buy_unit_size, :class => "fullwidth"
|
||||
= f.text_field :group_buy_unit_size, :class => "fullwidth"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
= f.field_container :supplier do
|
||||
= f.label :supplier
|
||||
%br
|
||||
= f.collection_select(:supplier_id, Enterprise.is_primary_producer.managed_by(spree_current_user), :id, :name, {:include_blank => true}, {:class => "select2"})
|
||||
= f.collection_select(:supplier_id, Enterprise.is_primary_producer.managed_by(spree_current_user).by_name, :id, :name, {:include_blank => true}, {:class => "select2"})
|
||||
= f.error_message_on :supplier
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
Openfoodnetwork::Application.routes.draw do
|
||||
root :to => 'home#temp_landing_page'
|
||||
|
||||
|
||||
resource :shop, controller: :shop do
|
||||
resource :shop, controller: "shop/shop" do
|
||||
get :products
|
||||
post :order_cycle
|
||||
get :order_cycle
|
||||
end
|
||||
|
||||
namespace :shop do
|
||||
#resource :checkout, only: :edit, controller: :checkout do
|
||||
#get '', to: ""
|
||||
#end
|
||||
get '/checkout', :to => 'checkout#edit' , :as => :checkout
|
||||
put '/checkout', :to => 'checkout#update' , :as => :update_checkout
|
||||
end
|
||||
|
||||
resources :enterprises do
|
||||
collection do
|
||||
get :suppliers
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddRequireShipAddressToShippingMethods < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :spree_shipping_methods, :require_ship_address, :boolean, :default => true
|
||||
add_column :spree_shipping_methods, :description, :text
|
||||
end
|
||||
end
|
||||
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(:version => 20140204011203) do
|
||||
ActiveRecord::Schema.define(:version => 20140213003443) do
|
||||
|
||||
create_table "adjustment_metadata", :force => true do |t|
|
||||
t.integer "adjustment_id"
|
||||
@@ -744,14 +744,16 @@ ActiveRecord::Schema.define(:version => 20140204011203) do
|
||||
create_table "spree_shipping_methods", :force => true do |t|
|
||||
t.string "name"
|
||||
t.integer "zone_id"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.string "display_on"
|
||||
t.integer "shipping_category_id"
|
||||
t.boolean "match_none"
|
||||
t.boolean "match_all"
|
||||
t.boolean "match_one"
|
||||
t.datetime "deleted_at"
|
||||
t.boolean "require_ship_address", :default => true
|
||||
t.text "description"
|
||||
end
|
||||
|
||||
create_table "spree_skrill_transactions", :force => true do |t|
|
||||
|
||||
29
lib/open_food_network/enterprise_fee_applicator.rb
Normal file
29
lib/open_food_network/enterprise_fee_applicator.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module OpenFoodNetwork
|
||||
class EnterpriseFeeApplicator < Struct.new(:enterprise_fee, :variant, :role)
|
||||
def create_line_item_adjustment(line_item)
|
||||
a = enterprise_fee.create_locked_adjustment(line_item_adjustment_label, line_item.order, line_item, true)
|
||||
AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: role
|
||||
end
|
||||
|
||||
def create_order_adjustment(order)
|
||||
a = enterprise_fee.create_locked_adjustment(order_adjustment_label, order, order, true)
|
||||
AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: role
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def line_item_adjustment_label
|
||||
"#{variant.product.name} - #{base_adjustment_label}"
|
||||
end
|
||||
|
||||
def order_adjustment_label
|
||||
"Whole order - #{base_adjustment_label}"
|
||||
end
|
||||
|
||||
def base_adjustment_label
|
||||
"#{enterprise_fee.fee_type} fee by #{role} #{enterprise_fee.enterprise.name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
39
spec/controllers/shop/checkout_controller_spec.rb
Normal file
39
spec/controllers/shop/checkout_controller_spec.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Shop::CheckoutController do
|
||||
let(:distributor) { double(:distributor) }
|
||||
let(:order_cycle) { create(:order_cycle) }
|
||||
let(:order) { create(:order) }
|
||||
before do
|
||||
order.stub(:checkout_allowed?).and_return true
|
||||
controller.stub(:check_authorization).and_return true
|
||||
end
|
||||
it "redirects home when no distributor is selected" do
|
||||
get :edit
|
||||
response.should redirect_to root_path
|
||||
end
|
||||
|
||||
it "redirects to the shop when no order cycle is selected" do
|
||||
controller.stub(:current_distributor).and_return(distributor)
|
||||
get :edit
|
||||
response.should redirect_to shop_path
|
||||
end
|
||||
|
||||
it "redirects to the shop when no line items are present" do
|
||||
controller.stub(:current_distributor).and_return(distributor)
|
||||
controller.stub(:current_order_cycle).and_return(order_cycle)
|
||||
controller.stub(:current_order).and_return(order)
|
||||
order.stub_chain(:insufficient_stock_lines, :present?).and_return true
|
||||
get :edit
|
||||
response.should redirect_to shop_path
|
||||
end
|
||||
|
||||
it "renders when both distributor and order cycle is selected" do
|
||||
controller.stub(:current_distributor).and_return(distributor)
|
||||
controller.stub(:current_order_cycle).and_return(order_cycle)
|
||||
controller.stub(:current_order).and_return(order)
|
||||
order.stub_chain(:insufficient_stock_lines, :present?).and_return false
|
||||
get :edit
|
||||
response.should be_success
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe ShopController do
|
||||
describe Shop::ShopController do
|
||||
let(:d) { create(:distributor_enterprise) }
|
||||
|
||||
it "redirects to the home page if no distributor is selected" do
|
||||
@@ -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)
|
||||
|
||||
@@ -31,5 +31,4 @@ feature %q{
|
||||
page.should_not have_content "ComfortableMexicanSofa"
|
||||
page.should have_content "WHERE WOULD YOU LIKE TO SHOP?"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -14,15 +14,18 @@ feature %q{
|
||||
end
|
||||
|
||||
context "creating a product" do
|
||||
scenario "assigning a supplier and distributors to the product" do
|
||||
scenario "assigning a supplier, distributors and units to the product" do
|
||||
login_to_admin_section
|
||||
|
||||
click_link 'Products'
|
||||
click_link 'New Product'
|
||||
|
||||
fill_in 'product_name', :with => 'A new product !!!'
|
||||
fill_in 'product_price', :with => '19.99'
|
||||
select 'New supplier', :from => 'product_supplier_id'
|
||||
fill_in 'product_name', with: 'A new product !!!'
|
||||
fill_in 'product_price', with: '19.99'
|
||||
select 'New supplier', from: 'product_supplier_id'
|
||||
select 'Weight', from: 'product_variant_unit'
|
||||
fill_in 'product_variant_unit_scale', with: 1000
|
||||
fill_in 'product_variant_unit_name', with: ''
|
||||
|
||||
click_button 'Create'
|
||||
|
||||
@@ -31,6 +34,11 @@ feature %q{
|
||||
product.supplier.should == @supplier
|
||||
product.group_buy.should be_false
|
||||
|
||||
product.variant_unit.should == 'weight'
|
||||
product.variant_unit_scale.should == 1000
|
||||
product.variant_unit_name.should == ''
|
||||
product.option_types.first.name.should == 'unit_weight'
|
||||
|
||||
# Distributors
|
||||
within('#sidebar') { click_link 'Product Distributions' }
|
||||
|
||||
@@ -46,7 +54,8 @@ feature %q{
|
||||
product.product_distributions.map { |pd| pd.enterprise_fee }.should == [@enterprise_fees[0], @enterprise_fees[2]]
|
||||
end
|
||||
|
||||
scenario "making a group buy product" do
|
||||
|
||||
scenario "creating a group buy product" do
|
||||
login_to_admin_section
|
||||
|
||||
click_link 'Products'
|
||||
|
||||
86
spec/features/admin/variants_spec.rb
Normal file
86
spec/features/admin/variants_spec.rb
Normal file
@@ -0,0 +1,86 @@
|
||||
require "spec_helper"
|
||||
|
||||
feature %q{
|
||||
As an admin
|
||||
I want to manage product variants
|
||||
} do
|
||||
include AuthenticationWorkflow
|
||||
include WebHelper
|
||||
|
||||
scenario "creating a new variant" do
|
||||
# Given a product with a unit-related option type
|
||||
p = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1")
|
||||
|
||||
# When I create a variant on the product
|
||||
login_to_admin_section
|
||||
click_link 'Products'
|
||||
within('#sub_nav') { click_link 'Products' }
|
||||
click_link p.name
|
||||
click_link 'Variants'
|
||||
click_link 'New Variant'
|
||||
|
||||
fill_in 'variant_unit_value', with: '1'
|
||||
fill_in 'variant_unit_description', with: 'foo'
|
||||
click_button 'Create'
|
||||
|
||||
# Then the variant should have been created
|
||||
page.should have_content "Variant \"#{p.name}\" has been successfully created!"
|
||||
end
|
||||
|
||||
|
||||
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
|
||||
23
spec/features/consumer/authentication_spec.rb
Normal file
23
spec/features/consumer/authentication_spec.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
require 'spec_helper'
|
||||
|
||||
feature "Authentication", js: true do
|
||||
describe "login" do
|
||||
let(:user) { create(:user, password: "password", password_confirmation: "password") }
|
||||
scenario "with valid credentials" do
|
||||
visit "/login"
|
||||
fill_in "Email", with: user.email
|
||||
fill_in "Password", with: "password"
|
||||
click_button "Login"
|
||||
current_path.should == "/"
|
||||
end
|
||||
|
||||
scenario "with invalid credentials" do
|
||||
visit "/login"
|
||||
fill_in "Email", with: user.email
|
||||
fill_in "Password", with: "this isn't my password"
|
||||
click_button "Login"
|
||||
page.should have_content "Invalid email or password"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,7 +68,7 @@ feature %q{
|
||||
@zone = create(:zone)
|
||||
c = Spree::Country.find_by_name('Australia')
|
||||
Spree::ZoneMember.create(:zoneable => c, :zone => @zone)
|
||||
sm = create(:shipping_method, zone: @zone, calculator: Spree::Calculator::FlatRate.new)
|
||||
sm = create(:shipping_method, zone: @zone, calculator: Spree::Calculator::FlatRate.new, require_ship_address: false)
|
||||
sm.calculator.set_preference(:amount, 0); sm.calculator.save!
|
||||
|
||||
@payment_method_distributor = create(:payment_method, :name => 'Edible Garden payment method', :distributors => [@distributor])
|
||||
@@ -128,16 +128,14 @@ feature %q{
|
||||
["Bananas - transport fee by supplier Supplier 1", "$4.00", ""],
|
||||
["Bananas - packing fee by distributor FruitAndVeg", "$7.00", ""],
|
||||
["Bananas - transport fee by distributor FruitAndVeg", "$8.00", ""],
|
||||
["Bananas - admin fee by coordinator My coordinator", "$1.00", ""],
|
||||
["Bananas - sales fee by coordinator My coordinator", "$2.00", ""],
|
||||
["Zucchini - admin fee by supplier Supplier 2", "$5.00", ""],
|
||||
["Zucchini - sales fee by supplier Supplier 2", "$6.00", ""],
|
||||
["Zucchini - packing fee by distributor FruitAndVeg", "$7.00", ""],
|
||||
["Zucchini - transport fee by distributor FruitAndVeg", "$8.00", ""],
|
||||
["Zucchini - admin fee by coordinator My coordinator", "$1.00", ""],
|
||||
["Zucchini - sales fee by coordinator My coordinator", "$2.00", ""]]
|
||||
["Whole order - admin fee by coordinator My coordinator", "$1.00", ""],
|
||||
["Whole order - sales fee by coordinator My coordinator", "$2.00", ""]]
|
||||
|
||||
page.should have_selector 'span.distribution-total', :text => '$54.00'
|
||||
page.should have_selector 'span.distribution-total', :text => '$51.00'
|
||||
end
|
||||
|
||||
scenario "attempting to purchase products that mix product and order cycle distribution", future: true do
|
||||
@@ -390,13 +388,11 @@ feature %q{
|
||||
|
||||
# Disabled until this form takes order cycles into account
|
||||
# page.should have_selector "select#order_distributor_id option[value='#{@distributor_alternative.id}']"
|
||||
|
||||
click_checkout_continue_button
|
||||
|
||||
# -- Checkout: Delivery
|
||||
order_charges = page.all("tbody#summary-order-charges tr").map {|row| row.all('td').map(&:text)}.take(2)
|
||||
order_charges.should == [["Delivery:", "$0.00"],
|
||||
["Distribution:", "$54.00"]]
|
||||
order_charges.should == [["Delivery:", "$0.00"], ["Distribution:", "$51.00"]]
|
||||
click_checkout_continue_button
|
||||
|
||||
# -- Checkout: Payment
|
||||
@@ -411,12 +407,13 @@ feature %q{
|
||||
page.should have_selector 'figure#logo h1', text: @distributor_oc.name
|
||||
|
||||
page.should have_selector 'tfoot#order-charges tr.total td', text: 'Distribution'
|
||||
page.should have_selector 'tfoot#order-charges tr.total td', text: '54.00'
|
||||
page.should have_selector 'tfoot#order-charges tr.total td', text: '51.00'
|
||||
|
||||
|
||||
# -- Checkout: Email
|
||||
email = ActionMailer::Base.deliveries.last
|
||||
email.reply_to.include?(@distributor_oc.email).should == true
|
||||
email.body.should =~ /Distribution[\s+]\$54.00/
|
||||
email.body.should =~ /Distribution[\s+]\$51.00/
|
||||
end
|
||||
|
||||
scenario "when I have past orders, it fills in my address", :js => true do
|
||||
@@ -481,7 +478,7 @@ feature %q{
|
||||
# -- Checkout: Delivery
|
||||
order_charges = page.all("tbody#summary-order-charges tr").map {|row| row.all('td').map(&:text)}.take(2)
|
||||
order_charges.should == [["Delivery:", "$0.00"],
|
||||
["Distribution:", "$54.00"]]
|
||||
["Distribution:", "$51.00"]]
|
||||
click_checkout_continue_button
|
||||
|
||||
# -- Checkout: Payment
|
||||
@@ -495,11 +492,11 @@ feature %q{
|
||||
page.should have_content @payment_method_distributor_oc.description
|
||||
|
||||
page.should have_selector 'tfoot#order-charges tr.total td', text: 'Distribution'
|
||||
page.should have_selector 'tfoot#order-charges tr.total td', text: '54.00'
|
||||
page.should have_selector 'tfoot#order-charges tr.total td', text: '51.00'
|
||||
|
||||
# -- Checkout: Email
|
||||
email = ActionMailer::Base.deliveries.last
|
||||
email.body.should =~ /Distribution[\s+]\$54.00/
|
||||
email.body.should =~ /Distribution[\s+]\$51.00/
|
||||
end
|
||||
|
||||
|
||||
@@ -509,8 +506,8 @@ feature %q{
|
||||
@order_cycle = oc = create(:simple_order_cycle, coordinator: create(:distributor_enterprise, name: 'My coordinator'))
|
||||
|
||||
# Coordinator
|
||||
coordinator_fee1 = create(:enterprise_fee, enterprise: oc.coordinator, fee_type: 'admin', amount: 1)
|
||||
coordinator_fee2 = create(:enterprise_fee, enterprise: oc.coordinator, fee_type: 'sales', amount: 2)
|
||||
coordinator_fee1 = create(:enterprise_fee, enterprise: oc.coordinator, fee_type: 'admin', calculator: Spree::Calculator::FlatRate.new(preferred_amount: 1))
|
||||
coordinator_fee2 = create(:enterprise_fee, enterprise: oc.coordinator, fee_type: 'sales', calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2))
|
||||
oc.coordinator_fees << coordinator_fee1
|
||||
oc.coordinator_fees << coordinator_fee2
|
||||
|
||||
@@ -561,7 +558,7 @@ feature %q{
|
||||
ex4.variants << @product_4.master
|
||||
|
||||
# Shipping method and payment method
|
||||
sm = create(:shipping_method, zone: @zone, calculator: Spree::Calculator::FlatRate.new, distributors: [@distributor_oc])
|
||||
sm = create(:shipping_method, zone: @zone, calculator: Spree::Calculator::FlatRate.new, distributors: [@distributor_oc], require_ship_address: false)
|
||||
sm.calculator.set_preference(:amount, 0); sm.calculator.save!
|
||||
@payment_method_distributor_oc = create(:payment_method, :name => 'FruitAndVeg payment method', :distributors => [@distributor_oc])
|
||||
end
|
||||
|
||||
210
spec/features/consumer/shopping/checkout_spec.rb
Normal file
210
spec/features/consumer/shopping/checkout_spec.rb
Normal file
@@ -0,0 +1,210 @@
|
||||
require 'spec_helper'
|
||||
|
||||
|
||||
feature "As a consumer I want to check out my cart", js: true do
|
||||
include AuthenticationWorkflow
|
||||
include WebHelper
|
||||
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:order_cycle) { create(:order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) }
|
||||
let(:product) { create(:simple_product, supplier: supplier) }
|
||||
|
||||
before do
|
||||
create_enterprise_group_for distributor
|
||||
exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id)
|
||||
exchange.variants << product.master
|
||||
end
|
||||
|
||||
describe "Attempting to access checkout without meeting the preconditions" do
|
||||
it "redirects to the homepage if no distributor is selected" do
|
||||
visit "/shop/checkout"
|
||||
current_path.should == root_path
|
||||
end
|
||||
|
||||
it "redirects to the shop page if we have a distributor but no order cycle selected" do
|
||||
select_distributor
|
||||
visit "/shop/checkout"
|
||||
current_path.should == shop_path
|
||||
end
|
||||
|
||||
it "redirects to the shop page if the current order is empty" do
|
||||
select_distributor
|
||||
select_order_cycle
|
||||
visit "/shop/checkout"
|
||||
current_path.should == shop_path
|
||||
end
|
||||
|
||||
it "renders checkout if we have distributor and order cycle selected" do
|
||||
select_distributor
|
||||
select_order_cycle
|
||||
add_product_to_cart
|
||||
visit "/shop/checkout"
|
||||
current_path.should == "/shop/checkout"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Login behaviour" do
|
||||
let(:user) { create_enterprise_user }
|
||||
before do
|
||||
select_distributor
|
||||
select_order_cycle
|
||||
add_product_to_cart
|
||||
end
|
||||
|
||||
it "renders the login form if user is logged out" do
|
||||
visit "/shop/checkout"
|
||||
within "section[role='main']" do
|
||||
page.should have_content "I HAVE AN OFN ACCOUNT"
|
||||
end
|
||||
end
|
||||
|
||||
it "does not not render the login form if user is logged in" do
|
||||
login_to_consumer_section
|
||||
visit "/shop/checkout"
|
||||
within "section[role='main']" do
|
||||
page.should_not have_content "I HAVE AN OFN ACCOUNT"
|
||||
end
|
||||
end
|
||||
|
||||
it "renders the signup link if user is logged out" do
|
||||
visit "/shop/checkout"
|
||||
within "section[role='main']" do
|
||||
page.should have_content "NEW TO OFN"
|
||||
end
|
||||
end
|
||||
|
||||
it "does not not render the signup form if user is logged in" do
|
||||
login_to_consumer_section
|
||||
visit "/shop/checkout"
|
||||
within "section[role='main']" do
|
||||
page.should_not have_content "NEW TO OFN"
|
||||
end
|
||||
end
|
||||
|
||||
it "redirects to the checkout page when logging in from the checkout page" do
|
||||
visit "/shop/checkout"
|
||||
within "#checkout_login" do
|
||||
fill_in "spree_user[email]", with: user.email
|
||||
fill_in "spree_user[password]", with: user.password
|
||||
click_button "Login"
|
||||
end
|
||||
|
||||
current_path.should == "/shop/checkout"
|
||||
within "section[role='main']" do
|
||||
page.should_not have_content "I have an OFN Account"
|
||||
end
|
||||
end
|
||||
|
||||
it "redirects to the checkout page when signing up from the checkout page" do
|
||||
visit "/shop/checkout"
|
||||
within "#checkout_signup" do
|
||||
fill_in "spree_user[email]", with: "test@gmail.com"
|
||||
fill_in "spree_user[password]", with: "password"
|
||||
fill_in "spree_user[password_confirmation]", with: "password"
|
||||
click_button "Sign Up"
|
||||
end
|
||||
current_path.should == "/shop/checkout"
|
||||
within "section[role='main']" do
|
||||
page.should_not have_content "Sign Up"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "logged in, distributor selected, order cycle selected, product in cart" do
|
||||
let(:user) { create_enterprise_user }
|
||||
before do
|
||||
login_to_consumer_section
|
||||
select_distributor
|
||||
select_order_cycle
|
||||
add_product_to_cart
|
||||
end
|
||||
|
||||
describe "with shipping methods" do
|
||||
let(:sm1) { create(:shipping_method, require_ship_address: true, name: "Frogs", description: "yellow") }
|
||||
let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue") }
|
||||
before do
|
||||
distributor.shipping_methods << sm1
|
||||
distributor.shipping_methods << sm2
|
||||
visit "/shop/checkout"
|
||||
end
|
||||
it "shows all shipping methods" do
|
||||
page.should have_content "Frogs"
|
||||
page.should have_content "Donkeys"
|
||||
end
|
||||
|
||||
it "doesn't show ship address forms " do
|
||||
choose(sm2.name)
|
||||
find("#ship_address").visible?.should be_false
|
||||
end
|
||||
|
||||
it "shows ship address forms when selected shipping method requires one" do
|
||||
choose(sm1.name)
|
||||
save_and_open_page
|
||||
find("#ship_address").visible?.should be_true
|
||||
end
|
||||
|
||||
describe "with payment methods" do
|
||||
let(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::PaymentMethod::Check") }
|
||||
let(:pm2) { create(:payment_method, distributors: [distributor]) }
|
||||
|
||||
before do
|
||||
pm1 # Lazy evaluation of ze create()s
|
||||
pm2
|
||||
visit "/shop/checkout"
|
||||
end
|
||||
|
||||
it "shows all available payment methods" do
|
||||
page.should have_content pm1.name
|
||||
page.should have_content pm2.name
|
||||
end
|
||||
|
||||
describe "Purchasing" do
|
||||
it "re-renders with errors when we submit the incomplete form" do
|
||||
choose sm2.name
|
||||
click_button "Purchase"
|
||||
current_path.should == "/shop/checkout"
|
||||
page.should have_content "can't be blank"
|
||||
end
|
||||
|
||||
it "renders errors on the shipping method where appropriate"
|
||||
|
||||
it "takes us to the order confirmation page when we submit a complete form" do
|
||||
choose sm2.name
|
||||
choose pm1.name
|
||||
within "#details" do
|
||||
fill_in "First Name", with: "Will"
|
||||
fill_in "Last Name", with: "Marshall"
|
||||
fill_in "Billing Address", with: "123 Your Face"
|
||||
select "Australia", from: "Country"
|
||||
select "Victoria", from: "State"
|
||||
fill_in "Customer E-Mail", with: "test@test.com"
|
||||
fill_in "Phone", with: "0468363090"
|
||||
fill_in "City", with: "Melbourne"
|
||||
fill_in "Zip Code", with: "3066"
|
||||
end
|
||||
click_button "Purchase"
|
||||
page.should have_content "Your order has been processed successfully"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def select_distributor
|
||||
visit "/"
|
||||
click_link distributor.name
|
||||
end
|
||||
|
||||
def select_order_cycle
|
||||
exchange = Exchange.find(order_cycle.exchanges.to_enterprises(distributor).outgoing.first.id)
|
||||
visit "/shop"
|
||||
select exchange.pickup_time, from: "order_cycle_id"
|
||||
end
|
||||
|
||||
def add_product_to_cart
|
||||
|
||||
fill_in "variants[#{product.master.id}]", with: 5
|
||||
first("form.custom > input.button.right").click
|
||||
end
|
||||
@@ -116,9 +116,9 @@ feature "As a consumer I want to shop with a distributor", js: true do
|
||||
let(:oc) { create(:simple_order_cycle, distributors: [distributor]) }
|
||||
let(:product) { create(:simple_product) }
|
||||
let(:variant) { create(:variant, product: product) }
|
||||
let(:exchange) { Exchange.find(oc.exchanges.to_enterprises(distributor).outgoing.first.id) }
|
||||
|
||||
before do
|
||||
exchange = Exchange.find(oc.exchanges.to_enterprises(distributor).outgoing.first.id)
|
||||
exchange.update_attribute :pickup_time, "frogs"
|
||||
exchange.variants << product.master
|
||||
exchange.variants << variant
|
||||
@@ -141,7 +141,17 @@ feature "As a consumer I want to shop with a distributor", js: true do
|
||||
find(".collapse").trigger "click"
|
||||
page.should_not have_text variant.options_text
|
||||
end
|
||||
it "allows the user to expand variants"
|
||||
|
||||
it "uses the adjusted price" do
|
||||
enterprise_fee1 = create(:enterprise_fee, amount: 20)
|
||||
enterprise_fee2 = create(:enterprise_fee, amount: 3)
|
||||
exchange.enterprise_fees = [enterprise_fee1, enterprise_fee2]
|
||||
exchange.save
|
||||
|
||||
visit shop_path
|
||||
select "frogs", :from => "order_cycle_id"
|
||||
page.should have_content "$#{(product.price + 23.00)}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Filtering on hand and on demand products" do
|
||||
@@ -28,8 +28,8 @@ describe OrderCyclesHelper do
|
||||
exchange = Exchange.find(oc1.exchanges.to_enterprises(d).outgoing.first.id)
|
||||
exchange.update_attribute :pickup_time, "turtles"
|
||||
|
||||
helper.stub!(:current_order_cycle).and_return oc1
|
||||
helper.stub!(:current_distributor).and_return d
|
||||
helper.stub(:current_order_cycle).and_return oc1
|
||||
helper.stub(:current_distributor).and_return d
|
||||
helper.pickup_time.should == "turtles"
|
||||
end
|
||||
|
||||
@@ -41,8 +41,8 @@ describe OrderCyclesHelper do
|
||||
exchange = Exchange.find(oc2.exchanges.to_enterprises(d).outgoing.first.id)
|
||||
exchange.update_attribute :pickup_time, "turtles"
|
||||
|
||||
helper.stub!(:current_order_cycle).and_return oc1
|
||||
helper.stub!(:current_distributor).and_return d
|
||||
helper.stub(:current_order_cycle).and_return oc1
|
||||
helper.stub(:current_distributor).and_return d
|
||||
helper.pickup_time(oc2).should == "turtles"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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", ->
|
||||
@@ -460,6 +513,16 @@ describe "AdminProductEditCtrl", ->
|
||||
variant = {unit_value: 5}
|
||||
expect(scope.variantUnitValue(product, variant)).toEqual 5000
|
||||
|
||||
it "returns the unscaled value when the product has no scale", ->
|
||||
product = {}
|
||||
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 = {}
|
||||
@@ -573,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", ->
|
||||
@@ -605,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}
|
||||
@@ -649,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)
|
||||
@@ -662,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"}
|
||||
@@ -672,6 +800,16 @@ describe "AdminProductEditCtrl", ->
|
||||
unit_description: ''
|
||||
unit_value_with_description: "250.5"
|
||||
|
||||
it "does not convert value when using a non-scaled unit", ->
|
||||
testProduct = {id: 123}
|
||||
testVariant = {unit_value_with_description: "12"}
|
||||
scope.products = [testProduct]
|
||||
scope.packVariant(testProduct, testVariant)
|
||||
expect(testVariant).toEqual
|
||||
unit_value: 12
|
||||
unit_description: ''
|
||||
unit_value_with_description: "12"
|
||||
|
||||
|
||||
describe "filtering products", ->
|
||||
beforeEach ->
|
||||
@@ -774,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'}]
|
||||
@@ -805,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", ->
|
||||
@@ -830,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
|
||||
@@ -883,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"
|
||||
|
||||
|
||||
|
||||
|
||||
69
spec/lib/open_food_network/enterprise_fee_applicator_spec.rb
Normal file
69
spec/lib/open_food_network/enterprise_fee_applicator_spec.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
require 'open_food_network/enterprise_fee_applicator'
|
||||
|
||||
module OpenFoodNetwork
|
||||
describe EnterpriseFeeApplicator do
|
||||
it "creates an adjustment for a line item" do
|
||||
line_item = create(:line_item)
|
||||
enterprise_fee = create(:enterprise_fee)
|
||||
product = create(:simple_product)
|
||||
|
||||
efa = EnterpriseFeeApplicator.new enterprise_fee, product.master, 'role'
|
||||
efa.stub(:line_item_adjustment_label) { 'label' }
|
||||
efa.create_line_item_adjustment line_item
|
||||
|
||||
adjustment = Spree::Adjustment.last
|
||||
adjustment.label.should == 'label'
|
||||
adjustment.adjustable.should == line_item.order
|
||||
adjustment.source.should == line_item
|
||||
adjustment.originator.should == enterprise_fee
|
||||
adjustment.should be_mandatory
|
||||
|
||||
md = adjustment.metadata
|
||||
md.enterprise.should == enterprise_fee.enterprise
|
||||
md.fee_name.should == enterprise_fee.name
|
||||
md.fee_type.should == enterprise_fee.fee_type
|
||||
md.enterprise_role.should == 'role'
|
||||
end
|
||||
|
||||
it "creates an adjustment for an order" do
|
||||
order = create(:order)
|
||||
#line_item = create(:line_item)
|
||||
enterprise_fee = create(:enterprise_fee)
|
||||
product = create(:simple_product)
|
||||
|
||||
efa = EnterpriseFeeApplicator.new enterprise_fee, nil, 'role'
|
||||
efa.stub(:order_adjustment_label) { 'label' }
|
||||
efa.create_order_adjustment order
|
||||
|
||||
adjustment = Spree::Adjustment.last
|
||||
adjustment.label.should == 'label'
|
||||
adjustment.adjustable.should == order
|
||||
adjustment.source.should == order
|
||||
adjustment.originator.should == enterprise_fee
|
||||
adjustment.should be_mandatory
|
||||
|
||||
md = adjustment.metadata
|
||||
md.enterprise.should == enterprise_fee.enterprise
|
||||
md.fee_name.should == enterprise_fee.name
|
||||
md.fee_type.should == enterprise_fee.fee_type
|
||||
md.enterprise_role.should == 'role'
|
||||
end
|
||||
|
||||
it "makes an adjustment label for a line item" do
|
||||
variant = double(:variant, product: double(:product, name: 'Bananas'))
|
||||
enterprise_fee = double(:enterprise_fee, fee_type: 'packing', enterprise: double(:enterprise, name: 'Ballantyne'))
|
||||
|
||||
efa = EnterpriseFeeApplicator.new enterprise_fee, variant, 'distributor'
|
||||
|
||||
efa.send(:line_item_adjustment_label).should == "Bananas - packing fee by distributor Ballantyne"
|
||||
end
|
||||
|
||||
it "makes an adjustment label for an order" do
|
||||
enterprise_fee = double(:enterprise_fee, fee_type: 'packing', enterprise: double(:enterprise, name: 'Ballantyne'))
|
||||
|
||||
efa = EnterpriseFeeApplicator.new enterprise_fee, nil, 'distributor'
|
||||
|
||||
efa.send(:order_adjustment_label).should == "Whole order - packing fee by distributor Ballantyne"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,6 +9,44 @@ describe EnterpriseFee do
|
||||
it { should validate_presence_of(:name) }
|
||||
end
|
||||
|
||||
describe "scopes" do
|
||||
describe "finding per-item enterprise fees" do
|
||||
it "does not return fees with FlatRate and FlexiRate calculators" do
|
||||
create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new)
|
||||
create(:enterprise_fee, calculator: Spree::Calculator::FlexiRate.new)
|
||||
|
||||
EnterpriseFee.per_item.should be_empty
|
||||
end
|
||||
|
||||
it "returns fees with any other calculator" do
|
||||
ef1 = create(:enterprise_fee, calculator: Spree::Calculator::DefaultTax.new)
|
||||
ef2 = create(:enterprise_fee, calculator: Spree::Calculator::FlatPercentItemTotal.new)
|
||||
ef3 = create(:enterprise_fee, calculator: Spree::Calculator::PerItem.new)
|
||||
ef4 = create(:enterprise_fee, calculator: Spree::Calculator::PriceSack.new)
|
||||
|
||||
EnterpriseFee.per_item.sort.should == [ef1, ef2, ef3, ef4].sort
|
||||
end
|
||||
end
|
||||
|
||||
describe "finding per-order enterprise fees" do
|
||||
it "returns fees with FlatRate and FlexiRate calculators" do
|
||||
ef1 = create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new)
|
||||
ef2 = create(:enterprise_fee, calculator: Spree::Calculator::FlexiRate.new)
|
||||
|
||||
EnterpriseFee.per_order.sort.should == [ef1, ef2].sort
|
||||
end
|
||||
|
||||
it "does not return fees with any other calculator" do
|
||||
ef1 = create(:enterprise_fee, calculator: Spree::Calculator::DefaultTax.new)
|
||||
ef2 = create(:enterprise_fee, calculator: Spree::Calculator::FlatPercentItemTotal.new)
|
||||
ef3 = create(:enterprise_fee, calculator: Spree::Calculator::PerItem.new)
|
||||
ef4 = create(:enterprise_fee, calculator: Spree::Calculator::PriceSack.new)
|
||||
|
||||
EnterpriseFee.per_order.should be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "clearing all enterprise fee adjustments for a line item" do
|
||||
it "clears adjustments originating from many different enterprise fees" do
|
||||
p = create(:simple_product)
|
||||
@@ -38,7 +76,7 @@ describe EnterpriseFee do
|
||||
end
|
||||
|
||||
describe "clearing all enterprise fee adjustments on an order" do
|
||||
it "clears adjustments from many fees and one all line items" do
|
||||
it "clears adjustments from many fees and on all line items" do
|
||||
order = create(:order)
|
||||
|
||||
p1 = create(:simple_product)
|
||||
@@ -60,6 +98,17 @@ describe EnterpriseFee do
|
||||
end.to change(order.adjustments, :count).by(-4)
|
||||
end
|
||||
|
||||
it "clears adjustments from per-order fees" do
|
||||
order = create(:order)
|
||||
ef = create(:enterprise_fee)
|
||||
efa = OpenFoodNetwork::EnterpriseFeeApplicator.new(ef, nil, 'coordinator')
|
||||
efa.create_order_adjustment(order)
|
||||
|
||||
expect do
|
||||
EnterpriseFee.clear_all_adjustments_on_order order
|
||||
end.to change(order.adjustments, :count).by(-1)
|
||||
end
|
||||
|
||||
it "does not clear adjustments from another originator" do
|
||||
order = create(:order)
|
||||
tax_rate = create(:tax_rate, calculator: stub_model(Spree::Calculator))
|
||||
|
||||
@@ -62,6 +62,20 @@ describe Exchange do
|
||||
end
|
||||
end
|
||||
|
||||
describe "reporting its role" do
|
||||
it "returns 'supplier' when it is an incoming exchange" do
|
||||
e = Exchange.new
|
||||
e.stub(:incoming?) { true }
|
||||
e.role.should == 'supplier'
|
||||
end
|
||||
|
||||
it "returns 'distributor' when it is an outgoing exchange" do
|
||||
e = Exchange.new
|
||||
e.stub(:incoming?) { false }
|
||||
e.role.should == 'distributor'
|
||||
end
|
||||
end
|
||||
|
||||
describe "scopes" do
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:coordinator) { create(:distributor_enterprise) }
|
||||
@@ -97,6 +111,15 @@ describe Exchange do
|
||||
Exchange.with_variant(v).should == [ex]
|
||||
end
|
||||
|
||||
it "finds exchanges with any of a number of variants" do
|
||||
v1 = create(:variant)
|
||||
v2 = create(:variant)
|
||||
ex = create(:exchange)
|
||||
ex.variants << v1
|
||||
|
||||
Exchange.any_variant([v1, v2]).should == [ex]
|
||||
end
|
||||
|
||||
it "finds exchanges with a particular product's master variant" do
|
||||
p = create(:simple_product)
|
||||
ex = create(:exchange)
|
||||
|
||||
@@ -310,19 +310,36 @@ describe OrderCycle do
|
||||
end
|
||||
|
||||
describe "calculating fees for a variant via a particular distributor" do
|
||||
it "sums all the fees for the variant in the specified hub + order cycle" do
|
||||
it "sums all the per-item fees for the variant in the specified hub + order cycle" do
|
||||
coordinator = create(:distributor_enterprise)
|
||||
distributor = create(:distributor_enterprise)
|
||||
order_cycle = create(:simple_order_cycle)
|
||||
enterprise_fee1 = create(:enterprise_fee, amount: 20)
|
||||
enterprise_fee2 = create(:enterprise_fee, amount: 3)
|
||||
enterprise_fee3 = create(:enterprise_fee,
|
||||
calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2))
|
||||
product = create(:simple_product)
|
||||
|
||||
create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor,
|
||||
enterprise_fees: [enterprise_fee1, enterprise_fee2], variants: [product.master])
|
||||
enterprise_fees: [enterprise_fee1, enterprise_fee2, enterprise_fee3], variants: [product.master])
|
||||
|
||||
order_cycle.fees_for(product.master, distributor).should == 23
|
||||
end
|
||||
|
||||
|
||||
it "sums percentage fees for the variant" do
|
||||
coordinator = create(:distributor_enterprise)
|
||||
distributor = create(:distributor_enterprise)
|
||||
order_cycle = create(:simple_order_cycle)
|
||||
enterprise_fee1 = create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20))
|
||||
product = create(:simple_product, price: 10.00)
|
||||
|
||||
create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor,
|
||||
enterprise_fees: [enterprise_fee1], variants: [product.master])
|
||||
|
||||
product.master.price.should == 10.00
|
||||
order_cycle.fees_for(product.master, distributor).should == 2.00
|
||||
end
|
||||
end
|
||||
|
||||
describe "creating adjustments for a line item" do
|
||||
@@ -332,59 +349,67 @@ describe OrderCycle do
|
||||
let(:order) { double(:order, distributor: distributor) }
|
||||
let(:line_item) { double(:line_item, variant: variant, order: order) }
|
||||
|
||||
it "creates adjustment for each fee" do
|
||||
fee = {enterprise_fee: 'ef', label: 'label', role: 'role'}
|
||||
oc.should_receive(:enterprise_fees_for).with(variant, distributor) { [fee] }
|
||||
oc.should_receive(:create_adjustment_for_fee).with(line_item, 'ef', 'label', 'role')
|
||||
it "creates an adjustment for each fee" do
|
||||
applicator = double(:enterprise_fee_applicator)
|
||||
applicator.should_receive(:create_line_item_adjustment).with(line_item)
|
||||
oc.should_receive(:per_item_enterprise_fee_applicators_for).with(variant, distributor) { [applicator] }
|
||||
|
||||
oc.send(:create_adjustments_for, line_item)
|
||||
oc.send(:create_line_item_adjustments_for, line_item)
|
||||
end
|
||||
|
||||
it "finds fees for a line item" do
|
||||
it "makes fee applicators for a line item" do
|
||||
distributor = double(:distributor)
|
||||
ef1 = double(:enterprise_fee)
|
||||
ef2 = double(:enterprise_fee)
|
||||
ef3 = double(:enterprise_fee)
|
||||
incoming_exchange = double(:exchange, enterprise_fees: [ef1], incoming?: true)
|
||||
outgoing_exchange = double(:exchange, enterprise_fees: [ef2], incoming?: false)
|
||||
incoming_exchange = double(:exchange, role: 'supplier')
|
||||
outgoing_exchange = double(:exchange, role: 'distributor')
|
||||
incoming_exchange.stub_chain(:enterprise_fees, :per_item) { [ef1] }
|
||||
outgoing_exchange.stub_chain(:enterprise_fees, :per_item) { [ef2] }
|
||||
|
||||
oc.stub(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] }
|
||||
oc.stub(:coordinator_fees) { [ef3] }
|
||||
oc.stub(:adjustment_label_for) { 'label' }
|
||||
oc.stub_chain(:coordinator_fees, :per_item) { [ef3] }
|
||||
|
||||
oc.send(:enterprise_fees_for, line_item.variant, distributor).should ==
|
||||
[{enterprise_fee: ef1, label: 'label', role: 'supplier'},
|
||||
{enterprise_fee: ef2, label: 'label', role: 'distributor'},
|
||||
{enterprise_fee: ef3, label: 'label', role: 'coordinator'}]
|
||||
end
|
||||
|
||||
it "creates an adjustment for a fee" do
|
||||
line_item = create(:line_item)
|
||||
enterprise_fee = create(:enterprise_fee)
|
||||
|
||||
oc.send(:create_adjustment_for_fee, line_item, enterprise_fee, 'label', 'role')
|
||||
|
||||
adjustment = Spree::Adjustment.last
|
||||
adjustment.label.should == 'label'
|
||||
adjustment.adjustable.should == line_item.order
|
||||
adjustment.source.should == line_item
|
||||
adjustment.originator.should == enterprise_fee
|
||||
adjustment.should be_mandatory
|
||||
|
||||
md = adjustment.metadata
|
||||
md.enterprise.should == enterprise_fee.enterprise
|
||||
md.fee_name.should == enterprise_fee.name
|
||||
md.fee_type.should == enterprise_fee.fee_type
|
||||
md.enterprise_role.should == 'role'
|
||||
end
|
||||
|
||||
it "makes adjustment labels" do
|
||||
variant = double(:variant, product: double(:product, name: 'Bananas'))
|
||||
enterprise_fee = double(:enterprise_fee, fee_type: 'packing', enterprise: double(:enterprise, name: 'Ballantyne'))
|
||||
|
||||
oc.send(:adjustment_label_for, variant, enterprise_fee, 'distributor').should == "Bananas - packing fee by distributor Ballantyne"
|
||||
oc.send(:per_item_enterprise_fee_applicators_for, line_item.variant, distributor).should ==
|
||||
[OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'),
|
||||
OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'),
|
||||
OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "creating adjustments for an order" do
|
||||
let(:oc) { OrderCycle.new }
|
||||
let(:distributor) { double(:distributor) }
|
||||
let(:order) { double(:order, distributor: distributor) }
|
||||
|
||||
it "creates an adjustment for each fee" do
|
||||
applicator = double(:enterprise_fee_applicator)
|
||||
applicator.should_receive(:create_order_adjustment).with(order)
|
||||
oc.should_receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] }
|
||||
|
||||
oc.send(:create_order_adjustments_for, order)
|
||||
end
|
||||
|
||||
it "makes fee applicators for an order" do
|
||||
distributor = double(:distributor)
|
||||
ef1 = double(:enterprise_fee)
|
||||
ef2 = double(:enterprise_fee)
|
||||
ef3 = double(:enterprise_fee)
|
||||
incoming_exchange = double(:exchange, role: 'supplier')
|
||||
outgoing_exchange = double(:exchange, role: 'distributor')
|
||||
incoming_exchange.stub_chain(:enterprise_fees, :per_order) { [ef1] }
|
||||
outgoing_exchange.stub_chain(:enterprise_fees, :per_order) { [ef2] }
|
||||
|
||||
oc.stub(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] }
|
||||
oc.stub_chain(:coordinator_fees, :per_order) { [ef3] }
|
||||
|
||||
oc.send(:per_order_enterprise_fee_applicators_for, order).should ==
|
||||
[OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'),
|
||||
OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'),
|
||||
OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')]
|
||||
end
|
||||
end
|
||||
|
||||
describe "finding recently closed order cycles" do
|
||||
it "should give the most recently closed order cycle for a distributor" do
|
||||
distributor = create(:distributor_enterprise)
|
||||
|
||||
@@ -29,6 +29,12 @@ describe Spree::Address do
|
||||
end
|
||||
end
|
||||
|
||||
describe "setters" do
|
||||
it "lets us set a country" do
|
||||
expect { Spree::Address.new.country = "A country" }.to raise_error ActiveRecord::AssociationTypeMismatch
|
||||
end
|
||||
end
|
||||
|
||||
describe "notifying bugsnag when saved with missing data" do
|
||||
it "notifies on create" do
|
||||
Bugsnag.should_receive(:notify)
|
||||
|
||||
@@ -54,6 +54,15 @@ describe Spree::Order do
|
||||
subject.update_distribution_charge!
|
||||
end
|
||||
|
||||
it "skips order cycle per-order adjustments for orders that don't have an order cycle" do
|
||||
EnterpriseFee.stub(:clear_all_adjustments_on_order)
|
||||
subject.stub(:line_items) { [] }
|
||||
|
||||
subject.stub(:order_cycle) { nil }
|
||||
|
||||
subject.update_distribution_charge!
|
||||
end
|
||||
|
||||
it "ensures the correct adjustment(s) are created for order cycles" do
|
||||
EnterpriseFee.stub(:clear_all_adjustments_on_order)
|
||||
line_item = double(:line_item)
|
||||
@@ -61,7 +70,19 @@ describe Spree::Order do
|
||||
subject.stub(:provided_by_order_cycle?) { true }
|
||||
|
||||
order_cycle = double(:order_cycle)
|
||||
order_cycle.should_receive(:create_adjustments_for).with(line_item)
|
||||
order_cycle.should_receive(:create_line_item_adjustments_for).with(line_item)
|
||||
order_cycle.stub(:create_order_adjustments_for)
|
||||
subject.stub(:order_cycle) { order_cycle }
|
||||
|
||||
subject.update_distribution_charge!
|
||||
end
|
||||
|
||||
it "ensures the correct per-order adjustment(s) are created for order cycles" do
|
||||
EnterpriseFee.stub(:clear_all_adjustments_on_order)
|
||||
subject.stub(:line_items) { [] }
|
||||
|
||||
order_cycle = double(:order_cycle)
|
||||
order_cycle.should_receive(:create_order_adjustments_for).with(subject)
|
||||
subject.stub(:order_cycle) { order_cycle }
|
||||
|
||||
subject.update_distribution_charge!
|
||||
@@ -233,7 +254,41 @@ describe Spree::Order do
|
||||
|
||||
Spree::Order.not_state(:canceled).should_not include o
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "shipping address prepopulation" do
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:order) { build(:order, distributor: distributor) }
|
||||
|
||||
before do
|
||||
order.ship_address = distributor.address.clone
|
||||
order.save # just to trigger our autopopulate the first time ;)
|
||||
end
|
||||
|
||||
it "autopopulates the shipping address on save" do
|
||||
order.should_receive(:shipping_address_from_distributor).and_return true
|
||||
order.save
|
||||
end
|
||||
|
||||
it "populates the shipping address if the shipping method doesn't require a delivery address" do
|
||||
order.shipping_method = create(:shipping_method, require_ship_address: false)
|
||||
order.ship_address.update_attribute :firstname, "will"
|
||||
order.save
|
||||
order.ship_address.firstname.should == distributor.address.firstname
|
||||
end
|
||||
|
||||
it "does not populate the shipping address if the shipping method requires a delivery address" do
|
||||
order.shipping_method = create(:shipping_method, require_ship_address: true)
|
||||
order.ship_address.update_attribute :firstname, "will"
|
||||
order.save
|
||||
order.ship_address.firstname.should == "will"
|
||||
end
|
||||
|
||||
it "doesn't attempt to create a shipment if the order is not yet valid" do
|
||||
order.shipping_method = create(:shipping_method, require_ship_address: false)
|
||||
#Shipment.should_not_r
|
||||
order.create_shipment!
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -412,6 +412,35 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
describe "option types" do
|
||||
describe "removing an option type" do
|
||||
it "removes the associated option values from all variants" do
|
||||
# Given a product with a variant unit option type and values
|
||||
p = create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1)
|
||||
v1 = create(:variant, product: p, unit_value: 100, option_values: [])
|
||||
v2 = create(:variant, product: p, unit_value: 200, option_values: [])
|
||||
|
||||
# And a custom option type and values
|
||||
ot = create(:option_type, name: 'foo', presentation: 'foo')
|
||||
p.option_types << ot
|
||||
ov1 = create(:option_value, option_type: ot, name: 'One', presentation: 'One')
|
||||
ov2 = create(:option_value, option_type: ot, name: 'Two', presentation: 'Two')
|
||||
v1.option_values << ov1
|
||||
v2.option_values << ov2
|
||||
|
||||
# When we remove the custom option type
|
||||
p.option_type_ids = p.option_type_ids.reject { |id| id == ot.id }
|
||||
|
||||
# Then the associated option values should have been removed from the variants
|
||||
v1.option_values(true).should_not include ov1
|
||||
v2.option_values(true).should_not include ov2
|
||||
|
||||
# And the option values themselves should still exist
|
||||
Spree::OptionValue.where(id: [ov1.id, ov2.id]).count.should == 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "Stock filtering" do
|
||||
it "considers products that are on_demand as being in stock" do
|
||||
product = create(:simple_product, on_demand: true)
|
||||
|
||||
@@ -113,8 +113,8 @@ module Spree
|
||||
|
||||
ov = Spree::OptionValue.last
|
||||
ov.option_type.should == @ot
|
||||
ov.name.should == '10 g foo'
|
||||
ov.presentation.should == '10 g foo'
|
||||
ov.name.should == '10g foo'
|
||||
ov.presentation.should == '10g foo'
|
||||
|
||||
v.option_values.should include ov
|
||||
end
|
||||
@@ -139,8 +139,8 @@ module Spree
|
||||
|
||||
ov = v.option_values.last
|
||||
ov.option_type.should == @ot
|
||||
ov.name.should == '10 g foo'
|
||||
ov.presentation.should == '10 g foo'
|
||||
ov.name.should == '10g foo'
|
||||
ov.presentation.should == '10g foo'
|
||||
|
||||
v_orig.option_values.should include ov
|
||||
end
|
||||
@@ -171,20 +171,48 @@ module Spree
|
||||
it "when description is blank" do
|
||||
v = Spree::Variant.new unit_description: nil
|
||||
v.stub(:option_value_value_unit) { %w(value unit) }
|
||||
v.send(:option_value_name).should == "value unit"
|
||||
v.stub(:value_scaled?) { true }
|
||||
v.send(:option_value_name).should == "valueunit"
|
||||
end
|
||||
|
||||
it "when description is present" do
|
||||
v = Spree::Variant.new unit_description: 'desc'
|
||||
v.stub(:option_value_value_unit) { %w(value unit) }
|
||||
v.send(:option_value_name).should == "value unit desc"
|
||||
v.stub(:value_scaled?) { true }
|
||||
v.send(:option_value_name).should == "valueunit desc"
|
||||
end
|
||||
|
||||
it "when value is blank and description is present" do
|
||||
v = Spree::Variant.new unit_description: 'desc'
|
||||
v.stub(:option_value_value_unit) { [nil, nil] }
|
||||
v.stub(:value_scaled?) { true }
|
||||
v.send(:option_value_name).should == "desc"
|
||||
end
|
||||
|
||||
it "spaces value and unit when value is unscaled" do
|
||||
v = Spree::Variant.new unit_description: nil
|
||||
v.stub(:option_value_value_unit) { %w(value unit) }
|
||||
v.stub(:value_scaled?) { false }
|
||||
v.send(:option_value_name).should == "value unit"
|
||||
end
|
||||
end
|
||||
|
||||
describe "determining if a variant's value is scaled" do
|
||||
it "returns true when the product has a scale" do
|
||||
p = Spree::Product.new variant_unit_scale: 1000
|
||||
v = Spree::Variant.new
|
||||
v.stub(:product) { p }
|
||||
|
||||
v.send(:value_scaled?).should be_true
|
||||
end
|
||||
|
||||
it "returns false otherwise" do
|
||||
p = Spree::Product.new
|
||||
v = Spree::Variant.new
|
||||
v.stub(:product) { p }
|
||||
|
||||
v.send(:value_scaled?).should be_false
|
||||
end
|
||||
end
|
||||
|
||||
describe "generating option value's value and unit" do
|
||||
|
||||
Reference in New Issue
Block a user