Merge branch 'master' into bom

This commit is contained in:
Rob H
2014-03-05 10:15:39 +11:00
87 changed files with 2492 additions and 403 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -20,30 +20,31 @@ productEditModule.directive "ofnDecimal", ->
viewValue
productEditModule.directive "ofnTrackProduct", ->
productEditModule.directive "ofnTrackProduct", ['$parse', ($parse) ->
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
property_name = attrs.ofnTrackProduct
ngModel.$parsers.push (viewValue) ->
if ngModel.$dirty
addDirtyProperty scope.dirtyProducts, scope.product.id, property_name, viewValue
parsedPropertyName = $parse(attrs.ofnTrackProduct)
addDirtyProperty scope.dirtyProducts, scope.product.id, parsedPropertyName, viewValue
scope.displayDirtyProducts()
viewValue
]
productEditModule.directive "ofnTrackVariant", ->
productEditModule.directive "ofnTrackVariant", ['$parse', ($parse) ->
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
property_name = attrs.ofnTrackVariant
ngModel.$parsers.push (viewValue) ->
dirtyVariants = {}
dirtyVariants = scope.dirtyProducts[scope.product.id].variants if scope.dirtyProducts.hasOwnProperty(scope.product.id) and scope.dirtyProducts[scope.product.id].hasOwnProperty("variants")
if ngModel.$dirty
addDirtyProperty dirtyVariants, scope.variant.id, property_name, viewValue
addDirtyProperty scope.dirtyProducts, scope.product.id, "variants", dirtyVariants
parsedPropertyName = $parse(attrs.ofnTrackVariant)
addDirtyProperty dirtyVariants, scope.variant.id, parsedPropertyName, viewValue
addDirtyProperty scope.dirtyProducts, scope.product.id, $parse("variants"), dirtyVariants
scope.displayDirtyProducts()
viewValue
]
productEditModule.directive "ofnToggleVariants", ->
link: (scope, element, attrs) ->
@@ -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) ->

View File

@@ -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()

View 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')

View File

@@ -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()

View 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 );

View 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/

View File

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

View File

@@ -0,0 +1,2 @@
checkout
display: block

View File

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

View 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

View File

@@ -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

View File

@@ -1 +1,2 @@
$fawn: #f6efe5
$dark-grey: #c7c7c7

View 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/

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -24,4 +24,4 @@ Spree::UserSessionsController.class_eval do
end
end
end
end
end

View File

@@ -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('/'))

View File

@@ -0,0 +1,2 @@
module Shop::CheckoutHelper
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,6 @@
Spree::Address.class_eval do
has_one :enterprise
belongs_to :country, class_name: "Spree::Country"
geocoded_by :full_address

View File

@@ -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

View File

@@ -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

View 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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

@@ -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"}

View File

@@ -14,6 +14,7 @@
%body.off-canvas
= render partial: "shared/menu"
= display_flash_messages
%section{ role: "main" }
= yield

View File

@@ -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

View 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"

View File

@@ -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 }}

View 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 }

View 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"

View 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'

View 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"

View 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"

View 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

View 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

View File

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

View File

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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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|

View 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

View 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

View File

@@ -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

View File

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

View File

@@ -31,5 +31,4 @@ feature %q{
page.should_not have_content "ComfortableMexicanSofa"
page.should have_content "WHERE WOULD YOU LIKE TO SHOP?"
end
end

View File

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

View File

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

View 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

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -121,6 +121,27 @@ describe "filtering products for submission to database", ->
]
]
it "returns variants with a negative id without that id", ->
testProduct =
id: 1
variants: [
id: -1
on_hand: 5
price: 12.0
unit_value: 250
unit_description: "(bottle)"
]
expect(filterSubmitProducts([testProduct])).toEqual [
id: 1
variants_attributes: [
on_hand: 5
price: 12.0
unit_value: 250
unit_description: "(bottle)"
]
]
it "does not return variants_attributes property if variants is an empty array", ->
testProduct =
id: 1
@@ -171,6 +192,10 @@ describe "filtering products for submission to database", ->
group_buy: null
group_buy_unit_size: null
on_demand: false
master:
id: 2
unit_value: 250
unit_description: "foo"
variants: [
id: 1
on_hand: 2
@@ -190,6 +215,8 @@ describe "filtering products for submission to database", ->
variant_unit: 'volume'
variant_unit_scale: 1
variant_unit_name: 'loaf'
unit_value: 250
unit_description: "foo"
available_on: available_on
variants_attributes: [
id: 1
@@ -202,10 +229,17 @@ describe "filtering products for submission to database", ->
describe "Maintaining a live record of dirty products and properties", ->
parse = null
beforeEach ->
module "ofn.bulk_product_edit"
beforeEach inject(($parse) ->
parse = $parse
)
describe "adding product properties to the dirtyProducts object", -> # Applies to both products and variants
it "adds the product and the property to the list if property is dirty", ->
dirtyProducts = {}
addDirtyProperty dirtyProducts, 1, "name", "Product 1"
addDirtyProperty dirtyProducts, 1, parse("name"), "Product 1"
expect(dirtyProducts).toEqual 1:
id: 1
name: "Product 1"
@@ -216,7 +250,7 @@ describe "Maintaining a live record of dirty products and properties", ->
id: 1
notaname: "something"
addDirtyProperty dirtyProducts, 1, "name", "Product 3"
addDirtyProperty dirtyProducts, 1, parse("name"), "Product 3"
expect(dirtyProducts).toEqual 1:
id: 1
notaname: "something"
@@ -228,7 +262,7 @@ describe "Maintaining a live record of dirty products and properties", ->
id: 1
name: "Product 1"
addDirtyProperty dirtyProducts, 1, "name", "Product 2"
addDirtyProperty dirtyProducts, 1, parse("name"), "Product 2"
expect(dirtyProducts).toEqual 1:
id: 1
name: "Product 2"
@@ -420,12 +454,24 @@ describe "AdminProductEditCtrl", ->
scope.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "items"
it "loads data for variants (inc. master)", ->
spyOn scope, "loadVariantVariantUnit"
product =
variant_unit_scale: 1.0
master: {id: 1, unit_value: 1, unit_description: '(one)'}
variants: [{id: 2, unit_value: 2, unit_description: '(two)'}]
scope.loadVariantUnit product
expect(scope.loadVariantVariantUnit).toHaveBeenCalledWith product, product.variants[0]
expect(scope.loadVariantVariantUnit).toHaveBeenCalledWith product, product.master
describe "setting variant unit_value_with_description", ->
it "sets by combining unit_value and unit_description", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}]
scope.loadVariantUnit product
scope.loadVariantVariantUnit product, product.variants[0]
expect(product.variants[0]).toEqual
id: 1
unit_value: 1
@@ -436,23 +482,30 @@ describe "AdminProductEditCtrl", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 1}]
scope.loadVariantUnit product
scope.loadVariantVariantUnit product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '1'
it "uses unit_description when value is missing", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_description: 'Small'}]
scope.loadVariantUnit product
scope.loadVariantVariantUnit product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual 'Small'
it "converts values from base value to chosen unit", ->
product =
variant_unit_scale: 1000.0
variants: [{id: 1, unit_value: 2500}]
scope.loadVariantUnit product
scope.loadVariantVariantUnit product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '2.5'
it "displays a unit_value of zero", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 0}]
scope.loadVariantVariantUnit product, product.variants[0]
expect(product.variants[0].unit_value_with_description).toEqual '0'
describe "calculating the scaled unit value for a variant", ->
it "returns the scaled value when variant has a unit_value", ->
@@ -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"

View 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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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