diff --git a/Gemfile b/Gemfile index 3271c30756..6965ae1a43 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' ruby "1.9.3" -gem 'rails', '3.2.14' +gem 'rails', '3.2.17' gem 'pg' gem 'spree', :github => 'openfoodfoundation/spree', :branch => '1-3-stable' @@ -16,6 +16,7 @@ gem 'comfortable_mexican_sofa' gem 'simple_form', :github => 'RohanM/simple_form' gem 'unicorn' +gem 'angularjs-rails' gem 'bugsnag' gem 'newrelic_rpm' gem 'haml' @@ -50,6 +51,7 @@ group :assets do gem 'turbo-sprockets-rails3' gem 'zurb-foundation', :github => 'zurb/foundation' end +gem 'foundation_rails_helper', github: 'willrjmarshall/foundation_rails_helper', branch: "rails3" gem 'jquery-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 78b60d1977..b498268a74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -93,6 +93,16 @@ GIT i18n (~> 0.5) spree (~> 1.1) +GIT + remote: git://github.com/willrjmarshall/foundation_rails_helper.git + revision: 4d5d53fdc4b1fb71e66524d298c5c635de82cfbb + branch: rails3 + specs: + foundation_rails_helper (0.4) + actionpack (>= 3.0) + activemodel (>= 3.0) + railties (>= 3.0) + GIT remote: git://github.com/zurb/foundation.git revision: a81d639847ba5fc2e324b749b8e409e31e8d83c9 @@ -117,12 +127,12 @@ PATH GEM remote: https://rubygems.org/ specs: - actionmailer (3.2.14) - actionpack (= 3.2.14) + actionmailer (3.2.17) + actionpack (= 3.2.17) mail (~> 2.5.4) - actionpack (3.2.14) - activemodel (= 3.2.14) - activesupport (= 3.2.14) + actionpack (3.2.17) + activemodel (= 3.2.17) + activesupport (= 3.2.17) builder (~> 3.0.0) erubis (~> 2.7.0) journey (~> 1.0.4) @@ -142,25 +152,26 @@ GEM json (~> 1.7) money (< 6.0.0) nokogiri (~> 1.4) - activemodel (3.2.14) - activesupport (= 3.2.14) + activemodel (3.2.17) + activesupport (= 3.2.17) builder (~> 3.0.0) - activerecord (3.2.14) - activemodel (= 3.2.14) - activesupport (= 3.2.14) + activerecord (3.2.17) + activemodel (= 3.2.17) + activesupport (= 3.2.17) arel (~> 3.0.2) tzinfo (~> 0.3.29) - activeresource (3.2.14) - activemodel (= 3.2.14) - activesupport (= 3.2.14) - activesupport (3.2.14) + activeresource (3.2.17) + activemodel (= 3.2.17) + activesupport (= 3.2.17) + activesupport (3.2.17) i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) acts_as_list (0.1.4) addressable (2.3.3) andand (1.3.3) + angularjs-rails (1.2.13) ansi (1.4.2) - arel (3.0.2) + arel (3.0.3) awesome_nested_set (2.1.5) activerecord (>= 3.0.0) awesome_print (1.0.2) @@ -291,7 +302,7 @@ GEM httparty (0.11.0) multi_json (~> 1.0) multi_xml (>= 0.5.2) - i18n (0.6.5) + i18n (0.6.9) journey (1.0.4) jquery-rails (2.2.2) railties (>= 3.0, < 5.0) @@ -319,12 +330,12 @@ GEM mime-types (~> 1.16) treetop (~> 1.4.8) method_source (0.8.1) - mime-types (1.25) + mime-types (1.25.1) mini_portile (0.5.2) money (5.0.0) i18n (~> 0.4) json - multi_json (1.8.2) + multi_json (1.8.4) multi_xml (0.5.5) net-scp (1.1.2) net-ssh (>= 2.6.5) @@ -346,7 +357,7 @@ GEM http_parser.rb (~> 0.5.3) polyamorous (0.5.0) activerecord (~> 3.0) - polyglot (0.3.3) + polyglot (0.3.4) pry (0.9.12.2) coderay (~> 1.0.5) method_source (~> 0.8) @@ -366,23 +377,23 @@ GEM rack rack-test (0.6.2) rack (>= 1.0) - rails (3.2.14) - actionmailer (= 3.2.14) - actionpack (= 3.2.14) - activerecord (= 3.2.14) - activeresource (= 3.2.14) - activesupport (= 3.2.14) + rails (3.2.17) + actionmailer (= 3.2.17) + actionpack (= 3.2.17) + activerecord (= 3.2.17) + activeresource (= 3.2.17) + activesupport (= 3.2.17) bundler (~> 1.0) - railties (= 3.2.14) - railties (3.2.14) - actionpack (= 3.2.14) - activesupport (= 3.2.14) + railties (= 3.2.17) + railties (3.2.17) + actionpack (= 3.2.17) + activesupport (= 3.2.17) rack-ssl (~> 1.3.2) rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) raindrops (0.9.0) - rake (10.1.0) + rake (10.1.1) ransack (0.7.2) actionpack (~> 3.0) activerecord (~> 3.0) @@ -495,6 +506,7 @@ PLATFORMS DEPENDENCIES andand + angularjs-rails awesome_print aws-sdk bugsnag @@ -510,6 +522,7 @@ DEPENDENCIES eaterprises_feature! factory_girl_rails faker + foundation_rails_helper! geocoder gmaps4rails guard @@ -531,7 +544,7 @@ DEPENDENCIES rabl rack-livereload rack-ssl - rails (= 3.2.14) + rails (= 3.2.17) representative_view rspec-rails sass diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 64c52a6cbe..97116f673b 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -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) -> diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 32580e15ee..1f2ecf155d 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -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() - - diff --git a/app/assets/javascripts/darkswarm/checkout.js.coffee b/app/assets/javascripts/darkswarm/checkout.js.coffee new file mode 100644 index 0000000000..65bd062d5d --- /dev/null +++ b/app/assets/javascripts/darkswarm/checkout.js.coffee @@ -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') diff --git a/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee new file mode 100644 index 0000000000..6c4ec5d2cb --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/checkout_controller.js.coffee @@ -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() diff --git a/app/assets/javascripts/jquery-migrate-1.0.0.js b/app/assets/javascripts/jquery-migrate-1.0.0.js new file mode 100644 index 0000000000..c374e6ba16 --- /dev/null +++ b/app/assets/javascripts/jquery-migrate-1.0.0.js @@ -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 ); diff --git a/app/assets/javascripts/shop/checkout.js.coffee b/app/assets/javascripts/shop/checkout.js.coffee new file mode 100644 index 0000000000..761567942f --- /dev/null +++ b/app/assets/javascripts/shop/checkout.js.coffee @@ -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/ diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index 026e4cf83b..20f9c49a84 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -106,6 +106,8 @@ ul.column-list { } table#listing_products.bulk { + clear: both; + td.supplier { select { width: 125px; diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass new file mode 100644 index 0000000000..c864d4928a --- /dev/null +++ b/app/assets/stylesheets/darkswarm/checkout.css.sass @@ -0,0 +1,2 @@ +checkout + display: block diff --git a/app/assets/stylesheets/darkswarm/footer.sass b/app/assets/stylesheets/darkswarm/footer.sass index ba2424e94b..df649a8d59 100644 --- a/app/assets/stylesheets/darkswarm/footer.sass +++ b/app/assets/stylesheets/darkswarm/footer.sass @@ -10,3 +10,7 @@ img display: block margin: 0px auto 8px + + .contact + strong + padding-right: 1em diff --git a/app/assets/stylesheets/darkswarm/forms.css.sass b/app/assets/stylesheets/darkswarm/forms.css.sass new file mode 100644 index 0000000000..bada1fb68d --- /dev/null +++ b/app/assets/stylesheets/darkswarm/forms.css.sass @@ -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 diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 59c39133b3..1b9109cfaa 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -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 diff --git a/app/assets/stylesheets/darkswarm/variables.css.sass b/app/assets/stylesheets/darkswarm/variables.css.sass index a13eb9d014..c8edbc4a46 100644 --- a/app/assets/stylesheets/darkswarm/variables.css.sass +++ b/app/assets/stylesheets/darkswarm/variables.css.sass @@ -1 +1,2 @@ $fawn: #f6efe5 +$dark-grey: #c7c7c7 diff --git a/app/assets/stylesheets/shop/checkout.css.scss b/app/assets/stylesheets/shop/checkout.css.scss new file mode 100644 index 0000000000..727d410b19 --- /dev/null +++ b/app/assets/stylesheets/shop/checkout.css.scss @@ -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/ diff --git a/app/controllers/admin/enterprise_fees_controller.rb b/app/controllers/admin/enterprise_fees_controller.rb index 6e1a7da113..085828d7b3 100644 --- a/app/controllers/admin/enterprise_fees_controller.rb +++ b/app/controllers/admin/enterprise_fees_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 88ec04f574..4b0cc37e9f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/shop/checkout_controller.rb b/app/controllers/shop/checkout_controller.rb new file mode 100644 index 0000000000..b253128d3e --- /dev/null +++ b/app/controllers/shop/checkout_controller.rb @@ -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 diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop/shop_controller.rb similarity index 89% rename from app/controllers/shop_controller.rb rename to app/controllers/shop/shop_controller.rb index a4fe70c71b..e0862d205e 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop/shop_controller.rb @@ -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 diff --git a/app/controllers/spree/admin/variants_controller_decorator.rb b/app/controllers/spree/admin/variants_controller_decorator.rb new file mode 100644 index 0000000000..16a37d5286 --- /dev/null +++ b/app/controllers/spree/admin/variants_controller_decorator.rb @@ -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 diff --git a/app/controllers/spree/checkout_controller_decorator.rb b/app/controllers/spree/checkout_controller_decorator.rb index a08f2aa463..3f83ca10ec 100644 --- a/app/controllers/spree/checkout_controller_decorator.rb +++ b/app/controllers/spree/checkout_controller_decorator.rb @@ -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 diff --git a/app/controllers/spree/user_sessions_controller_decorator.rb b/app/controllers/spree/user_sessions_controller_decorator.rb index 3f3345b54a..c0d5d72381 100644 --- a/app/controllers/spree/user_sessions_controller_decorator.rb +++ b/app/controllers/spree/user_sessions_controller_decorator.rb @@ -24,4 +24,4 @@ Spree::UserSessionsController.class_eval do end end end -end \ No newline at end of file +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 794aee744b..7afe5e180b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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('/')) diff --git a/app/helpers/shop/checkout_helper.rb b/app/helpers/shop/checkout_helper.rb new file mode 100644 index 0000000000..263dcc8321 --- /dev/null +++ b/app/helpers/shop/checkout_helper.rb @@ -0,0 +1,2 @@ +module Shop::CheckoutHelper +end diff --git a/app/helpers/spree/products_helper_decorator.rb b/app/helpers/spree/products_helper_decorator.rb index 8c7b7fbe54..f918a47649 100644 --- a/app/helpers/spree/products_helper_decorator.rb +++ b/app/helpers/spree/products_helper_decorator.rb @@ -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 diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb index 8f3f038ded..148c18d367 100644 --- a/app/models/enterprise_fee.rb +++ b/app/models/enterprise_fee.rb @@ -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 diff --git a/app/models/exchange.rb b/app/models/exchange.rb index 0f8b472443..90db51c36f 100644 --- a/app/models/exchange.rb +++ b/app/models/exchange.rb @@ -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 diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 39fed33674..519e693b1e 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -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 diff --git a/app/models/spree/address_decorator.rb b/app/models/spree/address_decorator.rb index 7cfd6cca0b..28f292fbd3 100644 --- a/app/models/spree/address_decorator.rb +++ b/app/models/spree/address_decorator.rb @@ -1,5 +1,6 @@ Spree::Address.class_eval do has_one :enterprise + belongs_to :country, class_name: "Spree::Country" geocoded_by :full_address diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 45caf95df9..2490c30c28 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -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 diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 5150a760c8..33ccf73a81 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -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 diff --git a/app/models/spree/product_option_type_decorator.rb b/app/models/spree/product_option_type_decorator.rb new file mode 100644 index 0000000000..e091b62efa --- /dev/null +++ b/app/models/spree/product_option_type_decorator.rb @@ -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 diff --git a/app/models/spree/product_set.rb b/app/models/spree/product_set.rb index db3095187e..e79408d8c5 100644 --- a/app/models/spree/product_set.rb +++ b/app/models/spree/product_set.rb @@ -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 diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 8150ce6529..12a4ca1a0e 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -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 diff --git a/app/overrides/spree/admin/products/_form/add_units_form.html.haml.deface b/app/overrides/spree/admin/products/_form/add_units_form.html.haml.deface new file mode 100644 index 0000000000..9a5b6a87d6 --- /dev/null +++ b/app/overrides/spree/admin/products/_form/add_units_form.html.haml.deface @@ -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 diff --git a/app/overrides/spree/admin/products/new/add_unit_form.html.haml.deface b/app/overrides/spree/admin/products/new/add_unit_form.html.haml.deface new file mode 100644 index 0000000000..03021b41d9 --- /dev/null +++ b/app/overrides/spree/admin/products/new/add_unit_form.html.haml.deface @@ -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 diff --git a/app/overrides/spree/admin/variants/_form/add_unit_value_and_description.html.haml.deface b/app/overrides/spree/admin/variants/_form/add_unit_value_and_description.html.haml.deface new file mode 100644 index 0000000000..8696134177 --- /dev/null +++ b/app/overrides/spree/admin/variants/_form/add_unit_value_and_description.html.haml.deface @@ -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" diff --git a/app/overrides/spree/admin/variants/_form/hide_unit_option_types.html.haml.deface b/app/overrides/spree/admin/variants/_form/hide_unit_option_types.html.haml.deface new file mode 100644 index 0000000000..16a0e572b4 --- /dev/null +++ b/app/overrides/spree/admin/variants/_form/hide_unit_option_types.html.haml.deface @@ -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') diff --git a/app/views/admin/enterprise_fees/index.html.haml b/app/views/admin/enterprise_fees/index.html.haml index a59ffc99e0..d3e9a0b88c 100644 --- a/app/views/admin/enterprise_fees/index.html.haml +++ b/app/views/admin/enterprise_fees/index.html.haml @@ -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"} diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index 823d2d7350..150ffaac52 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -14,6 +14,7 @@ %body.off-canvas = render partial: "shared/menu" + = display_flash_messages %section{ role: "main" } = yield diff --git a/app/views/shop/_contact_us.html.haml b/app/views/shop/_contact_us.html.haml deleted file mode 100644 index 2cb4d65984..0000000000 --- a/app/views/shop/_contact_us.html.haml +++ /dev/null @@ -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 diff --git a/app/views/shop/_details.html.haml b/app/views/shop/_details.html.haml new file mode 100644 index 0000000000..6b77e45a27 --- /dev/null +++ b/app/views/shop/_details.html.haml @@ -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" diff --git a/app/views/shop/_order_cycles.html.haml b/app/views/shop/_order_cycles.html.haml deleted file mode 100644 index 5c2986bedf..0000000000 --- a/app/views/shop/_order_cycles.html.haml +++ /dev/null @@ -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 }} - diff --git a/app/views/shop/checkout/_form.html.haml b/app/views/shop/checkout/_form.html.haml new file mode 100644 index 0000000000..2add9a5c39 --- /dev/null +++ b/app/views/shop/checkout/_form.html.haml @@ -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 } diff --git a/app/views/shop/checkout/_login.html.haml b/app/views/shop/checkout/_login.html.haml new file mode 100644 index 0000000000..33bf41fed5 --- /dev/null +++ b/app/views/shop/checkout/_login.html.haml @@ -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" diff --git a/app/views/shop/checkout/_signup.html.haml b/app/views/shop/checkout/_signup.html.haml new file mode 100644 index 0000000000..81ed7744d0 --- /dev/null +++ b/app/views/shop/checkout/_signup.html.haml @@ -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' diff --git a/app/views/shop/checkout/_summary.html.haml b/app/views/shop/checkout/_summary.html.haml new file mode 100644 index 0000000000..edda8bc43a --- /dev/null +++ b/app/views/shop/checkout/_summary.html.haml @@ -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" diff --git a/app/views/shop/checkout/edit.html.haml b/app/views/shop/checkout/edit.html.haml new file mode 100644 index 0000000000..5eb6c2bcd4 --- /dev/null +++ b/app/views/shop/checkout/edit.html.haml @@ -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" diff --git a/app/views/shop/_about_us.html.haml b/app/views/shop/shop/_about_us.html.haml similarity index 100% rename from app/views/shop/_about_us.html.haml rename to app/views/shop/shop/_about_us.html.haml diff --git a/app/views/shop/shop/_contact_us.html.haml b/app/views/shop/shop/_contact_us.html.haml new file mode 100644 index 0000000000..58a69ed38f --- /dev/null +++ b/app/views/shop/shop/_contact_us.html.haml @@ -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 diff --git a/app/views/shop/_last_order_cycle.html.haml b/app/views/shop/shop/_last_order_cycle.html.haml similarity index 100% rename from app/views/shop/_last_order_cycle.html.haml rename to app/views/shop/shop/_last_order_cycle.html.haml diff --git a/app/views/shop/_next_order_cycle.html.haml b/app/views/shop/shop/_next_order_cycle.html.haml similarity index 100% rename from app/views/shop/_next_order_cycle.html.haml rename to app/views/shop/shop/_next_order_cycle.html.haml diff --git a/app/views/shop/_order_cycle.rabl b/app/views/shop/shop/_order_cycle.rabl similarity index 100% rename from app/views/shop/_order_cycle.rabl rename to app/views/shop/shop/_order_cycle.rabl diff --git a/app/views/shop/shop/_order_cycles.html.haml b/app/views/shop/shop/_order_cycles.html.haml new file mode 100644 index 0000000000..aff1a3259d --- /dev/null +++ b/app/views/shop/shop/_order_cycles.html.haml @@ -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 diff --git a/app/views/shop/_products.html.haml b/app/views/shop/shop/_products.html.haml similarity index 92% rename from app/views/shop/_products.html.haml rename to app/views/shop/shop/_products.html.haml index 57140531cd..3456ec29e7 100644 --- a/app/views/shop/_products.html.haml +++ b/app/views/shop/shop/_products.html.haml @@ -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"} diff --git a/app/views/shop/_variant.html.haml b/app/views/shop/shop/_variant.html.haml similarity index 96% rename from app/views/shop/_variant.html.haml rename to app/views/shop/shop/_variant.html.haml index 0adb5a2421..9a71ac8b39 100644 --- a/app/views/shop/_variant.html.haml +++ b/app/views/shop/shop/_variant.html.haml @@ -1,5 +1,6 @@ -%td{colspan: 2} +%td +%td.notes %td {{variant.options_text}} %td %input{type: :number, diff --git a/app/views/shop/products.rabl b/app/views/shop/shop/products.rabl similarity index 100% rename from app/views/shop/products.rabl rename to app/views/shop/shop/products.rabl diff --git a/app/views/shop/show.html.haml b/app/views/shop/shop/show.html.haml similarity index 64% rename from app/views/shop/show.html.haml rename to app/views/shop/shop/show.html.haml index 6f1f0b0181..5072576f6f 100644 --- a/app/views/shop/show.html.haml +++ b/app/views/shop/shop/show.html.haml @@ -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" diff --git a/app/views/spree/admin/products/_supplier_and_group_buy_for_new.html.haml b/app/views/spree/admin/products/_supplier_and_group_buy_for_new.html.haml index c0615a753b..d17a45fa6c 100644 --- a/app/views/spree/admin/products/_supplier_and_group_buy_for_new.html.haml +++ b/app/views/spree/admin/products/_supplier_and_group_buy_for_new.html.haml @@ -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" \ No newline at end of file + = f.text_field :group_buy_unit_size, :class => "fullwidth" diff --git a/app/views/spree/admin/products/_supplier_form.html.haml b/app/views/spree/admin/products/_supplier_form.html.haml index b73a9eb462..3e5c01c2a3 100644 --- a/app/views/spree/admin/products/_supplier_form.html.haml +++ b/app/views/spree/admin/products/_supplier_form.html.haml @@ -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 diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml index e2ae2dcee4..28cefe564f 100644 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ b/app/views/spree/admin/products/bulk_edit.html.haml @@ -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" } diff --git a/app/views/spree/api/products/bulk_show.v1.rabl b/app/views/spree/api/products/bulk_show.v1.rabl index 1f07d3d91f..a220737007 100644 --- a/app/views/spree/api/products/bulk_show.v1.rabl +++ b/app/views/spree/api/products/bulk_show.v1.rabl @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 371620f0ec..76c192dc67 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20140213003443_add_require_ship_address_to_shipping_methods.rb b/db/migrate/20140213003443_add_require_ship_address_to_shipping_methods.rb new file mode 100644 index 0000000000..a38ad29242 --- /dev/null +++ b/db/migrate/20140213003443_add_require_ship_address_to_shipping_methods.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 313c7ddf48..b9f151fedf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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| diff --git a/lib/open_food_network/enterprise_fee_applicator.rb b/lib/open_food_network/enterprise_fee_applicator.rb new file mode 100644 index 0000000000..45905bfaca --- /dev/null +++ b/lib/open_food_network/enterprise_fee_applicator.rb @@ -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 + diff --git a/spec/controllers/shop/checkout_controller_spec.rb b/spec/controllers/shop/checkout_controller_spec.rb new file mode 100644 index 0000000000..ae560a26b8 --- /dev/null +++ b/spec/controllers/shop/checkout_controller_spec.rb @@ -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 diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop/shop_controller_spec.rb similarity index 99% rename from spec/controllers/shop_controller_spec.rb rename to spec/controllers/shop/shop_controller_spec.rb index 6b539fdbef..f440da9805 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop/shop_controller_spec.rb @@ -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 diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 352c3d14c7..4644aae8c1 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -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) diff --git a/spec/features/admin/cms_spec.rb b/spec/features/admin/cms_spec.rb index aec64cf291..c3b2fe3454 100644 --- a/spec/features/admin/cms_spec.rb +++ b/spec/features/admin/cms_spec.rb @@ -31,5 +31,4 @@ feature %q{ page.should_not have_content "ComfortableMexicanSofa" page.should have_content "WHERE WOULD YOU LIKE TO SHOP?" end - end diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 8f2c1b2a34..5cadd11a29 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -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 diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb index 3399785a3c..092d5239d4 100644 --- a/spec/features/admin/products_spec.rb +++ b/spec/features/admin/products_spec.rb @@ -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' diff --git a/spec/features/admin/variants_spec.rb b/spec/features/admin/variants_spec.rb new file mode 100644 index 0000000000..d05aa75ef8 --- /dev/null +++ b/spec/features/admin/variants_spec.rb @@ -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 diff --git a/spec/features/consumer/authentication_spec.rb b/spec/features/consumer/authentication_spec.rb new file mode 100644 index 0000000000..e1dd681af6 --- /dev/null +++ b/spec/features/consumer/authentication_spec.rb @@ -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 + diff --git a/spec/features/consumer/checkout_spec.rb b/spec/features/consumer/checkout_spec.rb index 0c21352400..29a7eed19d 100644 --- a/spec/features/consumer/checkout_spec.rb +++ b/spec/features/consumer/checkout_spec.rb @@ -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 diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb new file mode 100644 index 0000000000..4e08b9d5e4 --- /dev/null +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -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 diff --git a/spec/features/consumer/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb similarity index 95% rename from spec/features/consumer/shopping_spec.rb rename to spec/features/consumer/shopping/shopping_spec.rb index 7a73801169..b7e085db10 100644 --- a/spec/features/consumer/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -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 diff --git a/spec/helpers/order_cycles_helper_spec.rb b/spec/helpers/order_cycles_helper_spec.rb index fce1095591..3717f34462 100644 --- a/spec/helpers/order_cycles_helper_spec.rb +++ b/spec/helpers/order_cycles_helper_spec.rb @@ -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 diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index ef8a7c3cb2..bd85419ac9 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -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" diff --git a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb new file mode 100644 index 0000000000..d10a5e9b72 --- /dev/null +++ b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb @@ -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 diff --git a/spec/models/enterprise_fee_spec.rb b/spec/models/enterprise_fee_spec.rb index 98484d6545..806eb7759c 100644 --- a/spec/models/enterprise_fee_spec.rb +++ b/spec/models/enterprise_fee_spec.rb @@ -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)) diff --git a/spec/models/exchange_spec.rb b/spec/models/exchange_spec.rb index f4ebfc2d8a..7095dd03f9 100644 --- a/spec/models/exchange_spec.rb +++ b/spec/models/exchange_spec.rb @@ -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) diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index d5ee190efa..47e49a73e3 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -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) diff --git a/spec/models/spree/addresses_spec.rb b/spec/models/spree/addresses_spec.rb index 30497c8c26..6c6a88ba11 100644 --- a/spec/models/spree/addresses_spec.rb +++ b/spec/models/spree/addresses_spec.rb @@ -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) diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 06f82dacac..ff17188998 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -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 diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 6c7dfb9091..7e8f679f26 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -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) diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 930ba631ad..2031a4d22b 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -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